Skip to content

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.technology in your module name, e.g. lyceum.technology/my-project (also note that we use hyphens, not underscores).

  • Our shared daedalus library (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 into build/, which is already ignored, so no extra .gitignore patterns 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.go file. 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 omit doc.go, e.g. foo/foo.go above.

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, Has or Can. For example: IsActive(), HasChild(), CanRetry(), ...