Skip to content

Instantly share code, notes, and snippets.

@suhlig
Last active April 20, 2026 19:22
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 my preferred coding conventions.

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

Go Coding Conventions

Currency

  • 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

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

Templates

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

Ansible Conventions

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