go-zero 的 svc 目录结构规范

「svc 目录下有哪些文件、各自装什么内容、规范是啥?」—— 这篇把 internal/svc/ 这个「组合根」目录的职责、文件清单、每类文件的内容与写法一次讲清。

一句话结论 svc 目录职责 目录树长啥样 各文件内容详解 与内部其他层关系 规范清单 对比 结论

svc 目录 = 组合根(Composition Root),通常就 2 个文件

internal/svc/ 里最核心、goctl 必生成的是 servicecontext.go(装依赖的结构体 + 构造函数);随项目需要还会加 variables.go(类型安全的 ctx key 等常量)。它就是程序的「装配车间」,别的层只消费、不装配。

记得住的一句话:svc 目录只干一件事——把 config + 所有外部依赖(DB/Redis/RPC/HTTP/MQ)打包成一个 ServiceContext,通过构造函数一次性建好,再交给 handler / logic 用。文件少而精,没有 handler、没有 logic、没有路由。

svc 目录在 go-zero 里扮演什么角色

它是「组合根」——整个程序依赖装配的唯一入口。

🏭 装配车间
所有进程级依赖(连接池、客户端、配置)在这里创建、组装成一个 struct,再下发。handler / logic 不自己 new 任何连接。
🔒 单一写入点
想知道服务依赖了哪些外部资源,打开 servicecontext.go 这一个文件就够了,不会散落十几个角落。
🧪 测试友好的接缝
单测时手动 new 一个 ServiceContext,把 DB / RPC 字段换成 mock,logic 代码一行不用改。
🚫 它不干什么
不放业务代码、不放路由、不放 HTTP 中间件实现、不放请求级数据(那些走 ctx)。svc 是「静态装配」,与单次请求无关。

svc 目录树长啥样

最小形态只有一个文件;随规模增长会自然长出 2~3 个。

internal/svc/ ├── servicecontext.go ★ 必生成:ServiceContext 结构体 + NewServiceContext 构造 ├── variables.go ◯ 常见:类型安全的 ctx key、服务级常量(放这被 middleware/logic 共享) └── group.go ◯ 少见:用 service group 批量管理生命周期时才出现 (注:goctl 默认只生成 servicecontext.go;其余按需手建)
文件是否默认生成装什么谁会引用它
servicecontext.go✅ goctl 必生成ServiceContext 结构体 + NewServiceContext 构造函数main、handler、logic
variables.go❌ 按需手建ctx key 类型与常量、共享枚举/常量middleware、logic
group.go❌ 按需手建servicegroup 管理多 server 生命周期main

每个文件具体装什么、怎么写

逐文件给出内容与规范写法。

servicecontext.go —— svc 的心脏

固定两块:结构体(挂所有依赖字段)和 构造函数(建一次、赋值)。字段命名见名知意,构造里只「建」不写业务。

package svc

import (
    "github.com/zeromicro/go-zero/core/stores/sqlx"
    "github.com/zeromicro/go-zero/core/stores/redis"
    "github.com/zeromicro/go-zero/zrpc"
    "github.com/zeromicro/go-zero/core/httpc"
    "your_project/user/internal/config"
    "your_project/user/rpc/user/user"
)

// ① 结构体:把进程级依赖都挂上来
type ServiceContext struct {
    Config  config.Config          // 配置(只读)
    DB      sqlx.SqlConn          // 数据库
    Redis   *redis.Redis          // 缓存
    UserRpc user.User             // 下游 RPC 客户端
    PayHttp httpc.Service         // 外部 HTTP 客户端
}

// ② 构造函数:在这里把每个依赖建一次
func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config:  c,
        DB:      sqlx.NewMysql(c.Mysql.DataSource),
        Redis:   redis.New(c.CacheRedis),
        UserRpc: user.NewUser(zrpc.MustNewClient(c.UserRpc)),
        PayHttp: httpc.NewService(c.PayHttp),
    }
}
规范:① 结构体字段用大写开头(跨包可导,handler/logic 在别的包需要它);② Config 字段保留原样,业务读配置一律 svcCtx.Config.XXX;③ 构造里只建连接、不写业务逻辑;④ 依赖有先后时按字段顺序排,go-zero 不帮你排序。

variables.go —— 类型安全的 ctx key 与常量

把「放进/取出 ctx 用的 key」定义成私有类型常量,避免各文件重复定义、字符串 key 冲突。middleware 写入、logic 读取都引用这里。

package svc

// 私有类型,杜绝与其他包 key 冲突
type contextKey string

const (
    // 中间件写入、logic 读取的请求级数据 key
    UserIdKey contextKey = "userId"
    TenantKey contextKey = "tenant"
    TraceKey  contextKey = "traceId"
)

// 也可以放服务级共享常量 / 枚举
const (
    CacheExpire = 300          // 秒
    OrderPrefix = "order:"
)
为什么放 svc 而不是 middleware:variables.go 在 svc 包下,middleware 和 logic 都能 import(注意避免循环依赖——这里只放 key 和常量,不放任何依赖 svc 的东西)。实际上更常见是放 internal/middleware 或单独的 internal/ctxkeys 包;放 svc 是「约定俗成、就近」的选择,只要团队统一即可。

group.go —— 多 server 生命周期(少见)

当进程要同时跑 rest + zrpc 等多个服务、需要统一管理启停时才出现,用 service.ServiceGroup

package svc

import "github.com/zeromicro/go-zero/core/service"

func Register(g *service.ServiceGroup, c config.Config) {
    // 把多个 server 加进同一个 group,统一 Start/Stop
    g.Add(rest.MustNewServer(c.RestConf))
    g.Add(zrpc.MustNewServer(c.RpcConf, ...))
}
绝大多数业务服务用不到这个文件。go-zero 单体服务里 svc 目录基本就 servicecontext.go 一个文件,别为了「看起来完整」硬加。

svc 与 internal 内其他层的依赖流向

svc 被「消费」,不反向依赖业务层。

config 只定义 struct svc ★ 装配所有依赖 handler 路由+薄壳 logic 业务(消费 svc) types 请求/响应 DTO

箭头方向只有一个含义:config 供给 svc(svc 引用 config 类型),svc 供给 handler / logic(它们通过 svcCtx 字段用依赖)。svc 不 import handler / logic / types,所以 svc 永远是「底层、被依赖」,不会反向耦合业务逻辑。

svc 目录的规范清单

团队统一遵守,避免 svc 变成大杂烩。

✅ 只放装配相关文件
servicecontext.go 必备,variables.go / group.go 按需。不塞 handler、logic、路由、业务函数。
✅ 字段大写在 svc 包外可见
因为 handler / logic 在别的包,需要 svcCtx.DB 这样的访问,字段必须导出(首字母大写)。
✅ 构造函数只建不写业务
NewServiceContext 内只做「创建依赖 + 赋值」,不调用业务逻辑、不发请求、不处理错误(用 MustNewXxx 快速失败)。
✅ 依赖顺序自己排
字段间若有依赖,在构造函数里按先后顺序 new,go-zero 不会自动排序。
🚫 不放 init() 装配
所有装配走构造函数,不用 init(),否则顺序难以控制、测试难替换。
🚫 不反向依赖业务包
svc 包不要 import handler / logic / types,保持它是「被依赖的底层」,否则会出现循环依赖。

其他框架有「svc 目录」这种约定吗?

「集中装配」是共识,但「专门为它建一个目录」是 go-zero 的强约束。

🔹 Gin / Echo 小项目
依赖多挂在 main() 里临时建,或塞进 engine / App 结构体。没有专门的 svc 目录,靠自觉,容易退化成全局变量。
🔹 整洁架构 / DDD
会有 wirecontainerinfrastructure 这类「组装层」,理念同 svc,但不像 go-zero 用 goctl 强制生成固定文件名
🔹 Spring
@Configuration + @Bean 在类里声明装配,运行期 IoC 容器托管。没有目录级约定,靠注解。go-zero 是编译期、无反射、纯 struct。
🔸 小结
go-zero 的标签是:专门一个 svc 目录 + 固定文件名 servicecontext.go + goctl 强制 + 无反射。把「组合根」从「推荐做法」变成「框架给你的骨架」。

一句话记住 svc 目录规范

🧠 记忆口诀

· svc = 组合根,是程序依赖装配的唯一入口
· 默认只有一个文件 servicecontext.go(结构体 + 构造函数);
· 按需加 variables.go(ctx key / 常量)、group.go(多 server 生命周期);
· 字段大写可导出、构造函数只建不写业务、不放 init();
· svc 被 config 供给、被 handler/logic 消费,绝不反向依赖业务层
· 不放 handler / logic / 路由 / 请求级数据(请求级走 ctx)。