Go style guide
This style guide provides conventions for working on Go code within GDS. It is owned and maintained by the GOV.UK Platform Engineering team, who are the primary users of Golang at GDS.
Go is an actively used language at GDS for Platform Engineering work, including Kubernetes operators and controllers, CLI tools, APIs, and Crossplane composition functions. A large proportion of the CNCF (Cloud Native Computing Foundation) tooling the team relies on — including Kubernetes itself — is written in Go, making it a natural and productive fit for this kind of work.
There are already good resources on writing Go code, which are worth reading first:
GDS Projects using Go
These projects are currently using Go:
Code formatting
Use gofmt to automatically format your code. All Go code must be gofmt-formatted. Most editors can be configured to run gofmt on save.
You should also use goimports, which runs gofmt and additionally manages import grouping and ordering.
Linting
Use golangci-lint as your primary linting tool. It is a fast, configurable meta-linter that runs many linters in parallel and is widely adopted across the CNCF ecosystem.
A .golangci.yml configuration file in the root of your repository controls which linters are enabled. Consult the golangci-lint documentation for configuration options.
Recommended linters include:
staticcheck— correctness, performance, and simplicity checkserrcheck— ensures errors are not silently discardedgosec— security-focussed static analysisrevive— general style checks (a maintained replacement for the now-unmaintainedgolint)
Code checking
go vet checks for correctness and should be used as part of your build process. It is typically included in a golangci-lint run.
If you are writing concurrent code, use the race detector to detect race conditions:
go test -race ./...
External dependencies
Use Go modules (go mod) for all dependency management. This has been the official, default approach since Go 1.16.
Older tools such as dep, godep, and glide are no longer in development and must not be used in new projects.
Vendoring
Vendoring is not generally necessary when using Go modules. The Go toolchain downloads and authenticates modules via the Go module mirror and the Go checksum database.
You may still choose to vendor when:
- your build environment has restricted internet access
- you want fully self-contained, reproducible builds without relying on external mirrors
Use go mod vendor if you choose to vendor.
Kubernetes and CNCF development
GOV.UK Platform Engineering uses Go extensively for Kubernetes-related work, as Go is the primary language of the Kubernetes ecosystem and the wider CNCF landscape.
CLI tools
Use cobra for building CLI tools. It is the de facto standard for Go CLIs in the CNCF ecosystem, used by kubectl, helm, kind, flux, and many others. Pair it with viper if you need configuration file support alongside flags and environment variables.
For smaller, simpler CLIs where subcommands are not needed, urfave/cli is a viable alternative.
Web frameworks
Go’s standard library is modern and capable. For simple HTTP routing and handling, the net/http package will likely meet your needs.
For more complex routing requirements such as path parameters or structured middleware, consider:
go-chi/chi— lightweight, idiomatic router that is 100% compatible withnet/httpgin-gonic/gin— performant framework with a larger feature set
Prefer a router or framework that integrates well with the standard net/http interface, so that standard library middleware and tooling remain compatible.
Go is not routinely used at GDS for building fully-fledged, HTML-rendering web applications. If you find yourself in that position, consider whether one of the other GDS supported backend languages might be more appropriate.
Channels
Signalling
Channels that are being used purely for signalling should use an empty struct rather than boolean or int types.
Using an empty struct declares that we’re not interested in the value sent or received; only in its closed property.
See this talk
func worker(quit <-chan struct{}, result chan<- int) {
for {
select {
case result <- rand.Intn(10000000):
case <-quit:
return
}
}
}
func main() {
quit, result := make(chan struct{}), make(chan int)
for i := 0; i < 100; i++ {
go worker(quit, result)
}
// Wait for a worker to return a good result
for {
if <-result > 9999998 {
break
}
}
close(quit) // terminate all the workers
fmt.Println("All done!")
}
Testing
Use Ginkgo with Gomega as the preferred testing framework for Go code at GDS. Ginkgo provides a BDD-style structure that makes test intent clear and readable, and is widely used across the Kubernetes and CNCF ecosystem.
Ginkgo and Gomega
Ginkgo structures tests using Describe, Context, and It blocks, making it easy to express behaviour and organise related scenarios:
var _ = Describe("Something", func() {
Context("when a condition is true", func() {
It("should do a thing", func() {
Expect("hi").To(HaveLen(2))
})
})
})
Gomega provides the matcher library and makes assertions read naturally:
err := doAThing()
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal("expected value"))
Run a Ginkgo test suite with:
ginkgo ./...
Tests can be focused to speed up development cycles:
ginkgo --focus "a thing" ./...
Standard library testing
For simpler tests, or where a BDD style is not appropriate, the standard library testing package is a safe fallback. Prefer table-driven tests to reduce boilerplate:
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive numbers", 1, 2, 3},
{"zero values", 0, 0, 0},
{"negative numbers", -1, -2, -3},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := Add(tc.a, tc.b); got != tc.want {
t.Errorf("Add(%d, %d) = %d, want %d", tc.a, tc.b, got, tc.want)
}
})
}
}
testify can complement the standard library with cleaner assertions and mocking:
testify/assert— non-fatal assertions (test continues on failure)testify/require— fatal assertions (test stops immediately on failure)testify/mock— mock objects
Configuration parsing
For CLI tools, use cobra for command-line arguments and subcommands. Pair it with spf13/viper when you also need to read from configuration files or environment variables.
For applications without a CLI entry-point, spf13/viper supports YAML, JSON, TOML, and environment variable binding on its own.
These tools make it easier to accept configuration parameters and help to make your application self-documenting.