service层是业务编排职责边界,需通过接口注入依赖、返回domain结构、函数单一职责、输入输出结构化、错误类型化,避免直连db/http及泛型污染。

Service 层不是包名,是职责边界
Go 里没有语法强制的 “Service 层”,它只是你对 service 包(或类似命名)下函数职责的约定:只做业务编排,不碰 HTTP、DB 驱动、日志框架等具体实现。很多人一上来就建 service/ 目录,却把 db.QueryRow、http.Post 直接塞进 UserServiceImpl.Create,结果测试写不了,换数据库改得到处都是。
关键判断:Service 函数必须通过接口注入依赖,而不是直接 new 或全局调用。比如定义 UserRepository 接口,而非传入 *sql.DB;返回值只能是 Go 原生类型或你定义的 domain struct(如 User),绝不暴露 *sql.Rows 或 redis.Conn。
怎么设计 Service 函数签名才不容易返工
常见错误是把 Service 方法设计成 “一个函数干完所有事”,比如 CreateUserWithProfileAndNotify(ctx, req),看着省事,但只要通知渠道从邮件换成站内信,就得改函数签名、改测试、改所有调用方。
- 一个 Service 函数只解决一个业务动作,命名用动词+名词,如
CreateUser、SendWelcomeEmail、ChargeOrder - 输入用明确的 struct,避免传一堆零散参数:
func (s *UserService) CreateUser(ctx context.Context, input CreateUserInput) (*User, error),其中CreateUserInput是你定义的 struct - 错误返回统一用自定义 error 类型(如
ErrUserExists),而不是靠字符串匹配,方便上层switch处理 - 真要组合多个动作(比如注册 + 发邮件),用另一个更高阶的 Service 函数来编排,而不是塞进
CreateUser
为什么你的 Service 单元测试总在 mock DB 上卡住
错误现象:写了个 TestCreateUser,发现要 sqlmock.New + redisgomock + gomock.Controller 三套工具,测个创建用户花了 80 行 setup,还老因为 driver 版本升级挂掉。
根本原因是你没把依赖抽象干净,或者 mock 粒度太细(比如 mock 某个具体 SQL 语句)。
- 只 mock 接口,不 mock 具体 driver —— 例如你定义了
UserRepository接口,测试时直接传个内存实现:memRepo := &memUserRepo{users: make(map[int]*User)} - 避免在 test 里出现
sqlmock.New、gomock.Controller这类框架关键词,它们是泄漏的抽象 - 不要为每个 DB 查询单独写 mock,而是为整个接口提供轻量实现(比如 map-based repo)
泛型能帮你封装通用 DAO,但别让它污染 Service 层
Go 1.20+ 的泛型确实能写出 GenericDAO[T any] 这类通用数据访问结构,但它只该出现在 Repository 层。Service 层仍要保持业务语义清晰,不能出现 dao.Get[User](id) 这种调用。
- DAO 泛型封装适合解决 CRUD 模板代码重复,但它的返回类型仍需被 Service 转换成 domain struct(如
User),而非直接透传T - Service 函数内部不应感知泛型参数,它只关心“我要查一个用户”,而不是“我要查一个泛型 T”
- 如果 Service 开始接受
GenericService[T]这类类型参数,说明职责已越界 —— 它正在变成工具层,而非业务编排层
最易被忽略的一点:Service 层的“通用”,从来不是指代码复用率高,而是指职责稳定、边界清晰。一旦开始为“减少重复”而牺牲语义表达,就离不可维护不远了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











