- 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 morefunc(w http.ResponseWriter, r *http.Request)
handlers, if your endpoint receives astring
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.
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)
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" |
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"] + "!")
}
...
http.ResponseWriter
*http.Request
context.Context
http.Header
io.Reader
io.ReadCloser
[]byte
string
map[string]string
url.Values
struct
implementingencoding.BinaryUnmarshaler
orencoding.TextUnmarshaler
interface{}
(GoiocSerializer
bean is used to deserialize such arguments)
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
implementingencoding.BinaryMarshaler
orencoding.TextMarshaler
interface{}
(GoiocSerializer
bean is used to serialize such returned object)
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 💡
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! :)
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")))
})
})