Repository navigation
07 observability
omeyang edited this page Sep 16, 2026
·
1 revision
本文档定义 Go 项目的可测试性、日志、监控、追踪和可读性标准。
可观测性三支柱:
- 可测试性(Testability) - 代码必须易于编写和执行测试
- 可观测性(Observability) - 日志、监控、追踪必须完善
- 可读性(Readability) - 代码必须清晰易懂
- 依赖必须可注入
- 函数职责必须单一
- 禁止使用全局状态
- 时间应该可控(注入 TimeProvider)
- 应该优先使用纯函数
1. 依赖可注入
// 错误:硬编码依赖
func Query(id string) (*Order, error) {
db := mongo.Connect("mongodb://...")
return db.FindByID(id)
}
// 正确:依赖注入
func Query(ctx context.Context, repo Repository, id string) (*Order, error) {
return repo.FindByID(ctx, id)
}2. 无全局状态
// 错误:依赖全局状态
var GlobalCache map[string]*Item
func GetItem(id string) *Item {
return GlobalCache[id]
}
// 正确:通过参数传递
func GetItem(cache Cache, id string) (*Item, error) {
return cache.Get(id)
}3. 时间可控
// 错误:使用 time.Now()
func IsExpired(item *Item) bool {
return time.Now().After(item.ExpireAt)
}
// 正确:注入时间源
type TimeProvider interface {
Now() time.Time
}
func IsExpired(tp TimeProvider, item *Item) bool {
return tp.Now().After(item.ExpireAt)
}必须根据场景选择合适的日志级别:
| 级别 | 使用场景 | 示例 |
|---|---|---|
| Trace | 最详细的调试信息 | 函数入口/出口、变量值 |
| Debug | 调试信息 | 中间结果、条件判断 |
| Info | 关键操作 | 记录创建、更新、删除 |
| Warn | 警告信息 | 缓存失败、非核心功能降级 |
| Error | 错误信息 | 数据库错误、业务异常 |
| Fatal | 致命错误 | 启动失败、配置错误 |
必须使用结构化日志:
// 正确:结构化日志
log.Info(ctx, "querying item",
"id", id,
"user_id", userID,
"request_id", requestID,
)
log.Error(ctx, "query failed",
"id", id,
"error", err,
"duration_ms", duration.Milliseconds(),
)
// 错误:字符串拼接
log.Info(fmt.Sprintf("querying item: id=%s, user=%s", id, userID))业务代码必须只依赖项目内定义的 Logger 接口,日志实现必须基于标准库 log/slog 或其封装。
type Logger interface {
Info(ctx context.Context, msg string, args ...any)
Error(ctx context.Context, msg string, args ...any)
}关键路径必须有日志(入口、出口、错误):
func Query(ctx context.Context, id string) (*Order, error) {
log.Info(ctx, "query started", "id", id)
if err := ValidateID(id); err != nil {
log.Error(ctx, "invalid id", "id", id, "error", err)
return nil, err
}
order, err := repo.FindByID(ctx, id)
if err != nil {
log.Error(ctx, "find failed", "id", id, "error", err)
return nil, fmt.Errorf("find by id %s failed: %w", id, err)
}
log.Info(ctx, "query success", "id", id, "order_id", order.ID)
return order, nil
}应该暴露监控指标(/metrics):
1. 请求指标
- 请求总数(Counter)
- 请求延迟(Histogram)
- 错误率(Gauge)
- 并发数(Gauge)
2. 业务指标
- 业务对象总数(Gauge)
- 每秒查询数(Counter)
- 缓存命中率(Gauge)
3. 资源指标
- CPU 使用率(Gauge)
- 内存使用(Gauge)
- Goroutine 数量(Gauge)
- GC 耗时(Histogram)
var (
queryTotal = metrics.NewCounter("query_total{status=\"success\"}")
queryErrTotal = metrics.NewCounter("query_total{status=\"error\"}")
queryDuration = metrics.NewHistogram("query_duration_seconds")
)
func Query(ctx context.Context, id string) (*Order, error) {
start := time.Now()
defer func() {
queryDuration.Update(time.Since(start).Seconds())
}()
order, err := repo.FindByID(ctx, id)
if err != nil {
queryErrTotal.Inc()
return nil, err
}
queryTotal.Inc()
return order, nil
}应该支持分布式追踪:
import (
"go.opentelemetry.io/otel"
)
func Query(ctx context.Context, id string) (*Order, error) {
ctx, span := otel.Tracer("order-service").Start(ctx, "Query")
defer span.End()
span.SetAttributes(attribute.String("id", id))
order, err := repo.FindByID(ctx, id)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
return order, nil
}业务代码必须只依赖项目内定义的观测接口(如 MetricsRecorder、Tracer),具体实现可以替换。
// 错误
func proc(d interface{}) interface{} { ... }
var x int = 10
// 正确
func ProcessOrder(order *Order) (*Result, error) { ... }
var maxRetryCount int = 10应该有必要的注释,解释"为什么"而非"是什么":
// 好的注释:解释"为什么"
// 使用指数退避重试,避免雪崩效应
backoff := time.Duration(1<<uint(i)) * time.Second
// 坏的注释:重复代码
// 设置 i 为 0
i := 0函数应该不超过 70 行。
嵌套应该不超过 3 层,使用提前返回:
// 错误:深层嵌套
func Process(order *Order) error {
if order != nil {
if order.ID != "" {
if isValid(order.ID) {
// 业务逻辑
}
}
}
}
// 正确:提前返回
func Process(order *Order) error {
if order == nil {
return errors.New("order is nil")
}
if order.ID == "" {
return errors.New("ID is empty")
}
if !isValid(order.ID) {
return errors.New("invalid ID")
}
// 业务逻辑
return nil
}// 一致的错误处理
if err != nil {
return nil, fmt.Errorf("operation failed: %w", err)
}
// 一致的日志格式
log.Info(ctx, "operation completed", "key", value)
// 一致的命名风格
func FindByID(id string) (*Order, error)
func FindByName(name string) (*Order, error)- 依赖必须可注入
- 函数职责必须单一
- 禁止使用全局状态
- 时间应该可控(注入 TimeProvider)
- 应该优先使用纯函数
- 关键路径必须有日志(入口、出口、错误)
- 必须使用结构化日志
- 应该暴露监控指标(/metrics)
- 应该支持分布式追踪
- 错误应该有堆栈信息
- 命名必须见名知义
- 函数应该不超过 70 行
- 参数应该不超过 3 个
- 嵌套应该不超过 3 层
- 应该有必要的注释
- 代码风格必须一致
- OpenTelemetry Go:https://opentelemetry.io/docs/instrumentation/go/
- 结构化日志(log/slog):https://go.dev/blog/slog
- Effective Go:https://go.dev/doc/effective_go
- Uber Go Style Guide:https://github.com/uber-go/guide
本页由 Maat 仓库的 scripts/sync-wiki.sh 自动生成,请勿直接编辑。