Core concepts
This page describes foundational concepts that are required to be proficient of using Flamego to build web applications that are most optimal.
Classic Flame
The classic Flame instance is the one that comes with a reasonable list of default middleware and could be your starting point for build web applications using Flamego.
A fresh classic Flame instance is returned every time you call flamego.Classic
, and following middleware are registered automatically:
flamego.Logger
for logging requested routes.flamego.Recovery
for recovering from panic.flamego.Static
for serving static files.
TIP
If you look up the source code of the flamego.Classic
, it is fairly simple:
func Classic() *Flame {
f := New()
f.Use(
Logger(),
Recovery(),
Static(
StaticOptions{
Directory: "public",
},
),
)
return f
}
Do keep in mind that flamego.Classic
may not always be what you want if you do not use these default middleware (e.g. for using custom implementations), or to use different config options, or even just want to change the order of middleware as sometimes the order matters (i.e. middleware are being invoked in the same order as they are registered).
Instances
The function flamego.New
is used to create bare Flame instances that do not have default middleware registered, and any type that contains the flamego.Flame
can be seen as a Flame instance.
Each Flame instace is independent of other Flame instances in the sense that instance state is not shared and is maintained separately by each of them. For example, you can have two Flame instances simultaneously and both of them can have different middleware, routes and handlers registered or defined:
func main() {
f1 := flamego.Classic()
f2 := flamego.New()
f2.Use(flamego.Recovery())
...
}
In the above example, f1
has some default middleware registered as a classic Flame instance, while f2
only has a single middleware flamego.Recovery
.
💬 Do you agree?
Storing states in the way that is polluting global namespace is such a bad practice that not only makes the code hard to maintain in the future, but also creates more tech debt with every single new line of the code.
It feels so elegent to have isolated state managed by each Flame instance, and make it possible to migrate existing web applications to use Flamego progressively.
Handlers
Flamego handlers are defined as flamego.Handler
, and if you look closer, it is just an empty interface (interface{}
):
// Handler is any callable function. Flamego attempts to inject services into
// the Handler's argument list and panics if any argument could not be fulfilled
// via dependency injection.
type Handler interface{}
As being noted in the docstring, any callable function is a valid flamego.Handler
, doesn't matter if it's an anonymous, a declared function or even a method of a type:
package main
import (
"github.com/flamego/flamego"
)
func main() {
f := flamego.New()
f.Get("/anonymous", func() string {
return "Respond from an anonymous function"
})
f.Get("/declared", declared)
t := &customType{}
f.Get("/method", t.handler)
f.Run()
}
func declared() string {
return "Respond from a declared function"
}
type customType struct{}
func (t *customType) handler() string {
return "Respond from a method of a type"
}
$ curl http://localhost:2830/anonymous
Respond from an anonymous function
$ curl http://localhost:2830/declared
Respond from a declared function
$ curl http://localhost:2830/method
Respond from a method of a type
Return values
Generally, your web application needs to write content directly to the http.ResponseWriter
(which you can retrieve using ResponseWriter
method of flamego.Context
). In some web frameworks, they offer returning an extra error
as the indication of the server error as follows:
func handler(w http.ResponseWriter, r *http.Request) error
However, you are still being limited to a designated list of return values from your handlers. In contrast, Flamego provides the flexibility of having different lists of return values from handlers based on your needs case by case, whether it's an error, a string, or just a status code.
Let's see some examples that you can use for your handlers:
package main
import (
"errors"
"github.com/flamego/flamego"
)
func main() {
f := flamego.New()
f.Get("/string", func() string {
return "Return a string"
})
f.Get("/bytes", func() []byte {
return []byte("Return some bytes")
})
f.Get("/error", func() error {
return errors.New("Return an error")
})
f.Run()
}
$ curl -i http://localhost:2830/string
HTTP/1.1 200 OK
...
Return a string
$ curl -i http://localhost:2830/bytes
HTTP/1.1 200 OK
...
Return some bytes
$ curl -i http://localhost:2830/error
HTTP/1.1 500 Internal Server Error
...
Return an error
...
As you can see, if an error is returned, the Flame instance automatically sets the HTTP status code to be 500.
TIP
Try returning nil
for the error on line 18, then redo the test request and see what changes.
Return with a status code
In the cases that you want to have complete control over the status code of your handlers, that is also possible!
package main
import (
"errors"
"net/http"
"github.com/flamego/flamego"
)
func main() {
f := flamego.New()
f.Get("/string", func() (int, string) {
return http.StatusOK, "Return a string"
})
f.Get("/bytes", func() (int, []byte) {
return http.StatusOK, []byte("Return some bytes")
})
f.Get("/error", func() (int, error) {
return http.StatusForbidden, errors.New("Return an error")
})
f.Run()
}
$ curl -i http://localhost:2830/string
HTTP/1.1 200 OK
...
Return a string
$ curl -i http://localhost:2830/bytes
HTTP/1.1 200 OK
...
Return some bytes
$ curl -i http://localhost:2830/error
HTTP/1.1 403 Forbidden
...
Return an error
...
Return body with potential error
Body or error? Not a problem!
package main
import (
"errors"
"net/http"
"github.com/flamego/flamego"
)
func main() {
f := flamego.New()
f.Get("/string", func() (string, error) {
return "Return a string", nil
})
f.Get("/bytes", func() ([]byte, error) {
return []byte("Return some bytes"), nil
})
f.Run()
}
$ curl -i http://localhost:2830/string
HTTP/1.1 200 OK
...
Return a string
$ curl -i http://localhost:2830/bytes
HTTP/1.1 200 OK
...
Return some bytes
If the handler returns a non-nil
error, the error message will be responded to the client instead.
Service injection
Flamego is claimed to be boiled with dependency injection because of the service injection, it is the soul of the framework. The Flame instance uses the inject.Injector
to manage injected services and resolves dependencies of a handler's argument list at the time of the handler invocation.
Both dependency injection and service injection are very abstract concepts, so it is much easier to explain with examples:
// Both `http.ResponseWriter` and `*http.Request` are injected,
// so they can be used as handler arguments.
f.Get("/", func(w http.ResponseWriter, r *http.Request) { ... })
// The `flamego.Context` is probably the most frequently used
// service in your web applications.
f.Get("/", func(c flamego.Context) { ... })
What happens if you try to use a service that hasn't been injected?
package main
import (
"github.com/flamego/flamego"
)
type myService struct{}
func main() {
f := flamego.New()
f.Get("/", func(s myService) {})
f.Run()
}
http: panic serving 127.0.0.1:50061: unable to invoke the 0th handler [func(main.myService)]: value not found for type main.myService
...
TIP
If you're interested in learning how exactly the service injection works in Flamego, the custom services has the best resources you would want.
Builtin services
There are services that are always injected thus available to every handler, including *log.Logger
, flamego.Context
, http.ResponseWriter
and *http.Request
.
Middleware
Middleware are the special kind of handlers that are designed as reusable components, and often accepting configurable options. There is no difference between middleware and handlers from compiler's point of view.
Technically speaking, you may use the term middleware and handlers interchangably but the common sense would be that middleware are providing some services, either by injecting to the context or intercepting the request, or both. On the other hand, handlers are mainly focusing on the business logic that is unique to your web application and the route that handlers are registered with.
Middleware can be used at anywhere that a flamego.Handler
is accepted, including at global, group and route level.
// Global middleware that are invoked before all other handlers.
f.Use(middleware1, middleware2, middleware3)
// Group middleware that are scoped down to a group of routes.
f.Group("/",
func() {
f.Get("/hello", func() { ... })
},
middleware4, middleware5, middleware6,
)
// Route-level middleware that are scoped down to a single route.
f.Get("/hi", middleware7, middleware8, middleware9, func() { ... })
Please be noted that middleware are always invoked first when a route is matched, i.e. even though that middleware on line 9 appear to be after the route handlers in the group (from line 6 to 8), they are being invoked first regardless.
💡 Did you know?
Global middleware are always invoked regardless whether a route is matched.
TIP
If you're interested in learning how to inject services for your middleware, the custom services has the best resources you would want.
Env
Flamego environment provides the ability to control behaviors of middleware and handlers based on the running environment of your web application. It is defined as the type EnvType
and has some pre-defined values, including flamego.EnvTypeDev
, flamego.EnvTypeProd
and flamego.EnvTypeTest
, which is for indicating development, production and testing environment respectively.
For example, the template middleware rebuilds template files for every request when in flamego.EnvTypeDev
, but caches the template files otherwise.
The Flamego environment is typically configured via the environment variable FLAMEGO_ENV
:
export FLAMEGO_ENV=development
export FLAMEGO_ENV=production
export FLAMEGO_ENV=test
In case you want to retrieve or alter the environment in your web application, Env
and SetEnv
methods are also available, and both of them are safe to be used concurrently.