Go编程代码规范指南
# Go编程代码规范指南
基于 Effective Go (opens new window) 和 Uber Go Style Guide (opens new window)。
# 目录
- 01.规范概述
- 02.命名规范
- 03.代码格式
- 04.错误处理
- 05.并发模式
- 06.泛型规范
- 07.零值利用
- 08.defer-使用
- 09.接口设计
- 10.测试规范
- 11.包组织与依赖注入
- 12.性能优化
- 13.常见反模式
- 14.工具链与自动化
- 15.代码审查清单
- 16.常见陷阱速查
# 01.规范概述
# 1.1 为何需要 Go 规范
疑惑:Go 有 go fmt 统一格式,还需要编码规范吗?
答疑:go fmt 只解决了格式问题——命名、错误处理、并发模式、包组织这些设计决策,依然需要规范来统一。Go 社区有一句名言:"Don't be clever. Be clear." 规范的核心就是让团队的 Go 代码简单、清晰、一致。
# 1.2 核心原则
| 原则 | 说明 |
|---|---|
| 简洁 | 拒绝过度抽象,能直白就不花哨 |
| 显式 | 错误显式处理,零值显式利用 |
| 并发安全 | goroutine 必须有退出机制,channel 由发送方关闭 |
| 接受接口,返回结构体 | 调用方定义接口,实现方返回具体类型 |
# 02.命名规范
| 类型 | 规范 | 示例 |
|---|---|---|
| 包名 | 小写,单数,无下划线 | http, json, userservice |
| 导出标识符 | 大写开头 | User, GetName() |
| 私有标识符 | 小写开头 | user, parseData() |
| 接口(单方法) | 方法名 + er | Reader, Writer, Closer |
| 变量 | 驼峰法 | userCount, maxRetry |
| 常量 | 驼峰法 | MaxSize, DefaultPort |
| 缩写 | 全大写或全小写 | HTTPClient, urlPath |
// ✅ 正确
package user
type Service struct {
repo Repository
}
func (s *Service) GetByID(id int64) (*User, error) {
return s.repo.FindByID(id)
}
// ✅ 接口命名
type Reader interface {
Read(p []byte) (n int, err error)
}
// ❌ 错误
type User_Service struct {} // 不要下划线
func (s *Service) get_user() // 不要下划线
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 03.代码格式
Go 有官方工具,直接使用即可:
go fmt ./... # 格式化
goimports -w . # 自动管理 import
go vet ./... # 静态分析
golangci-lint run # 综合检查
1
2
3
4
2
3
4
// ✅ 分组导入(标准库 → 第三方 → 本模块)
import (
"context"
"fmt"
"github.com/pkg/errors"
"myproject/internal/config"
)
// ✅ 结构体字段对齐
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
}
// ❌ 空标识符 import
import _ "net/http/pprof" // 仅在有充分理由时使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 04.错误处理
# 4.1 基础模式
// ✅ 始终检查错误
result, err := doSomething()
if err != nil {
return fmt.Errorf("doSomething: %w", err)
}
// ✅ 包装错误,保留上下文(Go 1.13+ %w)
if err := validate(user); err != nil {
return fmt.Errorf("validating user %d: %w", user.ID, err)
}
// ✅ 错误只处理一次(记日志或返回,不要同时做)
data, err := fetchData()
if err != nil {
log.Printf("fetchData failed: %v", err) // 这里记日志
return err // 上层不应再记
}
// ✅ 哨兵错误
var ErrNotFound = errors.New("not found")
// ✅ errors.Is 和 errors.As 判断错误链
if errors.Is(err, ErrNotFound) {
// 处理未找到
}
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Println(valErr.Field) // 提取自定义错误属性
}
// ❌ 忽略错误(除非有充分理由并加注释)
// _ = json.Unmarshal(data, &v) // 应解释为何可忽略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 4.2 多错误与自定义错误 【推荐】
// ✅ 自定义错误类型(带上下文)
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
// ✅ 多错误聚合(Go 1.20+ errors.Join)
var errs []error
for _, item := range items {
if err := validate(item); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...) // 聚合所有错误
}
// ✅ panic 仅用于不可恢复的情况
// ❌ 不要用 panic 替代 error 返回值
// 初始化阶段的致命错误可以用 panic(如配置文件损坏)
if err := config.Load(); err != nil {
panic(fmt.Sprintf("加载配置失败: %v", err)) // 仅 main 或 init 中
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 05.并发模式
# 5.1 goroutine 与 channel
// ✅ sync.WaitGroup
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
fetch(u)
}(url) // ⚠️ 注意:复制循环变量
}
wg.Wait()
// ✅ context 传递取消信号和超时
func Process(ctx context.Context, input string) error {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
select {
case <-ctx.Done():
return ctx.Err()
case result := <-doWork(ctx, input):
return handle(result)
}
}
// ✅ channel 关闭:由发送方负责
ch := make(chan int)
go func() {
defer close(ch)
for _, v := range data {
ch <- v
}
}()
// ✅ sync.Once 单例初始化
var (
instance *Service
once sync.Once
)
func GetService() *Service {
once.Do(func() { instance = &Service{} })
return instance
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 5.2 errgroup 与工作池 【推荐】
import "golang.org/x/sync/errgroup"
// ✅ errgroup:并行任务 + 错误传播
func FetchAll(ctx context.Context, urls []string) ([]*Response, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]*Response, len(urls))
for i, url := range urls {
i, url := i, url // ⚠️ Go 1.22 前闭包需复制
g.Go(func() error {
resp, err := fetch(ctx, url)
if err != nil {
return err // 第一个错误取消其他 goroutine
}
results[i] = resp
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
// ✅ 工作池:限制并发数(用 channel 作信号量)
func ProcessAll(items []Item, concurrency int) {
sem := make(chan struct{}, concurrency)
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
sem <- struct{}{} // 获取令牌
go func(it Item) {
defer wg.Done()
defer func() { <-sem }() // 释放令牌
process(it)
}(item)
}
wg.Wait()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 06.泛型规范(Go 1.18+)【推荐】
// ✅ 泛型函数:简洁约束
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
// ✅ 泛型约束用 interface
type Number interface {
~int | ~int64 | ~float64 // ~ 表示允许底层类型
}
func Sum[T Number](vals []T) T {
var total T
for _, v := range vals {
total += v
}
return total
}
// ✅ 泛型类型:自定义容器
type Set[T comparable] map[T]struct{}
func NewSet[T comparable](items ...T) Set[T] {
s := make(Set[T])
for _, item := range items {
s[item] = struct{}{}
}
return s
}
// ❌ 泛型不是必须的——能用接口解决就不用泛型
// Go 的泛型设计哲学:显式优于隐式,简单优于灵活
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 07.零值利用
// ✅ 零值就是有用的初始值
var buf bytes.Buffer // 可直接 Write
var mu sync.Mutex // 可直接 Lock/Unlock
var wg sync.WaitGroup // 可直接 Add/Wait
// ✅ 切片判空用 nil
if users == nil {
users = make([]User, 0)
}
// ✅ 空 map 可以读,但不能写
var m map[string]int
v := m["key"] // ✅ OK: v = 0
// m["key"] = 1 // ❌ panic: assignment to nil map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 08.defer 使用
// ✅ 资源释放
f, err := os.Open("file.txt")
if err != nil {
return err
}
defer f.Close()
// ✅ defer 配合函数返回值
func trace(name string) func() {
start := time.Now()
return func() {
log.Printf("%s took %v", name, time.Since(start))
}
}
defer trace("query")() // 注意 () 调用
// ❌ 循环中 defer 导致资源泄漏
for _, f := range files {
fd, _ := os.Open(f)
defer fd.Close() // 全部延迟到函数结束!
}
// ✅ 循环中用闭包
for _, f := range files {
func() {
fd, _ := os.Open(f)
defer fd.Close()
process(fd)
}()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 10.测试规范
// ✅ 表驱动测试
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"正数", 1, 2, 3},
{"零", 0, 0, 0},
{"负数", -1, 1, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.a, tt.b); got != tt.want {
t.Errorf("Add() = %v, want %v", got, tt.want)
}
})
}
}
// ✅ testify 断言库
func TestUser(t *testing.T) {
u := NewUser("name")
assert.NotNil(t, u)
assert.Equal(t, "name", u.Name)
}
// ✅ 并行测试
func TestParallel(t *testing.T) {
t.Parallel()
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 11.包组织与依赖注入
// ✅ 按功能分层,避免循环依赖
myapp/
├── cmd/ // 入口
│ └── server/
│ └── main.go
├── internal/ // 私有包
│ ├── handler/ // HTTP handler(薄层)
│ ├── service/ // 业务逻辑
│ ├── repository/ // 数据访问
│ └── model/ // 数据模型
├── pkg/ // 可公开的库
├── go.mod
└── go.sum
// ✅ 依赖注入:构造函数注入
type UserService struct {
repo Repository // 依赖接口,不依赖具体实现
cache Cache
}
func NewUserService(repo Repository, cache Cache) *UserService {
return &UserService{repo: repo, cache: cache}
}
// ❌ 全局变量——阻碍测试,并发不安全
var db *sql.DB
func GetUser(id int64) (*User, error) { ... } // 隐式依赖全局 db
// ✅ 显式传递依赖
func GetUser(db *sql.DB, id int64) (*User, error) { ... }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 09.接口设计
// ✅ 接口要小(单一职责)
type Reader interface {
Read(p []byte) (n int, err error)
}
// ✅ 由调用方定义接口(Accept interfaces, return structs)
func Save(w io.Writer, data []byte) error { // 接受接口
_, err := w.Write(data)
return err
}
// ✅ 编译期检查接口实现
var _ io.Reader = (*MyReader)(nil)
// ❌ 不要过早抽象
// 等到确实有第二个实现时再抽取接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 12.性能优化
// ✅ 切片预分配容量(减少扩容)
var s []int // nil slice, append 会扩容
s := make([]int, 0, 100) // ✅ len=0, cap=100, 预分配
// ✅ strings.Builder 替代 + 拼接
var b strings.Builder
for _, s := range strs {
b.WriteString(s) // 零内存分配
}
result := b.String()
// ✅ sync.Pool 复用临时对象
var bufPool = sync.Pool{
New: func() any { return new(bytes.Buffer) },
}
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
// ✅ 传值 vs 传指针
func Process(u User) { ... } // 小结构体(< 64 bytes)传值更快
func Process(u *User) { ... } // 大结构体或需要修改时传指针
// ✅ 避免不必要的 []byte ↔ string 转换
// ❌ 每次都拷贝
// ✅ 只在最终需要时转换一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 13.常见反模式
| 反模式 | 问题 | 改进 |
|---|---|---|
| 忽略 error | 隐患难排查 | 始终检查 |
panic 用于普通错误 | 崩溃 | 用 error 返回值 |
循环中 defer | 资源泄漏 | 用闭包 |
裸 go func() | goroutine 泄漏 | 配合 context |
| 全局变量 | 并发不安全 | 依赖注入 |
| 过长的函数 | 难以维护 | 40 行内拆分 |
interface{} 滥用 | 丢失类型安全 | 用泛型或具体接口 |
# 14.工具链与自动化
# 必须用
go fmt ./... # 格式化(无参数,Go 的风格是统一的)
go vet ./... # 静态分析
golangci-lint run # 综合检查
# 推荐用
go test ./... # 单元测试
go test -race ./... # 竞态检测(CI 中必须跑)
go test -cover ./... # 代码覆盖率
go test -bench=. ./... # 性能基准测试
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# .golangci.yml
linters:
enable:
- errcheck # 检查未处理的 error
- goimports # 自动管理 import
- govet # go vet
- staticcheck # 深度静态分析
- unused # 未使用的变量/函数
- revive # 风格检查
- gocritic # 代码优化建议
- gocyclo # 圈复杂度
linters-settings:
gocyclo:
min-complexity: 15 # 超过 15 的圈复杂度报警
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# GitHub Actions
- name: Lint & Test
run: |
go fmt ./... && git diff --exit-code # 格式不对则 CI 红
go vet ./...
golangci-lint run
go test -race -cover ./...
1
2
3
4
5
6
7
2
3
4
5
6
7
# 15.代码审查清单
每次 Code Review 时,按以下清单逐项检查:
## 命名与格式
- [ ] 包名小写无下划线,导出符号大写开头,缩写全大写/全小写
- [ ] 单方法接口带 er 后缀(Reader/Writer)
- [ ] `go fmt` 通过(无 diff)
## 错误处理
- [ ] 所有 error 被检查,无 `_ = ...` 忽略
- [ ] 用 `%w` 包装错误保留链
- [ ] 错误只处理一次(log 或 return,不两者都做)
## 并发
- [ ] goroutine 有退出机制(context/cancel)
- [ ] channel 由发送方关闭
- [ ] 无竞态条件(`go test -race` 通过)
- [ ] 并发任务优先用 errgroup
## 设计
- [ ] 接口小而精,由调用方定义
- [ ] 接受接口,返回结构体
- [ ] 无循环依赖,无全局变量
- [ ] 能用接口解决就不引入泛型
## 性能
- [ ] 切片预分配容量(`make([]T, 0, cap)`)
- [ ] 字符串拼接用 `strings.Builder`
- [ ] 小结构体传值,大结构体传指针
## 测试
- [ ] 表驱动测试
- [ ] 覆盖正常和异常路径
- [ ] 有竞态检测和覆盖率报告
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 16.常见陷阱速查
# 16.1 goroutine 陷阱
| # | 陷阱 | 正解 |
|---|---|---|
| 1 | 闭包捕获循环变量 → 所有 goroutine 用同一个值 | go func(v T) {}(item) 传参或 item := item(Go 1.22 前) |
| 2 | goroutine 没有退出 → 泄漏 | 用 context 或 channel 通知退出 |
| 3 | 向已关闭的 channel 发送 → panic | defer close(ch) 确保只关闭一次 |
| 4 | 无缓冲 channel 死锁 | 用 select + default 或改用缓冲 channel |
# 16.2 error 与 defer 陷阱
| # | 陷阱 | 正解 |
|---|---|---|
| 1 | defer 中修改具名返回值 → 覆盖错误 | 注意 defer 执行时机,return err 后 defer 仍可改 err |
| 2 | defer f.Close() 吞掉 Close 的 error | 用 defer func() { err = f.Close() }() 或 errors.Join |
| 3 | errors.Is(err, io.EOF) 未 wrap 无法匹配 | fmt.Errorf("read: %w", err) 用 %w |
| 4 | nil interface 不是 nil error | return (*MyError)(nil) → 返回值 != nil(interface 有类型信息) |
# 16.3 性能陷阱
| # | 陷阱 | 正解 |
|---|---|---|
| 1 | 循环中 += 拼接字符串 | strings.Builder 或 strings.Join |
| 2 | []byte(s) 和 string(b) 反复转换 | 一次转换到底,减少不必要的拷贝 |
| 3 | 大片连续内存的 slice → 底层数组无法 GC | copy 到新 slice 再只保留新 slice |
| 4 | defer 在热循环中 → 函数退出才执行 | 热循环中不用 defer |
上次更新: 2026/06/17, 11:39:29