Use when you have a written implementation plan to execute in a separate session with review checkpoints
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.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.
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 {
}
# 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.