Go项目结构怎么划分包_Go包设计最佳实践

Go包名应使用简洁、小写的单数形式,如user、http;拆包依据是“可独立演进”,非功能分层;internal/为私有实现,pkg/为可复用库,cmd/为入口;接口应定义在调用方或抽象包中。

go项目结构怎么划分包_go包设计最佳实践

包名应该用单数还是小写?

Go 语言规范明确要求包名必须是合法的标识符,且惯例是使用简洁、小写的单数形式,比如 userhttpsql。不要用复数(users)、驼峰(userHandler)或下划线(user_repo)。因为包名会出现在所有导入后的调用中,例如 user.New()users.NewUser() 更自然,也避免和类型名重复造成混淆。

  • 错误示例:package users → 导入后变成 users.User{},语义冗余
  • 正确示例:package useruser.User{}user.New(),清晰无歧义
  • 包名不强制与目录名一致,但绝大多数项目都保持一致;若不一致,需在 go.mod 中确保模块路径能解析到该包

何时该拆出新包?不是按功能层,而是按“可独立演进”

常见误区是机械照搬 MVC 或 Clean Architecture 的目录结构,把 handlerservicerepository 强行分包。Go 的包边界核心标准是:是否具备独立的依赖、测试、版本控制和演化节奏。一个包如果总是和另一个包一起修改、一起发布、无法单独测试,那它大概率不该拆。

  • 适合拆包的信号:go test ./pkg/xxx 能跑通且不依赖其他业务包;go list -f '{{.Deps}}' ./pkg/xxx 显示只依赖标准库或稳定第三方(如 github.com/google/uuid
  • 反模式:internal/handler 里全是 HTTP 相关逻辑,但每个 handler 都强依赖 internal/serviceinternal/repository —— 这三者实际是一个演化单元,合并在 internal/api 包里更合理
  • 典型合理拆分:domain(纯结构+方法,零外部依赖)、storage(封装 SQL/Redis 实现,依赖 database/sql 但不依赖业务逻辑)

internal/ vs pkg/ vs cmd/:这三个目录的真实分工

Go 官方推荐的顶层结构不是教条,而是解决具体问题的工具internal/ 是私有实现边界,pkg/ 是可被外部复用的库,cmd/ 是可执行入口。混用会导致依赖泄漏或复用困难。

Sologo AI

Sologo AI

SologoAI 是一款AI在线LOGO生成工具,帮助用户快速创建独特且专业的品牌标识和配套VI设计。

下载

  • 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/myappcmd/migrate,便于构建多个二进制

接口定义放在哪?别在实现包里 export interface

Go 没有“接口必须提前声明”的约束,但把接口和实现耦合在同一包里,会锁死扩展能力。正确做法是让接口由使用者定义,或放在更抽象的包中。

  • 错误做法:storage/postgres.go 里定义 type UserRepo interface { GetByID(id int) (*User, error) },然后 postgres.UserRepoImpl 实现它 —— 外部无法替换实现,且测试只能用 mock 或真实 DB
  • 推荐做法:在 domain/internal/port/ 中定义 type UserRepository interfacestorage/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,它的任何导出变更(哪怕只是加个方法)都可能引发连锁重构。所以别为了“看起来整洁”而早拆包,先让代码在同一个包里跑通核心流程,再根据测试隔离性、部署粒度、团队协作节奏,逐步识别出真正的边界。

https://www.php.cn/faq/2027363.html

发表回复

Your email address will not be published. Required fields are marked *