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.ClassicCore concepts - 图1open in new window, and following middleware are registered automatically:

TIP

If you look up the source code of the flamego.ClassicCore concepts - 图2open in new window, it is fairly simple:

  1. func Classic() *Flame {
  2. f := New()
  3. f.Use(
  4. Logger(),
  5. Recovery(),
  6. Static(
  7. StaticOptions{
  8. Directory: "public",
  9. },
  10. ),
  11. )
  12. return f
  13. }

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.NewCore concepts - 图3open in new window is used to create bare Flame instances that do not have default middleware registered, and any type that contains the flamego.FlameCore concepts - 图4open in new window 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:

  1. func main() {
  2. f1 := flamego.Classic()
  3. f2 := flamego.New()
  4. f2.Use(flamego.Recovery())
  5. ...
  6. }

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.HanderCore concepts - 图5open in new window, and if you look closer, it is just an empty interface (interface{}):

  1. // Handler is any callable function. Flamego attempts to inject services into
  2. // the Handler's argument list and panics if any argument could not be fulfilled
  3. // via dependency injection.
  4. 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:

  • Code
  • Test
  1. package main
  2. import (
  3. "github.com/flamego/flamego"
  4. )
  5. func main() {
  6. f := flamego.New()
  7. f.Get("/anonymous", func() string {
  8. return "Respond from an anonymous function"
  9. })
  10. f.Get("/declared", declared)
  11. t := &customType{}
  12. f.Get("/method", t.handler)
  13. f.Run()
  14. }
  15. func declared() string {
  16. return "Respond from a declared function"
  17. }
  18. type customType struct{}
  19. func (t *customType) handler() string {
  20. return "Respond from a method of a type"
  21. }
  1. $ curl http://localhost:2830/anonymous
  2. Respond from an anonymous function
  3. $ curl http://localhost:2830/declared
  4. Respond from a declared function
  5. $ curl http://localhost:2830/method
  6. Respond from a method of a type

Return values

Generally, your web application needs to write content directly to the http.ResponseWriterCore concepts - 图6open in new window (which you can retrieve using ResponseWriter method of flamego.ContextCore concepts - 图7open in new window). In some web frameworks, they offer returning an extra error as the indication of the server error as follows:

  1. 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:

  • Code
  • Test
  1. package main
  2. import (
  3. "errors"
  4. "github.com/flamego/flamego"
  5. )
  6. func main() {
  7. f := flamego.New()
  8. f.Get("/string", func() string {
  9. return "Return a string"
  10. })
  11. f.Get("/bytes", func() []byte {
  12. return []byte("Return some bytes")
  13. })
  14. f.Get("/error", func() error {
  15. return errors.New("Return an error")
  16. })
  17. f.Run()
  18. }
  1. $ curl -i http://localhost:2830/string
  2. HTTP/1.1 200 OK
  3. ...
  4. Return a string
  5. $ curl -i http://localhost:2830/bytes
  6. HTTP/1.1 200 OK
  7. ...
  8. Return some bytes
  9. $ curl -i http://localhost:2830/error
  10. HTTP/1.1 500 Internal Server Error
  11. ...
  12. Return an error
  13. ...

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!

  • Code
  • Test
  1. package main
  2. import (
  3. "errors"
  4. "net/http"
  5. "github.com/flamego/flamego"
  6. )
  7. func main() {
  8. f := flamego.New()
  9. f.Get("/string", func() (int, string) {
  10. return http.StatusOK, "Return a string"
  11. })
  12. f.Get("/bytes", func() (int, []byte) {
  13. return http.StatusOK, []byte("Return some bytes")
  14. })
  15. f.Get("/error", func() (int, error) {
  16. return http.StatusForbidden, errors.New("Return an error")
  17. })
  18. f.Run()
  19. }
  1. $ curl -i http://localhost:2830/string
  2. HTTP/1.1 200 OK
  3. ...
  4. Return a string
  5. $ curl -i http://localhost:2830/bytes
  6. HTTP/1.1 200 OK
  7. ...
  8. Return some bytes
  9. $ curl -i http://localhost:2830/error
  10. HTTP/1.1 403 Forbidden
  11. ...
  12. Return an error
  13. ...

Return body with potential error

Body or error? Not a problem!

  • Code
  • Test
  1. package main
  2. import (
  3. "errors"
  4. "net/http"
  5. "github.com/flamego/flamego"
  6. )
  7. func main() {
  8. f := flamego.New()
  9. f.Get("/string", func() (string, error) {
  10. return "Return a string", nil
  11. })
  12. f.Get("/bytes", func() ([]byte, error) {
  13. return []byte("Return some bytes"), nil
  14. })
  15. f.Run()
  16. }
  1. $ curl -i http://localhost:2830/string
  2. HTTP/1.1 200 OK
  3. ...
  4. Return a string
  5. $ curl -i http://localhost:2830/bytes
  6. HTTP/1.1 200 OK
  7. ...
  8. Return some bytes

If the handler returns a non-nil error, the error message will be responded to the client instead.

How cool is that?

Service injection

Flamego is claimed to be boiled with dependency injectionCore concepts - 图9open in new window because of the service injection, it is the soul of the framework. The Flame instance uses the inject.InjectorCore concepts - 图10open in new window 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:

  1. // Both `http.ResponseWriter` and `*http.Request` are injected,
  2. // so they can be used as handler arguments.
  3. f.Get("/", func(w http.ResponseWriter, r *http.Request) { ... })
  4. // The `flamego.Context` is probably the most frequently used
  5. // service in your web applications.
  6. f.Get("/", func(c flamego.Context) { ... })

What happens if you try to use a service that hasn’t been injected?

  • Code
  • Test
  1. package main
  2. import (
  3. "github.com/flamego/flamego"
  4. )
  5. type myService struct{}
  6. func main() {
  7. f := flamego.New()
  8. f.Get("/", func(s myService) {})
  9. f.Run()
  10. }
  1. http: panic serving 127.0.0.1:50061: unable to invoke the 0th handler [func(main.myService)]: value not found for type main.myService
  2. ...

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.LoggerCore concepts - 图11open in new window, flamego.ContextCore concepts - 图12open in new window, http.ResponseWriterCore concepts - 图13open in new window and *http.RequestCore concepts - 图14open in new window.

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 contextCore concepts - 图15open in new window or intercepting the requestCore concepts - 图16open in new window, 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.

  1. // Global middleware that are invoked before all other handlers.
  2. f.Use(middleware1, middleware2, middleware3)
  3. // Group middleware that are scoped down to a group of routes.
  4. f.Group("/",
  5. func() {
  6. f.Get("/hello", func() { ... })
  7. },
  8. middleware4, middleware5, middleware6,
  9. )
  10. // Route-level middleware that are scoped down to a single route.
  11. 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 EnvTypeCore concepts - 图17open in new window 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.EnvTypeDevCore concepts - 图18open in new window, but caches the template files otherwise.

The Flamego environment is typically configured via the environment variable FLAMEGO_ENV:

  1. export FLAMEGO_ENV=development
  2. export FLAMEGO_ENV=production
  3. export FLAMEGO_ENV=test

In case you want to retrieve or alter the environment in your web application, EnvCore concepts - 图19open in new window and SetEnvCore concepts - 图20open in new window methods are also available, and both of them are safe to be used concurrently.