goioc/web: Web Framework for Go, based on goioc/di
How is this framework different from others?
- First of all,
goioc/web
is working using Dependency Injection and is based on goioc/di, which is the IoC Container.
- Secondly - and this is the most exciting part - web-endpoints in
goioc/web
can have (almost) arbitrary signature!
No more func(w http.ResponseWriter, r *http.Request)
handlers, if your endpoint receives a string
and produces a binary stream, just declare it as is:
...
func (e *endpoint) Hello(name string) io.Reader {
return bytes.NewBufferString("Hello, " + name + "!")
}
...
Cool, huh? 🤠 Of course, you can still directly use http.ResponseWriter
and *http.Request
, if you like.
Basic concepts
The main entity in goioc/web
is the Endpoint, which is represented by the interface of the same name. Here's the example implementation:
type endpoint struct {
}
func (e endpoint) HandlerFuncName() string {
return "Hello"
}
func (e *endpoint) Hello(name string) io.Reader {
return bytes.NewBufferString("Hello, " + name + "!")
}
Endpoint
interface has one method that returns the name of the method that will be used as an endpoint.
In order for goioc/web
to pick up this endpoint, it should be registered in the DI Container:
_, _ = di.RegisterBean("endpoint", reflect.TypeOf((*endpoint)(nil)))
Then the container should be initialized (please, refer to the goioc/di documentation for more details):
_ = di.InitializeContainer()
Finally, the web-server can be started, either using the built-in function:
_ = web.ListenAndServe(":8080")
... or using returned Router
router, _ := web.CreateRouter()
_ = http.ListenAndServe(":8080", router)
Routing
So, how does the framework know where to bind this endpoint to?
For the routing functionality goioc/web
leverages gorilla/mux library.
Don't worry: you don't have to cope with this library directly: goioc/web
provides a set of convenient wrappers around it.
The wrappers are implemented as tags in the endpoint-structure. Let's slightly update our previous example:
...
type endpoint struct {
method interface{} `web.methods:"GET"`
path interface{} `web.path:"/hello"`
}
...
Now our endpoint is bound to a GET
requests at the /hello
path. Yes, it's that simple! 🙂
Tag |
Value |
Example |
web.methods |
List of HTTP-methods. |
web.methods:"POST,PATCH" |
web.path |
URL sub-path. Can contain path variables. |
web.path:"/articles/{category}/{id:[0-9]+}" |
web.queries |
Key-value paris of the URL query part. |
web.queries:"foo,bar,id,{id:[0-9]+}" |
web.headers |
Key-value paris of the request headers. |
web.headers:"Content-Type,application/octet-stream" |
web.matcher |
ID of the bean of type *mux.MatcherFunc . |
web.matcher:"matcher" |
In and Out types
As was mentioned above, with goioc/web
you get a lot of freedom in terms of defining the signature of your endpoint's method.
Just look at these examples:
...
func (e *endpoint) Error() (int, string) {
return 505, "Something bad happened :("
}
...
...
func (e *endpoint) KeyValue(ctx context.Context) string {
return ctx.Value(di.BeanKey("key")).(string)
}
...
...
func (e *endpoint) Hello(pathParams map[string]string) (http.Header, int) {
return map[string][]string{
"Content-Type": {"application/octet-stream"},
}, []byte("Hello, " + pathParams["name"] + "!")
}
...
Supported argument types
http.ResponseWriter
*http.Request
context.Context
http.Header
io.Reader
io.ReadCloser
[]byte
string
map[string]string
url.Values
struct
implementing encoding.BinaryUnmarshaler
or encoding.TextUnmarshaler
interface{}
(GoiocSerializer
bean is used to deserialize such arguments)
Supported return types
http.Header
(response headers, must be first return argument, if used)
int
(status code, must be first argument after response headers, if used)
io.Reader
io.ReadCloser
[]byte
string
struct
implementing encoding.BinaryMarshaler
or encoding.TextMarshaler
interface{}
(GoiocSerializer
bean is used to serialize such returned object)
Templates
goioc/web
supports templates!
todo.html
<h1>{{.PageTitle}}</h1>
<ul>
{{range .Todos}}
{{if .Done}}
<li class="done">{{.Title}}</li>
{{else}}
<li>{{.Title}}</li>
{{end}}
{{end}}
</ul>
endpoint.go
type todo struct {
Title string
Done bool
}
type todoPageData struct {
PageTitle string
Todos []todo
}
type todoEndpoint struct {
method interface{} `web.methods:"GET"`
path interface{} `web.path:"/todo"`
}
func (e todoEndpoint) HandlerFuncName() string {
return "TodoList"
}
func (e *todoEndpoint) TodoList() (template.Template, interface{}) {
tmpl := template.Must(template.ParseFiles("todo.html"))
return *tmpl, todoPageData{
PageTitle: "My TODO list",
Todos: []todo{
{Title: "Task 1", Done: false},
{Title: "Task 2", Done: true},
{Title: "Task 3", Done: true},
},
}
}
Note that in case of using templates, the next returned object after template.Template
must be the actual structure that will be used to fill in the template 💡
Custom matchers
If functionality of web.methods
, web.path
, web.queries
and web.headers
is not enough for you, you can use custom matcher,
based on Gorilla's mux.MatcherFunc
:
...
_, _ = di.RegisterBeanFactory("matcher", di.Singleton, func(context.Context) (interface{}, error) {
matcherFunc := mux.MatcherFunc(func(request *http.Request, match *mux.RouteMatch) bool {
return strings.HasSuffix(request.URL.Path, "bar")
})
return &matcherFunc, nil
})
...
type endpoint struct {
method interface{} `web.methods:"GET"`
path interface{} `web.path:"/endpoint/{key}/{*?}"`
matcher interface{} `web.matcher:"matcher"`
}
func (e endpoint) HandlerFuncName() string {
return "Match"
}
func (e *endpoint) Match() string {
return "It's a match! :)"
}
...
$ curl localhost:8080/endpoint/foo/bar
It's a match! :)
Middleware
Of course, custom middleware is also supported by the framework:
web.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), di.BeanKey("key"), "value")))
})
})