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
只包含 Save
和 GetByPhone
),而非大而全的接口。
- 好处:实现者只需关注自己需要的方法(如 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 国际许可协议 进行许可。