mcpField
G
golang-best-practices

golang-best-practices

github.com/difyz9开发294 次安装38 周前更新
代码仓库

关于 golang-best-practices

面向 Go 开发者的实践指南,适用于编写、审查和重构代码。内容涵盖项目组织、错误处理、并发、测试、性能及 Uber FX 依赖注入。

主要功能

  1. 01Go 项目结构建议
  2. 02错误检查与包装
  3. 03并发和通道模式
  4. 04接口与分层设计
  5. 05测试和性能指导
  6. 06Uber FX 依赖注入

适用场景

  1. 01规划 Go 项目目录
  2. 02审查错误处理和并发代码
  3. 03为依赖编写测试
  4. 04使用 FX 管理应用依赖与生命周期

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/difyz9/golang-best-practices --skill golang-best-practices

安装后在 Claude Code 中输入/golang-best-practices确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.gitignore
.DS_Store
fx-advanced-guide.md
LICENSE
CONTRIBUTING.md
fx-simplified-architecture.md
CHANGELOG.md
fx-basic-integration.md
README.md
references/
fx-architecture.md
patterns.md
tools.md
stdlib.md

详细介绍

Golang 最佳实践

概述

本技能介绍了经过实战验证的 Go 开发最佳实践,涵盖项目组织、错误处理、并发模式、测试策略、性能优化和代码质量标准。编写、审查或重构 Go 代码时,请遵循这些指南。

项目结构

标准布局

遵循简洁实用的 Go 项目结构:

project/
├── cmd/
│   └── server/
│       └── main.go           # Application entry point
├── internal/
│   ├── model/                # Domain models/entities
│   │   ├── user.go
│   │   └── product.go
│   ├── handler/              # HTTP handlers
│   │   ├── user_handler.go
│   │   ├── product_handler.go
│   │   └── middleware.go
│   ├── service/              # Business logic & data access
│   │   ├── user_service.go
│   │   └── product_service.go
│   └── config/               # Configuration
│       └── config.go
├── pkg/                      # Reusable utilities
│   ├── logger/
│   ├── validator/
│   └── utils/
├── migrations/               # Database migrations
│   ├── 001_create_users.up.sql
│   └── 001_create_users.down.sql
├── configs/                  # Config files
│   ├── config.yaml
│   └── config.local.yaml
├── scripts/                  # Helper scripts
│   └── build.sh
├── docs/                     # Documentation
├── Dockerfile
├── docker-compose.yml
├── go.mod
├── go.sum
├── Makefile
├── .env.example
├── .gitignore
└── README.md

目录说明:

  • cmd/server - 应用程序入口,包含精简的 main.go
  • internal/model - 领域实体和业务模型
  • internal/handler - HTTP 请求处理器和中间件
  • internal/service - 业务逻辑和数据库操作
  • internal/config - 应用程序配置加载
  • pkg/ - 可提取到独立包中的共享工具
  • migrations/ - 数据库架构的 SQL 迁移文件
  • configs/ - 不同环境的 YAML/JSON 配置文件
核心原则
  • 使用 internal/ 存放不应被外部项目导入的代码
  • 使用 pkg/ 存放可复用的库代码
  • 保持 cmd/ 精简——仅包含初始化和依赖组装
  • 一个包只负责一项职责

错误处理

始终检查错误
// ❌ BAD
data, _ := os.ReadFile("file.txt")

// ✅ GOOD
data, err := os.ReadFile("file.txt")
if err != nil {
    return fmt.Errorf("read file: %w", err)
}
错误包装

使用 %w 包装错误,以便检查错误链:

if err != nil {
    return fmt.Errorf("failed to process user %s: %w", userID, err)
}
自定义错误

创建哨兵错误和自定义错误类型:

var ErrNotFound = errors.New("resource not found")

type ValidationError struct {
    Field string
    Err   error
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed on %s: %v", e.Field, e.Err)
}
错误检查
// Check for specific errors
if errors.Is(err, ErrNotFound) {
    // handle not found
}

// Check error type
var valErr *ValidationError
if errors.As(err, &valErr) {
    // handle validation error
}

并发

Goroutine 最佳实践

始终管理 goroutine 的生命周期:

// ✅ Use context for cancellation
func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            // do work
        }
    }
}

// ✅ Use WaitGroup for synchronization
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        // work
    }(i)
}
wg.Wait()
通道模式

生产者-消费者:

func producer(ch chan<- int) {
    defer close(ch)
    for i := 0; i < 10; i++ {
        ch <- i
    }
}

func consumer(ch <-chan int) {
    for val := range ch {
        fmt.Println(val)
    }
}

扇出/扇入:

func fanOut(in <-chan int, workers int) []<-chan int {
    channels := make([]<-chan int, workers)
    for i := 0; i < workers; i++ {
        channels[i] = worker(in)
    }
    return channels
}

func fanIn(channels ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    for _, ch := range channels {
        wg.Add(1)
        go func(c <-chan int) {
            defer wg.Done()
            for val := range c {
                out <- val
            }
        }(ch)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}
避免竞态条件
  • 对共享状态使用 sync.Mutex
  • 使用通道进行通信
  • 使用 -race 标志运行测试
  • 优先使用不可变数据结构

接口设计

小型接口
// ✅ Small, focused interfaces
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// Compose when needed
type ReadWriter interface {
    Reader
    Writer
}
接受接口,返回结构体
// ✅ Accept interface for flexibility
func Process(r io.Reader) error {
    // implementation
}

// ✅ Return concrete type
func NewClient() *Client {
    return &Client{}
}

代码质量

命名约定
  • 私有名称使用 camelCase,公开名称使用 PascalCase
  • 名称应简短且有描述性
  • 避免名称冗余:user.UserID → user.ID
  • 保持命名一致:Get、Set、New、Init
函数设计
  • 函数应小巧且专注
  • 限制参数数量(最多 3-4 个)
  • 使用函数选项进行配置:
type Option func(*Server)

func WithTimeout(d time.Duration) Option {
    return func(s *Server) {
        s.timeout = d
    }
}

func NewServer(opts ...Option) *Server {
    s := &Server{timeout: 30 * time.Second}
    for _, opt := range opts {
        opt(s)
    }
    return s
}
常量和枚举
// Use iota for enums
type Status int

const (
    StatusPending Status = iota
    StatusActive
    StatusInactive
)

// Group related constants
const (
    maxRetries    = 3
    retryDelay    = time.Second
    timeout       = 30 * time.Second
)

测试

表驱动测试
func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 2, 3, 5},
        {"negative", -2, -3, -5},
        {"mixed", -2, 3, 1},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("got %d, want %d", result, tt.expected)
            }
        })
    }
}
测试辅助函数
func TestMain(m *testing.M) {
    // Setup
    code := m.Run()
    // Teardown
    os.Exit(code)
}

func setupTest(t *testing.T) func() {
    // Setup code
    return func() {
        // Cleanup code
    }
}

func TestSomething(t *testing.T) {
    cleanup := setupTest(t)
    defer cleanup()
    // test code
}
模拟

使用接口表示依赖项,以便进行测试:

type UserStore interface {
    Get(id string) (*User, error)
}

type Service struct {
    store UserStore
}

// In tests, provide mock implementation
type mockStore struct{}
func (m *mockStore) Get(id string) (*User, error) {
    return &User{ID: id}, nil
}

性能

内存分配
  • 已知切片大小时预先分配:make([]int, 0, capacity)
  • 使用 sync.Pool 复用缓冲区
  • 大型结构体使用指针
  • 避免在循环中拼接字符串(使用 strings.Builder)
性能分析
import _ "net/http/pprof"

// CPU profiling
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()

运行性能分析:

go test -cpuprofile cpu.prof -memprofile mem.prof -bench .
go tool pprof cpu.prof
基准测试
func BenchmarkFunction(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Function()
    }
}

// With setup
func BenchmarkWithSetup(b *testing.B) {
    data := setupData()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        Process(data)
    }
}

依赖项

