AgentSkillsCN

go-style

在处理 Go 代码时,始终遵循 Go 风格的最佳实践、编程规范与设计模式。本技能囊括文件结构、命名规则、依赖注入、错误处理、测试方法、接口设计,以及常用依赖库的使用指南。

SKILL.md
--- frontmatter
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:

sh
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

sh
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/:

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

Libraries

Reusable packages:

sh
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

ElementConventionExample
Packagelowercase, singular, shortuser, parser, mux
TypePascalCaseClient, Parser, Router
ConstructorNew() *Type or Load() (*Type, error)
Error varErr prefixErrNotFound, ErrDuplicate
InterfaceSemantic, no "I" prefixReader, Store, Visitor
Receiver1-2 charsc, p, r, tr
UnexportedcamelCaseparseFile, validateInput

Dependency Injection

Constructor injection with all dependencies as parameters:

go
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

PurposePackage
Testinggithub.com/matryer/is
Diff in testsgithub.com/matthewmueller/diff
Concurrencygolang.org/x/sync/errgroup
CLIsgithub.com/livebud/cli
Logsgithub.com/matthewmueller/logs
HTTP Routersgithub.com/livebud/mux
Migration CLIgithub.com/matthewmueller/migrate
Database Clientgithub.com/jackc/pgx/v5
ORM Generatorgithub.com/matthewmueller/pogo
Env parsergithub.com/caarlos0/env/v11
Validationgithub.com/go-playground/validator/v10
Virtual filesgithub.com/matthewmueller/virt
Queueingcirello.io/pgqueue
JS Bundlinggithub.com/evanw/esbuild
Form decodinggithub.com/go-playground/form/v4

Error Handling

go
// 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:

go
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:

go
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:

go
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:

go
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:

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

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

Sealed interfaces (prevent external implementations):

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

Logging

Use log/slog with structured fields:

go
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:

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

Concurrency

Use errgroup for parallel operations:

go
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:

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

go
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

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

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