- Do not assume you know about the latest Go version. Always check the release history and the release notes of all releases since the version you thought was current, e.g. for Go 1.25 and 1.26.
- Use the latest stable Go version that's available
- Use newlines as a means of grouping important parts
- Prefer extracting small functions as a way to document the function
- There should be a empty line before a return statement, except the scope consists of nothing but the return statement.
conn, err := establishDatabaseConnection()
if err != nil {
return fmt.Errorf("could not establish database connection: %w", err)
}
err = ensureSchemaIsAtLatestVersion(conn)
if err != nil {
return fmt.Errorf("migrating the schema to the lastest version failed: %w", err)
}
return thing
- All database access through Repository structs
- Repository methods take
context.Context as first parameter
- Repository holds
*sql.DB reference
- Method naming:
Create*, Get*, Update*, Delete*
type Repository struct {
db *sql.DB
}
func NewRepository(db *sql.DB) *Repository {
return &Repository{db: db}
}
func (r *Repository) CreateAccount(ctx context.Context, account *caterbill.Account) error {
// Implementation
}
- Split error assignment and -handling into separate lines; never inline the assignment into the if-condition
- Always return error as last return value
- Use early returns for error cases
- No panics in library code
- Wrap errors with context when propagating:
fmt.Errorf("creating account: %w", err)
- Always check and return errors — no silent failures
- There should be an empty line between assigning and checking errors
func (r *Repository) CreateVenue(ctx context.Context, venue *caterbill.Venue) error {
res, err := r.db.ExecContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("executing insert: %w", err)
}
id, err := res.LastInsertId()
if err != nil {
return fmt.Errorf("getting last insert id: %w", err)
}
venue.ID = id
return nil
}
- Domain models in root package
- Separate packages for different concerns (billing, web, backend)
- Embed structs for composite types
- Group related types together
// Composite types using embedding
type GuestWithAccount struct {
caterbill.Guest
caterbill.Account
}
type AccountWithGuests struct {
caterbill.Account
Guests []GuestWithPreferredVenue
}
- Prefer to place types where they are used. Use a
models.go file for a package only if there are a lot of public models, and they would be scattered across many places otherwise.
- Create a constructor function called
New* (e.g., NewRepository, NewInvoiceBuilder) if the type is non-trivial.
- Return the new variable and an error type if the construction may fail
- Return pointer for structs with methods
- Initialize all required fields
func NewInvoiceBuilder(repository *Repository) *InvoiceBuilder {
return &InvoiceBuilder{
repository: repository,
clock: time.Now,
taxRate: 0.07,
}
}
- Validate dependencies as they are passed in the constructor
- Ensure that types can only be constructed as being valid (e.g. apply defaults)
- Whenever possible add required tools with
go get -tool, so that they become part of the go.mod
-
Prefer explicit over implicit; avoid magic
-
Use named return values only when they meaningfully document the return
-
Package-level variables for actions/constants should use var blocks, not const, when the type is a struct
-
Add a pre-commit hook that executes
- go fmt
- go vet
- go mod tidy
- go build
- golangci-lint
- go tool gosec ./...
- go tool govulncheck ./...
- go fix ./...
- go test
before the commit is accepted, so that we never check in any code with deficiencies.
For golangci-lint, assume that it is installed on the developer machine and do not use it via go tool.
- Single Responsibility: Every file, type, and function should do one thing.
- Short Functions: Keep functions under 30 lines when possible.
- Use goroutines and channels where suitable (for parallelism and asynchronous tasks).
- Avoid concurrency when it makes code less readable or more complex.
- Never log directly in modules; always call the logging package.
- Keep log messages meaningful and context-rich.
- Prefer
slog with JSON-formatted log lines
- For servers and daemons, configure logging to go to STDOUT
- For CLI programs, configure logging to go to STDERR and keep STDOUT for user messages
- Use camelCase for local variables
- Meaningful names, avoid single letters except for indexes
- Receiver names should be short (1-2 letters)
- The larger the scope of a variable, the longer and descriptive its name should be
- File, function, and variable names should be descriptive and follow Go's camelCase/snake_case conventions.
- No abbreviations except common ones (ctx, err, req, resp, cfg, etc.).
- Use singular names for files and types unless a plural is more semantically correct.
- Constructors:
New*
- Predicates:
Is*, Has*
- Getters: don't use
Get prefix
- End with
-er suffix when possible (e.g., Mailer)
Comments
Function Comments
- Start with the function name
- Use lowercase after function name
- Keep concise
// CreateAccount creates a new account in the database.
func (r *Repository) CreateAccount(ctx context.Context, account *caterbill.Account) error {
Struct Comments
- Document exported structs
- Document non-obvious fields
// InvoiceBuilder constructs invoices from consumption data.
type InvoiceBuilder struct {
repository *Repository
// clock holds the way to get the current date
clock func() time.Time
// taxRate is static so far. Store that with the product if that ever changes.
taxRate float64 // e.g. 0.07 for 7 %
}
General Use of Comments
- Limit to explaining why this are like that.
- Do not explain what's already written down as code.
- Prefer standard library where possible
As soon as a main program has a second point where it would exit, use the following structure:
package main
import (
"fmt"
"os"
)
func main() {
err := mainE(context.Background())
if err != nil {
fmt.Fprintf(os.Stderr, "Error %s\n", err)
os.Exit(1)
}
}
func mainE(ctx context.Context) error {
// ...
return nil
}
When a main program needs subcommands or more that two or three simple flags, switch to using github.com/spf13/cobra.
-
Use Ginkgo/Gomega for all tests
-
Test files should be in separate test packages (e.g., package billing_test)
-
Import Ginkgo/Gomega with dot imports:
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
- Use BDD-style blocks:
Describe, Context, It
- Use
BeforeEach and JustBeforeEach for setup
- Use
AfterEach for cleanup when needed
- Nest contexts logically to describe different scenarios
- Use the Arrange-Act-Assert (AAA) style of testing:
- Arrange code goes in
BeforeEach,
- Act belongs to
JustBeforeEach,
- Assert should be done in
It statements.
- A
Context describes a certain "state of the world" and should have its own variables as the encapsulation of that state.
- Only one
Expect should be in an It
var _ = Describe("Invoice", func() {
var (
invoice billing.Invoice
invoiceBuilder *billing.InvoiceBuilder
repository *backend.Repository
)
BeforeEach(func(ctx SpecContext) {
// Setup code
})
Context("with daily consumption", func() {
BeforeEach(func(ctx SpecContext) {
// More specific setup
})
It("calculates the correct total", func() {
Expect(invoice.TotalAmount).To(Equal("10,00 €"))
})
It("includes the consumption in line items", func() {
Expect(invoice.LineItems).To(HaveLen(1))
})
})
})
- Use
Expect(err).ToNot(HaveOccurred()) for error checking
- Use
Expect(x).To(Equal(y)) for equality
- Use
Expect(slice).To(HaveLen(n)) for length checks
- Prefer specific matchers over generic comparisons
- Prefer
go tool ginkgo
- Design tests to be executed in parallel (
go tool ginkgo -p)
- Label expensive tests with
integration or browser
- Use raw SQL with placeholders
- Use
ExecContext for INSERT/UPDATE/DELETE
- Use
QueryContext for SELECT
- Always use context-aware methods
res, err := r.db.ExecContext(ctx, `
INSERT INTO
venues (name, street, postal_code, city)
VALUES
(?, ?, ?, ?)
`,
venue.Name,
venue.Street,
venue.PostalCode,
venue.City,
)
- Parse form first, handle errors
- Validate input
- Use early returns for errors
- Set appropriate HTTP status codes
- Log errors before returning HTTP error codes
func (s *server) CreateAccount(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
if s.logger != nil {
s.logger.ErrorContext(r.Context(), "creating the account failed", "error", err)
}
http.Error(w, "unable to create account", http.StatusBadRequest)
return
}
// Validate and process input
// ...
// Render response
}