Go Patterns
Error Handling
Errors are values — wrap, check, and propagate explicitly.
var (
ErrNotFound = errors.New("not found")
ErrPermission = errors.New("permission denied")
)
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: %s — %s", e.Field, e.Message)
}
// Wrap with %w to preserve the chain for errors.Is / errors.As
func loadUser(id string) (User, error) {
u, err := db.FindUser(id)
if err != nil {
return User{}, fmt.Errorf("loadUser %s: %w", id, err)
}
return u, nil
}
if errors.Is(err, ErrNotFound) { /* handle */ }
var ve *ValidationError
if errors.As(err, &ve) { fmt.Println("bad field:", ve.Field) }
Interfaces
Define interfaces at the point of use (consumer), not at implementation.
// Small, focused interface
type Storer interface {
Save(ctx context.Context, item Item) error
Load(ctx context.Context, id string) (Item, error)
}
// Compose small interfaces
type ReadWriteCloser interface {
io.Reader
io.Writer
io.Closer
}
// Accept interfaces, return concrete structs
func Process(r io.Reader) error { ... }
func NewProcessor(cfg Config) (*Processor, error) { ... }
Defer
func writeFile(path string, data []byte) error {
f, err := os.Create(path)
if err != nil { return err }
defer f.Close()
_, err = f.Write(data)
return err
}
// Named returns + defer for logging
func fetchUser(id string) (user User, err error) {
defer func() {
if err != nil { log.Printf("fetchUser %s: %v", id, err) }
}()
user, err = db.Find(id)
return
}
Struct Embedding
type Logger struct{ prefix string }
func (l *Logger) Log(msg string) { fmt.Printf("[%s] %s\n", l.prefix, msg) }
type Server struct {
Logger // promotes Log method
addr string
}
s := Server{Logger: Logger{prefix: "server"}, addr: ":8080"}
s.Log("started")
iota for Typed Enums
type Direction int
const (
North Direction = iota
East
South
West
)
func (d Direction) String() string {
return [...]string{"North", "East", "South", "West"}[d]
}
type ByteSize float64
const (
_ = iota
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
)
Functional Options
type ServerOption func(*Server)
func WithTimeout(d time.Duration) ServerOption {
return func(s *Server) { s.timeout = d }
}
func WithMaxConns(n int) ServerOption {
return func(s *Server) { s.maxConns = n }
}
func NewServer(addr string, opts ...ServerOption) *Server {
s := &Server{addr: addr, timeout: 30 * time.Second, maxConns: 100}
for _, o := range opts { o(s) }
return s
}
// Usage
srv := NewServer(":8080", WithTimeout(60*time.Second), WithMaxConns(200))
Common Anti-Patterns
- Panic for recoverable errors — return errors; panic only for programming bugs
- Large interfaces — 1-3 methods max; compose if you need more
- Ignoring errors with
_— always check errors unless justified in comments init()with side effects — use explicit initialization functions instead- Naked returns in long functions — hurt readability; only OK in short functions