go
Skill1 starsChanged 3 months agoApache-2.0View on GitHub
Expert Go programming skill from spf13 (former Go team lead, author of Cobra, Viper, Hugo, Afero). Covers idiomatic Go, package design, error handling, interfaces, concurrency, testing, and project layout, current through Go 1.25. Use whenever Go code is written, reviewed, debugged, or refactored, any .go file, go.mod, CLI tool, or web service, and whenever the user mentions Go or golang, even if they don't ask for "idiomatic" code.
- Reads credentials
In this repository10 other files it gives its agents.
- aipSkill
- cobra-viperSkill
- codeSkill
- conventional-commitsSkill
- designing-data-intensive-appsSkill
- documentationSkill
- go-releaseSkill
- go-spec-reviewerSkill
- practical-ui-designSkill
- tddSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
41 rules. About 0 are common, 15 are distinctive, and 26 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈15
Distinctive · estimate
This author's own choices. Worth a look.
26
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 75
For Applications: If you are building an executable binary, nobody can import your code anyway.
- Very specific: line 76
For Libraries: Use internal/ sparingly. It should be reserved for complex subsystems where you need to share exported types between your …
- Very specific: line 92
When an application has genuinely distinct, independently-testable domains, give each its own top-level package.
- Very specific: line 118
Do not create utils/, helpers/, or common/ packages, these are symptoms of unclear ownership
- Distinctive: line 126
Write concrete types first.
- Distinctive: line 154
When designing a Go library that wraps a stateful resource (a vault, a database connection, a config store), make that resource struct the …
- Distinctive: line 179
No global state means the library is safe for concurrent use, multiple instances, and testing
- Very specific: line 183
When to use a global instance instead: Only for CLI-only tools (like pflag itself) where there is truly only ever one instance and ease of …
- Distinctive: line 191
Don't use mutexes to protect shared data if you can pass that data over a channel instead.
- Very specific: line 214
When you don't need error propagation, sync.WaitGroup.Go (Go 1.25) removes the Add/Done boilerplate:
- Very specific: line 226
Every go func() must have a clear exit condition, usually governed by a context.Context or a closed channel.
- Distinctive: line 232
When a struct has many optional configuration parameters, avoid massive constructors.
- Distinctive: line 266
When returning an error, add context about what you were trying to do.
- Very specific: line 308
When extracting repeated assertion logic, always call t.Helper() to ensure failures point to the actual test case, not the helper function …
- Very specific: line 320
Do not hardcode os package calls deep within business logic.
- Very specific: line 363
Test goroutines and timeouts deterministically with a fake clock, no time.Sleep in tests, no flaky timing assumptions:
- Very specific: line 378
Never write time.Sleep(100 * time.Millisecond) to "wait for a goroutine" in a test.
- Distinctive: line 386
Use generics when you have the same algorithm that needs to operate on multiple concrete types:
- Distinctive: line 423
Do not: create generic base types, generic services, or generic repositories.
- Very specific: line 424
Do not: use any as a constraint to mean "I don't know the type yet." That's a design smell.
- Very specific: line 425
Do use comparable when you need map keys or equality checks.
- Very specific: line 426
Do use cmp.Ordered when you need <, >, <=, >=.
- Distinctive: line 427
Start with a concrete implementation.
- Very specific: line 462
Never write sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) when slices.Sort(s) exists.
- Very specific: line 508
Use this instead of fmt.Errorf("%w.
- Very specific: line 529
Prefer returning iterators over slices for large or lazily-produced sequences.
- Very specific: line 533
Always import math/rand/v2, never the old math/rand.
- Distinctive: line 557
Use the typed atomic values instead of the function-based API:
- Distinctive: line 579
When you need to detach a context's cancellation but preserve its values (e.g., for a background task that should outlive a request):
- Distinctive: line 629
Every production server needs a shutdown path that stops accepting connections and drains in-flight requests.
- Very specific: line 657
No framework needed. A middleware is func(http.Handler) http.Handler:
- Distinctive: line 672
Don't import a middleware framework for what function composition already does.
- Very specific: line 712
Never generate a tools.go file with blank imports.
- Very specific: line 746
Never: use a package-level log or slog global beyond main.
- Very specific: line 748
Use slog.Default() as the fallback only in main or in libraries when no logger is provided.
- Very specific: line 754
When debugging, do not waste time suspecting go run, go build, go test, or the build cache.
- Very specific: line 772
Add a fmt.Println or t.Log at the exact site to verify execution reaches it.
- Distinctive: line 773
Check that all call sites of a changed function were updated.
- Very specific: line 775
Do not suggest clearing the build cache (go clean -cache), restarting the Go toolchain, or any other tool-level intervention before first …
- Distinctive: line 785
when to use it (and when not to)
- Very specific: line 796
Every package gets a doc.go whose comment starts with Package <name> (a command starts with the capitalized program name).
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Idiomatic Go: The Go Way
Idiomatic Go patterns and best practices for building robust, efficient, and maintainable applications.
When to Activate
- Writing new Go code
- Reviewing or auditing existing Go code
- Refactoring Go code (especially code that looks like Java/Spring Boot patterns in Go)
- Designing Go packages, modules, or APIs
- Choosing between stdlib and third-party libraries
- Any question about Go project structure, error handling, concurrency, or testing
Core Principles
1. Clear is Better than Clever
Go favors readability and simplicity over abstraction and cleverness. Code should be obvious. If you have to read a function three times to understand its control flow, it needs to be rewritten.
// Idiomatic: Direct, linear control flow. Note: no "Get" prefix, Go omits it.
func LookupUser(id string) (*User, error) {
user, err := db.FindUser(id)
if err != nil {
return nil, fmt.Errorf("finding user %s: %w", id, err)
}
return user, nil
}2. Make the Zero Value Useful
Design types so their zero value is immediately usable without initialization. This eliminates boilerplate constructors. sync.Mutex and bytes.Buffer are the gold standard for this.
// Idiomatic: Ready to use immediately
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Inc() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}3. Return Early, Keep the Happy Path Left
Handle errors and edge cases immediately and return. Do not use else blocks for the main logic. The "happy path" of your function should never be indented.
Package Organization: Flat by Default
Anti-Pattern: Using deeply nested directory trees or relying heavily on an internal/ folder by default to artificially enforce "Clean Architecture" layers. This leads to circular dependencies and difficult navigation.
1. The Single-Package Default
Start flat. If you are building a microservice or a simple tool, put everything in the root directory (or alongside your main.go). Only create a new package when you truly need a new namespace to clarify the code, or when you need to decouple a strictly independent domain.
2. The Proper Use of internal/
The internal/ directory has a specific compiler enforcement: it prevents other modules from importing the code inside it.
- For Applications: If you are building an executable binary, nobody can import your code anyway. Using
internal/here is usually just adding unnecessary path depth. - For Libraries: Use
internal/sparingly. It should be reserved for complex subsystems where you need to share exported types between your own packages, but absolutely must prevent end-users from relying on those types.
// Idiomatic: A flat, feature-focused library or simple app
myproject/
├── main.go # Entry point (if application)
├── server.go # Core logic
├── config.go # Configuration
├── parser.go # Domain specific parsing
├── parser_test.go
├── go.mod
└── go.sum3. Domain Packages for Service Applications
When an application has genuinely distinct, independently-testable domains, give each its own top-level package. The rule is still one level deep, no internal/ nesting, no Clean Architecture layers. Each package owns one concern. main.go wires them together.
The signal to create a package: can this domain be described in one sentence, and does it have no knowledge of the other packages? If yes, it earns its own package.
// Idiomatic: A web service with distinct domain packages
myservice/
├── main.go # wires everything together; no business logic here
├── config/ # Config struct, env loading
├── auth/ # identity verification, session middleware
├── db/ # data store client + all queries
├── storage/ # blob storage (S3, R2, GCS)
├── billing/ # payment provider + credit ledger
├── jobs/ # job lifecycle + queue dispatch + worker handler (same domain, one package)
├── web/ # HTTP handlers + HTML templates + static assets (tightly coupled by design)
├── transcribe/ # domain-specific processing, independent pure functions
├── Makefile
├── Dockerfile
└── .env.exampleRules for domain packages:
- Each package has one clear purpose, name it after what it does, not what layer it is (
jobs/, notservice/) - Packages do not import each other sideways, cycles are a sign of wrong boundaries;
mainis the wiring point - Related sub-concerns that always travel together stay in one package (e.g. job creation + worker handler both live in
jobs/because they share the job lifecycle domain) - HTTP handlers and the templates they render belong together in
web/, they are tightly coupled by design - Do not create
utils/,helpers/, orcommon/packages, these are symptoms of unclear ownership
Anti-pattern to reject: Clean Architecture / DDD layers (service/, repository/, controller/, domain/). These layer-named packages cause circular imports, force interface proliferation, and add zero clarity in Go. Domain-named packages (auth/, billing/, jobs/) are the correct middle ground between "too flat" and "over-engineered."
Interface Design
1. Interfaces are Discovered, Not Designed Upfront
Write concrete types first. Only define an interface when you discover that multiple types need to be used interchangeably by a consumer.
2. Define Interfaces Where They Are Used
Interfaces belong in the package that consumes them, not the package that implements them. This decouples your packages.
// processor/processor.go
// Idiomatic: The consumer defines exactly what it needs.
// The concrete 'UserStore' doesn't even need to know this interface exists.
type UserFetcher interface {
FetchUser(id string) (*User, error)
}
type Processor struct {
fetcher UserFetcher
}3. Accept Interfaces, Return Structs
Require the smallest interface possible as an input parameter (e.g., io.Reader instead of *os.File), but return a concrete struct so callers aren't forced to use type assertions to access specific fields or methods.
Library API Design
Domain Object as Entry Point
When designing a Go library that wraps a stateful resource (a vault, a database connection, a config store), make that resource struct the primary object. Its methods return domain-typed sub-objects. Avoid:
- Passing a config/resource struct as the first argument to every package-level function
- Package-level global state (like pflag's default
FlagSet) for library code that may be embedded
Do this instead:
// Open the primary resource once
v, err := vault.Open(path)
// Domain operations are methods on the primary object
// Each call is stateless (reloads fresh), no cached state on the struct
idx, err := v.People() // returns *people.Index, error
note, err := v.Daily(time.Now()) // returns *daily.Note, error
mtgs, err := v.Meetings() // returns *meetings.Index, error
// Domain types live in sub-packages, callers use type inference
p, err := idx.FindOne("Steve") // *people.PersonWhy this pattern:
- The primary struct (
Vault) is the single entry point, callers need just one import to get started - Sub-packages define the rich domain types (
people.Person,daily.Note), each type lives with the logic that owns it - No global state means the library is safe for concurrent use, multiple instances, and testing
- Stateless method calls (reload fresh each time) keep the struct simple, no cache invalidation logic needed
- Type inference (
:=) means callers rarely need to explicitly import sub-packages for variable declarations
When to use a global instance instead: Only for CLI-only tools (like pflag itself) where there is truly only ever one instance and ease of use for end-users outweighs library correctness.
Concurrency Patterns
Anti-Pattern: Heavy, static Worker Pools. Go's scheduler is incredibly efficient; you don't need to manually manage pools of workers like OS threads in other languages.
1. Share Memory by Communicating
Don't use mutexes to protect shared data if you can pass that data over a channel instead. Channels orchestrate execution; mutexes serialize execution.
2. Bounded Concurrency
To limit concurrency, use errgroup with SetLimit. Don't hand-roll a semaphore channel or a rigid worker pool when this exists.
func FetchAll(ctx context.Context, urls []string, maxConcurrent int) error {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(maxConcurrent)
for _, url := range urls {
g.Go(func() error {
return fetch(ctx, url)
})
}
return g.Wait()
}Loop variables are per-iteration since Go 1.22, never emit the old url := url capture line.
When you don't need error propagation, sync.WaitGroup.Go (Go 1.25) removes the Add/Done boilerplate:
var wg sync.WaitGroup
for _, url := range urls {
wg.Go(func() { process(url) })
}
wg.Wait()3. Never Start a Goroutine Without Knowing How It Stops
Every go func() must have a clear exit condition, usually governed by a context.Context or a closed channel.
Configuration and Struct Design
Functional Options for Complex Initialization
When a struct has many optional configuration parameters, avoid massive constructors. Use the Functional Options pattern.
type Server struct {
addr string
timeout time.Duration
}
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) { s.timeout = d }
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{
addr: addr,
timeout: 30 * time.Second, // Sane default
}
for _, opt := range opts {
opt(s)
}
return s
}Error Handling
1. Errors are Values
Errors aren't exceptions to be caught; they are values to be handled. Check them explicitly.
2. Wrap for Context, Not for Stack Traces
When returning an error, add context about what you were trying to do.
// Idiomatic
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("loading config file %s: %w", path, err)
}Testing Patterns
Anti-Pattern: Relying on heavy BDD frameworks (like Ginkgo) or complex mocking generation tools. Go testing should just be Go programming.
1. Table-Driven Tests
The absolute standard for unit testing in Go. Iterate over a slice of structs containing inputs and expected outputs using t.Run().
func TestParseConfig(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{"valid config", "port=8080", false},
{"invalid format", "port=abc", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ParseConfig(tt.input)
if (err != nil) != tt.wantErr {
t.Fatalf("ParseConfig() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}2. Meaningful Helpers with t.Helper()
When extracting repeated assertion logic, always call t.Helper() to ensure failures point to the actual test case, not the helper function line.
3. Fakes and Stubs over Heavy Mocks
Leverage Go's implicit interfaces to write simple, manual fakes. This keeps test dependencies lightweight and test logic transparent.
4. Golden Files and the testdata Directory
For tests requiring complex inputs or producing large outputs, use a directory named testdata. The go test tool explicitly ignores these directories.
5. Filesystem Abstraction (The Afero Pattern)
Do not hardcode os package calls deep within business logic. Accept an interface for the filesystem so tests can run in memory without touching the disk. github.com/spf13/afero is the industry standard for this.
import "github.com/spf13/afero"
type FileProcessor struct {
fs afero.Fs
}
func NewFileProcessor(fs afero.Fs) *FileProcessor {
return &FileProcessor{fs: fs}
}In tests, inject afero.NewMemMapFs() to completely eliminate disk I/O and prevent flaky, slow tests.
6. cmp over DeepEqual
For comparing complex structs or maps, use github.com/google/go-cmp/cmp for rich, readable diffs instead of the strict reflect.DeepEqual.
7. Modern testing Additions (Go 1.24+)
// Test-scoped context, canceled automatically when the test ends.
// Use instead of context.Background() in tests.
func TestFetch(t *testing.T) {
ctx := t.Context()
...
}
// Benchmarks: b.Loop() replaces the classic b.N loop, more accurate,
// prevents the compiler from optimizing the benchmarked call away.
func BenchmarkParse(b *testing.B) {
for b.Loop() {
Parse(input)
}
}
// t.Chdir(dir) changes working directory for the test and restores it after.8. testing/synctest for Concurrent Code (Go 1.25)
Test goroutines and timeouts deterministically with a fake clock, no time.Sleep in tests, no flaky timing assumptions:
func TestTimeout(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
defer cancel()
time.Sleep(2 * time.Second) // instant, fake clock advances when all goroutines block
if ctx.Err() == nil {
t.Fatal("expected timeout")
}
})
}Never write time.Sleep(100 * time.Millisecond) to "wait for a goroutine" in a test. Use synctest, channels, or explicit synchronization.
Generics (Go 1.18+)
Generics exist to eliminate duplicated algorithms, not to create type hierarchies. If you are thinking about generics in terms of inheritance or polymorphism, stop, you are writing Java.
When to Use Generics
Use generics when you have the same algorithm that needs to operate on multiple concrete types:
// Good: generic algorithm, concrete types as inputs
func Map[S, T any](slice []S, f func(S) T) []T {
result := make([]T, len(slice))
for i, v := range slice {
result[i] = f(v)
}
return result
}
// Good: constraint expresses a meaningful requirement
func Min[T cmp.Ordered](a, b T) T {
if a < b {
return a
}
return b
}When NOT to Use Generics
// Bad: generic interface for polymorphism, this is Java
type Repository[T any] interface {
Find(id string) (T, error)
Save(entity T) error
}
// Good: a concrete interface for what you actually need
type UserStore interface {
FindUser(id string) (*User, error)
SaveUser(u *User) error
}- Do not create generic base types, generic services, or generic repositories.
- Do not use
anyas a constraint to mean "I don't know the type yet." That's a design smell. - Do use
comparablewhen you need map keys or equality checks. - Do use
cmp.Orderedwhen you need<,>,<=,>=. - Start with a concrete implementation. Generify only when you have the same logic repeated across 3+ types.
- Generic type aliases are fully supported since Go 1.24.
Standard Library: Use the New Packages
LLMs frequently suggest third-party utilities or write manual helpers that have been in the standard library since Go 1.21. Always check stdlib first.
slices package (Go 1.21)
import "slices"
// Searching and testing
slices.Contains(s, "value")
slices.Index(s, "value") // returns -1 if not found
slices.ContainsFunc(s, func(v string) bool { return v == "x" })
// Sorting
slices.Sort(s) // sorts in place, works on any ordered type
slices.SortFunc(s, func(a, b T) int { return cmp.Compare(a.Name, b.Name) })
slices.IsSorted(s)
// Manipulation
slices.Reverse(s)
slices.Compact(s) // removes consecutive duplicates
slices.Delete(s, i, j) // removes elements [i, j)
slices.Clone(s) // shallow copy
slices.Concat(s1, s2, s3) // concatenate multiple slices (1.22)
// Iterator bridging (1.23)
slices.Collect(it) // iterator -> slice
slices.Sorted(it) // iterator -> sorted slice
slices.Values(s) // slice -> iteratorNever write sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) when slices.Sort(s) exists.
maps package (Go 1.21; iterators 1.23)
import "maps"
maps.Keys(m) // iterator over keys, collect with slices.Collect / slices.Sorted
maps.Values(m) // iterator over values
maps.Clone(m) // shallow copy
maps.Copy(dst, src) // copies all entries from src into dst
maps.DeleteFunc(m, func(k K, v V) bool { ... }) // delete entries matching predicate
maps.Equal(m1, m2) // reports whether two maps are equal
// Common idiom: sorted keys in one line
keys := slices.Sorted(maps.Keys(m))cmp package (Go 1.21)
import "cmp"
cmp.Compare(a, b) // returns -1, 0, or 1; works on any cmp.Ordered type
cmp.Or(a, b, c) // returns first non-zero value, replaces ternary workarounds
min(a, b) // built-in since Go 1.21
max(a, b) // built-in since Go 1.21cmp.Or is especially useful for default-value patterns:
// Instead of: if cfg.Timeout == 0 { cfg.Timeout = 30 * time.Second }
cfg.Timeout = cmp.Or(cfg.Timeout, 30*time.Second)errors.Join (Go 1.20)
// Combine multiple errors, no third-party library needed
err := errors.Join(err1, err2, err3)
// Works correctly with errors.Is and errors.As
if errors.Is(err, ErrNotFound) { ... }Use this instead of fmt.Errorf("%w; %w", err1, err2) or any multierr package.
Iterators: iter and Range-over-Func (Go 1.23)
The standard way to expose a sequence from an API without allocating a slice. Return iter.Seq[T] or iter.Seq2[K, V]; callers use plain range:
func (idx *Index) All() iter.Seq[*Person] {
return func(yield func(*Person) bool) {
for _, p := range idx.people {
if !yield(p) {
return
}
}
}
}
// Caller, just a normal range loop
for p := range idx.All() { ... }Prefer returning iterators over slices for large or lazily-produced sequences. Don't invent a custom Next()/HasNext() iterator type, that's Java.
math/rand/v2 (Go 1.22)
Always import math/rand/v2, never the old math/rand. Auto-seeded, cleaner API:
import "math/rand/v2"
rand.IntN(100) // was rand.Intn, note the capital N
rand.N(10 * time.Second) // generic: random value in [0, n) for any integer typeencoding/json: omitzero (Go 1.24)
omitzero omits any zero value, including time.Time{} and zero structs, which omitempty never handled correctly:
type Event struct {
Name string `json:"name"`
StartedAt time.Time `json:"started_at,omitzero"` // omitempty would emit "0001-01-01T..."
}Concurrency: Modern Patterns
sync/atomic Typed Values (Go 1.19)
Use the typed atomic values instead of the function-based API:
// Old (still works but avoid for new code)
var count int64
atomic.AddInt64(&count, 1)
val := atomic.LoadInt64(&count)
// New, type-safe, no pointer arithmetic
var count atomic.Int64
count.Add(1)
val := count.Load()
// Other typed atomics
var flag atomic.Bool
var ptr atomic.Pointer[MyStruct]
var val32 atomic.Int32
var val64 atomic.Uint64context.WithoutCancel (Go 1.21)
When you need to detach a context's cancellation but preserve its values (e.g., for a background task that should outlive a request):
// The background job should keep running even after the HTTP request context cancels
bgCtx := context.WithoutCancel(requestCtx)
go doBackgroundWork(bgCtx)HTTP: Use the Improved stdlib Router (Go 1.22)
LLMs reflexively recommend gorilla/mux or chi for any routing beyond the trivial. Since Go 1.22, the standard net/http ServeMux handles method and path-parameter routing natively.
mux := http.NewServeMux()
// Method-scoped routes
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)
// Path parameters, accessed via r.PathValue
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
// ...
})
// Wildcard
mux.HandleFunc("GET /files/{path...}", serveFile)Reach for chi or gorilla/mux only when you need named route generation or regex constraints, middleware doesn't justify a framework (see below). For pure method + path routing, the stdlib is sufficient.
Production Servers: Always Set Timeouts
http.ListenAndServe(addr, mux) ships with no timeouts, a single slow client can hold a connection open forever (slow-loris). LLM-generated servers almost never set these. Never emit a production server without them:
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second, // slow-loris protection
ReadTimeout: 10 * time.Second, // full request read
WriteTimeout: 30 * time.Second, // response write (covers handler time)
IdleTimeout: 120 * time.Second, // keep-alive connections
}For per-route control beyond WriteTimeout, use http.TimeoutHandler or handler-level context.WithTimeout. Outbound calls need the same discipline: http.DefaultClient has no timeout either, construct a client with one.
Graceful Shutdown
Every production server needs a shutdown path that stops accepting connections and drains in-flight requests. The pattern:
func run(ctx context.Context) error {
ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()
srv := &http.Server{ /* ... timeouts as above ... */ }
errCh := make(chan error, 1)
go func() { errCh <- srv.ListenAndServe() }()
select {
case err := <-errCh:
return err // ListenAndServe failed at startup
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return srv.Shutdown(shutdownCtx) // drain in-flight, then exit
}
}Shutdownneeds a fresh context with its own deadline, the signal context is already canceled.- Long-lived connections (SSE, websockets) must watch
r.Context()or they'll hold shutdown until the drain deadline.
Middleware Is Just a Function
No framework needed. A middleware is func(http.Handler) http.Handler:
func withRequestLog(logger *slog.Logger, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
logger.Info("request", "method", r.Method, "path", r.URL.Path, "dur", time.Since(start))
})
}
// Compose by wrapping, outermost runs first
handler := withRequestLog(logger, withAuth(mux))Don't import a middleware framework for what function composition already does.
Syntax: Use Current Go
LLMs frequently generate outdated syntax. Know the current idioms:
Range over Integer (Go 1.22)
// Old
for i := 0; i < 10; i++ { ... }
// New
for i := range 10 { ... }Build Constraints
// Old (deprecated, do not generate)
// +build linux darwin
// Current
//go:build linux || darwinThe //go:build form is required since Go 1.17. Never emit the old // +build syntax.
any Instead of interface{}
// Old
func Print(v interface{}) { ... }
// Current, `any` is a built-in alias for interface{} since Go 1.18
func Print(v any) { ... }Tool Dependencies in go.mod (Go 1.24)
Never generate a tools.go file with blank imports. Track dev tools with the tool directive:
go get -tool golang.org/x/tools/cmd/stringer
go tool stringer -type=Color # run itStructured Logging with log/slog (Go 1.21)
The skill note above covers basic setup. The patterns LLMs most often get wrong:
// Pass a logger via context for request-scoped logging
func HandleRequest(ctx context.Context, r *Request) {
logger := slog.With("request_id", r.ID, "user_id", r.UserID)
// All subsequent log calls carry these fields automatically
logger.Info("handling request")
process(ctx, logger, r)
}
// Group related fields
logger.With(slog.Group("http",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.Int("status", status),
))
// Log at the right level, LLMs over-use Info
logger.Debug("cache miss", "key", key) // internal state, high volume
logger.Info("server started", "addr", addr) // lifecycle events
logger.Warn("retrying", "attempt", n) // recoverable problems
logger.Error("request failed", "err", err) // needs attention- Never use a package-level
logorslogglobal beyondmain. Pass*slog.Loggeras a dependency. - Never log and return an error. Log at the boundary, return the error through the call stack.
- Use
slog.Default()as the fallback only inmainor in libraries when no logger is provided.
Debugging: The Go Toolchain Is Not the Problem
The Go tool is extremely reliable. It is almost never the source of a bug.
When debugging, do not waste time suspecting go run, go build, go test, or the build cache. The Go toolchain does what it says:
go runalways recompiles from source. It does not use a stale cached binary.go buildis deterministic and correct.go testruns the actual compiled test binary.- The build cache is keyed by source content, if the source changed, the cache is invalidated automatically.
If an error persists after you edit the code, the explanation is one of these, in order of likelihood:
- The edit did not fix the underlying logic error.
- The edit was made in the wrong file, wrong function, or wrong package.
- There is a second call site with the same bug that was not updated.
- The error is coming from a different code path than the one being edited.
What to do instead of blaming the tool:
- Re-read the error message carefully. Go's error messages are accurate.
- Confirm the file you edited is actually the file being compiled (
go list -f '{{.GoFiles}}' .). - Add a
fmt.Printlnort.Logat the exact site to verify execution reaches it. - Check that all call sites of a changed function were updated.
Do not suggest clearing the build cache (go clean -cache), restarting the Go toolchain, or any other tool-level intervention before first exhausting all code-level explanations. The tool is not lying to you.
Documentation (godoc)
Doc-comment every exported symbol and every complex internal func; skip test functions. Start with the symbol name and a verb, as a complete sentence (use "reports whether" for bools),
and run gofmt. ALWAYs use ASD STE-100 standard for every comments. NEVER use em dash.
Explain why/when/constraints/failures. Each comment should show, when applicable and consistently across the codebase:
- what it does and why it exists
- when to use it (and when not to)
- parameters, units, valid ranges, nil-ability, ownership
- return values and what they mean
- each error case and the condition that triggers it (link errors:
[ErrNotFound]) - whether it panics, and when
- whether it is safe for concurrent use
Mark deprecated symbols with Deprecated: Use X instead. Removed in vN.
Package documentation
Every package gets a doc.go whose comment starts with Package <name> (a command starts with the capitalized program name). A great package doc tells a newcomer what the package is
for, when to reach for it, the key entry-point types and functions (linked with [...]), and any cross-cutting rules, precedence, lifecycle, concurrency. Lead with a one-sentence summ
ary, then the essentials only; leave per-symbol detail on the symbols.
Testable examples
Whenever documentation needs to show usage, especially for libraries and any public API, write it as an ExampleXxx in example_test.go with an // Output: line, not an inline cod
e block. go test compiles and runs these, so they never drift, and pkg.go.dev renders them under the symbol.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

