This document describes the coding conventions used in this project.
- 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
- Write compact maps and arrays if they have only one or two members
- Check
.goreleaser.ymlfor a canonical example
- 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