关于 golang-best-practices
面向 Go 开发者的实践指南,适用于编写、审查和重构代码。内容涵盖项目组织、错误处理、并发、测试、性能及 Uber FX 依赖注入。
主要功能
- 01Go 项目结构建议
- 02错误检查与包装
- 03并发和通道模式
- 04接口与分层设计
- 05测试和性能指导
- 06Uber FX 依赖注入
适用场景
- 01规划 Go 项目目录
- 02审查错误处理和并发代码
- 03为依赖编写测试
- 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_Storefx-advanced-guide.mdLICENSECONTRIBUTING.mdfx-simplified-architecture.mdCHANGELOG.mdfx-basic-integration.mdREADME.mdreferences/fx-architecture.mdpatterns.mdtools.mdstdlib.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 最佳实践
应该 ✅
- 使用返回指针的构造函数:
func NewX() *X - 通过构造函数参数注入依赖项
- 使用
fx.Lifecycle处理启动和关闭逻辑 - 让处理器专注于 HTTP 相关事项
- 将复杂逻辑提取到服务中
不应该 ❌
- 不要在服务中访问
gin.Context - 不要创建全局变量
- 不要将 HTTP 逻辑与业务逻辑混在一起
- 不要忘记处理生命周期错误
- 不要创建循环依赖
使用 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