Go包名应使用简洁、小写的单数形式,如user、http;拆包依据是“可独立演进”,非功能分层;internal/为私有实现,pkg/为可复用库,cmd/为入口;接口应定义在调用方或抽象包中。
Go 语言规范明确要求包名必须是合法的标识符,且惯例是使用简洁、小写的单数形式,比如 user、http、sql。不要用复数(users)、驼峰(userHandler)或下划线(user_repo)。因为包名会出现在所有导入后的调用中,例如 user.New() 比 users.NewUser() 更自然,也避免和类型名重复造成混淆。
package users → 导入后变成 users.User{},语义冗余package user → user.User{} 或 user.New(),清晰无歧义go.mod 中确保模块路径能解析到该包常见误区是机械照搬 MVC 或 Clean Architecture 的目录结构,把 handler、service、repository 强行分包。Go 的包边界核心标准是:是否具备独立的依赖、测试、版本控制和演化节奏。一个包如果总是和另一个包一起修改、一起发布、无法单独测试,那它大概率不该拆。
go test ./pkg/xxx 能跑通且不依赖其他业务包;go list -f '{{.Deps}}' ./pkg/xxx 显示只依赖标准库或稳定第三方(如 github.com/google/uuid)internal/handler 里全是 HTTP 相关逻辑,但每个 handler 都强依赖 internal/service 和 internal/repository —— 这三者实际是一个演化单元,合
internal/api 包里更合理domain(纯结构+方法,零外部依赖)、storage(封装 SQL/Redis 实现,依赖 database/sql 但不依赖业务逻辑)Go 官方推荐的顶层结构不是教条,而是解决具体问题的工具:internal/ 是私有实现边界,pkg/ 是可被外部复用的库,cmd/ 是可执行入口。混用会导致依赖泄漏或复用困难。
internal/ 下的包不能被本项目以外的模块 import —— Go 编译器强制检查,适合放领域模型、应用服务、基础设施适配器等专用于当前项目的代码pkg/ 应该像第三方库一样设计:有清晰 API、导出类型最小化、带文档注释、可独立 go test;例如 pkg/email 提供 Send(ctx, to, subject, body),内部用 SMTP 或 SendGrid 都不影响调用方cmd/ 只做三件事:解析 flag / env、初始化依赖(DB、logger、config)、调用 main.Run();每个命令一个子目录,如 cmd/myapp、cmd/migrate,便于构建多个二进制Go 没有“接口必须提前声明”的约束,但把接口和实现耦合在同一包里,会锁死扩展能力。正确做法是让接口由使用者定义,或放在更抽象的包中。
storage/postgres.go 里定义 type UserRepo interface { GetByID(id int) (*User, error) },然后 postgres.UserRepoImpl 实现它 —— 外部无法替换实现,且测试只能用 mock 或真实 DBdomain/ 或 internal/port/ 中定义 type UserRepository interface,storage/postgres 包只 import 并实现它;调用方(如 internal/app)只依赖 domain 包,完全不知道 PostgreSQL 存在go list -f '{{.Imports}}' ./internal/app 会显示只依赖 domain,不出现 storage/postgres,证明依赖方向正确package domain
type User struct {
ID int
Name string
}
type UserRepository interface {
GetByID(id int) (*User, error)
Save(u *User) error
}
package postgres
import "myproject/domain"
type repo struct {
db *sql.DB
}
func (r *repo) GetByID(id int) (*domain.User, error) {
// 实现细节
}
// 注意:这里不 export repo 或 UserRepository
// 而是通过工厂函数返回 interface{}
func NewUserRepository(db *sql.DB) domain.UserRepository {
return &repo{db: db}
}
真正容易被忽略的,是包的「演化成本」:一个包一旦被多个地方 import,它的任何导出变更(哪怕只是加个方法)都可能引发连锁重构。所以别为了“看起来整洁”而早拆包,先让代码在同一个包里跑通核心流程,再根据测试隔离性、部署粒度、团队协作节奏,逐步识别出真正的边界。