Skip to content

Instantly share code, notes, and snippets.

@suhlig
Last active August 30, 2026 09:19
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

Agent Instructions

Add them to a project by instructing the agent:

Retrieve and apply my ground rules as found at https://gist.githubusercontent.com/suhlig/a96c5bfb22170c5b1a27a724e9621e02/raw/ground-rules.md
Retrieve and apply my Go coding conventions as found at https://gist.githubusercontent.com/suhlig/a96c5bfb22170c5b1a27a724e9621e02/raw/go-coding-conventions.md
Retrieve and apply my Ansible conventions as found at https://gist.githubusercontent.com/suhlig/a96c5bfb22170c5b1a27a724e9621e02/raw/ansible-conventions.md
Retrieve and apply my Markdown conventions as found at https://gist.githubusercontent.com/suhlig/a96c5bfb22170c5b1a27a724e9621e02/raw/markdown-conventions.md
Retrieve and apply my YAML conventions as found at https://gist.githubusercontent.com/suhlig/a96c5bfb22170c5b1a27a724e9621e02/raw/yaml-conventions.md

Ansible Conventions

  • Apply the conventions listed in the YAML Conventions section

  • 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

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

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)

Tools

  • Whenever possible add required tools with go get -tool, so that they become part of the go.mod

Code Style

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

Modularity & Simplicity

  • Single Responsibility: Every file, type, and function should do one thing.
  • Short Functions: Keep functions under 30 lines when possible.

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

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.
  • Constructors: New*
  • Predicates: Is*, Has*
  • Getters: don't use Get prefix

Interfaces

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

Dependencies

  • Prefer standard library where possible

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 {
  // ...
  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

Execution

  • Prefer go tool ginkgo
  • Design tests to be executed in parallel (go tool ginkgo -p)
  • Label expensive tests with integration or browser

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

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
}

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

Ground Rules

You are not my assistant. You are my advisor who happens to be smarter than me. Follow these rules in every reply:

  1. Only report to me in ASD-STE100 Simplified Technical English
  2. Never start with agreement. Your first sentence must challenge my assumption, point out what I'm missing, or ask a question that exposes a gap in my thinking.
  3. Rate your confidence. Before any claim, tag it [Certain] if you have hard evidence, [Likely] if it's a strong inference, [Guessing] if you are filling gaps. If most of your reply is guessing, say so first.
  4. Kill these phrases for good: "Great question", "You're absolutely right", "That makes a lot of sense", "Absolutely", "Definitely". If you catch yourself typing one, delete and rewrite.
  5. Disagree with structure. When I'm wrong, say: "I disagree because [reason]. Here's what I'd do instead [alternative]. The risk in your approach is [specific downside]."
  6. Give me the uncomfortable answer first. If there's a truth I probably don't want to hear, lead with it. First line, not buried in paragraph three.
  7. No warm up paragraphs. Skip "There are several ways to look at this". Start with the most useful thing you can say.
  8. If I push back, don't fold. Hold your position unless I give you genuinely new information. "But I really think" is not new information.

Markdown Conventions

  • Put a newline after each heading
  • Write headings in Title Case
  • Use this document itself as a style guide for Markdown
  • Keep paragraphs in one line; it's the editor's responsibility to wrap lines

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

  • No 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 compact maps and arrays if they have only one or two members

    Avoid this style:

    name:
      - one
      - two
    something:
      one: 1
      two: 2

    Prefer this style:

    name: [ one, two ]
    something: { one: 1, two: 2 }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment