Announcement

👇Official Account👇

图片

Welcome to join the group & private message

Article first/tail QR code

Skip to content

高质量 Golang 后端的现代软件工程原则

写出能运行的代码很容易,写出高质量的代码需要遵循工程原则。

一、项目结构

1.1 标准目录结构

myapp/
├── cmd/                    # 应用程序入口
│   ├── api/               # HTTP API 服务
│   │   └── main.go
│   └── worker/            # 后台任务
│       └── main.go
├── internal/              # 私有代码
│   ├── domain/           # 领域模型
│   │   ├── user.go
│   │   └── order.go
│   ├── service/          # 业务逻辑
│   │   ├── user_service.go
│   │   └── order_service.go
│   ├── repository/       # 数据访问
│   │   ├── user_repo.go
│   │   └── order_repo.go
│   ├── handler/          # HTTP 处理
│   │   ├── user_handler.go
│   │   └── order_handler.go
│   └── infrastructure/   # 基础设施
│       ├── database/
│       ├── cache/
│       └── queue/
├── pkg/                   # 公共库
│   ├── logger/
│   ├── errors/
│   └── utils/
├── configs/              # 配置文件
├── deployments/          # 部署配置
├── scripts/              # 脚本
├── docs/                 # 文档
├── tests/                # 测试
├── go.mod
├── go.sum
├── Makefile
├── Dockerfile
└── README.md

1.2 分层架构

┌─────────────┐
│   Handler   │  HTTP 处理、参数校验
├─────────────┤
│   Service   │  业务逻辑、事务管理
├─────────────┤
│ Repository  │  数据访问、持久化
├─────────────┤
│  Domain     │  领域模型、业务规则
└─────────────┘

二、依赖管理

2.1 接口隔离

go
// 定义接口
package repository

type UserRepository interface {
    GetByID(ctx context.Context, id int64) (*domain.User, error)
    GetByEmail(ctx context.Context, email string) (*domain.User, error)
    Create(ctx context.Context, user *domain.User) error
    Update(ctx context.Context, user *domain.User) error
    Delete(ctx context.Context, id int64) error
}

// 实现
type userRepository struct {
    db *sql.DB
}

func NewUserRepository(db *sql.DB) UserRepository {
    return &userRepository{db: db}
}

2.2 依赖注入

go
// 使用 wire 进行依赖注入
// wire.go
//go:build wireinject
// +build wireinject

package main

import (
    "github.com/google/wire"
)

func InitializeApp() (*App, error) {
    wire.Build(
        database.NewDB,
        repository.NewUserRepository,
        service.NewUserService,
        handler.NewUserHandler,
        NewApp,
    )
    return &App{}, nil
}

三、错误处理

3.1 自定义错误

go
package errors

import (
    "errors"
    "fmt"
)

// 错误类型
type ErrorType int

const (
    ErrorTypeUnknown ErrorType = iota
    ErrorTypeNotFound
    ErrorTypeInvalidInput
    ErrorTypeUnauthorized
    ErrorTypeForbidden
    ErrorTypeInternal
)

// 应用错误
type AppError struct {
    Type    ErrorType
    Code    string
    Message string
    Err     error
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("%s: %v", e.Message, e.Err)
    }
    return e.Message
}

func (e *AppError) Unwrap() error {
    return e.Err
}

// 错误构造函数
func NotFound(code, message string) *AppError {
    return &AppError{
        Type:    ErrorTypeNotFound,
        Code:    code,
        Message: message,
    }
}

func Internal(code string, err error) *AppError {
    return &AppError{
        Type:    ErrorTypeInternal,
        Code:    code,
        Message: "internal server error",
        Err:     err,
    }
}

3.2 错误处理最佳实践

go
// 服务层
func (s *userService) GetUser(ctx context.Context, id int64) (*domain.User, error) {
    user, err := s.repo.GetByID(ctx, id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, errors.NotFound("USER_NOT_FOUND", "user not found")
        }
        return nil, errors.Internal("DB_ERROR", err)
    }
    return user, nil
}

// 处理层
func (h *userHandler) GetUser(c *gin.Context) {
    id, err := strconv.ParseInt(c.Param("id"), 10, 64)
    if err != nil {
        c.JSON(400, gin.H{"error": "invalid user id"})
        return
    }
    
    user, err := h.service.GetUser(c.Request.Context(), id)
    if err != nil {
        var appErr *errors.AppError
        if errors.As(err, &appErr) {
            switch appErr.Type {
            case errors.ErrorTypeNotFound:
                c.JSON(404, gin.H{"error": appErr.Message})
            default:
                c.JSON(500, gin.H{"error": "internal error"})
            }
            return
        }
        c.JSON(500, gin.H{"error": "unknown error"})
        return
    }
    
    c.JSON(200, user)
}

