v1.0.26 — Go programming language skill for writing idiomatic Go code, code review, error handling, testing, concurrency, security, and program design. Use when writing, reviewing, debugging, or asking about Go code — even if the user doesn't explicitly mention 'Go best practices'. Also use when: 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, configuring golangci-lint, setting up structured logging with slog, or any question about Go idioms and patterns. 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.
From gopilotnpx claudepluginhub gonzaloserrano/gopilot --plugin gopilotThis skill uses the workspace's default tool permissions.
reference/access-control.mdreference/authentication.mdreference/cryptography.mdreference/csrf.mdreference/database-security.mdreference/error-handling.mdreference/file-security.mdreference/go-aip.mdreference/input-validation.mdreference/linting.mdreference/logging.mdreference/security-checklist.mdreference/session-management.mdreference/tls-https.mdreference/xss.mdbytes.Buffer and sync.Mutex work without init. When zero values compose, there's less API.continue for invalid items instead of nestinghttp.Client not http.HTTPClientFoo() not GetFoo()Must prefix for panicking functionsiota + 1 to start at one, distinguishing intentional values from zero defaultfmt.Errorf("get config: %w", err) -- use low-cardinality strings only (no IDs, names, or variable data in the format string; attach those as structured slog attributes so APM tools can group errors)
// Bad: high-cardinality error string -- APM sees each user as a unique error
return fmt.Errorf("fetch user %s: %w", userID, err)
// Good: stable error string + structured context
slog.ErrorContext(ctx, "fetch user", "user_id", userID, "error", err)
return fmt.Errorf("fetch user: %w", err)
var ErrNotFound = errors.New("not found")errors.Is(err, ErrNotFound) or errors.As(err, &target), or use generic errors.AsType[T] (Go 1.26+)errors.New over fmt.Errorf without formattingerr := errors.Join(err1, err2, err3) (Go 1.20+)"connect: %w" not "failed to connect: %w", "fetch config: %w" not "error fetching config: %w")Prefer opaque error handling: treat errors as opaque values, don't inspect internals. This minimizes coupling.
Three strategies in order of preference:
err != nil.var ErrNotFound = errors.New(...)): use sparingly for expected conditions callers must distinguish. They become public API.type NotFoundError struct{...}): use when callers need structured context. Also public API — avoid when opaque or sentinel suffices.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()
}
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.
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)
}
func Min[T cmp.Ordered] (a, b T) Tcomparable for map keys, cmp.Ordered for sortable typestype Number interface { ~int | ~int64 | ~float64 }type Set[T comparable] = map[T]struct{}type Adder[A Adder[A]] interface { Add(A) A }any sparingly; prefer specific constraintsmin(a, b, c) and max(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())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)
})
}
}
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.
require for fatal assertions, assert for non-fatalrequire.ErrorIs for sentinel errors (not string matching)require.JSONEq/require.YAMLEq for semantic comparisontestdata/ folders for expected valuesembed.FS for test data filesTestFooBar (PascalCase), not TestFoo_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-race flag alwaysgo.uber.org/goleak:
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m) // fails if any goroutine outlives the test suite
}
// Or per-test:
func TestFoo(t *testing.T) {
defer goleak.VerifyNone(t)
// ...
}
Use goleak.IgnoreTopFunction("...") to allowlist known long-lived goroutines (e.g., signal.Notify handler)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.NewTicker all use the fake clock inside the bubblesynctest.Test over manual timeNow mocking for new codeShare memory by communicating -- channels orchestrate; mutexes serialize.
| Need | Use | Why |
|---|---|---|
| Single counter/flag | atomic | Lock-free, simplest |
| Protect shared struct | sync.Mutex / sync.RWMutex | Direct, no goroutine overhead |
| Transfer ownership of data | Unbuffered channel | Synchronizes sender and receiver |
| Fan-out/fan-in, pipelines | Buffered channel + select | Composable, supports cancellation |
| N goroutines, first error aborts | errgroup.WithContext | Propagates cancellation |
| N goroutines, collect all errors | errgroup + errors.Join | No short-circuit |
| One-time init | sync.Once / sync.OnceValue | Race-free lazy init |
| Deduplicate concurrent calls | singleflight.Group | Coalesces in-flight requests |
errgroup.WithContext to launch goroutines that return errors; g.Wait() returns first errorsync.WaitGroup.Go() (Go 1.25+): combines Add(1) + goroutine launch
var wg sync.WaitGroup
wg.Go(func() { work() }) // Combines Add(1) + go
wg.Wait()
GOEXPERIMENT=goroutineleakprofile (Go 1.26+)context.Context for cancellation; subscribe to context.Done() for graceful shutdownsync.Mutex/RWMutex for shared state protection; zero value is ready to useRWMutex when reads far outnumber writessync.Once for one-time initialization; helpers: sync.OnceFunc(), sync.OnceValue(), sync.OnceValues() (Go 1.21+)atomic for primitive counters (simpler than mutex for single values)noCopy:
type Server struct {
noCopy noCopy // go vet reports "copies lock value" if struct is copied
mu sync.Mutex
}
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
| Operation | nil channel | closed channel |
|---|---|---|
| Send | blocks forever | panics |
| Receive | blocks forever | returns zero value |
| Close | panics | panics |
select to disable a casefor range ch to receive until closedv, ok := <-chfor i := range 10 {
fmt.Println(i) // 0..9
}
// String iterators
for line := range strings.Lines(s) { }
for part := range strings.SplitSeq(s, sep) { }
for field := range strings.FieldsSeq(s) { }
// Slice iterators
for i, v := range slices.All(items) { }
for v := range slices.Values(items) { }
for v := range slices.Backward(items) { }
for chunk := range slices.Chunk(items, 3) { }
// Map iterators
for k, v := range maps.All(m) { }
for k := range maps.Keys(m) { }
for v := range maps.Values(m) { }
// Collect iterator to slice
keys := slices.Collect(maps.Keys(m))
sorted := slices.Sorted(maps.Keys(m))
// Custom iterator
func (s *Set[T]) All() iter.Seq[T] {
return func(yield func(T) bool) {
for v := range s.items {
if !yield(v) {
return
}
}
}
}
var _ http.Handler = (*MyHandler)(nil)make([]User, 0, len(ids))var t []string (nil, JSON null) vs t := []string{} (non-nil, JSON [])slices.Clone(items) to prevent external mutationsstrings.Cut(s, "/") over strings.Split for prefix/suffix extractionvar items []Item; items = append(items, newItem)Define type Option func(*Config). Create WithX functions returning Option that set fields. Constructor takes ...Option, applies each to default config.
Use cmp.Or(a, b, c) to return first non-zero value—e.g., cmp.Or(cfg.Port, envPort, 8080).
func Foo(ctx context.Context, ...)context.WithoutCancel(ctx) (Go 1.21+): derive a context that keeps values but ignores parent cancellation -- use for background work that must outlive the request (e.g., async cleanup, audit logging)http.Server{} with explicit ReadTimeout/WriteTimeout; avoid http.ListenAndServedefer resp.Body.Close() after checking error*http.Client as dependency for testability// 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")
}
For building resource-oriented gRPC APIs following Google AIP standards with einride/aip-go, see AIP reference -- covers resource names, standard methods (CRUD), pagination, filtering, ordering, field masks, and field behavior annotations.
http.CrossOriginProtection(handler) (Go 1.25+) -- rejects cross-origin state-changing requestsos.OpenRoot("/var/data") (Go 1.24+) -- chroot-like scoped file accessruntime.AddCleanup(obj, fn) (Go 1.24+) -- replaces SetFinalizer, supports multiple cleanups per objectslog.Info("msg", "key", value, "key2", value2) with key-value pairslogger := slog.With("service", "api")slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))| Operation | nil map | nil slice | nil channel |
|---|---|---|---|
| Read/index | zero value | panics | blocks forever |
| Write | panics | panics (index) | blocks forever |
len / cap | 0 | 0 | 0 |
range | 0 iterations | 0 iterations | blocks forever |
append | N/A | works (returns new) | N/A |
delete | no-op | N/A | N/A |
close | N/A | N/A | panics |
Don't add nil guards for values that a dependency (database, library, protocol) guarantees non-nil. Trust the contract; redundant checks add noise without safety. Only guard at true system boundaries (user input, external APIs, untrusted data).
append on a sub-slice can silently mutate the original if capacity remains:
a := []int{1, 2, 3, 4}
b := a[:2] // b shares a's backing array
b = append(b, 99) // overwrites a[2]! a is now [1, 2, 99, 4]
Fix with full-slice expression to cap the capacity:
b := a[:2:2] // len=2, cap=2 -- append allocates a new array
b = append(b, 99) // a is unchanged
| Type | Assignment copies... |
|---|---|
| bool, int, float, complex, string | value (safe) |
| array | all elements (deep) |
| struct | all fields (shallow -- pointer fields share referent) |
| slice | header only (shares backing array) |
| map | header only (shares buckets) |
| pointer, func, channel | pointer (shares referent) |
| interface | header only (shares underlying value if pointer) |
Use slices.Clone / maps.Clone for shallow copies at API boundaries.
err != nil before 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 call Stop() to prevent leaksnil interface vs nil concrete: var err error = (*MyError)(nil) → err != nil is trueinit() is an anti-pattern; prefer explicit initializationUse golangci-lint with recommended linters: errcheck, govet, staticcheck, gocritic, gofumpt, wrapcheck, errorlint. See linting reference for the full .golangci.yml config template and commands.
Check for Makefile targets first (make help, or read Makefile). Common targets:
make lint or make checkmake testmake buildFallback if no Makefile:
go build ./...go test -v -race ./...golangci-lint rungo fix ./... (Go 1.26+: modernizes code to latest idioms)gofmt -w . or goimports -w .go mod tidy# 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
Based on OWASP Go Secure Coding Practices. See security checklist and guides for the full checklist, detailed per-topic guides (input validation, auth, crypto, sessions, TLS, CSRF, file security, XSS, access control, logging), and security scanning tools (gosec, govulncheck, trivy).
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.