Go style guidelines
This guide describes how to structure Go projects within our monorepo and outlines some best practices for writing Go code.
It is not meant to be comprehensive and should rather supplement established patterns and best practices. A good reference for the latter is Google's Go style guide ⧉. We try to explicitly document instances in which we intentionally break with this style guide.
The rules that gate a merge are in language rules: the task
set, what lint must pass, the daedalus dependency. This page is the layout and naming around
them.
Project structure
To simplify buliding, testing etc. all Go projects in the monorepo should follow a similar layout. A simple example could look as follows:
my-project
|- mise.toml
|- go.mod
|- go.sum
|- cmd
| |- server
| | `- main.go
| `- cli
| `- main.go
|- foo
| `- foo.go
|- internal
| `- bar
| |- doc.go
| |- something.go
| |- something_test.go
| |- some_other_thing.go
| `- some_other_thing_test.go
`- testsintegration
|- test_main_test.go
`- server_test.go
Aspects of this are explained in the following subsections.
mise.toml
Task definitions are shared by every Go project, so yours includes them rather than defining anything of its own:
[task_config]
includes = ["{{vars.repo_root}}/mise/includes/go.toml"]
Add mise/includes/go-service-image.toml to that list if the project produces a container image,
plus an [env] block declaring its identity (DOCKER_PROJECT_DIR, DOCKER_IMAGE_NAME,
DOCKER_BINARY). Then list the directory in [monorepo].config_roots in the root mise.toml, or
nothing will see the project at all.
You may add a task the shared file does not define. You may not redefine one it does: the include wins silently, and your body never runs. See Mise tasks for what the includes give you, and BUILD-4.
go.mod
The "tidyness" of go.mod should be verified automatically, other than that:
-
Include
lyceum.technologyin your module name, e.g.lyceum.technology/my-project(also note that we use hyphens, not underscores). -
Our shared
daedaluslibrary (daedalus_go/⧉) covers AMQP, HTTP serving, SQL, Kubernetes, GCP, metrics, logging, encryption and notifications. Reach for it before writing your own authentication, messaging, encryption or observability primitives. Divergence there is expensive to undo. To use it (adjust the relative path to the correct location):
require (
...
lyceum.technology/daedalus v0.0.0
)
replace lyceum.techology/daedalus => ../daedalus_go
Binaries, public and internal packages
- Place binaries to build under
cmd. The shared tasks build intobuild/, which is already ignored, so no extra.gitignorepatterns are needed. - Create a directory per package that you want to make available to other projects (more likely than not you will have none of these)
- Place all other packages under
internal. - Package names should contain neither hyphens nor underscores and directory names and packages names should match exactly
- Place package doc comments in a
doc.gofile. An exception being packages that contain only a single file. In this case it's okay to give the file the same name as the package directory and omitdoc.go, e.g.foo/foo.goabove.
Integration tests
Place all integration tests (any that require external components such as test containers) in a testsintegration (and
thus a testsintegration package).
These must not run when running go test with -short. To achieve this you can use the following minimal
test_main_test.go template:
package testsintegration
import (
"flag"
"log"
"os"
"testing"
)
func TestMain(m *testing.M) {
flag.Parse()
if testing.Short() {
log.Println("Skipping integration tests in short mode")
os.Exit(0)
}
os.Exit(m.Run())
}
Naming conventions
Aside from following typical Go conventions please note the following when naming things (variables, functions etc.) in your code:
- Name boolean-returning functions so that they naturally read as predicates. Prefer prefixes such as
Is,HasorCan. For example:IsActive(),HasChild(),CanRetry(), ...