Skip to content

Instantly share code, notes, and snippets.

@suhlig
Created March 2, 2026 21:21
Show Gist options
  • Select an option

  • Save suhlig/a96c5bfb22170c5b1a27a724e9621e02 to your computer and use it in GitHub Desktop.

Select an option

Save suhlig/a96c5bfb22170c5b1a27a724e9621e02 to your computer and use it in GitHub Desktop.
My Coding Conventions

This document describes the coding conventions used in this project.

Markdown Conventions

  • Put a newline after each heading
  • Write headings in Title Case
  • Use this document itself as a style guide for Markdown

YAML Conventions

  • 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.yml for a canonical example

Go Coding Conventions

Code Structure

General

  • 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

Repository Pattern

  • 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
}

Error Handling

  • 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
}

Type Organization

  • 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.

Constructor Functions

  • 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)

Database Access

SQL Queries

  • 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,
)

Modularity & Simplicity

  • 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).

Concurrency

  • Use goroutines and channels where suitable (for parallelism and asynchronous tasks).
  • Avoid concurrency when it makes code less readable or more complex.

Logging

  • 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

Code Quality

  • 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.

Naming Conventions

Variables

  • 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

Functions/Methods

  • 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 Get prefix

Interfaces

  • End with -er suffix when possible (e.g., Mailer)
  • Keep interfaces small and focused

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.

HTTP Handlers

Handler Methods

  • 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
}

Dependencies

  • 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

Error Handling

  • 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)
}

main File

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.

Testing

Framework

  • 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"

Test Structure

  • 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

Example Test Structure

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))
    })
  })
})

Assertions

  • 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment