go-best-practices ยท diff
git:20260113.222b7de to git:20260705.e9e4712
12 added, 157 removed. Audit A to A.
---
name: go-best-practices
- description: Provides Go patterns for type-first development with custom types, interfaces, functional options, and error handling. Must use when reading or writing Go files.
+ description: Use when reading or writing Go files (.go, go.mod).
---
# Go Best Practices
- ## Type-First Development
-
- Types define the contract before implementation. Follow this workflow:
-
- 1. **Define data structures** - structs and interfaces first
- 2. **Define function signatures** - parameters, return types, and error conditions
- 3. **Implement to satisfy types** - let the compiler guide completeness
- 4. **Validate at boundaries** - check inputs where data enters the system
+ Follows type-first, functional, and error handling patterns from CLAUDE.md. This skill covers language-specific idioms only.
- ### Make Illegal States Unrepresentable
+ ## Make Illegal States Unrepresentable
Use Go's type system to prevent invalid states at compile time.
- **Structs for domain models:**
- ```go
- // Define the data model first
- type User struct {
- ID UserID
- Email string
- Name string
- CreatedAt time.Time
- }
-
- type CreateUserRequest struct {
- Email string
- Name string
- }
-
- // Functions follow from the types
- func CreateUser(req CreateUserRequest) (*User, error) {
- // implementation
- }
- ```
-
**Custom types for domain primitives:**
```go
// Distinct types prevent mixing up IDs
type UserID string
type OrderID string
func GetUser(id UserID) (*User, error) {
// Compiler prevents passing OrderID here
}
- func NewUserID(raw string) UserID {
- return UserID(raw)
- }
-
// Methods attach behavior to the type
func (id UserID) String() string {
return string(id)
}
```
**Interfaces for behavior contracts:**
```go
// Define what you need, not what you have
- type Reader interface {
- Read(p []byte) (n int, err error)
- }
-
type UserRepository interface {
GetByID(ctx context.Context, id UserID) (*User, error)
Save(ctx context.Context, user *User) error
}
// Accept interfaces, return structs
- func ProcessInput(r Reader) ([]byte, error) {
+ func ProcessInput(r io.Reader) ([]byte, error) {
return io.ReadAll(r)
}
```
- **Enums with iota:**
+ **Enums with iota and exhaustive switch:**
```go
type Status int
const (
StatusActive Status = iota + 1
StatusInactive
StatusPending
)
- func (s Status) String() string {
- switch s {
- case StatusActive:
- return "active"
- case StatusInactive:
- return "inactive"
- case StatusPending:
- return "pending"
- default:
- return fmt.Sprintf("Status(%d)", s)
- }
- }
-
- // Exhaustive handling in switch
func ProcessStatus(s Status) (string, error) {
switch s {
case StatusActive:
return "processing", nil
case StatusInactive:
return "skipped", nil
case StatusPending:
return "waiting", nil
default:
return "", fmt.Errorf("unhandled status: %v", s)
}
}
```
**Functional options for flexible construction:**
```go
type ServerOption func(*Server)
func WithPort(port int) ServerOption {
- return func(s *Server) {
- s.port = port
- }
- }
-
- func WithTimeout(d time.Duration) ServerOption {
- return func(s *Server) {
- s.timeout = d
- }
+ return func(s *Server) { s.port = port }
}
func NewServer(opts ...ServerOption) *Server {
- s := &Server{
- port: 8080, // sensible defaults
- timeout: 30 * time.Second,
- }
+ s := &Server{port: 8080, timeout: 30 * time.Second}
for _, opt := range opts {
opt(s)
}
return s
}
-
// Usage: NewServer(WithPort(3000), WithTimeout(time.Minute))
```
**Embed for composition:**
```go
type Timestamps struct {
CreatedAt time.Time
UpdatedAt time.Time
}
type User struct {
- Timestamps // embedded - User has CreatedAt, UpdatedAt
+ Timestamps // User gains CreatedAt, UpdatedAt
ID UserID
Email string
}
```
- ## Module Structure
-
- Prefer smaller files within packages: one type or concern per file. Split when a file handles multiple unrelated types or exceeds ~300 lines. Keep tests in `_test.go` files alongside implementation. Package boundaries define the API; internal organization is flexible.
-
- ## Functional Patterns
-
- - Use value receivers when methods don't mutate state; reserve pointer receivers for mutation.
- - Avoid package-level mutable variables; pass dependencies explicitly via function parameters.
- - Return new structs/slices rather than mutating inputs; makes data flow explicit.
- - Use closures and higher-order functions where they simplify code (e.g., `sort.Slice`, iterators).
-
- ## Instructions
-
- - Return errors with context using `fmt.Errorf` and `%w` for wrapping. This preserves the error chain for debugging.
- - Every function returns a value or an error; unimplemented paths return descriptive errors. Explicit failures are debuggable.
- - Handle all branches in `switch` statements; include a `default` case that returns an error. Exhaustive handling prevents silent bugs.
- - Pass `context.Context` to external calls with explicit timeouts. Runaway requests cause cascading failures.
- - Reserve `panic` for truly unrecoverable situations; prefer returning errors. Panics crash the program.
- - Add or update table-driven tests for new logic; cover edge cases (empty input, nil, boundaries).
-
- ## Examples
-
- Explicit failure for unimplemented logic:
- ```go
- func buildWidget(widgetType string) (*Widget, error) {
- return nil, fmt.Errorf("buildWidget not implemented for type: %s", widgetType)
- }
- ```
+ ## Go-Specific Error Handling
- Wrap errors with context to preserve the chain:
+ Wrap errors with `%w` to preserve the chain for `errors.Is` / `errors.As`:
```go
out, err := client.Do(ctx, req)
if err != nil {
return nil, fmt.Errorf("fetch widget failed: %w", err)
}
- return out, nil
```
- Exhaustive switch with default error:
- ```go
- func processStatus(status string) (string, error) {
- switch status {
- case "active":
- return "processing", nil
- case "inactive":
- return "skipped", nil
- default:
- return "", fmt.Errorf("unhandled status: %s", status)
- }
- }
- ```
+ ## Structured Logging
- Structured logging with slog:
+ Use `log/slog` with structured key-value pairs:
```go
import "log/slog"
var log = slog.With("component", "widgets")
func createWidget(name string) (*Widget, error) {
log.Debug("creating widget", "name", name)
widget := &Widget{Name: name}
log.Debug("created widget", "id", widget.ID)
return widget, nil
- }
- ```
-
- ## Configuration
-
- - Load config from environment variables at startup; validate required values before use. Missing config should cause immediate exit.
- - Define a Config struct as single source of truth; avoid `os.Getenv` scattered throughout code.
- - Use sensible defaults for development; require explicit values for production secrets.
-
- ### Examples
-
- Typed config struct:
- ```go
- type Config struct {
- Port int
- DatabaseURL string
- APIKey string
- Env string
- }
-
- func LoadConfig() (*Config, error) {
- dbURL := os.Getenv("DATABASE_URL")
- if dbURL == "" {
- return nil, fmt.Errorf("DATABASE_URL is required")
- }
- apiKey := os.Getenv("API_KEY")
- if apiKey == "" {
- return nil, fmt.Errorf("API_KEY is required")
- }
- port := 3000
- if p := os.Getenv("PORT"); p != "" {
- var err error
- port, err = strconv.Atoi(p)
- if err != nil {
- return nil, fmt.Errorf("invalid PORT: %w", err)
- }
- }
- return &Config{
- Port: port,
- DatabaseURL: dbURL,
- APIKey: apiKey,
- Env: getEnvOrDefault("ENV", "development"),
- }, nil
}
```