四、配置管理

4.1 配置结构

go
package config

type Config struct {
    App      AppConfig
    Database DatabaseConfig
    Redis    RedisConfig
    Log      LogConfig
}

type AppConfig struct {
    Name    string `env:"APP_NAME" default:"myapp"`
    Version string `env:"APP_VERSION" default:"1.0.0"`
    Port    int    `env:"APP_PORT" default:"8080"`
    Env     string `env:"APP_ENV" default:"development"`
}

type DatabaseConfig struct {
    Host     string `env:"DB_HOST" default:"localhost"`
    Port     int    `env:"DB_PORT" default:"5432"`
    User     string `env:"DB_USER" required:"true"`
    Password string `env:"DB_PASSWORD" required:"true"`
    Database string `env:"DB_NAME" required:"true"`
}

4.2 配置加载

go
func Load() (*Config, error) {
    var cfg Config
    
    if err := envconfig.Process("", &cfg); err != nil {
        return nil, fmt.Errorf("failed to load config: %w", err)
    }
    
    // 验证配置
    if err := validate.Struct(&cfg); err != nil {
        return nil, fmt.Errorf("config validation failed: %w", err)
    }
    
    return &cfg, nil
}

五、日志规范

5.1 结构化日志

go
package logger

import (
    "go.uber.org/zap"
)

var log *zap.Logger

func Init(env string) {
    var config zap.Config
    
    if env == "production" {
        config = zap.NewProductionConfig()
    } else {
        config = zap.NewDevelopmentConfig()
    }
    
    var err error
    log, err = config.Build()
    if err != nil {
        panic(err)
    }
}

func Info(msg string, fields ...zap.Field) {
    log.Info(msg, fields...)
}

func Error(msg string, fields ...zap.Field) {
    log.Error(msg, fields...)
}

func WithContext(ctx context.Context) *zap.Logger {
    if traceID := ctx.Value("trace_id"); traceID != nil {
        return log.With(zap.String("trace_id", traceID.(string)))
    }
    return log
}

5.2 日志使用

go
func (s *userService) CreateUser(ctx context.Context, user *domain.User) error {
    log := logger.WithContext(ctx)
    
    log.Info("creating user",
        zap.String("email", user.Email),
        zap.String("name", user.Name),
    )
    
    if err := s.repo.Create(ctx, user); err != nil {
        log.Error("failed to create user",
            zap.Error(err),
            zap.String("email", user.Email),
        )
        return err
    }
    
    log.Info("user created successfully",
        zap.Int64("user_id", user.ID),
    )
    
    return nil
}

六、测试策略

6.1 单元测试

go
func TestUserService_CreateUser(t *testing.T) {
    // Mock 仓库
    mockRepo := new(mockUserRepository)
    
    service := NewUserService(mockRepo)
    
    user := &domain.User{
        Email: "test@example.com",
        Name:  "Test User",
    }
    
    // 设置期望
    mockRepo.On("Create", mock.Anything, user).Return(nil)
    
    // 执行
    err := service.CreateUser(context.Background(), user)
    
    // 验证
    assert.NoError(t, err)
    mockRepo.AssertExpectations(t)
}

6.2 集成测试

go
func TestUserAPI_Integration(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test")
    }
    
    // 启动测试数据库
    db := setupTestDB(t)
    defer teardownTestDB(t, db)
    
    // 创建应用
    app := setupApp(db)
    
    // 测试请求
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("POST", "/api/users", strings.NewReader(`{
        "email": "test@example.com",
        "name": "Test User"
    }`))
    req.Header.Set("Content-Type", "application/json")
    
    app.ServeHTTP(w, req)
    
    assert.Equal(t, 201, w.Code)
}

七、总结

原则说明
分层架构关注点分离,便于测试和维护
依赖注入降低耦合,提高可测试性
接口隔离面向接口编程,便于替换实现
错误处理统一的错误类型和处理逻辑
配置管理环境变量 + 默认值
结构化日志便于检索和分析
测试覆盖单元测试 + 集成测试

遵循这些原则,能帮助你构建高质量的 Go 后端应用。

上次更新于: