Go 微服务

本页给出可直接运行的 Go 微服务模式:goroutine + channel 生产者消费者、小接口组合(io.Reader/io.Writer)、errors.Is/As/%w 链式包装、slog 结构化日志与 signal.Notify 优雅关闭;约定写进 SKILL,让 Agent 按惯用 Go 风格实现服务间通信。

SKILL 定义服务边界、接口与 mock 生成方式;禁止在库代码里 log.Fatal;优雅关闭通过 signal.Notifyhttp.Server.Shutdown(或 gRPC GracefulStop)实现。

客户端超时、重试与熔断策略写明默认值;protobuf 版本与 buf 工作流若存在,指向生成脚本与 go:generate 注释。

并发使用 errgroup 或 worker 池时的取消传播与资源上限(goroutine 泄漏防护)需在步骤中体现。

  • 配置:viper/env 与结构体校验,十二因子与容器入口一致。
  • 测试:表格驱动、httptest、集成测试的 build tag 约定。
  • 与「可观测性」技能联动:同一 trace / request id 贯穿日志与指标。

编写要点概览

将「传输层(HTTP/gRPC server)、用例层、适配器(DB/外部 RPC)」分层写清:对外可复用的稳定 API 放在 pkg(若确有跨仓需求),其余默认收进 internal,避免无意导出。

小接口组合:使用 io.Reader/io.Writer 组合而非大接口,降低测试替换成本。

// 小接口组合示例:io.Reader + io.Writer 组合
package storage

import "io"

// Reader 只暴露读能力,便于 mock
type Reader interface {
    Get(ctx context.Context, key string) (io.ReadCloser, error)
}

// Writer 只暴露写能力
type Writer interface {
    Put(ctx context.Context, key string, r io.Reader) error
}

// ReadWriter 按需组合,usecase 只接受它实际需要的接口
type ReadWriter interface {
    Reader
    Writer
}

// errors.Is/As/Wrap 正确用法
var ErrNotFound = errors.New("not found")

type StoreError struct {
    Key string
    Err error
}
func (e *StoreError) Error() string { return fmt.Sprintf("store: key=%s: %v", e.Key, e.Err) }
func (e *StoreError) Unwrap() error { return e.Err }

func GetItem(ctx context.Context, r Reader, key string) (io.ReadCloser, error) {
    rc, err := r.Get(ctx, key)
    if err != nil {
        // %w 保留原始类型,调用方可 errors.Is(err, ErrNotFound)
        return nil, fmt.Errorf("GetItem: %w", &StoreError{Key: key, Err: err})
    }
    return rc, nil
}

// 调用方判别
func handle(err error) {
    var se *StoreError
    if errors.As(err, &se) {
        log.Printf("key=%s failed", se.Key)
    }
    if errors.Is(err, ErrNotFound) {
        // 返回 HTTP 404
    }
}

请求主流程(transport → usecase)

下列为单次入站调用在典型 Go 微服务中的顺序;具体中间件与拦截器以项目注册顺序为准。重点是:context 从入口注入,deadline 与取消向下传递到客户端与查询。

  [ 入口:HTTP / gRPC listener ]
        │
        ▼
  ┌─────────────┐     OTel/gRPC stats、鉴权、recover、可选限流
  │  中间件/拦截器 │
  └─────────────┘
        │
        ▼
  ┌─────────────┐     解码请求;校验 DTO;注入依赖(接口实现)
  │  handler     │
  └─────────────┘
        │
        ▼
  ┌─────────────┐     纯业务与编排;只接收 context + 领域类型
  │  usecase     │
  └─────────────┘
        │
        ▼
  ┌─────────────┐     DB / 缓存 / 下游 gRPC-HTTP;重试需可取消
  │  adapters    │
  └─────────────┘
        │
        ▼
  [ 编码响应;记录指标与 span;返回 ]

优雅关闭:停止接受新连接 → 等待 in-flight 请求(带超时)→ 关闭 DB 池与后台 worker;与 Kubernetes preStop 休眠配合时写明文档。

// goroutine + channel 生产者消费者 示例(带 context 取消)
package worker

import (
    "context"
    "fmt"
    "golang.org/x/sync/errgroup"
)

type Job struct{ ID int }
type Result struct{ ID int; Out string }

func Run(ctx context.Context, jobs []Job, concurrency int) ([]Result, error) {
    jobCh := make(chan Job, len(jobs))
    resCh := make(chan Result, len(jobs))

    // 生产者:填入后立即关闭,不会泄漏
    go func() {
        defer close(jobCh)
        for _, j := range jobs {
            select {
            case jobCh <- j:
            case <-ctx.Done():
                return
            }
        }
    }()

    // errgroup 管理 N 个消费者 goroutine,任一出错即取消其余
    g, gctx := errgroup.WithContext(ctx)
    for i := 0; i < concurrency; i++ {
        g.Go(func() error {
            for job := range jobCh {
                select {
                case <-gctx.Done():
                    return gctx.Err()
                default:
                }
                // 模拟处理
                resCh <- Result{ID: job.ID, Out: fmt.Sprintf("done-%d", job.ID)}
            }
            return nil
        })
    }

    // 等待所有消费者完成,再关闭结果 channel
    go func() { g.Wait(); close(resCh) }()

    var results []Result
    for r := range resCh {
        results = append(results, r)
    }
    return results, g.Wait()
}
// 优雅关闭:signal.Notify + http.Server.Shutdown
package main

