跳转到内容

Go开发规范

Go 编码规范、包管理、并发模式


  • [Requirement] 代码必须通过 gofmt 格式化,提交前执行 gofmt -w .;缩进使用 Tab,对齐使用空格。
  • [Requirement] 包名全小写、简短、见名知意,与目录名一致;禁止下划线与驼峰,禁止与标准库重名。
  • [Requirement] 导出标识符(首字母大写)必须有注释,注释以标识符名开头:// UserService 提供用户业务逻辑。
  • [Requirement] 文件名全小写下划线(user_service.go);测试文件以 _test.go 结尾;命令入口文件为 main.go。
  • [Standard] 变量命名:局部变量小驼峰短名(u、uid),包级变量大驼峰;常量大驼峰导出、小驼峰包内;禁止 i、j 之外的无意义单字母用于非循环变量。
  • [Standard] 错误变量以 Err 前缀命名:ErrUserNotFound,且为包级 var。
  • [Standard] 接口名以 er 结尾(单方法接口):Reader、Writer、Closer;多方法接口按职责命名。
  • [Default] 函数控制流尽量减少嵌套,提前 return(early return),避免深层嵌套 if/else。
  • [Caution] 禁止使用 panic 处理可预期错误,必须用 error 返回;panic 仅用于程序启动时的不可恢复错误。
  • [Caution] init() 函数慎用,避免隐藏依赖与执行顺序问题,优先显式初始化。
// user_service.go
package userservice
 
import "errors"
 
// ErrUserNotFound 用户不存在
var ErrUserNotFound = errors.New("user not found")
 
// UserService 提供用户业务逻辑
type UserService struct {
    repo UserRepository
}
 
// NewUserService 创建用户服务
func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}
 
// GetByID 根据 ID 查询用户
func (s *UserService) GetByID(id int64) (*User, error) {
    if id <= 0 {
        return nil, errors.New("invalid user id")
    }
    u, err := s.repo.FindByID(id)
    if err != nil {
        return nil, err
    }
    if u == nil {
        return nil, ErrUserNotFound
    }
    return u, nil
}
// 反模式:包名带下划线、与标准库重名
package user_service   // 下划线
package strings        // 与标准库重名
 
// 反模式:导出类型无注释
type UserService struct { }  // 缺少注释
 
// 反模式:用 panic 处理业务错误
func (s *UserService) GetByID(id int64) *User {
    if id <= 0 {
        panic("invalid id")  // 应返回 error
    }
}
 
// 反模式:深层嵌套
func process() {
    if err := step1(); err == nil {
        if err := step2(); err == nil {
            if err := step3(); err == nil {
                // ...
            }
        }
    }
    // 应改为 early return
}

  • [Requirement] 使用 Go Modules(go mod init),必须提交 go.mod 与 go.sum;Go 版本在 go.mod 中固定(go 1.22)。
  • [Requirement] 禁止在代码中使用 replace 指向本地路径后提交到生产分支,仅用于本地临时调试。
  • [Standard] 依赖遵循最小化原则,引入新第三方库需评估维护活跃度、开源协议与安全性。
  • [Standard] 禁止循环依赖(A 包 import B,B 又 import A),通过抽取公共接口或下沉到更底层包解决。
  • [Standard] 包职责单一,一个包只做一件事;包大小超过 2000 行或职责混杂时考虑拆分。
  • [Default] 使用 go mod tidy 清理未使用依赖,CI 中检查 go mod tidy 后无差异。
  • [Caution] 禁止直接 import 内部包(含 /internal/ 的路径),该路径受 Go 工具链保护,外部包无法引用。
// go.mod
module github.com/company/user-service
 
go 1.22
 
require (
    github.com/gin-gonic/gin v1.10.0
    gorm.io/gorm v1.25.10
)
# 初始化模块
go mod init github.com/company/user-service
 
# 添加依赖(自动更新 go.mod)
go get github.com/gin-gonic/gin@v1.10.0
 
# 清理依赖
go mod tidy
 
# 检查依赖安全漏洞
govulncheck ./...
// go.mod — 反模式:本地 replace 提交到生产
require github.com/company/common v1.0.0
 
replace github.com/company/common => ../common  // 本地路径,CI/他人无法构建
// 反模式:循环依赖
// user/service.go
import "github.com/company/order"   // user 依赖 order
 
// order/service.go
import "github.com/company/user"    // order 依赖 user,循环

  • [Requirement] 所有可能失败的函数必须返回 error 作为最后一个返回值,调用方必须显式检查 err != nil,禁止用 _ 忽略错误(除非明确知道后果)。
  • [Requirement] 错误包装使用 fmt.Errorf("...: %w", err) 保留原始错误链,禁止用 fmt.Sprintf 拼接后丢失原始错误。
  • [Requirement] 自定义错误实现 error 接口,可携带错误码;哨兵错误(var ErrXxx = errors.New(...))用于可比较的错误类型。
  • [Standard] 错误信息小写开头、不以标点结尾,简洁描述问题而非堆栈。
  • [Standard] 使用 errors.Is(err, ErrXxx) 判断哨兵错误,errors.As(err, &target) 提取特定错误类型,禁止字符串比较。
  • [Default] 不重复包装错误:每经过一层职责边界(如 service → handler)包装一次上下文,避免层层包装导致信息冗余。
  • [Caution] 禁止在循环中忽略错误继续执行而不记录,会掩盖真实问题;禁止 recover() 后静默吞掉错误,必须记录日志或转换为 error 返回。
// 自定义错误类型
type BizError struct {
    Code    int
    Message string
}
 
func (e *BizError) Error() string {
    return e.Message
}
 
// 哨兵错误
var ErrUserNotFound = errors.New("user not found")
 
// 函数返回错误
func (s *UserService) GetByID(id int64) (*User, error) {
    u, err := s.repo.FindByID(id)
    if err != nil {
        return nil, fmt.Errorf("find user by id %d: %w", id, err)  // 保留原始错误链
    }
    if u == nil {
        return nil, ErrUserNotFound
    }
    return u, nil
}
 
// 调用方判断错误
user, err := svc.GetByID(id)
if err != nil {
    switch {
    case errors.Is(err, ErrUserNotFound):
        return nil, status.Error(codes.NotFound, "user not found")
    default:
        return nil, status.Error(codes.Internal, "internal error")
    }
}
// recover 必须记录并转换
func safeHandler(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                log.Printf("panic recovered: %v\n%s", rec, debug.Stack())
                http.Error(w, "internal server error", http.StatusInternalServerError)
            }
        }()
        h(w, r)
    }
}
// 反模式:忽略错误
result, _ := db.Query("SELECT ...")  // 忽略错误,可能 nil 崩溃
 
// 反模式:丢失原始错误链
if err != nil {
    return errors.New("query failed: " + err.Error())  // 无法用 errors.Is 判断
}
 
// 反模式:字符串比较错误
if err.Error() == "user not found" { ... }  // 脆弱,依赖错误文本
 
// 反模式:recover 静默吞掉
defer func() {
    if r := recover(); r != nil {
        // 什么都不做,掩盖 panic
    }
}()

  • [Requirement] 启动 goroutine 必须明确其生命周期与退出方式,禁止启动后无人管理的 goroutine(goroutine 泄漏)。
  • [Requirement] goroutine 间通信优先用 channel,禁止用共享内存 + 互斥锁的方式传递数据(“不要通过共享内存来通信,而要通过通信来共享内存”)。
  • [Requirement] 必须用 context.Context 控制取消与超时,所有阻塞操作(网络、DB、channel)都应接受 ctx 参数并响应取消。
  • [Standard] 等待多个 goroutine 用 sync.WaitGroup,任务分发用 worker pool,禁止无限制 go func()。
  • [Standard] channel 使用完毕必须由发送方关闭,接收方用 for range 或 select 监听 ctx.Done() 退出。
  • [Standard] sync.Mutex 保护共享可变状态时,锁粒度尽可能小;优先用 sync.Map、atomic 等更高层抽象。
  • [Default] 并发原语(Mutex、Once、WaitGroup、Cond)禁止拷贝使用,必须用指针。
  • [Caution] 禁止往已关闭的 channel 发送数据(会 panic);禁止向无缓冲 channel 在无接收方时发送(永久阻塞)。
// 使用 context 控制超时
func (s *UserService) GetByID(ctx context.Context, id int64) (*User, error) {
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()
 
    u, err := s.repo.FindByID(ctx, id)
    if err != nil {
        return nil, fmt.Errorf("find user: %w", err)
    }
    return u, nil
}
 
// Worker Pool 模式
func workerPool(ctx context.Context, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case job, ok := <-jobs:
            if !ok {
                return
            }
            results <- process(job)
        case <-ctx.Done():
            return
        }
    }
}
 
// 启动与等待
var wg sync.WaitGroup
jobs := make(chan Job, 100)
results := make(chan Result, 100)
 
for i := 0; i < runtime.NumCPU(); i++ {
    wg.Add(1)
    go workerPool(ctx, jobs, results, &wg)
}
 
// ... 发送 jobs
close(jobs)
go func() {
    wg.Wait()
    close(results)
}()
// 反模式:goroutine 泄漏,无退出机制
func leak() {
    go func() {
        for {
            // 无 ctx、无退出信号,永久运行
        }
    }()
}
 
// 反模式:无缓冲 channel 无接收方
ch := make(chan int)
ch <- 1  // 永久阻塞
 
// 反模式:接收方关闭 channel
func bad(chs []chan int) {
    for _, ch := range chs {
        close(ch)  // 接收方不应关闭,应发送方关闭
    }
}
 
// 反模式:拷贝 Mutex
type Counter struct {
    mu sync.Mutex
    n  int
}
 
func (c Counter) Inc() {  // 值接收者,拷贝了 Mutex,锁失效
    c.mu.Lock()
    c.n++
    c.mu.Unlock()
}

  • [Requirement] 禁止在循环内频繁创建大对象或拼接字符串,字符串拼接用 strings.Builder 或预分配 []byte。
  • [Requirement] slice/map 创建时预分配容量:make([]T, 0, n)、make(map[K]V, n),避免频繁扩容拷贝。
  • [Standard] 避免不必要的内存分配:优先用值类型而非指针(小对象);热路径上减少 interface{} 装箱拆箱。
  • [Standard] 数据库/HTTP 客户端必须设置连接池与超时,禁止使用默认无超时配置(http.DefaultClient 无超时)。
  • [Standard] 大对象(>1MB)或频繁分配的对象可考虑 sync.Pool 复用,但必须正确 Reset,禁止存储带状态的对象。
  • [Default] 使用 pprof 定位性能瓶颈,禁止凭直觉优化;优化前必须有基准测试(Benchmark)对比数据。
  • [Caution] sync.Pool 中的对象可能被 GC 回收,禁止存储必须长期存在的数据;禁止将 sync.Pool 当作缓存使用。
// 字符串拼接用 Builder
func buildSQL(fields []string) string {
    var b strings.Builder
    b.Grow(64)  // 预分配
    b.WriteString("SELECT ")
    for i, f := range fields {
        if i > 0 {
            b.WriteString(", ")
        }
        b.WriteString(f)
    }
    b.WriteString(" FROM t_user")
    return b.String()
}
 
// 预分配 slice
func collectUsers(ids []int64) []*User {
    users := make([]*User, 0, len(ids))  // 预分配容量
    for _, id := range ids {
        u, err := svc.GetByID(context.Background(), id)
        if err == nil {
            users = append(users, u)
        }
    }
    return users
}
 
// HTTP 客户端设置超时与连接池
var httpClient = &http.Client{
    Timeout: 5 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
    },
}
 
// sync.Pool 复用
var bufPool = sync.Pool{
    New: func() any {
        return new(bytes.Buffer)
    },
}
 
func process() {
    buf := bufPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()        // 必须 Reset,防止数据串扰
        bufPool.Put(buf)
    }()
    buf.WriteString("hello")
    // ...
}
// 反模式:循环内字符串 + 拼接
result := ""
for _, s := range items {
    result += s  // 每次创建新字符串,O(n²)
}
 
// 反模式:无预分配的 slice
var users []*User
for _, id := range ids {
    users = append(users, getUser(id))  // 频繁扩容
}
 
// 反模式:使用默认无超时 HTTP 客户端
resp, err := http.Get(url)  // http.DefaultClient 无超时,可能永久阻塞
 
// 反模式:sync.Pool 存储带状态对象(不能保证复用)
var pool = sync.Pool{New: func() any { return &User{} }}
u := pool.Get().(*User)
u.ID = 123
pool.Put(u)  // 下次取出可能是同一个,也可能不是