| name | golang-patterns |
| description | Idiomatic Go patterns, best practices, and conventions for building robust, efficient, and maintainable Go applications. Use when writing, reviewing, or refactoring Go code. |
Go Development Patterns
Idiomatic Go patterns and best practices for building robust, efficient, and maintainable applications.
When to Activate
- Writing new Go code
- Reviewing Go code
- Refactoring existing Go code
- Designing Go packages/modules
Core Principles
1. Simplicity and Clarity
Go favors simplicity over cleverness. Code should be obvious and easy to read.
func GetUser(ctx context.Context, id string) (*User, error) {
user, err := db.FindUser(ctx, id)
if err != nil {
return nil, fmt.Errorf("get 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.
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Inc() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
3. Accept Interfaces, Return Structs
Functions should accept interface parameters and return concrete types.
func ProcessData(r io.Reader) (*Result, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return &Result{Data: data}, nil
}
Error Handling Patterns
Error Wrapping with Context
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load config %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config %s: %w", path, err)
}
return &cfg, nil
}
Custom Error Types
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
)
Error Checking with errors.Is and errors.As
func HandleError(err error) {
if errors.Is(err, sql.ErrNoRows) {
log.Println("No records found")
return
}
var validationErr *ValidationError
if errors.As(err, &validationErr) {
log.Printf("Validation error on field %s: %s",
validationErr.Field, validationErr.Message)
return
}
log.Printf("unexpected error: %v", err)
}
Concurrency Patterns
Worker Pool
func WorkerPool(ctx context.Context, jobs <-chan Job, results chan<- Result, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- process(job)
}
}()
}
wg.Wait()
close(results)
}
Context for Cancellation and Timeouts
func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
Graceful Shutdown
func GracefulShutdown(server *http.Server) {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("server forced to shutdown: %v", err)
}
log.Println("Server exited")
}
errgroup for Coordinated Goroutines
func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([][]byte, len(urls))
for i, url := range urls {
i, url := i, url
g.Go(func() error {
data, err := FetchWithTimeout(ctx, url)
if err != nil {
return err
}
results[i] = data
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
Avoiding Goroutine Leaks
func safeFetch(ctx context.Context, url string) <-chan []byte {
ch := make(chan []byte, 1)
go func() {
data, err := fetch(url)
if err != nil {
return
}
select {
case ch <- data:
case <-ctx.Done():
}
}()
return ch
}
Interface Design
Small, Focused Interfaces
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
Define Interfaces Where They're Used
package service
type UserStore interface {
GetUser(ctx context.Context, id string) (*User, error)
SaveUser(ctx context.Context, user *User) error
}
type Service struct {
store UserStore
}
Package Organization
Avoid Package-Level State
var db *sql.DB
type Server struct {
db *sql.DB
}
func NewServer(db *sql.DB) *Server {
return &Server{db: db}
}
Struct Design
Functional Options Pattern
type Server struct {
addr string
timeout time.Duration
logger *log.Logger
}
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) { s.timeout = d }
}
func WithLogger(l *log.Logger) Option {
return func(s *Server) { s.logger = l }
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{
addr: addr,
timeout: 30 * time.Second,
logger: log.Default(),
}
for _, opt := range opts {
opt(s)
}
return s
}
Memory and Performance
Preallocate Slices When Size is Known
func processItems(items []Item) []Result {
results := make([]Result, 0, len(items))
for _, item := range items {
results = append(results, process(item))
}
return results
}
Avoid String Concatenation in Loops
func join(parts []string) string {
var sb strings.Builder
for i, p := range parts {
if i > 0 {
sb.WriteString(",")
}
sb.WriteString(p)
}
return sb.String()
}
Quick Reference: Go Idioms
| Idiom | Description |
|---|
| Accept interfaces, return structs | Functions accept interface params, return concrete types |
| Errors are values | Treat errors as first-class values, not exceptions |
| Don't communicate by sharing memory | Use channels for coordination |
| Make the zero value useful | Types should work without explicit initialization |
| A little copying is better than a little dependency | Avoid unnecessary external deps |
| Clear is better than clever | Prioritize readability |
| Return early | Handle errors first, keep happy path unindented |
Struct Tags
Always add JSON/YAML tags. Use Huma validation tags:
- All fields:
doc
- Strings:
maxLength, example, pattern, patternDescription
- Integers:
maximum, example
- Arrays:
maxItems (no example)
- Objects/maps: no
example tag
type Provider struct {
Name string `json:"name" yaml:"name" doc:"Provider name" maxLength:"128" example:"redis"`
Version string `json:"version" yaml:"version" doc:"Semantic version" pattern:"^v?\\d+\\.\\d+\\.\\d+$" patternDescription:"semver format" example:"1.0.0"`
Port int `json:"port" yaml:"port" doc:"Listen port" maximum:"65535" example:"8080"`
}
Anti-Patterns to Avoid
func process() (result int, err error) {
return
}
func GetUser(id string) *User {
user, err := db.Find(id)
if err != nil {
panic(err)
}
return user
}
type Request struct {
ctx context.Context
ID string
}
func ProcessRequest(ctx context.Context, id string) error {
return nil
}