import (
    "context"
    "log/slog"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    srv := &http.Server{Addr: ":8080", Handler: http.DefaultServeMux}

    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            slog.Error("server error", "err", err)
            os.Exit(1)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
    <-quit

    slog.Info("shutting down", "timeout", "30s")
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        slog.Error("shutdown error", "err", err)
    }
    slog.Info("server exited cleanly")
}

模块布局:internalpkg

cmd/<binary>/main.go 只做组装:读配置、建 logger/tracer、wire 依赖并启动 server。internal/ 下按边界分包(如 apiservicestore),包之间通过小接口解耦,便于单测替换。

  • internal:默认放置业务与实现细节;Go 编译器禁止其他模块导入,适合单体多二进制或多服务共仓。
  • pkg:仅当确有「本仓库外可导入」的契约时再放置;否则易变成杂物间,优先保持 internal 为主。
  • 生成代码buf / protoc 输出目录、mockgengo:generate 一行命令写进 SKILL,避免手工拷贝。
# 推荐目录布局
payments/
├── cmd/
│   └── server/
│       └── main.go          # 只组装:配置 + logger + wire + server.Start
├── internal/
│   ├── api/
│   │   └── handler.go       # HTTP/gRPC 传输层,解码/编码
│   ├── service/
│   │   └── payment.go       # 用例层,只接收 context + 领域类型
│   ├── store/
│   │   └── postgres.go      # DB 适配器,实现 service 依赖的接口
│   └── config/
│       └── config.go        # viper/env 读取 + 结构体校验
├── pkg/
│   └── money/               # 仅限「跨仓真实复用」才放这里
│       └── amount.go
├── gen/                     # buf/protoc 生成,不手动修改
│   └── proto/
├── go.mod
└── go.sum

# go.mod 示例
module github.com/acme/payments

go 1.22

require (
    golang.org/x/sync v0.7.0
    google.golang.org/grpc v1.63.2
)

# mockgen 生成命令(写进 SKILL 或 Makefile)
# go:generate go run go.uber.org/mock/mockgen -source=internal/service/payment.go -destination=internal/service/mock_payment.go

指标、日志与链路

SKILL 写明指标前缀(如 <service>_ 或 Prometheus 常量 Subsystem)、直方图 bucket 约定,以及 trace 与 log 中共享的 trace_id / span_id 字段名;与 OpenTelemetry Go SDK 的 resourceservice.nameservice.version)对齐。

  • Prometheus:HTTP 常用 http_server_request_duration_seconds 等语义,业务指标避免与高基数 label(如 user id)绑定。
  • 结构化日志:使用 slog 或 zap 等,字段键名与集中式查询(Loki/ELK)一致;错误日志带 error 与堆栈策略(是否仅在 debug 展开)。
  • 延伸阅读:日志与分布式追踪监控仪表盘
// slog 结构化日志(Go 1.21+)— 与 trace 字段对齐
package main

import (
    "context"
    "log/slog"
    "os"
    "go.opentelemetry.io/otel/trace"
)

// newLogger 创建 JSON handler,输出到 stdout
func newLogger(level slog.Level) *slog.Logger {
    opts := &slog.HandlerOptions{Level: level}
    return slog.New(slog.NewJSONHandler(os.Stdout, opts))
}

// withTrace 从 context 注入 trace_id / span_id,与 OTel 对齐
func withTrace(ctx context.Context, logger *slog.Logger) *slog.Logger {
    span := trace.SpanFromContext(ctx)
    if !span.SpanContext().IsValid() {
        return logger
    }
    sc := span.SpanContext()
    return logger.With(
        slog.String("trace_id", sc.TraceID().String()),
        slog.String("span_id", sc.SpanID().String()),
    )
}

// 使用示例:handler 中注入 trace 字段
func ProcessPayment(ctx context.Context, logger *slog.Logger, amount float64) error {
    log := withTrace(ctx, logger)
    log.Info("processing payment", slog.Float64("amount", amount))

    if err := chargeCard(ctx, amount); err != nil {
        // error 字段名与 Loki/ELK 标准查询对齐
        log.Error("charge failed", slog.String("error", err.Error()))
        return fmt.Errorf("ProcessPayment: %w", err)
    }
    log.Info("payment done", slog.Float64("amount", amount))
    return nil
}

go.mod 模块路径预览

将草稿路径整理为适合 module 指令的一行:反斜杠转正斜杠、合并重复斜杠、去掉首尾空白与末尾斜杠。复杂路径(版本后缀、子模块)仍以团队 go.mod 与 GOPROXY 策略为准。

规范化结果


              

---
name: go-microservice
description: 按 Go 惯用法实现微服务边界、并发模式与可观测性
---
# 模块布局与接口
- cmd/<binary>/main.go 只组装依赖,不含业务逻辑
- internal/ 按边界分包(api / service / store)
- 接口最小化:只暴露调用方实际需要的方法集
- mockgen 命令写进 go:generate 或 Makefile
# 并发模式
- goroutine + channel 生产者消费者;errgroup 管理取消与汇聚
- 明确 goroutine 生命周期:谁负责关闭 channel
- worker 池限制并发上限,防止内存无限增长
# 错误处理
- %w 包装保留可判别类型;errors.Is / errors.As 在边界处理
- 自定义错误类型实现 Unwrap();handler 层映射 HTTP 状态
- 不在库代码 log.Fatal;错误向上传递到入口统一处理
# 可观测性
- slog JSON handler;字段名 trace_id / span_id 与 OTel 对齐
- 业务指标前缀:payments_;直方图 bucket 约定写入 SKILL
- 优雅关闭:signal.Notify SIGTERM → Shutdown(30s ctx) → 关池

返回技能库 更多技能入口