(my) Go HTTP Server Best Practice

Tobias K
Tobias K
May 10, 2018 · 7 min read

The first thing most people stumble upon when they want to write an Http based Server/Application in Go is “you can do everything with the standard lib”. I would agree that the standard lib gives very good conventions and interfaces, and it’s probably better than in most other languages, but it still leaves a lot of room on how to actually use it.

About me

The reason I’m writing this post is because I got inspired by the blog post about How I write Go HTTP services after seven years. I noticed that I changed my own way of how I built web applications in GoLang during the last years regularly and still try to improve it. Writing this post helps to reflect about the way I write web applications and I also hope to start some discussions to further improve my own “best practices”. During all the iterations I tried and analysed a lot of different frameworks, while always trying to keep everything as minimal as possible. Finally I got opinionated on some of them. I’m using e.g. logrus for logging and chi for routing but they are very replaceable.

Package structure

Startup & Lifecycle

type Instance struct {
db *badger.DB

httpServer *http.Server
}
func NewInstance() *Instance {
s := &Instance{
// just in case you need some setup here
}

return s
}
func (s *Instance) Start() { // Startup all dependencies
// I usually panic if any essential like the DB fails
// e.g. due to wrong configurations
s.db = MustOpenDb(dataDir)
defer s.closeDb()
// Startup the http Server in a way that
// we can gracefully shut it down again
s.httpServer = &http.Server{Addr: addr, Handler: endpoints.Router}
err = s.httpServer.ListenAndServe() // Blocks!
if err != http.ErrServerClosed {
logrus.WithError(err).Error("Http Server stopped unexpected")
s.Shutdown()
} else {
logrus.WithError(err).Info("Http Server stopped")
}
}
func (s *Instance) Shutdown() {
if s.httpServer != nil {
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
err := s.httpServer.Shutdown(ctx)
if err != nil {
logrus.WithError(err).Error("Failed to shutdown http server gracefully")
} else {
s.httpServer = nil
}
}
}

By using defer to close dependencies like the database, we make sure they are closed when the Start() function returns. Start() will block as long as the server is running. You can still call Shutdown() from outside, e.g. in a signal.Notify callback to gracefully stop the server.

Dependency Management & Injection

After some back and force I noticed that having a reference to the server.Instance in dependencies like my endpoints package leads to problems during startup and testing. That’s why I inject the dependencies manually where needed “down the tree”. That also helps when it comes to testing, since you can just inject mocks or fakes. Most of the time a simple global variables is enough, then just callendpoints.Db = s.Db during Instance.Start() . Since the dependencies are only used in handlers after the server is started, it’s no problem to have them in nil state initially. If a package wide variable is not enough, you can always opt in for public Set...(), Setup...() or New...() functions.

As with all dependency injection patterns: A wrong setup e.g. missing to call Setup() will lead to run-time errors.

Router


var
Router = chi.NewRouter()
func SetupRoutes() {
Router.Group(func(r chi.Router) {
r.Use(apphandler.WithTypedContext)
r.Use(apphandler.WithSession)
r.Use(apphandler.WithTokenStore(TokenStore))
r.Use(WithUser)

InitIndex(r)
InitAuth(r)
InitDashboard(r)
InitPlugins(r)
InitUser(r)
InitApi(r)
r.Mount("/app/wiki", wiki.Router()")
})
}

I have a lot of files and packages like endpoints/user.go or wiki/routes.goto split up the handlers as needed.

I don’t want to go into too much detail about chi.Router but the main reason I’m using it is the way the middlewares are handled and that is stays pretty close to the standard lib.
With r.Group you get an empty list of middlewares for a new set of routes. r.Use() allows to add a middlewares in order and r.With() allows to add a middleware for a specific handler. Plus the r.Mount() function is nice to just add existing routers from other packages, compatible with the Go http package.

Handler dependencies & Context

Currently I have a separate package apphandler that contains all the code to setup middlewares and a context used by most endpoints. Depending on the middlewares used for a handler, some context fields might be empty and throw during runtime if called. If needed you can always mimic this setup in some other part of the application where a different kind of context is more appropriate.

// apphandler/context.gotype key int

const (
userInfoKey key = iota
sessionKey
// ...
)
type Context struct {
context.Context
}
func NewContext(ctx context.Context) Context {
return Context{ctx}
}
func GetContext(r *http.Request) Context {
if ctx, ok := r.Context().(Context); !ok {
log.Panic("Failed to get custom Context from request")
return ctx
} else {
return ctx
}
}
func (ctx Context) WithUserInfo(userInfo UserInfo) Context {
return Context{context.WithValue(ctx, userInfoKey, userInfo)}
}
func (ctx Context) UserInfo() UserInfo {
val, ok := ctx.Value(userInfoKey).(UserInfo)
if !ok {
log.Panic("Failed to get UserInfo from context", val)
}
return val
}
// apphandler/middlewares.gofunc WithTypedContext(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {

ctx := NewContext(r.Context())
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func WithUser(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := apphandler.GetContext(r)
// Get everything you need, it's okay to use other depdendencies from ctx e.g. to access the session or database

userInfo := apphandler.UserInfo{
SessionId: sessionId,
UserId: userId,
}
ctx = ctx.WithUserInfo(userInfo)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}

One drawback here is that we get runtime dependencies between middlewares. For example when the WithUser middleware needs the database, we must run the WithDatabase middleware first or we get a runtime panic. This should be covered in unit or integration tests.

If you want to handle the runtime panic more gracefully you can opt in for returning the error. I decided against this, because these kind of errors are non recoverable anyway and can only be fixed by changing code.

The biggest advantage about dependencies inside the context is, that we can move the middlewares and handlers into any other package without the need to inject alle the dependencies again.

For a small application and for some dependencies it might be enough to just have a SetDatabase() Function in your package to inject the dependency, and I would definitely start this way, but as soon as you get more packages with handlers that operate on the database, you don’t want to inject the database into all of these packages. Even worse when it’s not just the database but n other dependencies like OAuth2 Tokens, User Information, Caches, Sessions, Cookies, etc. If you want to opt out of some of the dependencies you can still assemble your own set of dependencies via middlewares inside router.Group(...) .

Handler implementation

To stop repeating myself I have build my own webapp package that is shared between all my applications. I might open source it one day, but not yet — sorry. For non web related helpers I continuously extend my already open source go-util package. It has a lot of nice helpers. I would not recommend to just use it, but better fork and modify it or copy out single packages / files if you want. The packages are very opinionated in some places and you might prefer different ways to handle some of the things.

The webapp project has some subpackages for more or less generic implementations to conveniently handle things. Since I have no open source code I just give some examples what's inside:

  • page Render HTML pages based on Go templates
  • forms Helper to deal with HTML forms
  • request Pagination & sorting, json parsing from requests, parsing query parameters, etc.
  • response Useful constants like media types, simple json responses, different kind of redirects, etc.
  • rest Client to make rest requests, default payload structs for REST API’s
  • … and some more

When ever I have the feeling to repeat my self, I move some code here. This way I get faster every time. It’s important that methods stay simple.
Handling an error in a json api can look like:

if err != nil {
response.Json(w, http.StatusInternalServerError, rest.NewErrorResponse(err))
return
}

Rendering a complex template ends up in a simple handler like this:

func appPageHandler(w http.ResponseWriter, r *http.Request) {
page := applications.NewApplicationPage(r, appId)

pages.RenderPage(w, page)
}

Another way to keep handlers clean is to handle generic things like authentication in middlewares.

Conclusion

For any feedback or discussion, please tweet me.