name: golang-idioms description: Go语言中关于错误处理、接口设计、并发编程、测试和模块管理的惯用模式
Go编程规范
错误处理
// 返回错误,永远不要在库代码中panic
func LoadConfig(path string) (Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("读取配置 %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return Config{}, fmt.Errorf("解析配置: %w", err)
}
return cfg, nil
}
规则:
- 始终使用
fmt.Errorf("上下文: %w", err)包装错误以提供上下文 - 使用
%w以便调用者可以使用errors.Is和errors.As - 在适当的层级处理错误;不要同时记录并返回相同的错误
- 为预期条件定义哨兵错误
var (
ErrNotFound = errors.New("未找到")
ErrUnauthorized = errors.New("未授权")
)
func GetUser(id string) (User, error) {
user, ok := store[id]
if !ok {
return User{}, fmt.Errorf("用户 %s: %w", id, ErrNotFound)
}
return user, nil
}
// 调用者
user, err := GetUser(id)
if errors.Is(err, ErrNotFound) {
http.Error(w, "用户未找到", http.StatusNotFound)
return
}
接口设计
// 保持接口小(1-3个方法)
type Reader interface {
Read(p []byte) (n int, err error)
}
type UserStore interface {
GetUser(ctx context.Context, id string) (User, error)
CreateUser(ctx context.Context, u User) error
}
// 接受接口,返回结构体
func NewService(store UserStore, logger *slog.Logger) *Service {
return &Service{store: store, logger: logger}
}
规则:
- 在接口被使用的地方定义接口(消费者侧),而不是实现的地方
- 优先选择小、可组合的接口,而不是大的
- 使用标准库中的
io.Reader、io.Writer、fmt.Stringer - 只有一个方法的接口应以方法名加上
er后缀命名
Goroutine和通道模式
工作池
func process(ctx context.Context, jobs <-chan Job, workers int) <-chan Result {
results := make(chan Result, workers)
var wg sync.WaitGroup
for range workers {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
select {
case <-ctx.Done():
return
case results <- job.Execute():
}
}
}()
}
go func() {
wg.Wait()
close(results)
}()
return results
}
扇出/扇入
func fanOut[T, R any](ctx context.Context, items []T, fn func(T) R, concurrency int) []R {
sem := make(chan struct{}, concurrency)
results := make([]R, len(items))
var wg sync.WaitGroup
for i, item := range items {
wg.Add(1)
sem <- struct{}{}
go func() {
defer func() { <-sem; wg.Done() }()
results[i] = fn(item)
}()
}
wg.Wait()
return results
}
规则:
- 始终将
context.Context作为第一个参数传递 - 始终确保goroutine可以被停止(通过上下文取消或通道关闭)
- 使用
sync.WaitGroup等待goroutine完成 - 当生产者和消费者运行速度不同时,使用缓冲通道
- 永远不要启动一个不知道如何停止的goroutine
上下文传播
func (s *Service) HandleRequest(ctx context.Context, req Request) (Response, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
user, err := s.store.GetUser(ctx, req.UserID)
if err != nil {
return Response{}, fmt.Errorf("获取用户: %w", err)
}
ctx = context.WithValue(ctx, userKey, user)
return s.processRequest(ctx, req)
}
规则:
- 对每个进行I/O操作的函数,将上下文作为第一个参数传递
- 对所有外部调用使用
context.WithTimeout或context.WithDeadline - 创建可取消上下文后始终使用
defer cancel() - 谨慎使用
context.WithValue(仅用于请求范围的值:跟踪ID、认证信息) - 永远不要在结构体中存储上下文
表驱动测试
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
want bool
}{
{"有效邮箱", "user@example.com", true},
{"缺少@", "userexample.com", false},
{"空字符串", "", false},
{"多个@", "user@@example.com", false},
{"带子域名的有效邮箱", "user@mail.example.com", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidateEmail(tt.email)
if got != tt.want {
t.Errorf("ValidateEmail(%q) = %v, 期望 %v", tt.email, got, tt.want)
}
})
}
}
测试辅助函数
func newTestServer(t *testing.T) *httptest.Server {
t.Helper()
handler := setupRoutes()
srv := httptest.NewServer(handler)
t.Cleanup(srv.Close)
return srv
}
func assertEqual[T comparable](t *testing.T, got, want T) {
t.Helper()
if got != want {
t.Errorf("得到 %v, 期望 %v", got, want)
}
}
在所有测试实用函数中使用 t.Helper()。使用 t.Cleanup() 而不是 defer 来清理测试资源。使用 testdata/ 目录存放测试夹具。
模块管理
go.mod 结构:
module github.com/org/project
go 1.23
require (
github.com/lib/pq v1.10.9
golang.org/x/sync v0.7.0
)
命令:
go mod tidy # 删除未使用的,添加缺失的
go mod verify # 验证校验和
go list -m -u all # 检查更新
go get -u ./... # 更新所有依赖
go mod vendor # 供应商化依赖(可选)
每次提交前使用 go mod tidy。锁定主要版本。更新前审查变更日志。
零值设计
设计类型,使其零值有用:
// sync.Mutex 的零值是未锁定的互斥锁(可直接使用)
var mu sync.Mutex
// bytes.Buffer 的零值是空缓冲区(可直接使用)
var buf bytes.Buffer
buf.WriteString("hello")
// 自定义类型:使零值有意义
type Server struct {
Addr string // 默认为 ""
Handler http.Handler // 默认为 nil
Timeout time.Duration // 默认为 0(无超时)
}
func (s *Server) ListenAndServe() error {
addr := s.Addr
if addr == "" {
addr = ":8080" // 有用的默认值
}
handler := s.Handler
if handler == nil {
handler = http.DefaultServeMux
}
// ...
}
规则:
- 优先选择具有有意义零值的结构体,而不是构造函数
- 当方法修改接收器时,使用指针接收器
- 当方法只读取时,使用值接收器
- 永远不要导出用户不应直接设置的字段;使用构造函数
结构化日志记录
import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
logger.Info("请求处理",
slog.String("方法", r.Method),
slog.String("路径", r.URL.Path),
slog.Int("状态", status),
slog.Duration("延迟", time.Since(start)),
)
使用 log/slog(标准库,Go 1.21+)。使用结构化字段,永远不要字符串插值。每个日志条目都包含请求ID、用户ID和操作名称。
常见反模式
- 返回
interface{}/any而不是具体类型 - 使用
init()进行复杂设置(使测试困难) - 忽略错误而不加注释
- 使用goroutine而不进行生命周期管理
- 互斥锁争用由于锁范围过宽
- 通道误用:对于简单共享状态,优先选择互斥锁
- 在函数较长时使用裸返回