matthewmueller

go-style

0
0
# Install this skill:
npx skills add matthewmueller/skills --skill "go-style"

Install specific skill from multi-skill repository

# Description

|

# SKILL.md


name: go-style
description: |
Use go-style whenever we're working with Go code. This skill contains best practices, conventions and patterns
for working with Go code. This skill covers file structure, naming, dependency injection, error handling, testing, interfaces, and common dependencies.


Go Style Guide

Project Structure

Applications

Web apps, servers, and long-running services:

myapp/
|-- cmd/myapp/main.go           # Entrypoint commands
|-- internal/
|   |-- app/                    # App routing and server
|   |-- controller/             # App controllers
|       |-- controller.go       # Root resource
|       |-- controller_test.go  # Tests co-located
|       |-- users/
|           |-- users.go        # Users resource
|           |-- users_test.go
|   |-- env/                    # Environment parsing
|   |-- log/                    # Centralized logging
|   |-- pogo/                   # Generated database client
|   |-- db/                     # Database connection
|   |-- slack/                  # Project-specific dependencies
|   |-- stripe/                 # Project-specific dependencies
|-- migrate/                    # Migrations
    |-- 001_setup.down.sql      # Up migration
    |-- 001_setup.up.sql        # Down migration
|-- scripts/                    # One-off scripts
    |-- add-teammates/main.go
    |-- team-resync/main.go
|-- e2e/                        # End-to-end tests
|-- terraform/                  # Terraform scripts
|-- .envrc                      # Direnv
|-- .gitignore
|-- Makefile                    # Command runner
|-- Readme.md                   # Project notes (install, development, test accounts, faq)
|-- go.mod
|-- go.sum

CLIs

mycli/
|-- main.go              # Entrypoint (for simple CLIs)
|-- internal/
|   |-- cli/             # CLIs and commands
|   |-- env/             # Environment parsing
|-- testdata/            # Test fixtures
|-- Makefile             # Command runner
|-- go.mod

For CLIs with multiple commands, use cmd/:

mycli/
|-- cmd/one/main.go    # Entrypoint
|-- cmd/two/main.go    # Entrypoint

Libraries

Reusable packages:

mylib/
|-- mylib.go             # Public API
|-- mylib_test.go        # Tests co-located
|-- another.go
|-- another_test.go
|-- internal/            # Implementation details
|   |-- parser/
|   |-- stripe/          # Project-specific dependencies
|-- testdata/            # Test fixtures
|-- Makefile             # Command runner
|-- go.mod

General Guidelines

  • Use internal/ for implementation details
  • Co-locate *_test.go with source files
  • Use testdata/ for test fixtures
  • Single-responsibility packages
  • Project dependencies should shadow most third-party dependencies
  • e.g. project/ depends on internal/slack that depends on github.com/slack-go/slack.

Naming

Element Convention Example
Package lowercase, singular, short user, parser, mux
Type PascalCase Client, Parser, Router
Constructor New() *Type or Load() (*Type, error)
Error var Err prefix ErrNotFound, ErrDuplicate
Interface Semantic, no "I" prefix Reader, Store, Visitor
Receiver 1-2 chars c, p, r, tr
Unexported camelCase parseFile, validateInput

Dependency Injection

Constructor injection with all dependencies as parameters:

func New(log *slog.Logger, db DB, cache Cache) *Client {
    return &Client{log: log, db: db, cache: cache}
}
  • Pass interfaces, not concrete types
  • Store dependencies as unexported struct fields
  • No package-level globals or singletons
  • Return concrete types *Client
  • Try to avoid returning internal state (e.g. func (c *Client) DB() DB)
  • Minimize the public interface, prefer private methods and functions

Common Dependencies

Purpose Package
Testing github.com/matryer/is
Diff in tests github.com/matthewmueller/diff
Concurrency golang.org/x/sync/errgroup
CLIs github.com/livebud/cli
Logs github.com/matthewmueller/logs
HTTP Routers github.com/livebud/mux
Migration CLI github.com/matthewmueller/migrate
Database Client github.com/jackc/pgx/v5
ORM Generator github.com/matthewmueller/pogo
Env parser github.com/caarlos0/env/v11
Validation github.com/go-playground/validator/v10
Virtual files github.com/matthewmueller/virt
Queueing cirello.io/pgqueue
JS Bundling github.com/evanw/esbuild
Form decoding github.com/go-playground/form/v4

Error Handling

// Wrap and prefix error with context
if err != nil {
    return fmt.Errorf("parser: parsing config: %w", err)
}

// Join multiple errors (validation)
func (in *Input) validate() (err error) {
    if in.Name == "" {
        err = errors.Join(err, errors.New("missing name"))
    }
    if in.Path == "" {
        err = errors.Join(err, errors.New("missing path"))
    }
    return err
}

// Check error types
if errors.Is(err, fs.ErrNotExist) {
    // handle not found
}

Input Struct Pattern

For complex operations, use input structs with validation:

type Upload struct {
    From        string
    To          string
    MaxSize     string  // User-friendly string
    maxSize     int     // Parsed value (unexported)
}

func (in *Upload) validate() (err error) {
    if in.From == "" {
        err = errors.Join(err, errors.New("missing from"))
    }
    if in.MaxSize != "" {
        size, e := humanize.ParseBytes(in.MaxSize)
        if e != nil {
            err = errors.Join(err, fmt.Errorf("invalid max size: %w", e))
        }
        in.maxSize = int(size)
    }
    return err
}

