// REQUIRED: Errors as last return value
func ProcessOrder(id string) (*Order, error)
// REQUIRED: Handle immediately, wrap with context
if err != nil {
return fmt.Errorf("process order %s: %w", id, err)
}
// REQUIRED: Canonical errors at boundaries
var ErrOrderNotFound = errors.New("order not found")
// GOOD: Static errors
var ErrInvalidInput = errors.New("invalid input")
// GOOD: Dynamic errors with context
return fmt.Errorf("failed to connect to %s: %w", addr, err)
// BAD: Never ignore errors
_ = file.Close() // DON'T DO THIS
// GOOD: Acknowledge deliberately
if err := file.Close(); err != nil {
log.Printf("failed to close file: %v", err)
}
// REQUIRED: Compile-time race prevention
type Counter struct {
mu sync.Mutex
value int64
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
// REQUIRED: Use atomic for simple state
import "go.uber.org/atomic"
type Server struct {
running atomic.Bool
count atomic.Int64
}
// REQUIRED: Always provide stop mechanism
func (s *Server) Start(ctx context.Context) error {
stop := make(chan struct{})
done := make(chan struct{})
go func() {
defer close(done)
for {
select {
case <-ctx.Done():
return
case <-stop:
return
case <-time.After(time.Second):
s.process()
}
}
}()
// Store references for shutdown
s.stop = stop
s.done = done
return nil
}
// REQUIRED: Graceful shutdown
func (s *Server) Shutdown(ctx context.Context) error {
close(s.stop)
select {
case <-s.done:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// REQUIRED: All I/O operations accept context
func (r *Repository) GetUser(ctx context.Context, id string) (*User, error) {
query := "SELECT * FROM users WHERE id = $1"
row := r.db.QueryRowContext(ctx, query, id)
// ...
}
// REQUIRED: HTTP handlers propagate context
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user, err := h.userRepo.GetUser(ctx, userID)
if err != nil {
http.Error(w, "user not found", http.StatusNotFound)
return
}
// ...
}
// GOOD: Timeouts at service boundaries
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// BAD: Never use context.Background() in application code
// GOOD: Use context.Background() only in main(), tests, and init
// REQUIRED: Verify interface compliance
var _ http.Handler = (*OrderHandler)(nil)
var _ io.Writer = (*Logger)(nil)
// GOOD: Small, focused interfaces
type UserGetter interface {
GetUser(ctx context.Context, id string) (*User, error)
}
type UserSetter interface {
SetUser(ctx context.Context, user *User) error
}
// GOOD: Combine when needed
type UserRepository interface {
UserGetter
UserSetter
}
// BAD: Pointers to interfaces
func Process(w *io.Writer) {} // DON'T
// GOOD: Pass interfaces by value
func Process(w io.Writer) {}
// BAD: Empty interface without justification
func Handle(data interface{}) {} // DON'T
// GOOD: Use specific types
func Handle(data *Order) {}
// GOOD: When generic behavior is truly needed
func Marshal(v any) ([]byte, error) // OK for encoding
// GOOD: Use field names for external packages
user := User{
ID: "123",
Email: "[email protected]",
Name: "John Doe",
}
// GOOD: Omit zero values
config := Config{
Host: "localhost", // Port: 0 omitted
Timeout: 30 * time.Second,
}
// REQUIRED: Embed at top with separation
type Handler struct {
http.Handler // Embedded at top
logger *log.Logger
db *sql.DB
}
// BAD: Don't embed for convenience alone
type User struct {
sync.Mutex // DON'T - exposes Lock/Unlock methods
data map[string]string
}
// GOOD: Composition over embedding
type User struct {
mu sync.Mutex
data map[string]string
}
// GOOD: Copy at boundaries to prevent mutation
func (s *Service) SetItems(items []Item) {
s.items = make([]Item, len(items))
copy(s.items, items)
}
func (s *Service) GetItems() []Item {
items := make([]Item, len(s.items))
copy(items, s.items)
return items
}
// GOOD: Return nil for empty slices
if len(results) == 0 {
return nil, nil // Not []Result{}
}
// GOOD: Check length, not nil
if len(slice) == 0 {
// handle empty case
}
// GOOD: Preallocate with known capacity
results := make([]Result, 0, expectedCount)
cache := make(map[string]Value, expectedSize)
// GOOD: Clear parameter purpose
func CreateUser(email, name string, isAdmin bool) (*User, error)
// BETTER: Use options for complex configuration
type CreateUserOptions struct {
Email string
Name string
IsAdmin bool
GroupID string
}
func CreateUser(opts CreateUserOptions) (*User, error)
// BEST: Functional options for public APIs
type Option func(*config)
func WithTimeout(d time.Duration) Option {
return func(c *config) { c.timeout = d }
}
func NewClient(addr string, opts ...Option) *Client
// REQUIRED: Named returns for complex functions
func ProcessPayment(amount int64, method string) (transactionID string, err error) {
defer func() {
if err != nil {
log.Printf("payment failed: amount=%d method=%s error=%v",
amount, method, err)
}
}()
// ...
return "txn_123", nil
}
// GOOD: Early returns reduce nesting
func ValidateUser(user *User) error {
if user == nil {
return errors.New("user cannot be nil")
}
if user.Email == "" {
return errors.New("email is required")
}
if !isValidEmail(user.Email) {
return errors.New("invalid email format")
}
return nil
}
// GOOD: Minimize variable scope
if err := validateInput(req); err != nil {
return err
}
// GOOD: Use continue to reduce nesting
for _, item := range items {
if !item.IsValid() {
continue
}
if !item.IsProcessable() {
continue
}
processItem(item)
}
Apply only after profiling identifies bottlenecks
// GOOD: Use strconv for number conversions
s := strconv.Itoa(42) // Not fmt.Sprint(42)
i, err := strconv.Atoi("42") // Not fmt.Sscanf
// GOOD: Use strings.Builder for concatenation
var buf strings.Builder
for _, item := range items {
buf.WriteString(item.String())
buf.WriteByte('\n')
}
result := buf.String()
// GOOD: Avoid repeated conversions in loops
data := []byte("payload")
for i := 0; i < n; i++ {
conn.Write(data) // Not []byte("payload") each time
}
// GOOD: Preallocate known capacity
items := make([]Item, 0, count)
cache := make(map[string]Value, expectedSize)
// GOOD: Reuse slices when possible
type Processor struct {
buffer []byte
}
func (p *Processor) Process(data []byte) []byte {
if cap(p.buffer) < len(data)*2 {
p.buffer = make([]byte, len(data)*2)
}
p.buffer = p.buffer[:len(data)*2]
// process into buffer...
return p.buffer
}
func TestProcessOrder(t *testing.T) {
tests := []struct {
name string
input OrderRequest
want *Order
wantErr string
}{
{
name: "valid order",
input: OrderRequest{
CustomerID: "cust_123",
Amount: 1000,
},
want: &Order{
ID: "order_123",
CustomerID: "cust_123",
Amount: 1000,
Status: StatusPending,
},
},
{
name: "missing customer",
input: OrderRequest{
Amount: 1000,
},
wantErr: "customer ID is required",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ProcessOrder(tt.input)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
// GOOD: Helper functions start with test prefix
func testCreateUser(t *testing.T, email string) *User {
t.Helper()
user, err := CreateUser(CreateUserOptions{Email: email})
require.NoError(t, err)
return user
}
// GOOD: Test doubles in separate package
// Package: usertest
type StubRepository struct {
users map[string]*User
err error
}
func (r *StubRepository) GetUser(ctx context.Context, id string) (*User, error) {
if r.err != nil {
return nil, r.err
}
return r.users[id], nil
}
// GOOD: Multiple behaviors
func AlwaysFailsRepository() UserRepository {
return &StubRepository{err: errors.New("repository error")}
}
// GOOD: Structured logging with fields
logger.Info("order processed",
zap.String("order_id", order.ID),
zap.String("customer_id", order.CustomerID),
zap.Int64("amount", order.Amount),
zap.Duration("processing_time", time.Since(start)),
)
// GOOD: Log levels by importance
logger.Debug("cache hit", zap.String("key", key)) // Development
logger.Info("user created", zap.String("id", userID)) // Normal operation
logger.Warn("rate limit approached", zap.Int("count", count)) // Attention needed
logger.Error("database connection failed", zap.Error(err)) // Requires action
// GOOD: RED metrics (Rate, Errors, Duration)
var (
requestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "endpoint", "status"},
)
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
},
[]string{"method", "endpoint"},
)
)
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
start := time.Now()
defer func() {
duration := time.Since(start).Seconds()
requestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
}()
// ... handle request ...
status := "200" // capture actual status
requestsTotal.WithLabelValues(r.Method, r.URL.Path, status).Inc()
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Start services
server := &http.Server{Addr: ":8080"}
// Handle shutdown signals
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("shutting down gracefully...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Printf("server shutdown error: %v", err)
}
cancel() // Signal other goroutines to stop
}()
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}
// GOOD: Package names
package user // Not users
package payment // Not payments
package httptransport // Not http_transport
// GOOD: Avoid generic names
package service // BAD - too generic
package userservice // BETTER
package user // BEST - clear context
// GOOD: Package-level documentation
// Package user provides user management functionality.
//
// This package handles user creation, authentication, and profile management.
// It provides both HTTP handlers and a service layer for business logic.
package user
// GOOD: Standard library first, then external, then internal
import (
"context"
"fmt"
"time"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
"myapp/internal/user"
"myapp/pkg/database"
)
// GOOD: Alias only when necessary
import (
"database/sql"
sqldriver "database/sql/driver" // Avoid conflict
)
// GOOD: Group by purpose, not alphabetically
// Constructor and main methods first
func New(opts Options) *Service
func (s *Service) Start(ctx context.Context) error
func (s *Service) Stop(ctx context.Context) error
// Core business methods
func (s *Service) CreateUser(ctx context.Context, req CreateUserRequest) (*User, error)
func (s *Service) GetUser(ctx context.Context, id string) (*User, error)
func (s *Service) UpdateUser(ctx context.Context, user *User) error
// Helper methods last
func (s *Service) validateUser(user *User) error
func (s *Service) hashPassword(password string) (string, error)
// REQUIRED: Always close resources
func ProcessFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("open file: %w", err)
}
defer func() {
if err := file.Close(); err != nil {
log.Printf("failed to close file: %v", err)
}
}()
// Process file...
return nil
}
// GOOD: Connection pooling and timeouts
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
},
}
type CircuitBreaker struct {
maxFailures int
failures atomic.Int64
state atomic.Int32 // 0=closed, 1=open, 2=half-open
lastFailure atomic.Value // time.Time
}
func (cb *CircuitBreaker) Execute(fn func() error) error {
if cb.isOpen() {
return errors.New("circuit breaker is open")
}
err := fn()
if err != nil {
cb.recordFailure()
return err
}
cb.recordSuccess()
return nil
}
// BAD: Global mutable state
var globalDB *sql.DB
var globalLogger *log.Logger
// GOOD: Dependency injection
type Service struct {
db *sql.DB
logger *log.Logger
}
func NewService(db *sql.DB, logger *log.Logger) *Service {
return &Service{db: db, logger: logger}
}
// BAD: Panic in library code
func MustParseConfig(data []byte) Config {
config, err := ParseConfig(data)
if err != nil {
panic(err) // DON'T DO THIS
}
return config
}
// GOOD: Return errors
func ParseConfig(data []byte) (Config, error) {
// ... parsing logic ...
if err != nil {
return Config{}, fmt.Errorf("parse config: %w", err)
}
return config, nil
}
// ACCEPTABLE: Panic only for unrecoverable states
func init() {
if runtime.GOOS == "unsupported" {
panic("this package requires a supported OS")
}
}
// BAD: Fire-and-forget goroutines
go func() {
for {
doWork() // Runs forever, no way to stop
time.Sleep(time.Second)
}
}()
// GOOD: Controllable goroutines
func (s *Service) Start(ctx context.Context) {
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
s.doWork()
}
}
}()
}
# .golangci.yml
linters:
enable:
- errcheck # Check for unchecked errors
- gosimple # Simplify code
- govet # Standard Go vet
- ineffassign # Detect ineffectual assignments
- staticcheck # Advanced static analysis
- typecheck # Type checking
- unused # Find unused code
- cyclop # Cyclomatic complexity
- dupl # Code duplication
- gocognit # Cognitive complexity
- gocyclo # Cyclomatic complexity
- godot # Comments end in period
- gomnd # Magic numbers
- gosec # Security issues
- misspell # Spelling mistakes
- unconvert # Unnecessary conversions
- unparam # Unused function parameters
//go:build production
// +build production
package config
const Environment = "production"
// File: config_dev.go
//go:build !production
// +build !production
package config
const Environment = "development"
func BenchmarkUserLookup(b *testing.B) {
repo := setupTestRepo(b)
users := createTestUsers(b, 1000)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
userID := users[i%len(users)].ID
_, err := repo.GetUser(context.Background(), userID)
if err != nil {
b.Fatal(err)
}
}
}
- Packages:
user
,payment
,httpserver
(lowercase, singular) - Types:
UserService
,PaymentProcessor
(PascalCase) - Functions:
GetUser
,processPayment
(PascalCase/camelCase) - Constants:
MaxRetries
,defaultTimeout
(PascalCase/camelCase) - Errors:
ErrUserNotFound
,errInvalidInput
(Err prefix)
- Small interfaces (1-3 methods ideal)
- Define in consumer packages
- Use
-er
suffix:Reader
,Writer
,Processor
- Verify compliance:
var _ Interface = (*Type)(nil)
- Errors as last return value
- Immediate error handling
- Wrap with
%w
for chains - Canonical errors at boundaries
- No ignored errors (
_
assignments)
- Context passed to all I/O operations
- Goroutines have clear stop conditions
- Shared state protected by mutexes or atomics
- No data races (verified with
-race
) - Graceful shutdown implemented
- Profile before optimizing
- Preallocate containers with known size
- Use
strconv
overfmt
for conversions - Avoid repeated allocations in hot paths
- Benchmark performance-critical code