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) // 下次取出可能是同一个,也可能不是