Style(代码风格)
Go 代码风格的核心不是个人排版偏好,而是让标准工具统一格式,让名称和结构直接表达意图。gofmt 负责机械排版,开发者则负责包边界、命名、控制流、错误处理和注释质量。
先交给 gofmt
gofmt 是 Go 工具链提供的标准格式化程序。格式化单个文件:
gofmt -w main.go
在模块根目录格式化当前模块中的包:
go fmt ./...
gofmt 会统一缩进、空格、换行和部分源码布局。团队不应围绕这些格式建立另一套互相冲突的手工规则;编辑器可以配置为保存时自动运行格式化。
下面的代码即使能表达大致意图,也不符合标准格式:
package main
import "fmt"
func main(){fmt.Println("Go")}
格式化后为:
package main
import "fmt"
func main() {
fmt.Println("Go")
}
导入分组
导入通常按来源分组:标准库一组,第三方包和当前项目包按项目约定分组。gofmt 会排序已有分组中的导入,但不会替开发者判断依赖的业务边界。
import (
"context"
"net/http"
"example.com/shop/internal/order"
)
删除未使用导入,不要用空白导入隐藏问题。需要自动增删导入时,可以在团队明确采用后使用 goimports;它不是 gofmt 本身,也不是基础 Go 发行版中所有命令行为的替代品。
命名要在调用处自然
Go 名称通常简短但不含糊:
- 包名使用简短、全小写单词,如
http、json、order。 - 使用
userID、HTTPClient、serveHTTP,保持ID、HTTP、URL等缩写一致。 - 不使用下划线模拟其他语言的常量或私有命名风格。
- 获取器通常命名为
Owner(),而不是GetOwner()。 - 接收者名称简短并保持一致,例如
func (c *Client) Close()。
避免名称在调用处产生重复:
package user
// 推荐:调用处是 user.New(...)
func New(name string) *User {
return &User{Name: name}
}
如果命名为 NewUser,调用处会变成重复的 user.NewUser。名称是否自然,应结合包限定符一起阅读。
控制流保持直接
Go 代码常通过提前返回处理无效条件和错误,使正常路径减少缩进:
func displayName(name string) (string, error) {
if name == "" {
return "", errors.New("name is empty")
}
return strings.TrimSpace(name), nil
}
当 if 分支已经 return、break 或 continue,通常无需再写 else:
if err != nil {
return err
}
process()
提前返回不是绝对规则。若分支过多或资源清理逻辑分散,应先改善函数职责,而不是机械地继续增加出口。
让零值有用
设计类型时,尽量让零值处于可理解、可安全使用的状态。标准库中的 bytes.Buffer 和 sync.Mutex 都可以在零值状态下直接使用:
package main
import (
"bytes"
"fmt"
)
func main() {
var buffer bytes.Buffer
buffer.WriteString("Go")
fmt.Println(buffer.String())
}
如果类型必须经过构造函数才能使用,应通过未导出字段、清晰文档和运行时校验维护不变量,不要让无效状态悄悄传播。
错误信息与错误处理
错误通常作为返回值处理,并在需要时用 %w 增加上下文。错误字符串一般以小写开头,不以句号或换行结尾,便于上层组合:
func loadConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config %q: %w", path, err)
}
return data, nil
}
不要既记录错误又在每一层原样返回,除非该层明确拥有日志职责;否则同一次失败会产生重复日志。错误判断应根据契约使用 errors.Is、errors.As 或明确类型,而不是依赖错误文本。
注释公开约束,而非翻译代码
导出 API 的注释应从声明名称开始,并说明调用方关心的行为:
// Client sends requests to the configured endpoint.
// Client is safe for concurrent use.
type Client struct {
// ...
}
注释语言可以服从项目约定,但标识符、代码和工具指令应保持原样。代码已经清楚表达步骤时,不必逐行翻译;更值得记录的是设计原因、失败方式、资源所有权和并发安全性。
接口与包边界
接口应由实际使用方根据所需行为定义,而不是提前为每个结构体创建一一对应的大接口:
type UserFinder interface {
FindUser(ctx context.Context, id int64) (User, error)
}
小接口更容易实现、替换和测试,但不应为了追求方法数量而拆散本来必须共同成立的契约。包也遵循同样原则:围绕内聚职责组织,让依赖方向清晰,避免含义模糊的 utils、common 成为所有代码的汇合点。
没有强制列宽
Go 风格没有要求所有代码都在固定列数处换行。长表达式通常说明名称、数据结构或函数职责可能需要调整;应先重构意图,再让 gofmt 决定合法布局。不要为了对齐而加入会被格式化工具移除的空格。
使用工具验证
在模块根目录可以依次运行:
go fmt ./...
go vet ./...
go test ./...
go fmt 统一格式,go vet 检查一部分可疑结构,go test 验证行为。go vet 是静态诊断工具,不证明程序没有缺陷;并发代码还应根据需要运行 go test -race ./...。团队可以增加静态检查器,但应记录版本和规则,避免本地与 CI 结果不一致。
表格驱动测试、示例测试、基准和 fixture 的使用方式见 Unit Test(单元测试)。
与 TypeScript 项目的区别
TypeScript 项目通常自行选择 Prettier、ESLint 及大量规则;Go 把基础格式化统一在官方工具链中,社区代码因此具有较一致的外观。格式统一不代表设计自动正确,包边界、错误语义、并发安全和 API 命名仍需要代码评审与测试。
小结
- 使用
gofmt或go fmt统一机械排版,不维护冲突的手工格式规则。 - 名称要结合包限定符阅读,保持缩写一致,并避免重复含义。
- 优先使用清晰的提前返回和可用零值,降低嵌套与初始化负担。
- 错误信息便于上层组合,错误应增加上下文并按契约判断。
- 注释解释约束和原因,接口与包围绕调用方需求和内聚职责设计。
- 用
go fmt、go vet和go test分别检查格式、可疑结构与行为。