跳到主要内容

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 名称通常简短但不含糊:

  • 包名使用简短、全小写单词,如 httpjsonorder
  • 使用 userIDHTTPClientserveHTTP,保持 IDHTTPURL 等缩写一致。
  • 不使用下划线模拟其他语言的常量或私有命名风格。
  • 获取器通常命名为 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 分支已经 returnbreakcontinue,通常无需再写 else

if err != nil {
return err
}

process()

提前返回不是绝对规则。若分支过多或资源清理逻辑分散,应先改善函数职责,而不是机械地继续增加出口。

让零值有用

设计类型时,尽量让零值处于可理解、可安全使用的状态。标准库中的 bytes.Buffersync.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.Iserrors.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)
}

小接口更容易实现、替换和测试,但不应为了追求方法数量而拆散本来必须共同成立的契约。包也遵循同样原则:围绕内聚职责组织,让依赖方向清晰,避免含义模糊的 utilscommon 成为所有代码的汇合点。

没有强制列宽

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 命名仍需要代码评审与测试。

小结

  • 使用 gofmtgo fmt 统一机械排版,不维护冲突的手工格式规则。
  • 名称要结合包限定符阅读,保持缩写一致,并避免重复含义。
  • 优先使用清晰的提前返回和可用零值,降低嵌套与初始化负担。
  • 错误信息便于上层组合,错误应增加上下文并按契约判断。
  • 注释解释约束和原因,接口与包围绕调用方需求和内聚职责设计。
  • go fmtgo vetgo test 分别检查格式、可疑结构与行为。