
Golang Base Practices
- 36 installs
- 4 repo stars
- Updated January 23, 2026
- cexll/golang-base-practices-skills
Provides Go best-practice rules for frameworks, GORM, DDD structure, error handling, concurrency, and testing when writing or reviewing Go code.
About
A Go best-practices guide with 53 prioritized rules covering Gin/Go-Kratos, GORM, Goose migrations, DDD layout, concurrency, and a 99% test-coverage target. A developer uses it when writing, reviewing, or refactoring Go services.
- 53 rules across frameworks, GORM, DDD, errors, concurrency, testing
- Targets 99% coverage; covers Gin/Go-Kratos, Goose, golangci-lint
Golang Base Practices by the numbers
- 36 all-time installs (skills.sh)
- Ranked #53 of 98 Go skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cexll/golang-base-practices-skills --skill golang-base-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 36 |
|---|---|
| repo stars | ★ 4 |
| Last updated | January 23, 2026 |
| Repository | cexll/golang-base-practices-skills ↗ |
What it does
Provides Go best-practice rules for frameworks, GORM, DDD structure, error handling, concurrency, and testing when writing or reviewing Go code.
Files
Golang Base Practices
Comprehensive Go development best practices guide, organized by priority for code generation, review, and refactoring. Contains 53 rules referenced from Effective Go, Google Go Style Guide, Uber Go Style Guide, and Go Code Review Comments.
When to Apply
Reference these guidelines when:
- Creating new Go projects or microservices
- Building API interfaces (REST/gRPC)
- Performing database operations and migrations
- Conducting code reviews and refactoring
- Optimizing performance and concurrency
- Improving test coverage
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Framework Selection | CRITICAL | framework- |
| 2 | Database & ORM | CRITICAL | db- |
| 3 | DDD Project Structure | HIGH | ddd- |
| 4 | Error Handling | HIGH | error- |
| 5 | Concurrency Patterns | HIGH | concurrency- |
| 6 | Idiomatic Go | MEDIUM | idiomatic- |
| 7 | Testing Practices | CRITICAL | testing- |
| 8 | Performance Optimization | MEDIUM | performance- |
| 9 | Lint & Toolchain | MEDIUM | lint- |
Quick Reference
1. Framework Selection (CRITICAL)
framework-gin-simple- Use Gin for simple projectsframework-kratos-complex- Use Go-Kratos for complex microservicesframework-middleware- Middleware design patternsframework-graceful-shutdown- Graceful server shutdown
2. Database & ORM (CRITICAL)
db-gorm-setup- GORM initialization and configurationdb-gorm-hooks- GORM Hook usage guidelinesdb-gorm-transactions- Transaction handling patternsdb-goose-migrations- Database migrations with Goosedb-connection-pool- Connection pool configuration
3. DDD Project Structure (HIGH)
ddd-project-layout- Standard project directory structureddd-domain-layer- Domain layer designddd-application-layer- Application layer designddd-infrastructure-layer- Infrastructure layer designddd-interface-layer- Interface layer designddd-dependency-injection- Dependency injection patterns
4. Error Handling (HIGH)
error-wrap-context- Error wrapping with contexterror-sentinel- Sentinel error definitionserror-custom-types- Custom error typeserror-handling-check- Always check error returnserror-api-response- API error response standardserror-panic-recover- Panic and recover usage guidelines
5. Concurrency Patterns (HIGH)
concurrency-goroutine-lifecycle- Goroutine lifecycle managementconcurrency-channel-patterns- Channel usage patternsconcurrency-channel-size- Channel buffer size selectionconcurrency-context-cancel- Context cancellation propagationconcurrency-errgroup- errgroup concurrency controlconcurrency-sync-primitives- sync package primitives usageconcurrency-race-detection- Race condition detection
6. Idiomatic Go (MEDIUM)
idiomatic-naming- Naming conventionsidiomatic-comment- Doc Comments guidelinesidiomatic-interface- Interface design (prefer small interfaces)idiomatic-receiver- Receiver naming and selectionidiomatic-struct-init- Struct initializationidiomatic-functional-options- Functional options patternidiomatic-defer- defer usage guidelinesidiomatic-slice-map- Slice and Map operationsidiomatic-zero-value- Zero value utilizationidiomatic-embedding- Type embeddingidiomatic-blank-identifier- Blank identifier usage
7. Testing Practices (CRITICAL)
testing-coverage-99- 99% test coverage targettesting-table-driven- Table-driven teststesting-mock- Mocking and interface abstractiontesting-helper- Test helper function guidelinestesting-benchmark- Benchmark testingtesting-integration- Integration testing standardstesting-testify- testify assertion library usage
8. Performance Optimization (MEDIUM)
performance-strconv- Use strconv instead of fmtperformance-prealloc- Container preallocation
9. Lint & Toolchain (MEDIUM)
lint-golangci- golangci-lint configurationlint-gofmt- Code formattinglint-govet- Static analysislint-staticcheck- Advanced static checkinglint-revive- Customizable linter
How to Use
Consult specific rule files in the rules/ directory for detailed explanations and code examples:
rules/framework-gin-simple.md
rules/db-gorm-setup.md
rules/testing-table-driven.mdEach rule file contains:
- Rule explanation and importance
- Incorrect example with analysis
- Correct example with explanation
- Additional context and references
Core Principles
1. KISS - Keep it simple, avoid over-engineering 2. YAGNI - Only implement what is currently needed 3. Explicit over Implicit - Code intent should be clear 4. Handle All Errors - Never ignore error returns 5. 99% Test Coverage - Foundation for high-quality code
Golang Base Practices
A comprehensive Go development best practices skill for Claude Code, covering frameworks, ORM, database migrations, DDD architecture, error handling, concurrency patterns, testing, and linting.
Overview
This skill provides 53 curated best practice rules organized into 9 categories, referenced from:
Installation
npx add-skill cexll/golang-base-practices-skillsRule Categories
| Priority | Category | Impact | Rules |
|---|---|---|---|
| 1 | Framework Selection | CRITICAL | 4 rules |
| 2 | Database & ORM | CRITICAL | 5 rules |
| 3 | DDD Project Structure | HIGH | 6 rules |
| 4 | Error Handling | HIGH | 6 rules |
| 5 | Concurrency Patterns | HIGH | 7 rules |
| 6 | Idiomatic Go | MEDIUM | 11 rules |
| 7 | Testing Practices | CRITICAL | 7 rules |
| 8 | Performance Optimization | MEDIUM | 2 rules |
| 9 | Lint & Toolchain | MEDIUM | 5 rules |
Quick Reference
1. Framework Selection
- Gin for simple projects and REST APIs
- Go-Kratos for complex microservices with gRPC/HTTP dual protocols
- Middleware patterns and graceful shutdown handling
2. Database & ORM
- GORM initialization, hooks, and transaction patterns
- Goose for version-controlled migrations
- Connection pool tuning for production
3. DDD Project Structure
internal/
├── domain/ # Business entities, value objects, repositories
├── application/ # Use cases, DTOs, service interfaces
├── infrastructure/ # External implementations (DB, cache, MQ)
└── interfaces/ # HTTP handlers, gRPC services4. Error Handling
- Wrap errors with context using
fmt.Errorf("...: %w", err) - Define sentinel errors for expected conditions
- Custom error types for domain-specific errors
- Unified API error response format
5. Concurrency Patterns
- Goroutine lifecycle management with context cancellation
- Channel patterns: fan-out, fan-in, timeout
- Channel buffer sizing (prefer 0 or 1)
- errgroup for parallel task coordination
- Race detection with
go test -race
6. Idiomatic Go
- Naming conventions (packages, variables, interfaces)
- Small interface design (Interface Segregation)
- Functional options pattern for flexible APIs
- Proper defer usage and zero value utilization
- Type embedding guidelines
7. Testing Practices
- 99% test coverage target for production code
- Table-driven tests for comprehensive coverage
- Interface-based mocking with mockgen
- Integration tests with testcontainers
- Benchmark tests for performance validation
8. Performance Optimization
- Use
strconvinstead offmtfor type conversion (4x faster) - Preallocate slices and maps when size is known
9. Lint & Toolchain
- golangci-lint with recommended configuration
- gofmt/goimports for consistent formatting
- go vet, staticcheck, and revive for static analysis
Core Principles
1. KISS - Keep it simple, avoid over-engineering 2. YAGNI - Only implement what is currently needed 3. Explicit over Implicit - Code intent should be clear 4. Handle All Errors - Never ignore error returns 5. 99% Test Coverage - Foundation for high-quality code
Usage
Claude will automatically reference these rules when:
- Creating new Go projects or microservices
- Building API interfaces (REST/gRPC)
- Performing database operations and migrations
- Conducting code reviews and refactoring
- Optimizing performance and concurrency
- Improving test coverage
File Structure
golang-base-practices/
├── SKILL.md # Skill definition and quick reference
├── README.md # This file
└── rules/ # 53 individual rule files
├── framework-*.md
├── db-*.md
├── ddd-*.md
├── error-*.md
├── concurrency-*.md
├── idiomatic-*.md
├── testing-*.md
├── performance-*.md
└── lint-*.mdEach rule file contains:
- Rule explanation and importance (impact level)
- Bad example with analysis
- Good example with explanation
- Additional context and references
License
MIT
Channel Usage Patterns
Use channels correctly for inter-goroutine communication.
Common Patterns:
// 1. Signal channel (no data transfer)
done := make(chan struct{})
close(done) // Broadcast signal
// 2. Send with timeout
select {
case ch <- data:
case <-time.After(5 * time.Second):
return errors.New("send timeout")
}
// 3. Receive with timeout
select {
case data := <-ch:
process(data)
case <-time.After(5 * time.Second):
return errors.New("receive timeout")
}
// 4. Non-blocking operation
select {
case ch <- data:
default:
// Channel is full, handle overflow
}
// 5. Fan-out
func fanOut(input <-chan int, workers int) []<-chan int {
outputs := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
outputs[i] = worker(input)
}
return outputs
}
// 6. Fan-in
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}Rules:
- The sender is responsible for closing the channel
- Closing an already closed channel will panic
- Sending to a closed channel will panic
- Receiving from a closed channel returns the zero value
Channel Size Selection
Channel size should be 0 or 1; larger buffers require careful justification.
Unbuffered Channel (size=0):
// Synchronous communication, sender blocks until receiver receives
done := make(chan struct{})
go func() {
// Work...
close(done) // Signal completion
}()
<-done // Wait for completionBuffer Size 1 Channel:
// Allow at most one pending item
// Commonly used for signal notification to prevent goroutine leaks
notify := make(chan struct{}, 1)
// Non-blocking send
select {
case notify <- struct{}{}:
default:
// Already has pending notification, skip
}Why Avoid Large Buffers:
// Bad: Large buffer hides problems
ch := make(chan Task, 1000)
// Problems:
// 1. Producer doesn't know if consumer is keeping up
// 2. Uncontrolled memory usage
// 3. Data may be lost on shutdown
// 4. Latency issues are hiddenValid Reasons for Large Buffers:
// 1. Explicit batch processing scenarios
batchSize := 100
batch := make(chan Item, batchSize)
// 2. Burst traffic smoothing (with clear bounds)
// Must have backpressure mechanism
const maxBurst = 1000
queue := make(chan Request, maxBurst)
// 3. Semaphore pattern
semaphore := make(chan struct{}, 10) // Limit to 10 concurrent
for _, task := range tasks {
semaphore <- struct{}{} // Acquire semaphore
go func(t Task) {
defer func() { <-semaphore }() // Release
process(t)
}(task)
}Selection Guide:
| Scenario | Recommended Size |
|---|---|
| Synchronous communication | 0 |
| Signal notification (prevent leaks) | 1 |
| Semaphore/rate limiting | N (explicit concurrency count) |
| Batch processing | Batch size |
| Other cases | Requires written justification |
Alternatives to Large Buffers:
// Use worker pool instead of large buffer
func workerPool(tasks <-chan Task, workers int) {
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for task := range tasks {
process(task)
}
}()
}
wg.Wait()
}Context Cancellation Propagation
Use context to propagate cancellation signals and deadlines.
Good Example:
func HandleRequest(w http.ResponseWriter, r *http.Request) {
// Request context automatically propagates cancellation
ctx := r.Context()
// Add timeout
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
result, err := processWithContext(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "request timeout", http.StatusGatewayTimeout)
return
}
if errors.Is(err, context.Canceled) {
// Client cancelled the request
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(result)
}
func processWithContext(ctx context.Context) (*Result, error) {
// Check context
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
// Pass context to downstream
data, err := fetchData(ctx)
if err != nil {
return nil, err
}
return &Result{Data: data}, nil
}
func fetchData(ctx context.Context) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}Context Chain:
// Base context
ctx := context.Background()
// Add timeout
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
// Add value
ctx = context.WithValue(ctx, userIDKey, userID)
// Pass to all downstream calls
result := doSomething(ctx)errgroup Concurrency Control
Use errgroup to simplify error handling for concurrent tasks.
Installation:
go get golang.org/x/sync/errgroupGood Example:
import "golang.org/x/sync/errgroup"
func FetchAllData(ctx context.Context, ids []int) (*AllData, error) {
g, ctx := errgroup.WithContext(ctx)
var (
users []User
orders []Order
products []Product
mu sync.Mutex
)
// Fetch users concurrently
for _, id := range ids {
id := id // Important: capture loop variable
g.Go(func() error {
user, err := fetchUser(ctx, id)
if err != nil {
return fmt.Errorf("fetch user %d: %w", id, err)
}
mu.Lock()
users = append(users, user)
mu.Unlock()
return nil
})
}
// Fetch orders concurrently
g.Go(func() error {
var err error
orders, err = fetchOrders(ctx)
return err
})
// Fetch products concurrently
g.Go(func() error {
var err error
products, err = fetchProducts(ctx)
return err
})
// Wait for all tasks to complete; if any fails, cancel others
if err := g.Wait(); err != nil {
return nil, err
}
return &AllData{Users: users, Orders: orders, Products: products}, nil
}Limiting Concurrency:
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10) // Maximum 10 concurrent goroutines
for _, url := range urls {
url := url
g.Go(func() error {
return fetch(ctx, url)
})
}Goroutine Lifecycle Management
Ensure every goroutine has a clear exit condition.
Bad Example (goroutine leak):
func StartWorker() {
go func() {
for {
processTask() // Never exits
}
}()
}Good Example (controlled exit):
type Worker struct {
tasks chan Task
done chan struct{}
wg sync.WaitGroup
}
func NewWorker() *Worker {
return &Worker{
tasks: make(chan Task, 100),
done: make(chan struct{}),
}
}
func (w *Worker) Start(ctx context.Context) {
w.wg.Add(1)
go func() {
defer w.wg.Done()
for {
select {
case <-ctx.Done():
return
case <-w.done:
return
case task := <-w.tasks:
w.processTask(task)
}
}
}()
}
func (w *Worker) Stop() {
close(w.done)
w.wg.Wait()
}
func (w *Worker) Submit(task Task) {
select {
case w.tasks <- task:
default:
log.Println("task queue full, dropping task")
}
}Key Principles:
- Use
context.Contextto propagate cancellation signals - Use
sync.WaitGroupto wait for goroutines to finish - Provide explicit shutdown mechanisms
Race Detection
Use the race detector to discover concurrency issues.
Enable Race Detection:
# Enable during testing
go test -race ./...
# Enable during runtime
go run -race main.go
# Enable during build
go build -race -o myappCommon Race Conditions:
// Bad Example 1: Unprotected shared variable
var counter int
func increment() {
counter++ // DATA RACE
}
go increment()
go increment()// Bad Example 2: Loop variable capture
for _, item := range items {
go func() {
process(item) // DATA RACE: item is shared by all goroutines
}()
}
// Good Example
for _, item := range items {
item := item // Create local copy
go func() {
process(item)
}()
}// Bad Example 3: Concurrent map writes
m := make(map[string]int)
go func() { m["a"] = 1 }() // DATA RACE
go func() { m["b"] = 2 }()
// Good Example: Use sync.Map or mutexCI Integration:
# .github/workflows/test.yml
- name: Test with race detector
run: go test -race -v ./...Notes:
- Race detector has performance overhead (2-20x)
- Do not enable in production
- CI must include race detection
sync Package Primitives Usage
Use sync package synchronization primitives correctly.
sync.Mutex - Mutual Exclusion Lock:
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}sync.RWMutex - Read-Write Lock:
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
func (c *Cache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}sync.Once - Single Execution:
var (
instance *DB
once sync.Once
)
func GetDB() *DB {
once.Do(func() {
instance = newDB()
})
return instance
}sync.Pool - Object Pool:
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func processData(data []byte) {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
buf.Write(data)
// ...
}sync.Map - Concurrent-Safe Map:
var cache sync.Map
cache.Store("key", "value")
if v, ok := cache.Load("key"); ok {
fmt.Println(v)
}Connection Pool Configuration
Properly configure database connection pool to balance performance and resource consumption.
Incorrect (default config not suitable for production):
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
// Default config: unlimited connections, may exhaust database resourcesCorrect (production environment configuration):
func ConfigureConnectionPool(db *gorm.DB, cfg PoolConfig) error {
sqlDB, err := db.DB()
if err != nil {
return err
}
// Maximum idle connections
// Recommended: 10-25% of MaxOpenConns
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
// Maximum open connections
// Calculate based on database limits and application instances
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
// Maximum idle time for connections
// Recommended: 5-10 minutes
sqlDB.SetConnMaxIdleTime(cfg.ConnMaxIdleTime)
// Maximum connection lifetime
// Recommended: less than database wait_timeout
sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime)
return nil
}
// Configuration example
type PoolConfig struct {
MaxIdleConns int // Recommended: 10
MaxOpenConns int // Recommended: 100
ConnMaxIdleTime time.Duration // Recommended: 5 * time.Minute
ConnMaxLifetime time.Duration // Recommended: 1 * time.Hour
}Configuration Guide:
| Parameter | Development | Production | Description |
|---|---|---|---|
| MaxIdleConns | 2 | 10-25 | Keep warm connections, reduce connection overhead |
| MaxOpenConns | 10 | 50-200 | Calculate based on total DB connections and instances |
| ConnMaxIdleTime | 5min | 5-10min | Release long-idle connections |
| ConnMaxLifetime | 1h | 30min-1h | Must be less than DB wait_timeout |
Database Migrations with Goose
Use Goose to manage database schema changes with version control.
Installation:
go install github.com/pressly/goose/v3/cmd/goose@latestCreate Migration File:
goose -dir migrations create add_users_table sqlMigration File Example (SQL):
-- migrations/20240115120000_add_users_table.sql
-- +goose Up
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_email (email)
);
-- +goose Down
DROP TABLE users;Go Code Migration:
// migrations/20240115130000_seed_admin.go
package migrations
import (
"context"
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigrationContext(upSeedAdmin, downSeedAdmin)
}
func upSeedAdmin(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx,
"INSERT INTO users (name, email) VALUES (?, ?)",
"admin", "admin@example.com",
)
return err
}
func downSeedAdmin(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, "DELETE FROM users WHERE email = ?", "admin@example.com")
return err
}Common Commands:
goose -dir migrations mysql "user:pass@/dbname" up # Run all pending migrations
goose -dir migrations mysql "user:pass@/dbname" down # Rollback one migration
goose -dir migrations mysql "user:pass@/dbname" status # View migration status
goose -dir migrations mysql "user:pass@/dbname" reset # Rollback all migrationsGORM Hook Usage Guidelines
Use GORM Hooks appropriately for automation logic, but avoid over-reliance.
Correct Example (common hook scenarios):
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100"`
Email string `gorm:"uniqueIndex"`
Password string `gorm:"-:all"` // Don't store plaintext
PassHash string `gorm:"column:password"`
CreatedAt time.Time
UpdatedAt time.Time
}
// BeforeCreate - Hash password before creation
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.Password != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.PassHash = string(hash)
}
return nil
}
// AfterFind - Handle sensitive data after query
func (u *User) AfterFind(tx *gorm.DB) error {
u.PassHash = "" // Don't expose password hash
return nil
}Hook Best Practices:
- Keep hook logic simple
- Don't put complex business logic in hooks
- Hook errors cause transaction rollback
- Avoid calling external services in hooks
Available Hooks:
BeforeSave/AfterSaveBeforeCreate/AfterCreateBeforeUpdate/AfterUpdateBeforeDelete/AfterDeleteAfterFind
GORM Initialization and Configuration
Properly initialize GORM with connection pool and logging configuration.
Incorrect (missing configuration):
func main() {
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
// Ignoring error, no connection pool config, no logging
}Correct (complete configuration):
package data
import (
"log"
"os"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func NewDB(dsn string) (*gorm.DB, func(), error) {
// Configure logger
gormLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: 200 * time.Millisecond,
LogLevel: logger.Warn,
IgnoreRecordNotFoundError: true,
Colorful: true,
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: gormLogger,
DisableForeignKeyConstraintWhenMigrating: true,
PrepareStmt: true,
})
if err != nil {
return nil, nil, err
}
// Configure connection pool
sqlDB, err := db.DB()
if err != nil {
return nil, nil, err
}
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
cleanup := func() {
sqlDB.Close()
}
return db, cleanup, nil
}Key Configuration:
MaxIdleConns: Number of idle connectionsMaxOpenConns: Maximum open connectionsConnMaxLifetime: Maximum connection lifetimePrepareStmt: Prepared statement cache
Transaction Handling Patterns
Use GORM transactions to ensure data consistency.
Incorrect (no transaction, data inconsistency):
func Transfer(fromID, toID uint, amount int) error {
db.Model(&Account{}).Where("id = ?", fromID).Update("balance", gorm.Expr("balance - ?", amount))
// If this fails, money is deducted but not added
db.Model(&Account{}).Where("id = ?", toID).Update("balance", gorm.Expr("balance + ?", amount))
return nil
}Correct (using transaction):
func Transfer(db *gorm.DB, fromID, toID uint, amount int) error {
return db.Transaction(func(tx *gorm.DB) error {
// Deduct
result := tx.Model(&Account{}).
Where("id = ? AND balance >= ?", fromID, amount).
Update("balance", gorm.Expr("balance - ?", amount))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("insufficient balance or account not found")
}
// Add
result = tx.Model(&Account{}).
Where("id = ?", toID).
Update("balance", gorm.Expr("balance + ?", amount))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("target account not found")
}
return nil // Return nil to commit transaction
})
}Nested Transactions (SavePoint):
db.Transaction(func(tx *gorm.DB) error {
tx.Create(&user1)
tx.Transaction(func(tx2 *gorm.DB) error {
tx2.Create(&user2)
return errors.New("rollback user2 only")
})
return nil // user1 will be committed
})Application Layer Design
The application layer orchestrates use cases, coordinating domain objects to complete business processes.
Command Query Separation (CQRS):
// internal/application/user/command.go
package user
type CreateUserCommand struct {
Email string
Name string
}
type UpdateUserCommand struct {
ID uint64
Name string
}// internal/application/user/query.go
package user
type GetUserQuery struct {
ID uint64
}
type ListUsersQuery struct {
Page int
PageSize int
Status string
}Handler (use case implementation):
// internal/application/user/handler.go
package user
import (
"context"
"myapp/internal/domain/user"
)
type Handler struct {
repo user.Repository
}
func NewHandler(repo user.Repository) *Handler {
return &Handler{repo: repo}
}
func (h *Handler) CreateUser(ctx context.Context, cmd CreateUserCommand) (*user.User, error) {
// Validate email format
email, err := user.NewEmail(cmd.Email)
if err != nil {
return nil, err
}
// Check if email already exists
existing, err := h.repo.FindByEmail(ctx, string(email))
if err == nil && existing != nil {
return nil, errors.New("email already exists")
}
// Create user
u := &user.User{
Email: string(email),
Name: cmd.Name,
Status: user.StatusActive,
}
if err := h.repo.Save(ctx, u); err != nil {
return nil, err
}
return u, nil
}
func (h *Handler) GetUser(ctx context.Context, q GetUserQuery) (*user.User, error) {
return h.repo.FindByID(ctx, q.ID)
}Dependency Injection Patterns
Use dependency injection for decoupling. Google Wire is recommended.
Install Wire:
go install github.com/google/wire/cmd/wire@latestProvider Definitions:
// internal/infrastructure/persistence/mysql/provider.go
package mysql
import "github.com/google/wire"
var ProviderSet = wire.NewSet(
NewUserRepository,
wire.Bind(new(user.Repository), new(*UserRepository)),
)// internal/application/user/provider.go
package user
import "github.com/google/wire"
var ProviderSet = wire.NewSet(NewHandler)// internal/interfaces/http/handler/provider.go
package handler
import "github.com/google/wire"
var ProviderSet = wire.NewSet(NewUserHandler)Wire Configuration:
// cmd/server/wire.go
//go:build wireinject
package main
import (
"github.com/google/wire"
"myapp/internal/application/user"
"myapp/internal/infrastructure/persistence/mysql"
"myapp/internal/interfaces/http/handler"
)
func InitializeApp(db *gorm.DB) (*App, error) {
wire.Build(
mysql.ProviderSet,
user.ProviderSet,
handler.ProviderSet,
NewApp,
)
return nil, nil
}Generate Dependency Injection Code:
cd cmd/server && wireUsage:
// cmd/server/main.go
func main() {
db := initDB()
app, err := InitializeApp(db)
if err != nil {
log.Fatal(err)
}
app.Run()
}Domain Layer Design
The domain layer contains core business logic with no dependencies on external frameworks.
Entity:
// internal/domain/user/entity.go
package user
import (
"errors"
"time"
)
type User struct {
ID uint64
Email string
Name string
Status Status
CreatedAt time.Time
UpdatedAt time.Time
}
type Status string
const (
StatusActive Status = "active"
StatusInactive Status = "inactive"
StatusBanned Status = "banned"
)
// Business rules encapsulated in entity
func (u *User) Activate() error {
if u.Status == StatusBanned {
return errors.New("cannot activate banned user")
}
u.Status = StatusActive
return nil
}
func (u *User) CanOrder() bool {
return u.Status == StatusActive
}Value Object:
// internal/domain/user/email.go
package user
import (
"errors"
"regexp"
)
type Email string
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
func NewEmail(s string) (Email, error) {
if !emailRegex.MatchString(s) {
return "", errors.New("invalid email format")
}
return Email(s), nil
}Repository Interface (defined in domain layer):
// internal/domain/user/repository.go
package user
import "context"
type Repository interface {
FindByID(ctx context.Context, id uint64) (*User, error)
FindByEmail(ctx context.Context, email string) (*User, error)
Save(ctx context.Context, user *User) error
Delete(ctx context.Context, id uint64) error
}Infrastructure Layer Design
The infrastructure layer implements interfaces defined in the domain layer and handles technical details.
Repository Implementation:
// internal/infrastructure/persistence/mysql/user_repo.go
package mysql
import (
"context"
"errors"
"myapp/internal/domain/user"
"gorm.io/gorm"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db: db}
}
// Database model (separate from domain entity)
type userModel struct {
ID uint64 `gorm:"primaryKey"`
Email string `gorm:"uniqueIndex"`
Name string
Status string
CreatedAt time.Time
UpdatedAt time.Time
}
func (userModel) TableName() string {
return "users"
}
func (r *UserRepository) FindByID(ctx context.Context, id uint64) (*user.User, error) {
var m userModel
if err := r.db.WithContext(ctx).First(&m, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, user.ErrNotFound
}
return nil, err
}
return r.toDomain(&m), nil
}
func (r *UserRepository) Save(ctx context.Context, u *user.User) error {
m := r.toModel(u)
return r.db.WithContext(ctx).Save(m).Error
}
// Model to domain entity conversion
func (r *UserRepository) toDomain(m *userModel) *user.User {
return &user.User{
ID: m.ID,
Email: m.Email,
Name: m.Name,
Status: user.Status(m.Status),
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
}
}
func (r *UserRepository) toModel(u *user.User) *userModel {
return &userModel{
ID: u.ID,
Email: u.Email,
Name: u.Name,
Status: string(u.Status),
}
}External Service Adapter:
// internal/infrastructure/external/payment/client.go
package payment
type Client struct {
baseURL string
apiKey string
}
func NewClient(baseURL, apiKey string) *Client {
return &Client{baseURL: baseURL, apiKey: apiKey}
}Interface Layer Design
The interface layer handles external requests and converts them to application layer commands/queries.
HTTP Handler:
// internal/interfaces/http/handler/user.go
package handler
import (
"net/http"
"strconv"
"myapp/internal/application/user"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
userHandler *user.Handler
}
func NewUserHandler(uh *user.Handler) *UserHandler {
return &UserHandler{userHandler: uh}
}
// Request/Response DTOs (separate from domain entities)
type CreateUserRequest struct {
Email string `json:"email" binding:"required,email"`
Name string `json:"name" binding:"required,min=2,max=100"`
}
type UserResponse struct {
ID uint64 `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Status string `json:"status"`
}
func (h *UserHandler) Create(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u, err := h.userHandler.CreateUser(c.Request.Context(), user.CreateUserCommand{
Email: req.Email,
Name: req.Name,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, h.toResponse(u))
}
func (h *UserHandler) Get(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
u, err := h.userHandler.GetUser(c.Request.Context(), user.GetUserQuery{ID: id})
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusOK, h.toResponse(u))
}
func (h *UserHandler) toResponse(u *domain.User) UserResponse {
return UserResponse{
ID: u.ID,
Email: u.Email,
Name: u.Name,
Status: string(u.Status),
}
}Router Registration:
// internal/interfaces/http/router.go
func SetupRouter(userHandler *handler.UserHandler) *gin.Engine {
r := gin.Default()
api := r.Group("/api/v1")
{
users := api.Group("/users")
users.POST("", userHandler.Create)
users.GET("/:id", userHandler.Get)
}
return r
}Standard Project Directory Structure
Follow DDD layered architecture with clear separation of concerns.
Recommended Project Structure:
myapp/
├── cmd/ # Application entry points
│ └── server/
│ └── main.go
├── internal/ # Private code
│ ├── domain/ # Domain layer (core business)
│ │ ├── user/
│ │ │ ├── entity.go
│ │ │ ├── repository.go
│ │ │ └── service.go
│ │ └── order/
│ ├── application/ # Application layer (use cases)
│ │ ├── user/
│ │ │ ├── command.go
│ │ │ ├── query.go
│ │ │ └── handler.go
│ │ └── order/
│ ├── infrastructure/ # Infrastructure layer
│ │ ├── persistence/
│ │ │ ├── mysql/
│ │ │ │ ├── user_repo.go
│ │ │ │ └── order_repo.go
│ │ │ └── redis/
│ │ └── external/
│ │ └── payment/
│ └── interfaces/ # Interface layer
│ ├── http/
│ │ ├── handler/
│ │ ├── middleware/
│ │ └── router.go
│ └── grpc/
├── pkg/ # Public libraries (can be imported externally)
│ ├── errors/
│ └── utils/
├── configs/ # Configuration files
├── migrations/ # Database migrations
├── api/ # API definitions (proto/openapi)
├── scripts/ # Build and deployment scripts
├── Makefile
└── go.modLayer Dependency Rules:
- interfaces → application → domain
- infrastructure → domain
- Domain layer has no external dependencies
API Error Response Standards
Define a unified API error response format.
Error Response Structure:
// pkg/response/error.go
package response
type ErrorResponse struct {
Code string `json:"code"`
Message string `json:"message"`
Details map[string]interface{} `json:"details,omitempty"`
}
// Common error codes
const (
ErrCodeValidation = "VALIDATION_ERROR"
ErrCodeNotFound = "NOT_FOUND"
ErrCodeUnauthorized = "UNAUTHORIZED"
ErrCodeForbidden = "FORBIDDEN"
ErrCodeInternal = "INTERNAL_ERROR"
ErrCodeConflict = "CONFLICT"
)Middleware for Unified Handling:
// internal/interfaces/http/middleware/error.go
package middleware
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
err := c.Errors.Last().Err
handleError(c, err)
}
}
}
func handleError(c *gin.Context, err error) {
var validErr *errors.ValidationError
if errors.As(err, &validErr) {
c.JSON(http.StatusBadRequest, response.ErrorResponse{
Code: response.ErrCodeValidation,
Message: validErr.Message,
Details: map[string]interface{}{"field": validErr.Field},
})
return
}
if errors.Is(err, user.ErrUserNotFound) {
c.JSON(http.StatusNotFound, response.ErrorResponse{
Code: response.ErrCodeNotFound,
Message: "User not found",
})
return
}
// Unknown error: log it, return generic error
log.Printf("internal error: %v", err)
c.JSON(http.StatusInternalServerError, response.ErrorResponse{
Code: response.ErrCodeInternal,
Message: "An internal error occurred",
})
}Example Response:
{
"code": "VALIDATION_ERROR",
"message": "Email format is invalid",
"details": {
"field": "email"
}
}Custom Error Types
Define custom error types when you need to carry additional information.
Good Example:
// pkg/errors/errors.go
package errors
import "fmt"
// ValidationError contains field-level validation errors
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
}
// NotFoundError contains resource type and ID
type NotFoundError struct {
Resource string
ID interface{}
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s with id %v not found", e.Resource, e.ID)
}
// BusinessError is a business error with error code
type BusinessError struct {
Code int
Message string
Details map[string]interface{}
}
func (e *BusinessError) Error() string {
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
func NewBusinessError(code int, message string) *BusinessError {
return &BusinessError{Code: code, Message: message}
}Using errors.As to extract:
func Handler(c *gin.Context) {
err := service.CreateUser(ctx, req)
if err != nil {
var validErr *errors.ValidationError
if errors.As(err, &validErr) {
c.JSON(http.StatusBadRequest, gin.H{
"error": validErr.Message,
"field": validErr.Field,
})
return
}
var bizErr *errors.BusinessError
if errors.As(err, &bizErr) {
c.JSON(http.StatusUnprocessableEntity, gin.H{
"code": bizErr.Code,
"message": bizErr.Message,
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
}
}Always Check Error Returns
Go requires explicit error handling. Never ignore error return values.
Bad Example (ignoring errors):
func ProcessData() {
data, _ := fetchData() // Ignoring error!
json.Unmarshal(data, &result) // Ignoring error!
saveResult(result) // This will break if data is empty
}Good Example (explicit handling):
func ProcessData() error {
data, err := fetchData()
if err != nil {
return fmt.Errorf("fetch data: %w", err)
}
var result Result
if err := json.Unmarshal(data, &result); err != nil {
return fmt.Errorf("unmarshal data: %w", err)
}
if err := saveResult(result); err != nil {
return fmt.Errorf("save result: %w", err)
}
return nil
}Special Cases (explicit ignore):
// When error is truly not needed, use blank identifier with comment
_ = conn.Close() // ignore close error, already logging
// Or log but don't return
if err := conn.Close(); err != nil {
log.Printf("warning: failed to close connection: %v", err)
}Lint Check:
# Use errcheck to detect unhandled errors
go install github.com/kisielk/errcheck@latest
errcheck ./...Panic and Recover Usage Guidelines
Use panic only for unrecoverable errors, not for normal error handling.
When to Use Panic:
// 1. Program initialization failure, cannot continue
func init() {
if os.Getenv("REQUIRED_VAR") == "" {
panic("REQUIRED_VAR environment variable is not set")
}
}
// 2. Programming errors, situations that should never happen
func MustCompile(pattern string) *Regexp {
re, err := Compile(pattern)
if err != nil {
panic(`regexp: Compile(` + quote(pattern) + `): ` + err.Error())
}
return re
}
// 3. Unreachable code paths
func unreachable() {
panic("unreachable")
}Never Panic in These Situations:
// Wrong: using panic for normal errors
func GetUser(id int) *User {
user, err := db.Find(id)
if err != nil {
panic(err) // Wrong! Should return error
}
return user
}
// Correct: return error
func GetUser(id int) (*User, error) {
user, err := db.Find(id)
if err != nil {
return nil, fmt.Errorf("get user %d: %w", id, err)
}
return user, nil
}Using Recover to Prevent Service Crashes:
func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic recovered: %v\n%s", r, debug.Stack())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
// Handle request...
}Recover Only Works in Defer:
func SafeCall(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
fn()
return nil
}
// Direct recover call is ineffective
func wrong() {
recover() // Ineffective, not in defer
}Do Not Expose Panic Across Package Boundaries:
// Internal panic is OK, but public APIs must return error
// If internal panic can be triggered, recover at package boundary
func (p *Parser) Parse(input string) (result *AST, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("parse error: %v", r)
}
}()
return p.parse(input), nil
}Sentinel Error Definition
Define package-level sentinel errors for callers to check.
Good Example:
// pkg/errors/errors.go
package errors
import "errors"
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized")
ErrForbidden = errors.New("forbidden")
ErrInvalidInput = errors.New("invalid input")
ErrAlreadyExists = errors.New("resource already exists")
)// internal/domain/user/errors.go
package user
import "errors"
var (
ErrUserNotFound = errors.New("user not found")
ErrEmailExists = errors.New("email already exists")
ErrInvalidPassword = errors.New("invalid password")
)Usage:
func (r *UserRepository) FindByID(ctx context.Context, id uint64) (*User, error) {
var m userModel
if err := r.db.WithContext(ctx).First(&m, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, user.ErrUserNotFound
}
return nil, fmt.Errorf("find user by id %d: %w", id, err)
}
return r.toDomain(&m), nil
}
// Caller
func (h *Handler) GetUser(c *gin.Context) {
u, err := h.service.GetUser(ctx, id)
if errors.Is(err, user.ErrUserNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
return
}
}Error Wrapping and Context
Use fmt.Errorf and %w to wrap errors, preserving the complete call chain.
Bad Example (losing context):
func GetUser(id int) (*User, error) {
user, err := db.FindUser(id)
if err != nil {
return nil, err // Lost GetUser context
}
return user, nil
}Good Example (wrapping error):
func GetUser(id int) (*User, error) {
user, err := db.FindUser(id)
if err != nil {
return nil, fmt.Errorf("get user %d: %w", id, err)
}
return user, nil
}
// Caller can check the root cause
func Handler(c *gin.Context) {
user, err := GetUser(id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
c.JSON(404, gin.H{"error": "user not found"})
return
}
c.JSON(500, gin.H{"error": "internal error"})
log.Printf("failed to get user: %v", err)
// Log output: failed to get user: get user 123: sql: no rows
return
}
}Key Points:
- Use
%wverb to wrap errors - Include operation name and key parameters
- Use
errors.Is()to check error types - Use
errors.As()to extract error types
Use Gin for Simple Projects
For small to medium projects, simple REST APIs, or rapid prototyping, prefer the Gin framework.
Incorrect (over-engineering a small project):
// Using a full microservice framework for a simple CRUD API
import (
"github.com/go-kratos/kratos/v2"
"github.com/go-kratos/kratos/v2/transport/grpc"
"github.com/go-kratos/kratos/v2/transport/http"
)
func main() {
// Lots of configuration code...
app := kratos.New(
kratos.Name("simple-api"),
kratos.Server(grpcServer, httpServer),
)
}Correct (use Gin for simple projects):
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/users/:id", getUser)
r.POST("/users", createUser)
r.PUT("/users/:id", updateUser)
r.DELETE("/users/:id", deleteUser)
r.Run(":8080")
}
func getUser(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"id": id})
}Selection Criteria:
- Monolith or simple microservice → Gin
- Team with limited Go experience → Gin (gentle learning curve)
- Rapid prototype validation → Gin
- No gRPC requirement → Gin
Graceful Server Shutdown
Services must support graceful shutdown to ensure in-flight requests complete before exit.
Incorrect (immediate exit):
func main() {
r := gin.Default()
r.GET("/", handler)
r.Run(":8080") // Exits immediately on signal, requests interrupted
}Correct (graceful shutdown):
func main() {
r := gin.Default()
r.GET("/", handler)
srv := &http.Server{
Addr: ":8080",
Handler: r,
}
// Start server
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
// Graceful shutdown with 5 second timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exiting")
}Key Points:
- Catch SIGINT and SIGTERM signals
- Use
http.Server.Shutdownfor graceful shutdown - Set timeout to avoid infinite waiting
- Ensure database connections and resources are properly released
Use Go-Kratos for Complex Microservices
For complex microservice systems requiring gRPC and HTTP dual protocols, service discovery, config centers, choose Go-Kratos.
When to Use:
- Multi-service collaborative microservice architecture
- Need gRPC + HTTP dual protocols
- Require service discovery, config center
- Team has extensive Go experience
Correct Example (Kratos project structure):
// cmd/server/main.go
package main
import (
"github.com/go-kratos/kratos/v2"
"github.com/go-kratos/kratos/v2/transport/grpc"
"github.com/go-kratos/kratos/v2/transport/http"
)
func main() {
app, cleanup, err := wireApp(conf.Server, conf.Data, logger)
if err != nil {
panic(err)
}
defer cleanup()
if err := app.Run(); err != nil {
panic(err)
}
}// internal/service/user.go
package service
type UserService struct {
pb.UnimplementedUserServer
uc *biz.UserUsecase
}
func NewUserService(uc *biz.UserUsecase) *UserService {
return &UserService{uc: uc}
}
func (s *UserService) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
user, err := s.uc.GetUser(ctx, req.Id)
if err != nil {
return nil, err
}
return &pb.User{
Id: user.ID,
Name: user.Name,
}, nil
}Kratos Key Features:
- Protocol Buffers API definition
- Wire dependency injection
- Middleware chain (logging, tracing, circuit breaker)
- Multiple registry support
Middleware Design Patterns
Use middleware pattern to handle cross-cutting concerns like logging, authentication, and rate limiting.
Incorrect (repeated code):
func GetUser(c *gin.Context) {
// Logging repeated in every handler
log.Printf("request: %s %s", c.Request.Method, c.Request.URL)
start := time.Now()
// Token validation repeated in every handler
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{"error": "unauthorized"})
return
}
// Business logic...
log.Printf("duration: %v", time.Since(start))
}Correct (middleware abstraction):
// Logger middleware
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
c.Next()
log.Printf("%s %s %d %v",
c.Request.Method,
path,
c.Writer.Status(),
time.Since(start),
)
}
}
// Auth middleware
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
return
}
claims, err := validateToken(token)
if err != nil {
c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"})
return
}
c.Set("user_id", claims.UserID)
c.Next()
}
}
func main() {
r := gin.New()
r.Use(Logger(), Recovery())
api := r.Group("/api", Auth())
{
api.GET("/users/:id", GetUser)
}
}Blank Identifier Usage
Use the blank identifier _ correctly.
Ignore Unneeded Return Values:
// Only need the error
_, err := io.Copy(dst, src)
// Only need the first return value
value, _ := cache.Load(key) // Caution: may hide errors
// Ignore key or value in range
for _, v := range slice {}
for i := range slice {} // Can omit value when only index is neededImport for Side Effects Only:
import (
"database/sql"
_ "github.com/go-sql-driver/mysql" // Register MySQL driver
)
import _ "net/http/pprof" // Register pprof handlersCompile-Time Type Check:
// Verify type implements interface
var _ io.Reader = (*MyReader)(nil)
var _ fmt.Stringer = MyType{}
// Verify struct satisfies interface
type Handler struct{}
var _ http.Handler = (*Handler)(nil)Temporary Handling of Unused Variables:
// Temporarily handle unused variable during development
func example() {
x := computeX()
_ = x // TODO: use later
// Better approach: delete or use directly
}Notes on Error Handling:
// Wrong: Ignoring errors is dangerous
data, _ := json.Marshal(obj) // What if it fails?
// If you must ignore, add comment explaining why
_ = conn.Close() // ignore close error, already logged
// Better approach: Log but don't return
if err := conn.Close(); err != nil {
log.Printf("warning: close failed: %v", err)
}Unused Parameters in Method Implementations:
// Parameter required by interface but not needed by method
func (h *Handler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte("Hello"))
}Comment Guidelines
Write comments and documentation following Go conventions.
Doc Comments:
// Package user provides user management functionality.
// It includes operations for creating, updating, and querying users.
package user
// User represents a registered user in the system.
// The zero value is not valid; use NewUser to create instances.
type User struct {
ID uint64
Name string
Email string
}
// NewUser creates a new User with the given name and email.
// It returns an error if the email format is invalid.
func NewUser(name, email string) (*User, error) {
// ...
}
// IsActive reports whether the user account is active.
func (u *User) IsActive() bool {
return u.Status == StatusActive
}Comment Rules:
// Correct: Start with the described item, complete sentence, end with period
// Request represents a client request to the server.
type Request struct{}
// Encode writes the JSON encoding of req to w.
func Encode(w io.Writer, req *Request) error {}
// Wrong: Does not start with the item name
// This struct represents a request... // Wrong
// A Request is... // Should be "Request represents..."Error Strings:
// Correct: Start lowercase, no trailing period
return fmt.Errorf("failed to connect: %w", err)
return errors.New("invalid user id")
// Wrong: Starts uppercase or has trailing period
return fmt.Errorf("Failed to connect: %w", err) // Wrong
return errors.New("Invalid user id.") // WrongPackage Comments:
// Package math provides basic constants and mathematical functions.
//
// This package does not guarantee bit-identical results across architectures.
package math
// Or use a doc.go file
/*
Package template implements data-driven templates for generating textual output.
The template is parsed from a string using Parse or related methods.
*/
package templateAvoid Useless Comments:
// Wrong: Comment provides no additional information
// GetName returns the name.
func (u *User) GetName() string { return u.Name }
// Correct: Only comment when needed
func (u *User) Name() string { return u.name }defer Usage Guidelines
Use defer to ensure proper resource cleanup.
Resource Cleanup:
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close() // Ensure file is closed
return io.ReadAll(f)
}Lock Release:
func (c *Cache) Get(key string) interface{} {
c.mu.Lock()
defer c.mu.Unlock() // Ensure unlock
return c.data[key]
}defer Execution Order (LIFO):
func example() {
defer fmt.Println("first")
defer fmt.Println("second")
defer fmt.Println("third")
}
// Output: third, second, firstRecover Panic in defer:
func SafeCall(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
fn()
return nil
}Important Notes:
// defer arguments are evaluated at defer time
func example() {
i := 0
defer fmt.Println(i) // Outputs 0
i++
}
// Use closure to capture latest value
func example() {
i := 0
defer func() { fmt.Println(i) }() // Outputs 1
i++
}defer in Loops:
// Wrong: defer accumulation
func processFiles(paths []string) error {
for _, path := range paths {
f, _ := os.Open(path)
defer f.Close() // All defers execute at function end
}
}
// Correct: Extract to function
func processFiles(paths []string) error {
for _, path := range paths {
if err := processFile(path); err != nil {
return err
}
}
return nil
}
func processFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
// ...
}Type Embedding
Use embedding for code reuse, but handle with care.
Correct Use of Embedding:
// Embed interfaces to gain methods
type ReadWriter interface {
io.Reader
io.Writer
}
// Embed structs to reuse implementation
type Logger struct {
*log.Logger
}
func NewLogger() *Logger {
return &Logger{
Logger: log.New(os.Stdout, "", log.LstdFlags),
}
}
// Can directly call embedded type's methods
logger := NewLogger()
logger.Println("hello") // Calls log.Logger.PrintlnInternal Implementation Embedding (Recommended):
// Embedding for internal implementation, not exposed externally
type Server struct {
config Config
// Embed mutex for internal synchronization
mu sync.Mutex
}
// But don't embed in exported structs to expose to usersAvoid Embedding in Public APIs (Uber Style):
// Wrong: Embedding in public API leaks implementation details
type Client struct {
http.Client // Exposes all http.Client methods
}
// Correct: Use explicit field and delegation
type Client struct {
client *http.Client
}
func (c *Client) Do(req *http.Request) (*http.Response, error) {
return c.client.Do(req)
}Name Collision Handling:
type A struct {
Name string
}
type B struct {
Name string
}
type C struct {
A
B
}
// c.Name will error: ambiguous selector
// Must specify explicitly: c.A.Name or c.B.Name
type D struct {
A
Name string // Outer field shadows inner
}
// d.Name uses D.Name, d.A.Name uses embedded fieldCompile-Time Interface Check:
// Verify type implements interface
var _ io.Reader = (*MyReader)(nil)
var _ json.Marshaler = (*MyType)(nil)Functional Options Pattern
Use the functional options pattern to design flexible configuration APIs.
Basic Implementation:
type Server struct {
host string
port int
timeout time.Duration
logger *log.Logger
}
type Option func(*Server)
func WithHost(host string) Option {
return func(s *Server) {
s.host = host
}
}
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
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(opts ...Option) *Server {
// Set defaults
s := &Server{
host: "localhost",
port: 8080,
timeout: 30 * time.Second,
logger: log.Default(),
}
// Apply options
for _, opt := range opts {
opt(s)
}
return s
}Usage Examples:
// Use default configuration
server := NewServer()
// Custom configuration
server := NewServer(
WithHost("0.0.0.0"),
WithPort(9000),
WithTimeout(60*time.Second),
)
// Partial customization
server := NewServer(
WithPort(9000),
)Options with Validation:
func WithPort(port int) Option {
return func(s *Server) {
if port < 1 || port > 65535 {
// Two approaches:
// 1. panic (suitable for Must functions)
panic(fmt.Sprintf("invalid port: %d", port))
// 2. Return error (see pattern below)
}
s.port = port
}
}Options with Error Returns:
type OptionErr func(*Server) error
func NewServerWithErr(opts ...OptionErr) (*Server, error) {
s := &Server{
host: "localhost",
port: 8080,
}
for _, opt := range opts {
if err := opt(s); err != nil {
return nil, err
}
}
return s, nil
}
func WithPortErr(port int) OptionErr {
return func(s *Server) error {
if port < 1 || port > 65535 {
return fmt.Errorf("invalid port: %d", port)
}
s.port = port
return nil
}
}When to Use Functional Options:
| Scenario | Recommended Approach |
|---|---|
| Few required parameters | Direct parameters |
| Multiple optional parameters | Functional options |
| Complex configuration structure | Config struct + functional options |
| Most calls don't need options | Functional options (variadic) |
Interface Design (Small Interfaces First)
Define small interfaces and compose as needed.
Bad Example (Large Interface):
// Too large, expensive to implement, hard to test
type UserService interface {
CreateUser(ctx context.Context, req *CreateUserRequest) (*User, error)
UpdateUser(ctx context.Context, id int, req *UpdateUserRequest) error
DeleteUser(ctx context.Context, id int) error
GetUser(ctx context.Context, id int) (*User, error)
ListUsers(ctx context.Context, filter *Filter) ([]*User, error)
ActivateUser(ctx context.Context, id int) error
DeactivateUser(ctx context.Context, id int) error
ResetPassword(ctx context.Context, id int) error
// ... more methods
}Good Example (Small Interfaces):
// Split by responsibility
type UserReader interface {
GetUser(ctx context.Context, id int) (*User, error)
}
type UserWriter interface {
CreateUser(ctx context.Context, req *CreateUserRequest) (*User, error)
UpdateUser(ctx context.Context, id int, req *UpdateUserRequest) error
DeleteUser(ctx context.Context, id int) error
}
type UserLister interface {
ListUsers(ctx context.Context, filter *Filter) ([]*User, error)
}
// Compose as needed
type UserRepository interface {
UserReader
UserWriter
UserLister
}Interface Definition Location:
// Interface is defined by the consumer, not the implementer
// internal/application/user/handler.go
package user
// Declare only the methods you need
type userReader interface {
GetUser(ctx context.Context, id int) (*User, error)
}
type Handler struct {
reader userReader
}Implicit Interfaces:
// Go interfaces are implicitly implemented
// Any type implementing Read method is an io.Reader
type MyReader struct{}
func (r *MyReader) Read(p []byte) (n int, err error) {
// ...
}
var _ io.Reader = (*MyReader)(nil) // Compile-time checkNaming Conventions
Follow Go community naming conventions.
Package Names:
// Correct: short, lowercase, single word
package user
package http
package json
// Wrong
package userService // No camelCase
package user_service // No underscores
package util // Too genericVariable Names:
// Short names for local scope
for i := 0; i < len(items); i++ {}
for _, v := range values {}
// Descriptive names for package-level or long scope
var userCount int
var httpClient *http.Client
// Keep abbreviations consistently cased
var userID string // Not userId
var httpURL string // Not httpUrl
type XMLParser struct{}Function Names:
// Start with verb
func GetUser(id int) *User
func CreateOrder(req *OrderRequest) error
func ValidateEmail(email string) bool
// Boolean functions use Is/Has/Can
func IsValid() bool
func HasPermission() bool
func CanDelete() bool
// Private functions start lowercase
func parseConfig() {}Constants:
// No ALL_CAPS with underscores
const maxRetries = 3 // Correct
const MAX_RETRIES = 3 // Wrong (C-style)
// Exported constants start uppercase
const DefaultTimeout = 30 * time.SecondInterface Names:
// Single-method interfaces use method name + er
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type Closer interface { Close() error }
// Multi-method interfaces use descriptive names
type UserRepository interface {
FindByID(ctx context.Context, id int) (*User, error)
Save(ctx context.Context, user *User) error
}Receiver Naming and Selection
Choose and name method receivers correctly.
Receiver Naming:
// Correct: 1-2 letters, abbreviation of type name
func (c *Client) Send(msg Message) error {}
func (r *Reader) Read(p []byte) (n int, err error) {}
func (b *Buffer) Write(p []byte) (n int, err error) {}
// Wrong: Do not use this, self, me
func (this *Client) Send(msg Message) error {} // Wrong
func (self *Reader) Read(p []byte) error {} // Wrong
// Be consistent: Use same receiver name for all methods of a type
func (c *Client) Connect() error {}
func (c *Client) Disconnect() error {}
func (c *Client) Send(msg Message) error {}When to Use Value Receiver:
// Value receiver is appropriate when:
// - The method does not modify the receiver
// - Small, immutable structs
// - Basic types (int, string)
// - map, func, chan types
// - Slices that don't need reslicing
type Point struct {
X, Y float64
}
func (p Point) Distance(q Point) float64 {
return math.Sqrt((p.X-q.X)*(p.X-q.X) + (p.Y-q.Y)*(p.Y-q.Y))
}When to Use Pointer Receiver:
// Pointer receiver is appropriate when:
// - The method modifies the receiver
// - Contains sync.Mutex or similar sync fields
// - Large structs (avoid copy overhead)
// - Contains pointer fields
// - Implements unmarshaling or similar methods
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}Do Not Mix Receivers:
// Wrong: Mixing value and pointer receivers on same type
type T struct{}
func (t T) Method1() {} // Value receiver
func (t *T) Method2() {} // Pointer receiver - inconsistent!
// Correct: Use pointer receiver consistently
func (t *T) Method1() {}
func (t *T) Method2() {}Slice and Map Operations
Use slices and maps correctly.
Slice Preallocation:
// Wrong: Frequent resizing
func collect(n int) []int {
var result []int
for i := 0; i < n; i++ {
result = append(result, i)
}
return result
}
// Correct: Preallocate capacity
func collect(n int) []int {
result := make([]int, 0, n)
for i := 0; i < n; i++ {
result = append(result, i)
}
return result
}Slice Copying:
// Shallow copy
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)
// Or use append
dst := append([]int(nil), src...)Map Initialization:
// Preallocate when size is known
m := make(map[string]int, 100)
// Check key existence
if v, ok := m["key"]; ok {
fmt.Println(v)
}
// Delete key
delete(m, "key")Delete While Iterating:
// Wrong: Modify during iteration
for k := range m {
if shouldDelete(k) {
delete(m, k) // Undefined behavior
}
}
// Correct: Collect then delete
var toDelete []string
for k := range m {
if shouldDelete(k) {
toDelete = append(toDelete, k)
}
}
for _, k := range toDelete {
delete(m, k)
}nil Slice vs Empty Slice:
var s1 []int // nil slice, len=0, cap=0
s2 := []int{} // empty slice, len=0, cap=0
s3 := make([]int,0) // empty slice, len=0, cap=0
// All can be appended to
s1 = append(s1, 1) // Works fine
// JSON serialization differs
json.Marshal(s1) // null
json.Marshal(s2) // []Struct Initialization
Use field names for initialization to avoid positional dependency.
Bad Example (Positional Initialization):
// Depends on field order, breaks when new fields are added
user := User{"John", "john@example.com", 25, true}Good Example (Named Field Initialization):
user := User{
Name: "John",
Email: "john@example.com",
Age: 25,
Active: true,
}Constructor Pattern:
// Simple constructor
func NewUser(name, email string) *User {
return &User{
Name: name,
Email: email,
CreatedAt: time.Now(),
}
}
// Constructor with validation
func NewUser(name, email string) (*User, error) {
if name == "" {
return nil, errors.New("name is required")
}
if !isValidEmail(email) {
return nil, errors.New("invalid email")
}
return &User{Name: name, Email: email}, nil
}Functional Options Pattern (Multiple Optional Parameters):
type Option func(*Server)
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}
func NewServer(opts ...Option) *Server {
s := &Server{
port: 8080,
timeout: 30 * time.Second,
}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage
server := NewServer(
WithPort(9000),
WithTimeout(60*time.Second),
)Zero Value Utilization
Use Go's zero value feature to simplify code.
Zero Values Ready to Use:
// sync.Mutex zero value is usable
var mu sync.Mutex
mu.Lock()
mu.Unlock()
// bytes.Buffer zero value is usable
var buf bytes.Buffer
buf.WriteString("hello")
// sync.WaitGroup zero value is usable
var wg sync.WaitGroup
wg.Add(1)
// sync.Once zero value is usable
var once sync.Once
once.Do(func() {})Design APIs with Meaningful Zero Values:
// Good design: Zero value is meaningful
type Config struct {
Timeout time.Duration // Zero value 0 can mean "use default"
Retries int // Zero value 0 can mean "no retries"
}
func NewClient(cfg Config) *Client {
if cfg.Timeout == 0 {
cfg.Timeout = 30 * time.Second
}
return &Client{cfg: cfg}
}
// Callers can omit fields
client := NewClient(Config{}) // Use all defaultsBoolean Zero Value:
// Zero value false should be a reasonable default
type Options struct {
DisableCache bool // false = caching enabled (default)
SkipValidate bool // false = validation enabled (default)
}
// Avoid double negatives
// Wrong: DisableDisableCache bool
// Correct: EnableCache bool (if default is no caching)Pointer Zero Value:
// nil pointer means "not set"
type User struct {
Name string
Nickname *string // nil = nickname not set
}
// Check if set
if user.Nickname != nil {
fmt.Println(*user.Nickname)
}Code Formatting
Use gofmt and goimports to maintain consistent code style.
gofmt - Standard Formatting:
# Format single file
gofmt -w file.go
# Format entire project
gofmt -w .
# Check without modifying (for CI)
gofmt -d . | grep -q . && echo "Formatting needed" && exit 1goimports - Formatting + Import Organization:
# Install
go install golang.org/x/tools/cmd/goimports@latest
# Format and organize imports
goimports -w .
# Specify local package prefix (for import grouping)
goimports -w -local mycompany.com/myproject .Import Grouping Convention:
import (
// Standard library
"context"
"fmt"
"net/http"
// Third-party libraries
"github.com/gin-gonic/gin"
"gorm.io/gorm"
// Local packages
"mycompany.com/myproject/internal/domain"
"mycompany.com/myproject/pkg/errors"
)IDE/Editor Configuration:
VS Code settings.json:
{
"go.formatTool": "goimports",
"editor.formatOnSave": true,
"[go]": {
"editor.defaultFormatter": "golang.go"
}
}Git hooks (pre-commit):
#!/bin/sh
# .git/hooks/pre-commit
STAGED_GO_FILES=$(git diff --cached --name-only | grep ".go$")
if [ -z "$STAGED_GO_FILES" ]; then
exit 0
fi
UNFMT=$(gofmt -l $STAGED_GO_FILES)
if [ -n "$UNFMT" ]; then
echo "The following files need formatting:"
echo "$UNFMT"
exit 1
figolangci-lint Configuration
Use golangci-lint for comprehensive code quality checking.
Installation:
# macOS
brew install golangci-lint
# Or via go install
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latestRecommended Configuration (.golangci.yml):
run:
timeout: 5m
tests: true
linters:
enable:
# Default enabled
- errcheck # Check unhandled errors
- gosimple # Code simplification suggestions
- govet # Suspicious code checking
- ineffassign # Ineffective assignment checking
- staticcheck # Static analysis
- unused # Unused code checking
# Additional recommended
- gofmt # Format checking
- goimports # Import formatting
- revive # golint replacement
- misspell # Spelling checking
- unconvert # Unnecessary type conversions
- unparam # Unused parameters
- prealloc # Slice preallocation suggestions
- exportloopref # Loop variable references
- bodyclose # HTTP Body close checking
- noctx # HTTP request missing context
- sqlclosecheck # SQL connection close checking
- gocritic # Code style suggestions
- gosec # Security checking
linters-settings:
revive:
rules:
- name: exported
arguments: [checkPrivateReceivers]
- name: blank-imports
- name: context-as-argument
- name: error-return
- name: error-strings
- name: error-naming
- name: increment-decrement
- name: var-naming
- name: package-comments
disabled: true
gocritic:
enabled-tags:
- diagnostic
- style
- performance
gosec:
excludes:
- G104 # Unhandled errors (covered by errcheck)
issues:
exclude-rules:
- path: _test\.go
linters:
- gosec
- errcheckRunning:
golangci-lint run ./...Static Analysis
Use go vet for static analysis.
Basic Usage:
go vet ./...Common Checks:
// 1. Printf format errors
fmt.Printf("%d", "string") // go vet: type mismatch
// 2. Unused results
strings.ToLower(s) // go vet: result not used
// 3. Unreachable code
func example() int {
return 1
fmt.Println("unreachable") // go vet: unreachable
}
// 4. Incorrect lock usage
mu.Lock()
// Forgot to Unlock
// 5. Loop variable capture
for _, v := range values {
go func() {
fmt.Println(v) // go vet: loop variable capture
}()
}
// 6. Incorrect struct tags
type User struct {
Name string `json:name` // go vet: malformed, should be json:"name"
}
// 7. Copying sync types
var mu sync.Mutex
mu2 := mu // go vet: copied lock
// 8. Incorrect atomic operations
var n int64
n = atomic.AddInt64(&n, 1) // go vet: assignment to nShadow Checking:
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
# Check variable shadowing
shadow ./...func example() {
err := doSomething()
if err != nil {
err := handleError() // shadow: err is shadowed
log.Println(err)
}
return err // Returns outer err
}CI Integration:
- name: Static Analysis
run: |
go vet ./...
shadow ./...Customizable Linter
Use revive for flexible code checking.
Installation:
go install github.com/mgechev/revive@latestRunning:
revive ./...Configuration File (revive.toml):
ignoreGeneratedHeader = true
severity = "warning"
confidence = 0.8
[rule.blank-imports]
[rule.context-as-argument]
[rule.context-keys-type]
[rule.dot-imports]
[rule.error-return]
[rule.error-strings]
[rule.error-naming]
[rule.exported]
arguments = ["checkPrivateReceivers", "disableStutteringCheck"]
[rule.if-return]
[rule.increment-decrement]
[rule.var-naming]
[rule.var-declaration]
[rule.package-comments]
[rule.range]
[rule.receiver-naming]
[rule.time-naming]
[rule.unexported-return]
[rule.indent-error-flow]
[rule.errorf]
[rule.empty-block]
[rule.superfluous-else]
[rule.unused-parameter]
[rule.unreachable-code]
[rule.redefines-builtin-id]
# Function complexity limit
[rule.cognitive-complexity]
arguments = [15]
# Function line limit
[rule.function-length]
arguments = [50, 0]
# Parameter count limit
[rule.argument-limit]
arguments = [5]
# Return value count limit
[rule.function-result-limit]
arguments = [3]Common Rules:
| Rule | Description |
|---|---|
blank-imports | Forbid blank imports (except for side effects) |
context-as-argument | context.Context must be the first parameter |
error-return | error must be the last return value |
error-naming | error variable names must start with err or Err |
exported | Exported items must have comments |
var-naming | Variable naming convention checking |
cognitive-complexity | Cognitive complexity limit |
function-length | Function length limit |
Integration with golangci-lint:
# .golangci.yml
linters:
enable:
- revive
linters-settings:
revive:
rules:
- name: blank-imports
- name: context-as-argument
- name: error-return
- name: cognitive-complexity
arguments: [15]Advanced Static Checking
Use staticcheck for deep static analysis.
Installation:
go install honnef.co/go/tools/cmd/staticcheck@latestRunning:
staticcheck ./...Common Checks:
// SA1: Various bug checks
// SA1000: Regex syntax error
regexp.MustCompile("[") // invalid regex
// SA1006: Printf arguments
fmt.Printf("%s", 123) // wrong type
// SA1012: nil context
context.WithValue(nil, key, val)
// SA2: Concurrency issues
// SA2000: sync.WaitGroup.Add called inside goroutine
go func() {
wg.Add(1) // SA2000
defer wg.Done()
}()
// SA4: Useless code
// SA4003: Pointless comparison
if x > 0 && x > 10 {} // x > 10 already implies x > 0
// SA4006: Value not used
x := 1
x = 2 // SA4006: first assignment to x never used
// SA5: Correctness issues
// SA5001: os.Exit called in defer
defer os.Exit(1) // SA5001
// SA9: Suspicious code structures
// SA9003: Empty branch
if condition {
} else {
doSomething()
}Configuration File (staticcheck.conf):
checks = ["all", "-ST1000", "-ST1003"]
[[exclude]]
checks = ["SA1019"] # Ignore deprecated API warningsIntegration with golangci-lint:
# .golangci.yml
linters:
enable:
- staticcheck
linters-settings:
staticcheck:
checks:
- all
- -SA1019 # Ignore deprecation warningsContainer Preallocation
Preallocate slice and map capacity when size is known.
Slice Preallocation:
// Wrong: Dynamic resizing causes multiple allocations
func collect(n int) []int {
var result []int // len=0, cap=0
for i := 0; i < n; i++ {
result = append(result, i) // Multiple resizes
}
return result
}
// Correct: Preallocate capacity
func collect(n int) []int {
result := make([]int, 0, n) // len=0, cap=n
for i := 0; i < n; i++ {
result = append(result, i) // No resizing
}
return result
}
// When exact length is known, set len directly
func collect(n int) []int {
result := make([]int, n) // len=n, cap=n
for i := 0; i < n; i++ {
result[i] = i // Direct assignment
}
return result
}Map Preallocation:
// Wrong: Frequent rehashing
m := make(map[string]int) // Default capacity is very small
for i := 0; i < 10000; i++ {
m[strconv.Itoa(i)] = i // Multiple rehashes
}
// Correct: Estimate capacity
m := make(map[string]int, 10000)
for i := 0; i < 10000; i++ {
m[strconv.Itoa(i)] = i // No rehashing
}Building from Other Containers:
// Correct: Preallocate based on source container size
func transform(input []string) []int {
result := make([]int, 0, len(input))
for _, s := range input {
if n, err := strconv.Atoi(s); err == nil {
result = append(result, n)
}
}
return result
}
// Map to slice
func keys(m map[string]int) []string {
result := make([]string, 0, len(m))
for k := range m {
result = append(result, k)
}
return result
}Performance Impact:
// Slice with 1000 elements
BenchmarkNoPrealloc-8 50000 30000 ns/op 40960 B/op 11 allocs/op
BenchmarkPrealloc-8 200000 8000 ns/op 8192 B/op 1 allocs/op
// Preallocation is 3.75x faster with 80% less memoryStrategy When Size is Unknown:
// Use reasonable initial estimate
result := make([]Item, 0, 64) // Usually sufficient for common cases
// Or estimate based on probability distribution
// If 90% of cases have fewer than 100 elements
result := make([]Item, 0, 100)Use strconv Instead of fmt
Use strconv instead of fmt for basic type conversions, for better performance.
String to Integer:
// Wrong: Using fmt.Sprintf
s := fmt.Sprintf("%d", 42)
// Correct: Using strconv
s := strconv.Itoa(42) // int -> string
s := strconv.FormatInt(42, 10) // int64 -> string
// Parsing
i, err := strconv.Atoi("42") // string -> int
i64, err := strconv.ParseInt("42", 10, 64) // string -> int64String to Float:
// Wrong
s := fmt.Sprintf("%f", 3.14)
// Correct
s := strconv.FormatFloat(3.14, 'f', -1, 64)
// Parsing
f, err := strconv.ParseFloat("3.14", 64)Boolean Conversion:
// Wrong
s := fmt.Sprintf("%t", true)
// Correct
s := strconv.FormatBool(true)
// Parsing
b, err := strconv.ParseBool("true")Performance Comparison (Benchmark Results):
BenchmarkFmtSprintf-8 10000000 120 ns/op 16 B/op 2 allocs/op
BenchmarkStrconvItoa-8 50000000 30 ns/op 3 B/op 1 allocs/opstrconv is 4x faster than fmt, with 50% less memory allocation.
When to Use fmt:
// Complex formatting still uses fmt
s := fmt.Sprintf("User %s (ID: %d) has %d items", name, id, count)
// Debug output
fmt.Printf("%+v\n", obj)
fmt.Printf("%#v\n", obj) // Go syntax representation
// Formatted output
fmt.Fprintf(w, "Status: %d\n", code)Conversions in Loops:
// Wrong: Repeated conversion in loop
for _, id := range ids {
key := fmt.Sprintf("user:%d", id) // Allocates each time
}
// Correct: Use strconv
for _, id := range ids {
key := "user:" + strconv.Itoa(id) // More efficient
}Performance Benchmark Testing
Use benchmark tests to quantify performance.
Basic Benchmark Test:
// user_test.go
func BenchmarkHashPassword(b *testing.B) {
password := "mysecretpassword"
for i := 0; i < b.N; i++ {
HashPassword(password)
}
}
func BenchmarkValidateEmail(b *testing.B) {
email := "test@example.com"
for i := 0; i < b.N; i++ {
ValidateEmail(email)
}
}Running Benchmark Tests:
# Run all benchmarks
go test -bench=. ./...
# Run specific benchmark
go test -bench=BenchmarkHashPassword ./...
# Include memory allocation statistics
go test -bench=. -benchmem ./...
# Run multiple times for average
go test -bench=. -count=5 ./...Output Example:
BenchmarkHashPassword-8 100 15234567 ns/op 4096 B/op 2 allocs/opComparing Benchmarks:
# Install benchstat
go install golang.org/x/perf/cmd/benchstat@latest
# Save benchmark results
go test -bench=. -count=10 > old.txt
# After code changes
go test -bench=. -count=10 > new.txt
# Compare results
benchstat old.txt new.txtSub-benchmarks:
func BenchmarkEncode(b *testing.B) {
sizes := []int{100, 1000, 10000}
for _, size := range sizes {
b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
data := make([]byte, size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
Encode(data)
}
})
}
}Avoiding Compiler Optimization:
var result string
func BenchmarkProcess(b *testing.B) {
var r string
for i := 0; i < b.N; i++ {
r = Process(input)
}
result = r // Prevent being optimized away
}99% Test Coverage Target
Target 99% test coverage to ensure code quality.
Running Coverage Detection:
# Generate coverage report
go test -coverprofile=coverage.out ./...
# View coverage summary
go tool cover -func=coverage.out
# Generate HTML report
go tool cover -html=coverage.out -o coverage.html
# View by package
go test -cover ./...CI Enforced Coverage:
# .github/workflows/test.yml
- name: Test with coverage
run: |
go test -coverprofile=coverage.out ./...
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
if (( $(echo "$COVERAGE < 99" | bc -l) )); then
echo "Coverage is below 99%: $COVERAGE%"
exit 1
fiCoverage Strategy:
| Layer | Target Coverage | Focus |
|---|---|---|
| Domain Layer | 100% | Core business logic |
| Application Layer | 99% | Use case flows |
| Infrastructure Layer | 95% | Integration tests primarily |
| Interface Layer | 90% | HTTP handlers |
Excluding Code from Coverage:
// Add comment to exclude generated code
//go:generate mockgen ...
// Wire-generated code is typically in wire_gen.goMakefile Integration:
.PHONY: test
test:
go test -race -coverprofile=coverage.out ./...
@go tool cover -func=coverage.out | grep total | awk '{print "Coverage: " $$3}'
.PHONY: coverage
coverage: test
go tool cover -html=coverage.out -o coverage.html
open coverage.htmlTest Helper Function Guidelines
Write test helper functions correctly.
Use t.Helper():
// Helper functions must call t.Helper()
func assertEqual(t *testing.T, got, want interface{}) {
t.Helper() // Mark as helper function
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
// When called, error line number points to test code, not helper function
func TestAdd(t *testing.T) {
result := Add(1, 2)
assertEqual(t, result, 3) // Error will point to this line
}Setup Helper Functions:
// Return cleanup function
func setupTestDB(t *testing.T) (*sql.DB, func()) {
t.Helper()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("failed to open db: %v", err)
}
return db, func() {
db.Close()
}
}
func TestUser(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
// Test...
}Using t.Cleanup() (Go 1.14+):
func setupTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("failed to open db: %v", err)
}
t.Cleanup(func() {
db.Close()
})
return db
}
func TestUser(t *testing.T) {
db := setupTestDB(t) // No manual cleanup needed
// Test...
}Assertion Helper Functions:
// Keep validation logic in test functions, not helpers
// Wrong: Helper function contains validation logic
func assertUserCreated(t *testing.T, db *sql.DB, email string) {
t.Helper()
var count int
db.QueryRow("SELECT COUNT(*) FROM users WHERE email = ?", email).Scan(&count)
if count != 1 {
t.Errorf("expected 1 user with email %s, got %d", email, count)
}
}
// Correct: Helper only fetches, validation stays in test
func getUserCount(t *testing.T, db *sql.DB, email string) int {
t.Helper()
var count int
if err := db.QueryRow("SELECT COUNT(*) FROM users WHERE email = ?", email).Scan(&count); err != nil {
t.Fatalf("failed to query: %v", err)
}
return count
}
func TestCreateUser(t *testing.T) {
db := setupTestDB(t)
createUser(db, "test@example.com")
count := getUserCount(t, db, "test@example.com")
if count != 1 {
t.Errorf("expected 1 user, got %d", count)
}
}Avoid Calling t.Fatal in Goroutines:
// Wrong: Calling t.Fatal in goroutine
func TestConcurrent(t *testing.T) {
go func() {
if err := doSomething(); err != nil {
t.Fatal(err) // Wrong! Will panic
}
}()
}
// Correct: Use channel to pass errors
func TestConcurrent(t *testing.T) {
errs := make(chan error, 1)
go func() {
errs <- doSomething()
}()
if err := <-errs; err != nil {
t.Fatal(err)
}
}Integration Testing Guidelines
Write integration tests to verify component collaboration.
Test File Naming:
user_test.go # Unit tests
user_integration_test.go # Integration testsUsing Build Tags for Isolation:
//go:build integration
package integration
func TestUserFlow(t *testing.T) {
// ...
}# Run only unit tests (default)
go test ./...
# Include integration tests
go test -tags=integration ./...Test Containers (testcontainers):
//go:build integration
package integration
import (
"context"
"testing"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/mysql"
)
func TestUserRepository(t *testing.T) {
ctx := context.Background()
// Start MySQL container
mysqlC, err := mysql.RunContainer(ctx,
testcontainers.WithImage("mysql:8.0"),
mysql.WithDatabase("testdb"),
mysql.WithUsername("test"),
mysql.WithPassword("test"),
)
if err != nil {
t.Fatal(err)
}
defer mysqlC.Terminate(ctx)
// Get connection string
connStr, err := mysqlC.ConnectionString(ctx)
if err != nil {
t.Fatal(err)
}
// Initialize database
db := setupDB(t, connStr)
repo := NewUserRepository(db)
// Test
t.Run("create and find user", func(t *testing.T) {
user := &User{Name: "John", Email: "john@example.com"}
err := repo.Save(ctx, user)
if err != nil {
t.Fatalf("Save() error = %v", err)
}
found, err := repo.FindByID(ctx, user.ID)
if err != nil {
t.Fatalf("FindByID() error = %v", err)
}
if found.Email != user.Email {
t.Errorf("Email = %s, want %s", found.Email, user.Email)
}
})
}HTTP API Integration Test:
func TestAPI(t *testing.T) {
router := setupRouter()
server := httptest.NewServer(router)
defer server.Close()
t.Run("create user", func(t *testing.T) {
body := `{"name":"John","email":"john@example.com"}`
resp, err := http.Post(server.URL+"/api/users", "application/json", strings.NewReader(body))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusCreated)
}
})
}Mock and Interface Abstraction
Achieve dependency injection and mock testing through interface abstraction.
Install mockgen:
go install go.uber.org/mock/mockgen@latestDefine Interface and Generate Mock:
// internal/domain/user/repository.go
package user
//go:generate mockgen -source=repository.go -destination=mock_repository.go -package=user
type Repository interface {
FindByID(ctx context.Context, id uint64) (*User, error)
Save(ctx context.Context, user *User) error
}go generate ./...Using Mock in Tests:
func TestUserHandler_CreateUser(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := user.NewMockRepository(ctrl)
// Set expectations
mockRepo.EXPECT().
FindByEmail(gomock.Any(), "john@example.com").
Return(nil, user.ErrUserNotFound)
mockRepo.EXPECT().
Save(gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, u *user.User) error {
u.ID = 1 // Simulate database-generated ID
return nil
})
handler := NewHandler(mockRepo)
u, err := handler.CreateUser(context.Background(), CreateUserCommand{
Name: "John",
Email: "john@example.com",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if u.ID != 1 {
t.Errorf("user.ID = %d, want 1", u.ID)
}
}Manual Mock (Simple Scenarios):
type mockUserRepo struct {
findByIDFunc func(ctx context.Context, id uint64) (*User, error)
saveFunc func(ctx context.Context, user *User) error
}
func (m *mockUserRepo) FindByID(ctx context.Context, id uint64) (*User, error) {
return m.findByIDFunc(ctx, id)
}
func (m *mockUserRepo) Save(ctx context.Context, user *User) error {
return m.saveFunc(ctx, user)
}Table-Driven Tests
Use table-driven tests to improve test maintainability.
Good Example:
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"mixed numbers", -2, 3, 1},
{"zeros", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, result, tt.expected)
}
})
}
}With Setup and Teardown:
func TestUserService(t *testing.T) {
tests := []struct {
name string
setup func(*testing.T) *UserService
input CreateUserRequest
wantErr bool
wantUser *User
}{
{
name: "create valid user",
setup: func(t *testing.T) *UserService {
repo := &mockRepo{}
return NewUserService(repo)
},
input: CreateUserRequest{
Name: "John",
Email: "john@example.com",
},
wantErr: false,
wantUser: &User{
Name: "John",
Email: "john@example.com",
},
},
{
name: "invalid email",
setup: func(t *testing.T) *UserService {
return NewUserService(&mockRepo{})
},
input: CreateUserRequest{
Name: "John",
Email: "invalid",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc := tt.setup(t)
user, err := svc.CreateUser(context.Background(), tt.input)
if (err != nil) != tt.wantErr {
t.Fatalf("CreateUser() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr && user.Email != tt.wantUser.Email {
t.Errorf("user.Email = %s, want %s", user.Email, tt.wantUser.Email)
}
})
}
}testify Assertion Library Usage
Use testify for clearer assertions.
Installation:
go get github.com/stretchr/testifyassert vs require:
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUser(t *testing.T) {
// assert - continues execution after failure
assert.Equal(t, "John", user.Name)
assert.NotNil(t, user.Email)
// require - stops immediately on failure
require.NoError(t, err) // Subsequent code depends on err being nil
require.NotNil(t, user)
// Subsequent assertions depend on user not being nil
assert.Equal(t, "john@example.com", user.Email)
}Common Assertions:
// Equality
assert.Equal(t, expected, actual)
assert.NotEqual(t, expected, actual)
// Nil checks
assert.Nil(t, obj)
assert.NotNil(t, obj)
// Boolean
assert.True(t, condition)
assert.False(t, condition)
// Errors
assert.NoError(t, err)
assert.Error(t, err)
assert.ErrorIs(t, err, ErrNotFound)
assert.ErrorContains(t, err, "not found")
// Collections
assert.Len(t, slice, 3)
assert.Contains(t, slice, item)
assert.Empty(t, slice)
// Types
assert.IsType(t, &User{}, obj)
// Comparisons
assert.Greater(t, 2, 1)
assert.Less(t, 1, 2)Custom Messages:
assert.Equal(t, expected, actual, "user name should match")
assert.Equalf(t, expected, actual, "user %d name should match", userID)Suite Testing:
type UserTestSuite struct {
suite.Suite
db *gorm.DB
repo *UserRepository
}
func (s *UserTestSuite) SetupSuite() {
s.db = setupTestDB()
s.repo = NewUserRepository(s.db)
}
func (s *UserTestSuite) TearDownSuite() {
s.db.Close()
}
func (s *UserTestSuite) TestCreateUser() {
user := &User{Name: "John"}
err := s.repo.Save(context.Background(), user)
s.NoError(err)
s.NotZero(user.ID)
}
func TestUserSuite(t *testing.T) {
suite.Run(t, new(UserTestSuite))
}