gopilot
Go programming language skill for writing idiomatic Go code, code review, error handling, testing, concurrency, security, and program design. Use when writing Go code, reviewing Go PRs, debugging Go tests, fixing Go errors, designing Go APIs, implementing security-sensitive code, handling user input, authentication, sessions, cryptography, or asking about Go best practices. Covers table-driven tests, error wrapping, goroutine patterns, interface design, generics, iterators, stdlib patterns up to Go 1.25, and OWASP security practices.
SKILL.md
| Name | gopilot |
| Description | Go programming language skill for writing idiomatic Go code, code review, error handling, testing, concurrency, security, and program design. Use when writing Go code, reviewing Go PRs, debugging Go tests, fixing Go errors, designing Go APIs, implementing security-sensitive code, handling user input, authentication, sessions, cryptography, or asking about Go best practices. Covers table-driven tests, error wrapping, goroutine patterns, interface design, generics, iterators, stdlib patterns up to Go 1.25, and OWASP security practices. |
name: gopilot description: "v1.0.20 — Go programming language skill for writing idiomatic Go code, code review, error handling, testing, concurrency, security, and program design. Use when writing Go code, reviewing Go PRs, debugging Go tests, fixing Go errors, designing Go APIs, implementing security-sensitive code, handling user input, authentication, sessions, cryptography, building resource-oriented gRPC APIs with Google AIP standards, or asking about Go best practices. Covers table-driven tests, error wrapping, goroutine patterns, interface design, generics, iterators, stdlib patterns up to Go 1.26, OWASP security practices, and Google AIP (API Improvement Proposals) with einride/aip-go for pagination, filtering, ordering, field masks, and resource names."
Go Engineering
Design Guidelines
- Keep things simple.
- Prefer stateless, pure functions over stateful structs with methods if no state is needed to solve the problem.
- Prefer synchronous code to concurrent code, an obvious concurrency pattern applies.
- Prefer simple APIs and small interfaces.
- Make the zero value useful:
bytes.Bufferandsync.Mutexwork without init. When zero values compose, there's less API. - Avoid external dependencies. A little copying is better than a little dependency.
- Clear is better than clever. Maintainability and readibility are important.
- Don't just check errors, handle them gracefully.
- Design the architecture, name the components, document the details: Good names carry the weight of expressing your design. If names are good, the design is clear on the page.
- Reduce nesting by using guard clauses.
- Invert conditions for early return
- In loops, use early
continuefor invalid items instead of nesting - Max 2-3 nesting levels
- Extract helpers for long methods
Code Style
- Avoid stuttering:
http.Clientnothttp.HTTPClient - Getters:
Foo()notGetFoo() - Receiver: short (1-2 chars), consistent across type methods
Mustprefix for panicking functions- Enums: use
iota + 1to start at one, distinguishing intentional values from zero default
Error Handling
- Errors are values. Design APIs around that.
- Wrap with context:
fmt.Errorf("get config %s: %w", name, err) - Sentinel errors:
var ErrNotFound = errors.New("not found") - Check with
errors.Is(err, ErrNotFound)orerrors.As(err, &target), or use genericerrors.AsType[T](Go 1.26+) - Static errors: prefer
errors.Newoverfmt.Errorfwithout formatting - Join multiple errors:
err := errors.Join(err1, err2, err3)(Go 1.20+) - Error strings: lowercase, no punctuation
- Avoid "failed to" prefixes - they accumulate through the stack (
"connect: %w"not"failed to connect: %w") - CRITICAL: Always check errors immediately before using returned values (Go 1.25 fixed compiler bug that could delay nil checks)
Error Strategy (Opaque Errors First)
Prefer opaque error handling: treat errors as opaque values, don't inspect internals. This minimizes coupling.
Three strategies in order of preference:
- Opaque errors (preferred): return and wrap errors without exposing type or value. Callers only check
err != nil. - Sentinel errors (
var ErrNotFound = errors.New(...)): use sparingly for expected conditions callers must distinguish. They become public API. - Error types (
type NotFoundError struct{...}): use when callers need structured context. Also public API — avoid when opaque or sentinel suffices.
Assert Behavior, Not Type
When you must inspect errors beyond errors.Is/errors.As, assert on behavior interfaces instead of concrete types:
type temporary interface {
Temporary() bool
}
func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}
Handle Errors Once
An error should be handled exactly once. Handling = logging, returning, or degrading gracefully. Never log and return — duplicates without useful context.
// Bad: logs AND returns
if err != nil {
log.Printf("connect failed: %v", err)
return fmt.Errorf("connect: %w", err)
}
// Good: wrap and return; let the top-level caller log
if err != nil {
return fmt.Errorf("connect to %s: %w", addr, err)
}
Wrap with context at each layer; log/handle only at the application boundary.
Context with Cause (Go 1.21+)
Propagate cancellation reasons through context:
ctx, cancel := context.WithCancelCause(parent)
cancel(fmt.Errorf("shutdown: %w", reason))
// Later retrieve the cause
if cause := context.Cause(ctx); cause != nil {
log.Printf("context cancelled: %v", cause)
}
Generics
- Type parameters:
func Min[T cmp.Ordered] (a, b T) T - Use
comparablefor map keys,cmp.Orderedfor sortable types - Custom constraints:
type Number interface { ~int | ~int64 | ~float64 } - Generic type alias (Go 1.24+):
type Set[T comparable] = map[T]struct{} - Self-referential constraints (Go 1.26+):
type Adder[A Adder[A]] interface { Add(A) A } - Prefer concrete types when generics add no value
- Use
anysparingly; prefer specific constraints
Built-in Functions
min(a, b, c)andmax(a, b, c)- compute smallest/largest values (Go 1.21+)clear(m)- delete all map entries;clear(s)- zero all slice elements (Go 1.21+)new(expr)- allocate and initialize with value (Go 1.26+):ptr := new(computeValue())
Testing
Table-Driven Tests
func TestFoo(t *testing.T) {
testCases := []struct {
name string
input string
expectedResult string
expectedError error
}{
{
name: "EmptyInput",
input: "",
expectedError: ErrEmpty,
},
{
name: "ValidInput",
input: "hello",
expectedResult: "HELLO",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := Foo(tc.input)
if tc.expectedError != nil {
require.ErrorIs(t, err, tc.expectedError)
return
}
require.NoError(t, err)
require.Equal(t, tc.expectedResult, got)
})
}
}
Benchmarks (Go 1.24+)
func BenchmarkFoo(b *testing.B) {
for b.Loop() { // Cleaner than for i := 0; i < b.N; i++
Foo()
}
}
Benefits: single execution per -count, prevents compiler optimizations away.
Assertions
- Use the testify library for conciseness. Use
requirefor fatal assertions,assertfor non-fatal require.ErrorIsfor sentinel errors (not string matching)require.JSONEq/require.YAMLEqfor semantic comparison- Use
testdata/folders for expected values - Use
embed.FSfor test data files
Practices
- Test function names: use
TestFooBar(PascalCase), notTestFoo_Bar(no underscores) t.Helper()in helper functionst.Cleanup()for resource cleanupt.Context()for test-scoped context (Go 1.24+)t.Chdir()for temp directory changes (Go 1.24+)t.ArtifactDir()for test output files (Go 1.26+)t.Parallel()for independent tests-raceflag always- Don't test stdlib; test YOUR code
- Bug fix → add regression test first
- Concurrent code needs concurrent tests
Testing Concurrent Code with synctest (Go 1.25+)
testing/synctest creates an isolated "bubble" with virtualized time. The fake clock advances automatically when all goroutines in the bubble are blocked.
import "testing/synctest"
func TestPeriodicWorker(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
var count atomic.Int32
go func() {
for {
time.Sleep(time.Second)
count.Add(1)
}
}()
// Fake clock advances 5s instantly (no real waiting)
time.Sleep(5 * time.Second)
synctest.Wait() // wait for all goroutines to settle
require.Equal(t, int32(5), count.Load())
})
}
Key rules:
synctest.Wait()blocks until all bubble goroutines are idletime.Sleep,time.After,time.NewTimer,time.NewTickerall use the fake clock inside the bubble- Goroutines spawned inside the bubble belong to it; goroutines outside are unaffected
- Blocking on I/O or syscalls does NOT advance the clock — only channel ops, sleeps, and sync primitives do
- Prefer
synctest.Testover manualtimeNowmocking for new code
Concurrency
Share memory by communicating. Channels orchestrate; mutexes serialize. Use errgroup.WithContext for goroutines returning errors, sync.WaitGroup.Go() (Go 1.25+) for simple fan-out. Prefer synchronous functions; let callers add concurrency. See concurrency reference for design patterns, mutex/channel guidelines, and channel axioms.
Iterators (Go 1.22+)
Range over integers (for i := range 10), functions (Go 1.23+), and use slices/maps iterator helpers. See iterators reference for full API and custom iterator patterns.
Interface Design
- Accept interfaces, return concrete types
- Define interfaces at the consumer, not the provider; keep them small (1-2 methods)
- Compile-time interface check:
var _ http.Handler = (*MyHandler)(nil) - For single-method dependencies, use function types instead of interfaces
- Don't embed types in exported structs—exposes methods and breaks API compatibility
Slice & Map Patterns
- Pre-allocate when size known:
make([]User, 0, len(ids)) - Nil vs empty:
var t []string(nil, JSON null) vst := []string{}(non-nil, JSON[]) - Copy at boundaries with
slices.Clone(items)to prevent external mutations - Prefer
strings.Cut(s, "/")overstrings.Splitfor prefix/suffix extraction - Append handles nil:
var items []Item; items = append(items, newItem)
Common Patterns
Options Pattern
Define type Option func(*Config). Create WithX functions returning Option that set fields. Constructor takes ...Option, applies each to default config.
Default Values (Go 1.22+)
Use cmp.Or(a, b, c) to return first non-zero value—e.g., cmp.Or(cfg.Port, envPort, 8080).
Context Usage
- First parameter:
func Foo(ctx context.Context, ...) - Don't store in structs
- Use for cancellation, deadlines, request-scoped values only
HTTP Best Practices
- Use
http.Server{}with explicitReadTimeout/WriteTimeout; avoidhttp.ListenAndServe - Always
defer resp.Body.Close()after checking error - Accept
*http.Clientas dependency for testability
HTTP Routing (Go 1.22+)
// Method-based routing with path patterns
mux.HandleFunc("POST /items/create", createHandler)
mux.HandleFunc("GET /items/{id}", getHandler)
mux.HandleFunc("GET /files/{path...}", serveFiles) // Greedy wildcard
// Extract path values
func getHandler(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
}
AIP: Resource-Oriented gRPC APIs
For building resource-oriented gRPC APIs following Google AIP standards, use einride/aip-go (go.einride.tech/aip).
- Resource names (AIP-122): hierarchical paths like
publishers/123/books/les-miserables; useresourcename.Sscan/Sprint/Matchfor parsing and construction - Standard methods: Get (131), List (132), Create (133), Update (134), Delete (135) — prefer these over custom methods
- Pagination (AIP-158): implement from day one with
pagination.ParsePageToken; opaque tokens, coerce oversizedpage_size, never requirepage_size - Filtering (AIP-160): parse with
filtering.ParseFilterand typedDeclarations; validate server-side, returnINVALID_ARGUMENT - Ordering (AIP-132): parse with
ordering.ParseOrderBy; validate against allowed fields withValidateForPaths - Field masks (AIP-134, AIP-161): use
fieldmask.Updatefor partial updates,fieldmask.Validatefor path validation; preferPATCHoverPUT - Field behavior (AIP-203): annotate every field —
REQUIRED,OPTIONAL,OUTPUT_ONLY,IMMUTABLE, orIDENTIFIER
See AIP reference for detailed patterns, code examples, and best practices.
CSRF Protection (Go 1.25+)
import "net/http"
handler := http.CrossOriginProtection(myHandler)
// Rejects non-safe cross-origin requests using Fetch metadata
Directory-Scoped File Access (Go 1.24+)
root, err := os.OpenRoot("/var/data")
if err != nil {
return err
}
f, err := root.Open("file.txt") // Can't escape /var/data
Prevents path traversal attacks; works like a chroot.
Cleanup Functions (Go 1.24+)
runtime.AddCleanup(obj, func() { cleanup() })
Advantages over SetFinalizer: multiple cleanups per object, works with interior pointers, no cycle leaks, faster.
Structured Logging (log/slog)
- Use
slog.Info("msg", "key", value, "key2", value2)with key-value pairs - Add persistent attributes:
logger := slog.With("service", "api") - JSON output:
slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
Common Gotchas
- Check errors immediately (Go 1.25 fixed compiler bug): always check
err != nilbefore using any returned values// WRONG: could execute f.Name() before err check in Go 1.21-1.24 f, err := os.Open("file") name := f.Name() if err != nil { return } // CORRECT: check immediately f, err := os.Open("file") if err != nil { return } name := f.Name() time.Ticker: always callStop()to prevent leaks- Slices hold refs to backing arrays (can retain large memory)
nilinterface vsnilconcrete:var err error = (*MyError)(nil)→err != nilis true- Loop variables: each iteration has own copy (Go 1.22+); older versions share
- Timer/Ticker channels: capacity 0 (Go 1.23+); previously capacity 1
init()is an anti-pattern; prefer explicit initialization
Linting
Use golangci-lint with recommended linters: errcheck, govet, staticcheck, gocritic, gofumpt, wrapcheck, errorlint. See linting reference for full config.
Pre-Commit
Check for Makefile targets first (make help, or read Makefile). Common targets:
make lintormake checkmake testmake build
Fallback if no Makefile:
go build ./...go test -v -race ./...golangci-lint rungo fix ./...(Go 1.26+: modernizes code to latest idioms)gofmt -w .orgoimports -w .go mod tidy
Performance
Profile-Guided Optimization (PGO)
# Collect profile
go test -cpuprofile=default.pgo
# PGO automatically enabled if default.pgo exists in main package
go build # Uses default.pgo
# Typical 2-7% performance improvement
Security
Based on OWASP Go Secure Coding Practices. Read the linked reference for each topic.
Quick Checklist
Input/Output:
- All user input validated server-side
- SQL queries use prepared statements only
- XSS protection via
html/template - CSRF tokens on state-changing requests
- File paths validated against traversal (
os.OpenRootGo 1.24+)
Auth/Sessions:
- Passwords hashed with bcrypt/Argon2/PBKDF2
-
crypto/randfor all tokens/session IDs (crypto/rand.Text()Go 1.24+) - Secure cookie flags (HttpOnly, Secure, SameSite)
- Session expiration enforced
Communication:
- HTTPS/TLS everywhere, TLS 1.2+ only (post-quantum ML-KEM default Go 1.24+)
- HSTS header set
-
InsecureSkipVerify = false
Data Protection:
- Secrets in environment variables, never in logs/errors
- Generic error messages to users
Detailed Guides
- Input Validation — whitelisting, boundary checks, escaping
- Database Security — prepared statements, parameterized queries
- Authentication — bcrypt, password storage
- Cryptography —
crypto/rand, nevermath/randfor security - Session Management — secure cookies, session lifecycle
- TLS/HTTPS — TLS config, HSTS, post-quantum key exchanges
- CSRF Protection — token generation,
http.CrossOriginProtection(Go 1.25+) - Secure Error Handling — generic user messages, detailed server logs
- File Security — path traversal prevention,
os.OpenRoot - Security Logging — what to log, what never to log
- Access Control
- XSS Prevention
Security Tools
| Tool | Purpose | Command |
|---|---|---|
| gosec | Security scanner | gosec ./... |
| govulncheck | Vulnerability scanner | govulncheck ./... |
| trivy | Container/dep scanner | trivy fs . |