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.gowith source files - •Use
testdata/for test fixtures - •Single-responsibility packages
- •Project dependencies should shadow most third-party dependencies
- •e.g.
project/depends oninternal/slackthat depends ongithub.com/slack-go/slack.
- •e.g.
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
-updateflag 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 {
}