接口应由调用方(如service)定义,被依赖方(如repository)实现而不导入调用方包,避免循环引用;api包仅含协议层契约,不含业务逻辑;handler需通过构造函数注入依赖,不硬编码初始化;接口方法名用动词开头并返回带上下文的error。

接口定义该放在哪个包里才不会循环引用
Go 模块化中,interface 放错位置是循环导入的头号原因。不是“抽象要提前”,而是“谁消费谁声明”——调用方(比如 handler 或 service)应定义它需要的 interface,而不是让被依赖方(比如 repository 或 client)提前暴露一堆接口。
常见错误现象:import cycle not allowed,尤其在把 UserService 接口塞进 model 包、又让 repository 实现它时爆发。
- service 层需要查用户 → 它定义
UserReader接口,只含GetByID(id int) (*User, error) - repository 包实现该接口,但不 import service 包;它只 export 实现类型,如
SQLUserRepo - main 或 wire 注册时,把
SQLUserRepo{...}传给 service 构造函数,完成依赖注入
这样做避免了 repository → service 反向依赖,也防止接口膨胀——UserReader 不会因为新增一个导出字段就强制所有实现改写。
API Package 里到底放什么,不放什么
api 包不是“所有对外暴露的东西都扔进来”,它只负责 HTTP/RPC 协议层契约:请求/响应结构、路由绑定、中间件接入点。业务逻辑、校验规则、领域模型都不该出现在这里。
立即学习“go语言免费学习笔记(深入)”;
使用场景:你正在用 gin 或 chi 写 REST API,或用 gRPC-Gateway 暴露 proto 接口。
系统包含模块:1、卖场系统适用客户:实体卖场,可以分类管理,每个分类设置一个客服,客服可以使用手机管理分类商品2、万能表单用户可以自定义表单字段,收集各样信息,并可以导出Excel3、第三方接口方便用户自己开发,目前仅支持text格式4、留言板可以显示用户的头像和昵称5、场景二维码这是高级接口的使用,方便统计用户来源6、一键分享一个仿微信公众号详情界面,可以分享到朋友圈7、婚纱摄影一个相册+店面展
- 放:
type CreateUserRequest struct { Name string `json:"name"` }、func RegisterHandlers(r chi.Router, h *Handler)、Validate() error方法(仅字段级校验) - 不放:
user.Create()调用、auth.CheckPermission()、数据库事务控制、自定义错误码映射(那是transport或handler层的事) - 参数差异:
api中的 struct 字段名和 tag(json/protobuf)必须稳定;内部 domain model 可随时重构,只要转换层适配即可
为什么不要在 api 包里直接 new service 实例
硬编码 svc := user.NewService(user.NewRepo(db)) 在 handler 函数里,等于把初始化逻辑和协议层耦死,测试无法 mock,wire 或 dig 无法接管生命周期,上线后也无法切换实现(比如从 SQL 切到 Redis 缓存版 user repo)。
性能影响:每次 HTTP 请求都新建 service,可能重复初始化连接池、日志器、限流器等单例资源。
- handler 应通过构造函数接收依赖:
type UserHandler struct { svc user.Service } - 在
cmd/xxx/main.go或internal/di包中统一 new 并传递,确保单例复用 - 容易踩的坑:忘记把
context.Context从 handler 透传到 service 方法,导致超时/取消信号中断
接口方法命名和 error 返回怎么保持一致性
Go 接口方法名不是越短越好,而是要反映调用者视角的动作意图。返回 error 是必须项,但别用 fmt.Errorf("failed to X") 这种模糊包装,更别返回 nil 错误却偷偷吞掉底层失败。
常见错误现象:前端收到 500 Internal Server Error,日志里只有 user service: unknown error,根本没法定位是 DB 连接失败还是 redis timeout。
- 方法名用动词开头:
GetUser()、CreateOrder(),不用UserGetter这类名词化接口名 - 错误需携带上下文:
return fmt.Errorf("user.Create: failed to insert into db: %w", err),保留原始 error 链 - 兼容性注意:如果某接口方法新增一个返回值(比如加个
bool表示是否创建了新记录),所有实现都要改——这违反接口演进原则;此时应新增方法,旧方法保留
真正难的不是定义接口,而是守住边界:当同事说“这个字段我前端急着用,你快加到 api.UserResponse 里”,得立刻想清楚——这是协议层该承担的职责吗,还是该由前端自己拼装?









