SoC code structure in golang

Posted on Mon 25 August 2025 in Journal

Abstract SoC code structure in golang
Authors Walter Fan
Category learning note
Status v1.0
Updated 2025-08-25
License CC-BY-NC-ND 4.0

在 Go 应用中实现“关注点分离”,核心是通过模块化拆分单一职责原则,将业务逻辑、数据访问、API 交互、配置管理等不同关注点隔离到独立目录/包中。这种结构不仅降低了代码耦合,还能让新开发者快速定位功能模块,同时便于后续维护和扩展。

结合 Go 语言“约定优于配置”的特性(无强制目录规范,但业界有成熟最佳实践),以下是一套兼顾易用性、可维护性可扩展性的代码结构方案,同时融入前文中提到的 MVC 思想、依赖注入等设计模式。

一、整体目录结构概览

以一个典型的 Web 后端应用(如用户管理系统)为例,目录结构如下(按“从外到内、从抽象到具体”的逻辑组织):

your-project/          # 项目根目录
├── cmd/               # 程序入口(命令行/服务启动)
│   └── api/           # API 服务入口(每个独立程序一个子目录)
│       └── main.go    # 初始化依赖、启动服务(仅负责“组装”,不写业务逻辑)
├── internal/          # 私有代码(不对外暴露的业务核心,Go 编译时禁止外部导入)
│   ├── domain/        # 领域层(业务实体/核心规则,与框架无关)
│   │   └── user/      # 按业务模块拆分(如用户模块、订单模块)
│   │       ├── model.go    # 业务实体(User 结构体,定义核心属性和规则)
│   │       └── service.go  # 领域服务(纯业务逻辑,不依赖数据访问细节)
│   ├── repository/    # 数据访问层(Repository 模式,隔离数据来源)
│   │   └── user/
│   │       ├── repo.go     # 数据访问接口(定义“做什么”,如 UserRepo 接口)
│   │       └── mysql.go    # 接口实现(MySQL 实现,“怎么做”,依赖具体数据库)
│   ├── service/       # 应用服务层(协调领域层与数据层,处理跨模块逻辑)
│   │   └── user/
│   │       └── service.go  # 应用服务(调用 domain 业务逻辑 + repository 数据操作)
│   └── handler/       # 接口适配层(处理 HTTP/gRPC 等外部请求,MVC 中的 Controller)
│       └── user/
│           └── handler.go  # 处理 HTTP 请求(参数校验、调用 service、返回响应)
├── pkg/               # 公共代码(可对外复用的工具/组件,非业务相关)
│   ├── db/            # 数据库工具(MySQL 连接池、初始化)
│   ├── http/          # HTTP 工具(路由封装、中间件)
│   ├── logger/        # 日志工具(统一日志格式、输出)
│   └── config/        # 配置工具(读取环境变量、配置文件)
├── api/               # 接口定义(对外暴露的 API 契约,如 OpenAPI/Swagger、gRPC proto)
│   └── user.proto     # gRPC 接口定义(若用 gRPC)
├── configs/           # 配置文件(yaml/json,不包含敏感信息)
├── scripts/           # 脚本(部署、数据库迁移、构建脚本)
└── go.mod/go.sum      # Go 模块依赖

二、各目录职责详解(关注点分离核心)

每个目录/包都有明确的“职责边界”,避免一个模块同时处理多个关注点(如 Handler 不直接操作数据库,Service 不处理 HTTP 参数)。

1. cmd/:程序入口(仅负责“组装”,不写业务)

cmd/ 是整个应用的“启动器”,只负责依赖初始化和服务启动,不包含任何业务逻辑——这是“关注点分离”的关键:将“服务启动”与“业务逻辑”彻底隔离。

示例:cmd/api/main.go

package main

import (
    "your-project/internal/handler/user"
    "your-project/internal/repository/user"
    "your-project/internal/service/user"
    "your-project/pkg/db"
    "your-project/pkg/http"
    "your-project/pkg/logger"
)

func main() {
    // 1. 初始化公共组件(依赖注入的“注入点”)
    log := logger.New()          // 初始化日志
    dbConn, err := db.NewMySQL() // 初始化 MySQL 连接
    if err != nil {
        log.Fatal("failed to init mysql:", err)
    }
    defer dbConn.Close()

    // 2. 组装依赖(依赖注入:高层模块不依赖低层模块,依赖接口)
    userRepo := userrepo.NewMySQLRepo(dbConn) // 数据层:MySQL 实现
    userDomainSvc := userdomain.NewService(userRepo) // 领域层:依赖 Repo 接口
    userAppSvc := usersvc.NewService(userDomainSvc)  // 应用层:依赖领域服务
    userHandler := userhandler.NewHandler(userAppSvc) // 接口层:依赖应用服务

    // 3. 启动 HTTP 服务
    router := http.NewRouter()
    router.POST("/api/users", userHandler.Create) // 注册路由
    log.Info("server start at :8080")
    if err := router.Run(":8080"); err != nil {
        log.Fatal("server exit:", err)
    }
}

核心思想main.go 像“搭积木”一样组装各层依赖,不关心每个模块的内部实现——如果后续要将 MySQL 换成 Redis,只需修改 userRepo 的初始化(如 userrepo.NewRedisRepo(redisConn)),其他层无需改动。

2. internal/:业务核心(私有,不对外暴露)

internal/ 是应用的“心脏”,包含所有业务相关代码,按“领域 -> 数据 -> 应用 -> 接口” 的层级拆分,每层关注点独立。

2.1 internal/domain:领域层(业务实体+核心规则)

领域层是业务的本质,不依赖任何框架或外部组件(如不依赖 MySQL、HTTP),只定义“业务是什么”——比如“用户必须有手机号”“密码必须大于 6 位”等核心规则。

示例:internal/domain/user/model.go(业务实体)
package user

// User 业务实体:定义用户的核心属性和业务规则
type User struct {
    ID       int64  `json:"id"`
    Phone    string `json:"phone"` // 核心属性:手机号(唯一)
    Password string `json:"-"`     // 密码:不对外暴露
    Name     string `json:"name"`
}

// Validate 业务规则校验:领域层自己负责“数据合法性”
func (u *User) Validate() error {
    if u.Phone == "" {
        return errors.New("phone cannot be empty")
    }
    if len(u.Password) < 6 {
        return errors.New("password must be at least 6 characters")
    }
    return nil
}
示例:internal/domain/user/service.go(领域服务)

领域服务处理纯业务逻辑(不依赖数据访问细节),如需操作数据,依赖 repository 层定义的接口(而非具体实现)。

package user

import "errors"

// UserRepo 数据访问接口:领域层定义“需要什么数据操作”,不关心“怎么实现”
type UserRepo interface {
    Save(u *User) error       // 保存用户
    GetByPhone(phone string) (*User, error) // 按手机号查询用户
}

// DomainService 领域服务:处理核心业务逻辑
type DomainService struct {
    repo UserRepo // 依赖接口,而非具体实现(控制反转)
}

// NewService 创建领域服务(注入 UserRepo 接口)
func NewService(repo UserRepo) *DomainService {
    return &DomainService{repo: repo}
}

// CreateUser 业务逻辑:创建用户(校验规则 + 保存数据)
func (s *DomainService) CreateUser(phone, password, name string) (*User, error) {
    // 1. 构建用户实体
    user := &User{
        Phone:    phone,
        Password: password, // 实际项目中应加密(如 bcrypt),此处简化
        Name:     name,
    }

    // 2. 校验业务规则(调用实体自身的 Validate 方法)
    if err := user.Validate(); err != nil {
        return nil, err
    }

    // 3. 检查手机号是否已存在(依赖 repo 接口,不关心是 MySQL 还是 Redis)
    existingUser, _ := s.repo.GetByPhone(phone)
    if existingUser != nil {
        return nil, errors.New("phone already exists")
    }

    // 4. 保存用户(依赖 repo 接口)
    if err := s.repo.Save(user); err != nil {
        return nil, err
    }

    return user, nil
}

2.2 internal/repository:数据访问层(隔离数据来源)

数据访问层(Repository 模式)负责“怎么获取/存储数据”,实现领域层定义的 UserRepo 接口,隔离业务逻辑与数据来源(MySQL、Redis、API 等)。

示例:internal/repository/user/repo.go(接口定义,与领域层一致)
package user

import "your-project/internal/domain/user"

// 注意:这里的 UserRepo 接口与领域层的 user.UserRepo 完全一致
// 目的是让 repository 层明确“要实现什么”,避免领域层依赖 repository 层
type UserRepo interface {
    Save(u *user.User) error
    GetByPhone(phone string) (*user.User, error)
}
示例:internal/repository/user/mysql.go(MySQL 实现)
package user

import (
    "database/sql"
    "your-project/internal/domain/user"
)

// MySQLRepo UserRepo 接口的 MySQL 实现
type MySQLRepo struct {
    db *sql.DB // 依赖 MySQL 连接(由外部注入)
}

// NewMySQLRepo 创建 MySQL 实现(注入 DB 连接)
func NewMySQLRepo(db *sql.DB) *MySQLRepo {
    return &MySQLRepo{db: db}
}

// Save 实现 UserRepo 接口:保存用户到 MySQL
func (r *MySQLRepo) Save(u *user.User) error {
    sqlStr := "INSERT INTO users (phone, password, name) VALUES (?, ?, ?)"
    result, err := r.db.Exec(sqlStr, u.Phone, u.Password, u.Name)
    if err != nil {
        return err
    }
    // 获取自增 ID
    id, err := result.LastInsertId()
    if err != nil {
        return err
    }
    u.ID = id
    return nil
}

// GetByPhone 实现 UserRepo 接口:按手机号查询
func (r *MySQLRepo) GetByPhone(phone string) (*user.User, error) {
    sqlStr := "SELECT id, phone, password, name FROM users WHERE phone = ?"
    row := r.db.QueryRow(sqlStr, phone)

    var u user.User
    err := row.Scan(&u.ID, &u.Phone, &u.Password, &u.Name)
    if err == sql.ErrNoRows {
        return nil, nil // 无数据返回 nil(业务层处理“不存在”逻辑)
    }
    if err != nil {
        return nil, err
    }
    return &u, nil
}

核心价值:如果后续需要将数据存储换成 Redis,只需新增 RedisRepo 实现 UserRepo 接口,领域层和应用层无需任何修改——这就是“依赖接口,而非实现”的好处。

2.3 internal/service:应用服务层(协调跨模块逻辑)

应用服务层是“业务逻辑的编排者”,不处理具体业务规则(交给领域层),而是协调领域层、数据层,处理跨模块逻辑(如创建用户后发送短信通知)。

示例:internal/service/user/service.go
package user

import "your-project/internal/domain/user"

// AppService 应用服务:协调领域服务与其他模块
type AppService struct {
    domainSvc *user.DomainService // 依赖领域服务(注入)
    // 可注入其他模块依赖,如短信服务:smsSvc *sms.Service
}

// NewService 创建应用服务(注入领域服务)
func NewService(domainSvc *user.DomainService) *AppService {
    return &AppService{domainSvc: domainSvc}
}

// CreateUser 应用层接口:对外提供“创建用户”能力
func (s *AppService) CreateUser(phone, password, name string) (*user.User, error) {
    // 1. 调用领域服务处理核心业务逻辑
    user, err := s.domainSvc.CreateUser(phone, password, name)
    if err != nil {
        return nil, err
    }

    // 2. 处理跨模块逻辑(如发送注册成功短信,此处简化)
    // if err := s.smsSvc.Send(phone, "注册成功!"); err != nil {
    //     log.Warn("failed to send sms:", err) // 短信失败不影响用户创建
    // }

    return user, nil
}

2.4 internal/handler:接口适配层(处理外部请求)

Handler 层是应用与外部的“桥梁”(对应 MVC 中的 Controller),只负责“请求解析、参数校验、响应返回”,不处理任何业务逻辑(交给应用服务层)。

示例:internal/handler/user/handler.go
package user

import (
    "net/http"
    "your-project/internal/domain/user"
    "your-project/internal/service/user"
    "github.com/gin-gonic/gin" // 假设用 Gin 框架
)