func (c *Client) Upload(ctx context.Context, in *Upload) error {
    if err := in.validate(); err != nil {
        return fmt.Errorf("client: invalid input: %w", err)
    }
    // ...
}

Testing

Use github.com/matryer/is for assertions:

func TestParser(t *testing.T) {
    is := is.New(t)
    input := "input"
    actual, err := Parse(input)
    is.NoErr(err)
    expect := "expected"
    is.Equal(actual.Name, expect)
}

Use helper functions and top-level tests for more complex assertions:

func equal(t *testing.T, input, expected string) {
  t.Helper()
    t.Run(input, func(t *testing.T) {
    t.Helper()
    // ...
  })
}

func equalFile(t *testing.T, path string) {
}

func TestSample(t *testing.T) {
    equal(t, "input", `expect`)
    equal(t, "input", `expect`)
}

func TestComplex(t *testing.T) {
    equal(t, "input", `expect`)
    equal(t, "input", `expect`)
    equal(t, "input", `expect`)
    equal(t, "input", `expect`)
}

func TestFile(t *testing.T) {
    equalFile(t, "input")
    equalFile(t, "input")
}

Patterns:

  • Write testable code and use test-driven development when adding new features
  • Avoid table-driven tests, use helper functions and top-level TestX functions
  • Golden file testing with -update flag for snapshots
  • t.Helper() in all test helper functions

Golden file pattern:

var update = flag.Bool("update", false, "update golden files")

func TestOutput(t *testing.T) {
    is := is.New(t)
    got := Generate()

    golden := filepath.Join("testdata", t.Name()+".golden")
    if *update {
        is.NoErr(os.WriteFile(golden, []byte(got), 0644))
    }

    want, err := os.ReadFile(golden)
    is.NoErr(err)
    is.Equal(got, string(want))
}

Interfaces

Keep interfaces small and focused:

type Store interface {
    Get(ctx context.Context, key string) ([]byte, error)
    Set(ctx context.Context, key string, value []byte) error
}

Compile-time interface checks:

Add compile-type type-checks on all types that implement interfaces.

var _ Store = (*FileStore)(nil)
var _ Store = (*MemoryStore)(nil)

Sealed interfaces (prevent external implementations):

type Block interface {
    blockType() // unexported marker method
}

Logging

Use log/slog with structured fields:

log.Info("processing file",
    slog.String("path", path),
    slog.Int("size", size),
)

log.Error("failed to parse",
    slog.String("file", file),
    slog.Any("error", err),
)

Prefix log messages with package context: "parser: failed to read file".

Context

Propagate context.Context through all I/O operations:

func (c *Client) Fetch(ctx context.Context, url string) (*Response, error)

Concurrency

Use errgroup for parallel operations:

g, ctx := errgroup.WithContext(ctx)
for _, item := range items {
    g.Go(func() error {
        return process(ctx, item)
    })
}
if err := g.Wait(); err != nil {
    return err
}

Patterns

Visitor pattern for AST/document traversal:

type Visitor interface {
    VisitHeader(*Header)
    VisitParagraph(*Paragraph)
}

type Node interface {
    Visit(Visitor)
}

Main

Keep main.go concise. Wherever possible, it should delegate to internal/ packages.

package main

import (
    "context"
    "github.com/matthewmueller/logs"
    "github.com/package/internal/cli"
)

func main() {
  ctx := context.Background()
  log := logs.Default()
  if err := cli.Parse(ctx, os.Args[1:]...); err != nil {
    log.Fatal(err)
    os.Exit(1)
  }
}

Env

For environment parsing in internal/env/env.go

package env

import (
    env11 "github.com/caarlos0/env/v11"
)

// Env environment
type Env struct {
    PORT         string `env:"PORT,required"`
    DATABASE_URL string `env:"DATABASE_URL,required"`
}

// Load the environment
func Load() (*Env, error) {
    env := new(Env)
  if err := env11.Parse(env); err != nil {
    return nil, err
  }
  return env, nil
}

CLIs

Follow this pattern for CLIs

package cli

type CLI struct {
  Stdout   io.Writer
  Stderr   io.Writer
  Stdin    io.Reader
  Env      []string
  Dir      string
  logLevel string
}

func Default() *CLI {
  return &CLI{
        Stdout: os.Stdout,
        Stderr: os.Stderr,
        Stdin:  os.Stdin,
        Env:    os.Environ(),
        Dir:    ".",
    }
}

// Parse the command line arguments
func (c *CLI) Parse(ctx context.Context, args ...string) error {
    cli := cli.New("root", "root description")
  // Enum with a default value
    cli.Flag("log", "log level").Enum(&c.logLevel, "debug", "info", "warn", "error").Default("info")

    { // commands
        in := &Install{}
        cmd := cli.Command("command", "command description")
    // Optional flag
    cmd.Flag("force", "force install").Short('f').Optional().String(&in.Force)
    // Required argument
    cmd.Arg("pkg", "pkg to install").String(&in.Pkg)
        cmd.Run(func(ctx context.Context) error {
            return c.Install(ctx, in)
        })
    }
}

// Install input
type Install struct {
  Pkg string
  Force *bool
}

// Run the install command
func (c *CLI) Install(ctx context.Context, in *Install) error {
}

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.