“Thing they said.”
—Person who said it
I’ve been writing Go web services for over seven years now, and in that time, I’ve developed a few patterns. But I’m realising I’ve never written them down anywhere, so I figured it’s time to change that. I’m not going to argue this is the best or only way to write web services in Go, nor that it will be suited for everyone, but it’s how I like to do it, and it works pretty well for me.
My strategy is optimised for testability, dependency management, working with the type system, adherence to the standard library, and clarity of code. I’ve found these to be good properties to optimise for.
High Level View
Most my web services end up looking something like this:
app
|-- app.go
|-- apiv1
| |-- app.go
| |-- endpoints.go
| |-- handlers.go
| |-- server.go
|-- cmd
| |-- appd
| | |-- main.go
| |-- appctl
| | |-- main.go
|-- migrations
| |-- generated.go
|-- sql
| |-- YYYYMMDD_ORDER_DESCRIPTOR.sql
|-- storers
| |-- storer.go
| |-- storers_test.go
| |-- memory
| | |-- memory.go
| |-- postgres
| | |-- postgres.go
| | |-- postgres_app.go
| | |-- postgres_sql.go
|-- webv1
| |-- app.go
| |-- endpoints.go
| |-- handlers.go
| |-- server.go
| |-- assets
| | |-- generated.go
| |-- templates
| | |-- index.go.html
| | |-- header.go.html
This assumes we’re working on an app called app. I usually give my apps a noun
name, usually the name of the main resource they manage. For example, I may have
a posts service, or an accounts service.
That first app.go contains the core types that define that resource, and the
helpers to interact with those types, and that’s about it. The apiv1 package
contains the v1 version of the API. We could also have an apiv2, apiv3,
etc. package, all living side by side. The cmd directory doesn’t get any code
in it, it just contains packages. Each binary associated with this service gets
a package named for the binary in this directory. The migrations directory
contains SQL that has been converted into Go code, no hand-written code ends up
in that directory. The sql directory contains a bunch of SQL files to run as
database migrations. The storers directory contains a storer.go file that
defines what a Storer is, and a storers_test.go file that runs tests for all
the storers that are supported, making sure they conform to that definition. It
also contains a package for each storer supported, with the implementation of
that storer living entirely in that package. Finally, a webv1 package contains
any HTML-rendering code, and like the apiv1 package, we can have multiple
versions of this side-by-side. It also contains a templates directory, filled
with .go.html files, and an assets package, containing only generated code.
Let’s talk about each of those things in more depth, covering the decisions made in them and why those decisions were made.