// Handler HTTP 处理器:适配外部请求
type Handler struct {
    appSvc *usersvc.AppService // 依赖应用服务(注入)
}

// NewHandler 创建 Handler(注入应用服务)
func NewHandler(appSvc *usersvc.AppService) *Handler {
    return &Handler{appSvc: appSvc}
}

// Create 处理“创建用户”的 HTTP 请求
func (h *Handler) Create(c *gin.Context) {
    // 1. 解析 HTTP 请求参数(关注点:参数解析)
    var req struct {
        Phone    string `json:"phone" binding:"required"`
        Password string `json:"password" binding:"required,min=6"`
        Name     string `json:"name" binding:"required"`
    }
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // 2. 调用应用服务处理业务(不关心业务逻辑,只传参)
    user, err := h.appSvc.CreateUser(req.Phone, req.Password, req.Name)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    // 3. 返回 HTTP 响应(关注点:响应封装)
    c.JSON(http.StatusOK, gin.H{
        "code": 0,
        "msg":  "success",
        "data": user,
    })
}

3. pkg/:公共工具(可复用,非业务相关)

pkg/ 存放所有非业务相关的公共组件,可被多个项目复用(如数据库连接、日志、配置),与业务逻辑完全隔离。

示例:pkg/db/mysql.go(数据库工具)

package db

import (
    "database/sql"
    "fmt"
    "your-project/pkg/config"
    _ "github.com/go-sql-driver/mysql" // MySQL 驱动
)

// NewMySQL 初始化 MySQL 连接池(从配置文件读取参数)
func NewMySQL() (*sql.DB, error) {
    cfg := config.GetMySQLConfig() // 从配置工具获取参数
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true",
        cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName)

    db, err := sql.Open("mysql", dsn)
    if err != nil {
        return nil, err
    }

    // 连接池配置
    db.SetMaxOpenConns(100)
    db.SetMaxIdleConns(20)
    return db, nil
}

三、关键设计原则(支撑关注点分离)

上述结构能落地,依赖以下 3 个核心设计原则,也是 Go 项目中“减少心智负担”的关键:

1. 依赖注入(DI)与控制反转(IOC)

  • 控制反转:高层模块(如领域服务)不依赖低层模块(如 MySQL 实现),而是依赖“接口”(如 UserRepo),低层模块的实现由外部注入(如 main.go 中注入 MySQLRepo)。
  • 好处:修改低层实现(如 MySQL → Redis)时,高层模块无需改动,降低耦合。

2. 单一职责原则(SRP)

每个模块/函数只做一件事: - Handler 只处理 HTTP 交互; - 领域服务只处理业务规则; - Repository 只处理数据访问。 - 好处:定位问题时,能快速缩小范围(如 HTTP 报错找 Handler,数据错误找 Repository)。

3. 接口隔离原则(ISP)

定义细粒度的接口(如 UserRepo 只包含 SaveGetByPhone),而非大而全的接口。 - 好处:实现者只需关注自己需要的方法(如 RedisRepo 无需实现 MySQL 特有的方法),降低理解成本。

四、与 Java MVC 的对比(Go 特色适配)

Java MVC 角色 Go 对应模块 关注点差异
Controller internal/handler Go 的 Handler 更轻量,只做请求适配,不处理业务
Service internal/service + internal/domain Go 拆分“应用服务”和“领域服务”,更强调业务本质
Repository/DAO internal/repository Go 用接口定义数据操作,实现更灵活
Model internal/domain/model Go 的 Model 包含业务规则(如 Validate),更贴近领域

五、总结

Go 应用的“关注点分离”结构,核心是“按职责分层、按模块拆分”: 1. 入口层(cmd):只负责组装依赖,不写业务; 2. 业务层(internal):按“领域→数据→应用→接口”拆分,每层关注点独立; 3. 工具层(pkg):存放公共组件,与业务隔离。

这种结构能让代码“各司其职”,新开发者能快速理解“某类功能在哪个目录”,修改时只需改动对应模块(如改数据库只动 repository,改 API 只动 handler),极大降低心智负担和维护成本。

本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。