Go Patterns
Description
Go Patterns teaches idiomatic Go coding: handle errors, manage concurrency with goroutines and channels, design interfaces, and structure packages for robust apps.
When to Use
How do I write idiomatic Go with proper error handling? | Give me Go patterns for concurrency and channels. | Show best practices for Go interfaces and dependency injection. | How should I structure Go packages and modules? | Provide error wrapping and unwrapping examples in Go.
Use Cases
Write idiomatic Go with explicit error handling. | Design small, cohesive interfaces. | Implement safe goroutines with context cancellation. | Organize code into clear packages and modules.
SKILL.md Content
---
name: go-patterns
description: "Go Patterns teaches idiomatic Go coding: handle errors, manage concurrency with goroutines and channels, design interfaces, and structure packages for robust apps."
metadata:
tags: "go, go-patterns, concurrency, error-handling, interfaces, package-structure"
source: "https://skilldb.dev/skills/autonomous-agent-skills/go-patterns"
pack: "autonomous-agent-skills"
category: "Autonomous Agents"
---
# Go Patterns
## When to use this skill
Use when the user says things like:
- "How do I write idiomatic Go with proper error handling?"
- "Give me Go patterns for concurrency and channels."
- "Show best practices for Go interfaces and dependency injection."
- "How should I structure Go packages and modules?"
- "Provide error wrapping and unwrapping examples in Go."
You are an autonomous agent that writes and maintains Go code. Your role is to produce idiomatic Go that follows community conventions, handles errors explicitly, uses concurrency primitives correctly, and organizes code into well-structured packages.
## Philosophy
Go values simplicity, readability, and explicitness. There is usually one obvious way to do things, and that way is intentionally simple. Do not fight the language by importing patterns from Java or Python. Embrace small interfaces, explicit error handling, and flat package structures. The Go proverb applies: "Clear is better than clever."
## Techniques
### Error Handling Conventions
- Return errors as the last return value: `func Read(path string) ([]byte, error)`.
- Check errors immediately after the call. Do not defer error checks.
- Wrap errors with context using `fmt.Errorf("failed to read config: %w", err)`. The `%w` verb allows callers to unwrap and inspect the original error.
- Use `errors.Is()` and `errors.As()` to check error types in the chain, not `==` comparison.
- Define sentinel errors with `var ErrNotFound = errors.New("not found")` for errors that callers need to match on.
- Create custom error types when you need to attach structured data to errors.
- Never ignore errors silently. If you intentionally discard an error, assign to `_` and add a comment explaining why.
### Goroutines and Channels
- Launch goroutines for independent concurrent work: `go func() { ... }()`.
- Always ensure goroutines can terminate. Pass a `context.Context` or a `done` channel for cancellation.
- Use channels for communication between goroutines. Prefer unbuffered channels unless you have a specific reason for buffering.
- Use `select` to multiplex channel operations and handle timeouts: `case <-ctx.Done():`.
- Use `sync.WaitGroup` to wait for a group of goroutines to complete.
- Use `sync.Mutex` or `sync.RWMutex` for shared state when channels are not a natural fit.
- Never launch goroutines without a plan for their lifecycle. Leaked goroutines are memory leaks.
### Interface Design
- Define interfaces where they are used (consumer side), not where they are implemented (producer side).
- Keep interfaces small. One or two methods is ideal. `io.Reader`, `io.Writer`, `fmt.Stringer` are good models.
- Accept interfaces, return structs. Functions should take interface parameters and return concrete types.
- Do not create interfaces preemptively. Wait until you have two or more implementations or need to mock in tests.
- Use the empty interface `any` (alias for `interface{}`) sparingly. Prefer typed parameters.
### Package Organization
- Organize by domain, not by technical layer. `package user` with its handlers, storage, and models, not `package handlers`, `package models`.
- Keep `main` packages thin. The `main` function should wire dependencies and start the application.
- Use `internal/` for packages that should not be imported by external modules.
- Avoid circular dependencies by moving shared types into a separate, dependency-free package.
- Name packages with short, lowercase, single-word names. No underscores, no camelCase.
### Context Propagation
- Pass `context.Context` as the first parameter of functions that perform I/O, run for a long time, or need cancellation: `func FetchUser(ctx context.Context, id string) (*User, error)`.
- Create child contexts with `context.WithCancel`, `context.WithTimeout`, or `context.WithValue`.
- Check `ctx.Err()` or `ctx.Done()` in long-running loops to respect cancellation.
- Do not store contexts in structs. Pass them through function parameters.
- Use `context.WithValue` sparingly — for request-scoped metadata (request ID, trace ID), not for passing dependencies.
### Dependency Injection in Go
- Use constructor functions that accept dependencies as parameters: `func NewUserService(repo UserRepository, logger *slog.Logger) *UserService`.
- Define dependencies as interfaces so they can be substituted in tests.
- Wire dependencies in `main` or a dedicated setup function. Avoid global state and init functions.
- For complex applications, consider a dependency injection tool like Wire for compile-time injection.
### Testing Patterns
- Write tests in `_test.go` files alongside the code they test.
- Use table-driven tests for testing multiple cases against the same function:
```go
tests := []struct{ name string; input int; want int }{ ... }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ... }) }
```
- Use `testing.T.Helper()` in test helper functions so error messages point to the caller.
- Use `testify/assert` or `testify/require` if the project already uses them, but standard library testing is sufficient.
- Use `httptest.NewServer` for testing HTTP handlers and clients.
- Write benchmarks with `func BenchmarkX(b *testing.B)` for performance-sensitive code.
### Go Modules
- Initialize modules with `go mod init module/path`. Use a repository URL as the module path.
- Run `go mod tidy` to add missing and remove unused dependencies.
- Vendor dependencies with `go mod vendor` if the project requires vendoring.
- Upgrade dependencies carefully. Review changelogs before bumping major versions.
- Use `go.sum` for dependency verification. Always commit both `go.mod` and `go.sum`.
### Struct Embedding and Receiver Methods
- Use struct embedding for composition, not inheritance: embed `sync.Mutex` in a struct to gain `Lock()`/`Unlock()` methods.
- Use pointer receivers (`func (s *Service) Do()`) when the method modifies the receiver or the struct is large.
- Use value receivers (`func (s Service) String()`) for small, immutable structs and when implementing interfaces like `fmt.Stringer`.
- Be consistent: if any method on a type uses a pointer receiver, all methods should use pointer receivers.
## Best Practices
- Run `go vet` and `golangci-lint` before committing. They catch common mistakes the compiler misses.
- Use `gofmt` or `goimports` for consistent formatting. Go has one standard style — use it.
- Use `slog` (Go 1.21+) for structured logging. Avoid `log.Println` in production code.
- Use `errors.Join` (Go 1.20+) to combine multiple errors.
- Document exported functions and types with comments starting with the name: `// NewServer creates a new HTTP server`.
## Anti-Patterns
- Using `panic` for error handling instead of returning errors.
- Launching goroutines without cancellation or termination mechanisms.
- Creating large interfaces with many methods that force implementers to stub unused methods.
- Using `init()` functions for complex setup logic instead of explicit initialization in `main`.
- Putting all code in one package or creating deeply nested package hierarchies.
- Ignoring context cancellation in long-running operations.
- Using global variables for configuration or shared state.
- Writing tests that depend on execution order or global state.