This document describes the my preferred coding conventions.
- Put a newline after each heading
- Write headings in Title Case
- Use this document itself as a style guide for Markdown
- Do not add newlines unless absolutely needed
- Use anchors for value re-use
- Start single-document YAML files with
--- - Do not wrap string values in quotes unless required
- Write compact maps and arrays if they have only one or two members
- 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.Contextas first parameter - Repository holds
*sql.DBreference - 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
- 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)
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.gofile 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)
- Use raw SQL with placeholders
- Use
ExecContextfor INSERT/UPDATE/DELETE - Use
QueryContextfor 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,
)- Single Responsibility: Every file, type, and function should do one thing.
- Short Functions: Keep functions under 30 lines when possible.
- Descriptive Names: Use meaningful file, type, and function names (follow Google Go standards).
- 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
slogwith 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
- DRY: Avoid duplication—use helpers or utility packages for repeated logic.
- Readability: Prefer clarity over cleverness. Add comments for complex logic.
- Scalability: Organize code into modules and packages so new features can be added without major refactoring.
- 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.
- Repository methods:
Create*,Get*,Update*,Delete* - Constructors:
New* - Predicates:
Is*,Has* - Getters: don't use
Getprefix
- End with
-ersuffix when possible (e.g.,Mailer) - Keep interfaces small and focused
- 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 {- 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 %
}- Limit to explaining why this are like that.
- Do not explain what's already written down as code.
- 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
}- Prefer standard library where possible
- Use established libraries for specific needs:
- Ginkgo/Gomega for testing
- database/sql for database access
- No ORM - use raw SQL
- Propagate Errors: Always return errors to a single handling point, never handle or print errors directly in business logic.
- Error Wrapping: Use Go’s error wrapping (
fmt.Errorf("context: %w", err)) for stack traces. - No Silent Failures: Always check and return errors, never ignore them.
- Prefer separating the assignment of errors from checking them
- There should be a empty line between assigning and checking errors
err := doSomething()
if err != nil {
return fmt.Errorf("something went wrong: %w", err)
}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 {
// Do what ever is to be done
res, err := r.db.ExecContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("executing insert: %w", err)
}
// On success, return no 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
BeforeEachandJustBeforeEachfor setup - Use
AfterEachfor 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
Itstatements.
- Arrange code goes in
- A
Contextdescribes a certain "state of the world" and should have its own variables as the encapsulation of that state. - Only one
Expectshould be in anIt
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
-
Always add a space after
{{and before}}and all other actions including{{ range }}and{{ end }}; but not for{{/* a comments */}}and{{- trimmed white spacw -}}.Do NOT do this:
{{if .ActiveTrip}} {{template "active-trip.html" .}} {{else}} {{template "start-view.html" .}} {{end}}Instead, do this:
{{ if .ActiveTrip }} {{ template "active-trip.html" . }} {{ else }} {{ template "start-view.html" . }} {{ end }}
-
Apply the conventions listed in the YAML section of this document
-
Write YAML without newlines between list members:
Avoid this style:
- name: something is available foo: some - name: something else is restarted bar: other
Prefer this style:
- name: something is available foo: some - name: something else is restarted bar: other
-
Write task names in the present tense, so that they describe the state of the system after the task is run.
Avoid this style:
- name: Create something foo: some
Prefer this style:
- name: something is available foo: some