模块管理
go mod init github.com/user/project
go mod tidy
go mod vendor  # Optional: vendor dependencies
固定版本
go get github.com/pkg/[email protected]
私有模块
export GOPRIVATE=github.com/myorg/*

文档

包文档
// Package mypackage provides utilities for X.
//
// This package includes:
//   - Feature A
//   - Feature B
package mypackage
函数文档
// ProcessData validates and transforms the input data.
// It returns an error if validation fails.
//
// Example:
//   result, err := ProcessData(input)
//   if err != nil {
//       log.Fatal(err)
//   }
func ProcessData(input string) (string, error) {
    // implementation
}

分层架构

双层模式

将关注点划分到不同层:

  • 处理器层:HTTP 请求/响应处理、输入验证、路由、数据库操作
  • 服务层:复杂业务逻辑、外部集成、共享工具

这种更简单的架构:

  • 降低大多数应用程序的复杂度
  • 让数据访问逻辑更贴近 HTTP 处理器
  • 更易于理解和维护
  • 加快 MVP 和中小型项目的开发速度

使用 Uber FX 进行依赖注入

核心概念

Uber FX 是一个依赖注入框架,可自动管理应用程序生命周期和依赖项。

主要优势:

  • ✅ 自动解析依赖项
  • ✅ 内置生命周期管理(OnStart/OnStop 钩子)
  • ✅ 清晰的依赖关系图
  • ✅ 通过注入模拟对象简化测试
处理器自管理路由模式

每个处理器都通过 RegisterRoutes 方法管理自己的路由:

package handler

import (
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

type UserHandler struct {
    db *gorm.DB
}

func NewUserHandler(db *gorm.DB) *UserHandler {
    return &UserHandler{db: db}
}

func (h *UserHandler) Register(c *gin.Context) {
    // Handle user registration
    var user User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    if err := h.db.Create(&user).Error; err != nil {
        c.JSON(500, gin.H{"error": "failed to create user"})
        return
    }
    
    c.JSON(200, gin.H{"data": user})
}

func (h *UserHandler) Login(c *gin.Context) {
    // Handle user login
}

func (h *UserHandler) GetProfile(c *gin.Context) {
    userID := c.GetUint("user_id")
    
    var user User
    if err := h.db.First(&user, userID).Error; err != nil {
        c.JSON(404, gin.H{"error": "user not found"})
        return
    }
    
    c.JSON(200, gin.H{"data": user})
}

// RegisterRoutes - Each handler registers its own routes
func (h *UserHandler) RegisterRoutes(r *gin.Engine) {
    api := r.Group("/api/user")
    {
        api.POST("/register", h.Register)
        api.POST("/login", h.Login)
        
        // Protected routes
        auth := api.Group("", AuthMiddleware())
        {
            auth.GET("/profile", h.GetProfile)
        }
    }
}
完整的 FX 应用示例
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
    
    "github.com/gin-gonic/gin"
    "go.uber.org/fx"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    
    "your-project/internal/config"
    "your-project/internal/handler"
    "your-project/internal/service"
    "your-project/internal/model"
)

func main() {
    app := fx.New(
        // ==================== Configuration ====================
        fx.Provide(config.Load),
        
        // ==================== Database ====================
        fx.Provide(NewDatabase),
        
        // ==================== Services ====================
        fx.Provide(service.NewEmailService),
        fx.Provide(service.NewSMSService),
        
        // ==================== Handlers ====================
        fx.Provide(handler.NewUserHandler),
        fx.Provide(handler.NewChatHandler),
        fx.Provide(handler.NewHealthHandler),
        
        // ==================== HTTP Server ====================
        fx.Provide(NewGinEngine),
        fx.Provide(NewHTTPServer),
        
        // ==================== Route Registration ====================
        fx.Invoke(RegisterAllRoutes),
        
        // ==================== Lifecycle ====================
        fx.Invoke(RegisterLifecycle),
    )
    
    // Start application
    startCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()
    
    if err := app.Start(startCtx); err != nil {
        log.Fatal("failed to start application:", err)
    }
    
    log.Println("✅ Application started successfully")
    
    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    
    log.Println("📡 Shutting down gracefully...")
    
    // Graceful shutdown
    stopCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    if err := app.Stop(stopCtx); err != nil {
        log.Fatal("failed to stop application:", err)
    }
    
    log.Println("👋 Application stopped")
}

// NewDatabase creates database connection with lifecycle management
func NewDatabase(lc fx.Lifecycle, cfg *config.Config) (*gorm.DB, error) {
    db, err := gorm.Open(mysql.Open(cfg.Database.DSN), &gorm.Config{})
    if err != nil {
        return nil, err
    }
    
    sqlDB, err := db.DB()
    if err != nil {
        return nil, err
    }
    
    // Configure connection pool
    sqlDB.SetMaxIdleConns(10)
    sqlDB.SetMaxOpenConns(100)
    sqlDB.SetConnMaxLifetime(time.Hour)
    
    // Auto-migrate models
    if err := db.AutoMigrate(&model.User{}, &model.Chat{}); err != nil {
        return nil, err
    }
    
    lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error {
            log.Println("📊 Database connected")
            return sqlDB.Ping()
        },
        OnStop: func(ctx context.Context) error {
            log.Println("📊 Closing database connection")
            return sqlDB.Close()
        },
    })
    
    return db, nil
}

// NewGinEngine creates Gin engine with middleware
func NewGinEngine(cfg *config.Config) *gin.Engine {
    if cfg.App.Env == "production" {
        gin.SetMode(gin.ReleaseMode)
    }
    
    engine := gin.New()
    engine.Use(gin.Recovery())
    engine.Use(LoggerMiddleware())
    engine.Use(CORSMiddleware())
    
    return engine
}

// NewHTTPServer creates HTTP server with lifecycle
func NewHTTPServer(lc fx.Lifecycle, engine *gin.Engine, cfg *config.Config) *http.Server {
    srv := &http.Server{
        Addr:           cfg.Server.Address,
        Handler:        engine,
        ReadTimeout:    cfg.Server.ReadTimeout,
        WriteTimeout:   cfg.Server.WriteTimeout,
        MaxHeaderBytes: 1 << 20,
    }
    
    lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error {
            go func() {
                log.Printf("🚀 HTTP server listening on %s", cfg.Server.Address)
                if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
                    log.Fatal("HTTP server error:", err)
                }
            }()
            return nil
        },
        OnStop: func(ctx context.Context) error {
            log.Println("🛑 Shutting down HTTP server")
            return srv.Shutdown(ctx)
        },
    })
    
    return srv
}

// RegisterAllRoutes registers all handler routes
func RegisterAllRoutes(
    engine *gin.Engine,
    healthHandler *handler.HealthHandler,
    userHandler *handler.UserHandler,
    chatHandler *handler.ChatHandler,
) {
    healthHandler.RegisterRoutes(engine)
    userHandler.RegisterRoutes(engine)
    chatHandler.RegisterRoutes(engine)
    
    log.Println("✅ All routes registered")
}

// RegisterLifecycle adds global lifecycle hooks
func RegisterLifecycle(lc fx.Lifecycle) {
    lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error {
            log.Println("🎯 Application starting...")
            return nil
        },
        OnStop: func(ctx context.Context) error {
            log.Println("🎯 Application stopping...")
            return nil
        },
    })
}
使用 FX 的服务层

当业务逻辑较复杂时,将其提取到服务中:

package service

import (
    "fmt"
    "gorm.io/gorm"
)

type EmailService struct {
    db     *gorm.DB
    apiKey string
}

func NewEmailService(db *gorm.DB, cfg *config.Config) *EmailService {
    return &EmailService{
        db:     db,
        apiKey: cfg.Email.APIKey,
    }
}

func (s *EmailService) SendWelcome(email string) error {
    // Complex email sending logic
    return nil
}

func (s *EmailService) SendVerification(userID uint) error {
    var user User
    if err := s.db.First(&user, userID).Error; err != nil {
        return fmt.Errorf("user not found: %w", err)
    }
    
    // Generate verification token and send email
    return nil
}
使用服务的处理器
type UserHandler struct {
    db           *gorm.DB
    emailService *service.EmailService
}

func NewUserHandler(db *gorm.DB, emailService *service.EmailService) *UserHandler {
    return &UserHandler{
        db:           db,
        emailService: emailService,
    }
}

func (h *UserHandler) Register(c *gin.Context) {
    var user User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    // Direct database access for simple operations
    if err := h.db.Create(&user).Error; err != nil {
        c.JSON(500, gin.H{"error": "failed to create user"})
        return
    }
    
    // Use service for complex operations
    go h.emailService.SendWelcome(user.Email)
    
    c.JSON(200, gin.H{"data": user})
}
FX 最佳实践

应该 ✅

  1. 使用返回指针的构造函数:func NewX() *X
  2. 通过构造函数参数注入依赖项
  3. 使用 fx.Lifecycle 处理启动和关闭逻辑
  4. 让处理器专注于 HTTP 相关事项
  5. 将复杂逻辑提取到服务中

不应该 ❌

  1. 不要在服务中访问 gin.Context
  2. 不要创建全局变量
  3. 不要将 HTTP 逻辑与业务逻辑混在一起
  4. 不要忘记处理生命周期错误
  5. 不要创建循环依赖
使用 FX 进行测试
func TestUserHandler(t *testing.T) {
    // Create test database
    db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
    db.AutoMigrate(&User{})
    
    // Create mock service
    emailService := &MockEmailService{}
    
    // Inject dependencies manually for testing
    handler := NewUserHandler(db, emailService)
    
    // Test handler methods
    w := httptest.NewRecorder()
    c, _ := gin.CreateTestContext(w)
    
    handler.Register(c)
    
    assert.Equal(t, 200, w.Code)
}

完整的实现示例请参见 references/fx-architecture.md。

资源

详细指南请参见随附的参考资料:

  • references/fx-architecture.md:完整的 Fx 依赖注入架构,涵盖处理器、服务、GORM 仓库和配置管理
  • references/patterns.md:常见的 Go 设计模式和惯用法
  • references/stdlib.md:重要标准库包指南
  • references/tools.md:开发工具和代码检查工具配置

评论

支持 Markdown