文件 / 目录 / 包 / 变量 / 常量 / 函数 / struct / 接口 / 方法接收者 —— 把《Effective Go》《Go Code Review Comments》的约定一次讲清,配对照表与图。
Go 的「规范」有三大权威来源,按优先级:
gofmt 自动统一,人和编辑器都不吵架。语法层面的规范没有商量余地。小写 + 下划线分隔,绝不用驼峰、绝不大写开头。
user_service.gohttp_handler.goorder_repository.gouserService.go(驼峰)UserService.go(大写开头)user-service.go(连字符,import 路径才允许)| 后缀 | 含义 | 说明 |
|---|---|---|
_test.go | 测试文件 | 仅 go test 编译;包名可同包或 包名_test(黑盒外部测试) |
_<GOOS>.go | 平台专属 | 如 socket_linux.go、read_windows.go(Go 1.17+ 自动按 GOOS 选编) |
_<GOARCH>.go | 架构专属 | 如 crc32_amd64.go、asm_arm64.s |
_test 包名 | 外部测试 | package user_test 只能访问导出符号,用来测「公开 API」而非内部 |
_ 在文件名里只用于「下划线分隔多词」和「构建约束后缀」两种场景。普通业务文件名一律小写加下划线即可,别画蛇添足。目录名小写单词、包名与目录同名且小写无下划线、避免 util/common 等泛词。
| 规则 | 正确 | 错误 | 原因 |
|---|---|---|---|
| 小写、单词、无下划线/驼峰 | http, strconv, user | urlUtils, user_svc | 包名是调用前缀,越短越好读 |
| 与目录同名 | dir user/ → package user | dir user/ → package usersvc | 降低心智负担,工具约定 |
| 避免泛词 | 具体语义名 | util, common, base, misc, tool | 泛词等于没命名,应就近拆包 |
| 不加类型后缀 | user(含 User 类型) | usermodel, userpkg | 包已表达容器,类型再表达语义 |
github.com/foo/bar-user),但路径里的连字符不会进入包名——包名仍是单驼峰小写。为省心,目录也尽量不用连字符。官方不强制布局,但社区事实标准长这样。小型库可只有 go.mod + 包目录。
internal/ 下的包只能被以 internal 父目录为根的子树 import,是「私有」的硬保障,不是约定。api/(.api 文件)、rpc/、etc/(yaml 配置)、internal/(handler/logic/svc),本质仍是这套思路。这是 Go 命名里最重要的一条——首字母大写 = 跨包可见,小写 = 仅包内可见。没有 public/private 关键字。
驼峰(MixedCaps)、缩写词全大写、短作用域用短名、不在名字里带类型。
| 规则 | 正确 | 错误 |
|---|---|---|
| 驼峰而非下划线 | userCount, requestID | user_count, request_id |
| 缩写词全大写一致 | userID, httpServer, parseURL | userId, HTTPServer, parseUrl |
| 不携带类型(反匈牙利) | users []User | userSlice, userList []User |
| 布尔用谓词/前缀 | isValid, hasPerm, canEdit | valid, permission, flag |
| 短作用域用短名 | i, err, ok, n, b, w, r | index, errorResult, isValidFlag |
// 函数内优先用短声明 := func handler(w http.ResponseWriter, r *http.Request) { userID := r.FormValue("id") // 局部短名 users, err := repo.List(r.Context()) if err != nil { /* err 是约定名 */ } var total int // 包级/零值/需显式类型时用 var const timeout = 30 * time.Second }
驼峰导出大写;枚举用 iota,首值显式给类型,后续继承。
// 普通常量:驼峰,导出大写 const ( MaxRetries = 3 DefaultTimeout = 30 * time.Second ) // 枚举:iota 自增,首值带类型 type OrderStatus int const ( StatusPending OrderStatus = iota // 0 StatusPaid // 1,继承 OrderStatus 类型 StatusShipped // 2 StatusDone // 3 ) // 需要「跳过值/位标志」时也用 iota const ( Read Perm = 1 << iota // 1 Write // 2 Exec // 4 )
func (s OrderStatus) String() string,而不是把常量直接写成字符串(避免魔法字符串满天飞)。驼峰、导出大写;构造函数 NewXxx;getter 不加 Get 前缀;error 永远放最后。
| 场景 | 约定 | 示例 | |
|---|---|---|---|
| 普通函数 | 驼峰,导出大写 | GetUser, SaveOrder, ParseConfig | |
| 构造函数 | NewXxx 返回指针(或值) | NewClient, NewServer | |
| Getter | 不加 Get 前缀 | u.Name() | u.GetName() |
| Setter | 可加 Set 前缀 | u.SetName(n) | |
| 返回 error | error 作为最后一个返回值 | (res Result, err error) | |
| 返回副本 vs 修改 | 语义清晰,避免歧义 | WithXxx() 返回新值常见 |
// 构造 + getter/setter(Go 风格,无 Get 前缀) type User struct { name string } func NewUser(name string) *User { // 构造函数 return &User{name: name} } func (u *User) Name() string { return u.name } // getter:无 Get func (u *User) SetName(n string) { u.name = n } // setter // error 永远最后 func GetUser(ctx context.Context, id int64) (*User, error) { if id <= 0 { return nil, fmt.Errorf("invalid id %d", id) } // ... }
GetXxx 反而更诚实。判断标准:是否只是读字段。类型名 UpperCamel 不带 Struct 后缀;字段驼峰、导出大写;用 tag 标注序列化;构造函数 NewXxx。
// ✅ 类型名即语义,不叫 UserStruct / UserModel type User struct { ID int64 `json:"id" db:"id"` // 导出字段大写 Name string `json:"name"` email string // 不导出:仅包内 CreatedAt time.Time `json:"created_at"` } // 构造函数返回指针(结构体较大或需共享状态时) func NewUser(name string) *User { return &User{Name: name, CreatedAt: time.Now()} }
| 规则 | 正确 | 错误 |
|---|---|---|
| 类型名不带冗余后缀 | User, Order, Config | UserStruct, UserModel, UserObj |
| 字段驼峰 | createdAt, userID | created_at, UserID_field |
| 序列化 tag 用反引号 | json:"created_at" | 双引号/漏 tag |
| 零值可用则提供零值构造 | 直接 User{} | 强制必须 NewUser |
NewXxx 只是社区约定。若该类型零值即可用(如 bytes.Buffer),就别强行要求 New,让用户直接 var b bytes.Buffer。单方法接口以 -er 结尾;小接口组合大接口;不写 I 前缀、不写 Impl 后缀。
// 单方法接口 → -er 后缀(来自方法名) type Reader interface { Read(p []byte) (int, error) } type Writer interface { Write(p []byte) (int, error) } type Stringer interface { String() string } type Closer interface { Close() error } // 小接口组合出大能力(io.ReadWriter) type ReadWriter interface { Reader Writer }
Reader, Formatter, NotifierIReader(别加 I 前缀)ReaderImpl(别加 Impl 后缀)implements 声明。所以「消费方定义小接口」是最佳实践——在调它的包里写 type Storage interface{ Save(...) error },而不是在实现方塞一个大接口。接收者名用类型缩写(1–2 字母);不用 this/self;同类型所有方法接收者名保持一致;值/指针选择要一致。
type Client struct { timeout time.Duration } // 接收者名 = 类型缩写 c,不是 this/self func (c *Client) Do(req Request) (Response, error) { // 用 c.timeout ... } func (c *Client) Close() error { // 同一类型统一叫 c return nil }
| 选择 | 用指针接收者 *T | 用值接收者 T |
|---|---|---|
| 判断依据 | 方法需修改接收者 / 结构体较大 / 含 sync.Mutex 等不可拷贝字段 | 小型不可变值(如 type Point struct{X,Y float64}) |
| 关键 | 同一类型的所有方法必须统一用指针或统一用值,不要混用 | |
sync.Mutex 等字段的结构体绝不能用值接收者——值接收会拷贝锁,直接出错。含锁或需修改状态时一律指针接收者。哨兵错误用 ErrXxx 导出;变量名用 err;用 %w 包装保留链。
// 哨兵错误:导出用 Err 前缀,首字母大写 var ErrNotFound = errors.New("not found") var ErrClosed = errors.New("already closed") // 包装错误用 %w 保留链路(Go 1.13+) if err != nil { return fmt.Errorf("query user %d: %w", id, err) } // 判断用 errors.Is / errors.As if errors.Is(err, ErrNotFound) { // 处理未找到 }
| 规则 | 正确 | 错误 |
|---|---|---|
| 哨兵错误名 | ErrNotFound, ErrTimeout | NotFoundErr, notFound |
| 局部错误变量 | err | e, error, err1 |
| 错误包装 | %w(可 errors.Is) | %v(断链) |
导出标识符必须有文档注释,且以标识符名开头;godoc 据此生成文档。
// Package user 提供了用户领域模型的存储与查询能力。 package user // User 表示一个已注册的用户。 type User struct { /* ... */ } // GetUser 按 ID 查询用户;不存在时返回 ErrNotFound。 func GetUser(ctx context.Context, id int64) (*User, error) { // ... }
| 规则 | 说明 |
|---|---|
| 导出符号必写注释 | 类型/函数/方法/包,注释以名字开头(// User 表示...) |
| 完整句子 | 首字母大写、结尾句号,方便 godoc 拼接 |
| 包注释位置 | 放 doc.go 或含 package xxx 的文件顶部,// Package xxx ... |
| 别解释「显然」代码 | 注释讲 why,不讲 what;好的命名可自解释 |
std / external / internal;func init() 难以控制顺序与测试,能免则免;必要时集中在一处。import _ "net/http/pprof" 仅为副作用;一般放 main 包。gofmt(或 goimports)+ go vet + golangci-lint 接进 Git pre-commit / CI,命名与格式问题在提交前自动修掉,评审只聊设计。| 对象 | 风格 | ✅ 正确 | ❌ 错误 |
|---|---|---|---|
| 文件名 | 小写_下划线 | user_service.go | userService.go |
| 目录名 | 小写单词 | user/ | userSvc/、User/ |
| 包名 | 小写·无下划线·与目录同名 | package user | package usersvc |
| 导出判定 | 首字母大写 | User, GetUser | user, getUser |
| 变量 | 驼峰 | userID, orderCount | user_id, UserID |
| 缩写词 | 全大写 | userID, httpURL | userId, httpUrl |
| 常量 | 驼峰/iota | MaxRetries, StatusDone | MAX_RETRIES |
| 函数 | 驼峰 | NewUser, Save | new_user, Create_User |
| getter | 无 Get | u.Name() | u.GetName() |
| struct | UpperCamel 无后缀 | User, Order | UserStruct |
| struct 字段 | 驼峰 | createdAt | created_at |
| 接口(单方法) | -er 结尾 | Reader, Closer | IReader, Readable |
| 接收者名 | 类型缩写 | (c *Client) | (this *Client) |
| 哨兵错误 | ErrXxx | ErrNotFound | NotFoundErr |
| 布尔变量 | 谓词 | isValid, hasKey | valid, flag |
· go-zero 基础 —— 框架整体认知
· go-zero 项目规约 —— 本仓库的目录/命名落地约定
· go-zero 类型 —— struct/tag 与代码生成
· go-zero 服务启动 —— main 到端口监听