Go 基础体系 · 第 51/113 篇。示例统一基于 Go 1.26.4;核心片段可能省略 package 与 import,完整程序可直接按文中结构运行。

Go Kratos 实战:分层、HTTP/gRPC 双协议与可观测治理

本文基于 Go 1.26.4 与 Kratos v2 稳定 API 编写,验证基线固定为 github.com/go-kratos/kratos/v2 v2.9.1。Kratos 是面向微服务的应用框架:App 统一管理进程生命周期,transport 暴露 HTTP/gRPC,middleware 承担横切治理,config 与 registry 对接基础设施。它不会替团队决定领域边界、事务或幂等策略,也不要求每个项目机械复制模板。

理解 Kratos 的关键是依赖方向。协议层把请求转换成业务命令,业务层只表达规则,数据层实现仓储;数据库、HTTP、gRPC 和注册中心都留在边界。这样同一 usecase 才能被 HTTP、gRPC、消息消费和测试复用。

1. 组件地图与适用边界

典型服务由 cmdinternal/serviceinternal/bizinternal/data 和生成的 api 组成。目录只是表达依赖关系的手段,不是目标:很小的 CRUD 服务可以减少层数,但不能让领域包反向导入 transport 或 ORM。

HTTP/gRPC client
  -> transport codec -> transport middleware -> generated service adapter
  -> service: DTO 与领域命令转换
  -> biz/usecase: 校验、授权后的业务规则、事务意图
  -> repository interface
  -> data: SQL/cache/remote client

Kratos 适合需要双协议、统一错误、配置发现和遥测约定的团队。单体内部函数调用不需要 RPC;只有部署、容量、权限或故障隔离确实独立时才拆服务,否则网络失败模式会大于收益。

2. Proto 契约与代码生成

Kratos 常用 Protobuf 同时生成 Go 消息、gRPC 与 HTTP 适配代码。go_package、package 和字段编号发布后都是契约;删除字段应 reserved,新增字段必须允许旧端忽略,枚举零值应为 UNSPECIFIED

syntax = "proto3";
package article.v1;
option go_package = "example.com/article/api/article/v1;v1";

import "google/api/annotations.proto";

service ArticleService {
  rpc GetArticle(GetArticleRequest) returns (Article) {
    option (google.api.http) = {get: "/v1/articles/{id}"};
  }
}
message GetArticleRequest { string id = 1; }
message Article { string id = 1; string title = 2; int64 version = 3; }
go install github.com/go-kratos/kratos/cmd/kratos/v2@v2.9.1
kratos proto client api/article/v1/article.proto
kratos proto server api/article/v1/article.proto -t internal/service
go generate ./...
go test ./...

生成文件不放业务规则,也不手工修改。工具、protoc、protoc-gen-go、Kratos 插件和模板都应固定版本;CI 重新生成并检查差异,同时做 proto breaking 检查。

3. 分层与依赖倒置

biz 定义实体、usecase 和调用方真正需要的仓储接口;data 实现接口。接口应靠近消费者,不能为了 mock 预先建立覆盖所有数据库操作的大接口。service 只负责协议转换和错误映射。

type ArticleRepo interface {
	Find(context.Context, string) (Article, error)
}

type ArticleUsecase struct {
	repo ArticleRepo
}

func NewArticleUsecase(repo ArticleRepo) *ArticleUsecase {
	return &ArticleUsecase{repo: repo}
}

func (uc *ArticleUsecase) Get(ctx context.Context, id string) (Article, error) {
	if id == "" {
		return Article{}, ErrInvalidID
	}
	article, err := uc.repo.Find(ctx, id)
	if err != nil {
		return Article{}, fmt.Errorf("find article %q: %w", id, err)
	}
	return article, nil
}

usecase 不导入 Kratos transport、SQL driver 或注册中心类型。事务边界由业务动作决定,可在仓储中提供语义化方法或 transaction runner;不要让 service 逐条操作多个 repo 后假装原子。

4. HTTP 请求生命周期

客户端连接到 Kratos HTTP server 后,路由匹配生成的 path;codec 解析 path/query/body;middleware 依次运行;生成 adapter 调用 service;service 转换 DTO 并调用 usecase;返回值编码为 JSON,错误由统一 error encoder 映射。

请求的 context.Context 必须贯穿数据库和下游调用。body 大小、header 数量、路由参数和批量元素数应在进入业务前受限。响应一旦发送首字节就不能可靠改变状态码,因此流式输出应先完成认证、校验和关键依赖探测。

HTTP 是外部语义边界:正确区分 400、401、403、404、409、422、429、503、504;不要把内部 SQL 文本或地址返回给调用方。同一 proto 的 HTTP 映射也不意味着公网 API 可无版本治理。

5. gRPC 调用生命周期

生成 client 在长期复用的 grpc.ClientConn 上发起 RPC,resolver 产生端点,balancer 选择 SubConn,客户端 middleware 注入认证和 trace,Protobuf 编码后经 HTTP/2 stream 发送。服务端解码、执行 middleware 和 service,最终 status 与响应返回;单次 RPC 完成不会关闭底层连接。

conn, err := grpc.DialInsecure(
	ctx,
	grpc.WithEndpoint("discovery:///article-service"),
	grpc.WithDiscovery(discovery),
	grpc.WithTimeout(2*time.Second),
)
if err != nil {
	return fmt.Errorf("dial article service: %w", err)
}
defer conn.Close()
client := v1.NewArticleServiceClient(conn)

客户端对象与连接应长期复用且按文档并发安全;每请求拨号会重复 DNS、TCP/TLS 与 HTTP/2 建连,并破坏负载均衡。创建 client 成功不等于服务已就绪,需要有界健康调用或 readiness 策略。

6. HTTP 与 gRPC 双协议装配

同一 service 实现可以注册到两个 server。生成的 RegisterArticleHTTPServerRegisterArticleServer 负责协议适配,业务只实现生成接口。server 的监听、middleware、codec、TLS 和超时分别配置,不应因为共用实现就假设两个协议完全等价。

httpServer := http.NewServer(
	http.Address(":8000"),
	http.Timeout(3*time.Second),
	http.Middleware(recovery.Recovery(), tracing.Server(), logging.Server(logger)),
)
grpcServer := grpc.NewServer(
	grpc.Address(":9000"),
	grpc.Timeout(2*time.Second),
	grpc.Middleware(recovery.Recovery(), tracing.Server(), logging.Server(logger)),
)
v1.RegisterArticleHTTPServer(httpServer, service)
v1.RegisterArticleServer(grpcServer, service)

HTTP timeout 包含解码、业务和编码预算,gRPC deadline 还可能由调用方传入更短值。配置一致不代表预算应相同:入口要为后续序列化和回传留余量。

7. Middleware 顺序和关键 API

middleware 形态是 func(Handler) Handler,外层可观察内层最终结果。常见顺序为 recovery、trace/request ID、访问日志、指标、限流、认证;资源级授权仍应在 usecase 根据目标资源判断。认证 middleware 只能放精简主体到 context,不把完整 token 向下游传播。

func Budget(limit time.Duration) middleware.Middleware {
	return func(next middleware.Handler) middleware.Handler {
		return func(ctx context.Context, req any) (any, error) {
			opCtx, cancel := context.WithTimeout(ctx, limit)
			defer cancel()
			return next(opCtx, req)
		}
	}
}

middleware 不能无限读取 body、记录秘密或吞掉取消错误。panic recovery 是最后防线,不是普通错误处理;记录一次错误后不要再向上返回让每层重复记录。

8. 错误模型与跨协议映射

领域层定义可用 errors.Is/As 判断的错误,边界用 errors.FromError 或生成的 reason 将它们映射为 Kratos error。稳定字段包括 HTTP code、reason、对外 message 和必要 metadata;内部根因只进入受控日志。

switch {
case errors.Is(err, biz.ErrArticleNotFound):
	return nil, kratoserrors.NotFound("ARTICLE_NOT_FOUND", "article not found")
case errors.Is(err, context.DeadlineExceeded):
	return nil, kratoserrors.New(504, "DEPENDENCY_TIMEOUT", "dependency timed out")
case errors.Is(err, context.Canceled):
	return nil, kratoserrors.New(499, "CANCELED", "request canceled")
default:
	return nil, kratoserrors.InternalServer("INTERNAL", "internal error")
}

业务校验失败不应计入基础设施熔断。调用方按 reason/code 决策,不能解析 message。超时响应也不证明写入未提交,写接口必须使用幂等键、业务唯一约束或结果查询协议。

9. 配置、注册发现和应用生命周期

Kratos config.Config 聚合 file、env 或远程 source;加载后 scan 到强类型结构并校验,再构造依赖。业务路径不要反复 Value。热更新只发布完整合法快照,监听地址、数据目录等拓扑字段通常要求重启。

registry.Registrar 在 App 启动时注册实例,registry.Discovery 为 client resolver 提供地址。发现只回答“地址在哪里”,不替代连接健康、负载均衡、超时和容量保护。注册信息必须包含稳定 service name、instance ID、端点、版本与少量可筛选 metadata。

app := kratos.New(
	kratos.ID(instanceID),
	kratos.Name("article-service"),
	kratos.Version(version),
	kratos.Metadata(map[string]string{"region": region}),
	kratos.Server(grpcServer, httpServer),
	kratos.Registrar(registrar),
)

启动失败应关闭已构造资源。退出时先停止注册和接收新流量,再等待在途调用,最后关闭 data client 和遥测 exporter。TTL 传播和负载均衡缓存意味着“注销成功”并非所有消费者立即停止调用。

10. 超时、取消、重试和幂等

总 deadline 从入口向下传播,子操作只能缩短,不能用 context.Background() 重置。数据库、HTTP 和 RPC 都接收同一请求 context;必须超出请求生命周期的动作写入持久队列,由应用级 context 管理。

重试只针对明确瞬时错误、幂等操作,并共享总预算。指数退避需要抖动、最大次数与最小剩余时间。网关、Kratos client、sidecar 若层层重试会乘法放大流量。Unavailable 说明结果不可得,不说明服务端没有执行。

写操作用调用方生成的幂等键,在同一事务中保存键、请求摘要和最终结果;相同键不同请求应拒绝。客户端取消后服务端仍可能处于不可中断的提交阶段,应监控取消后耗时并缩短不可取消区间。

11. 并发安全、连接和后台任务

service/usecase 通常被多个 goroutine 并发调用。不可保护普通 map,缓存快照用 RWMutexatomic.Pointer,发布前复制 map/slice,避免调用方修改内部状态。锁内不做 RPC 和磁盘 I/O;否则一个慢依赖会串行阻塞所有请求。

所有后台 goroutine 必须有停止信号和等待路径。Watch、刷新和批处理循环监听应用 context,使用 ticker 时 defer ticker.Stop(),退出时由 owner 等待。不要在 init 启动 goroutine,也不要把请求 DTO 指针交给异步任务。

连接池上限、空闲数和生命周期要与实例并发、数据库容量和负载均衡共同计算。无限 goroutine 加有限连接池只会把超时转移到池等待。

12. 可运行的 App 生命周期示例

下面程序实现一个真实 transport.Server,交给 Kratos App 管理。它展示 Start/Stop、取消、错误返回和等待后台 goroutine的完整边界;实际项目把它替换为 Kratos HTTP/gRPC server。验证模块固定 v2.9.1。

package main

import (
	"context"
	"fmt"
	"os"
	"sync"

	"github.com/go-kratos/kratos/v2"
)

type workerServer struct {
	mu     sync.Mutex
	cancel context.CancelFunc
	done   chan struct{}
}

func newWorkerServer() *workerServer {
	return &workerServer{done: make(chan struct{})}
}

func (s *workerServer) Start(ctx context.Context) error {
	workerCtx, cancel := context.WithCancel(ctx)
	s.mu.Lock()
	s.cancel = cancel
	s.mu.Unlock()
	go func() {
		defer close(s.done)
		<-workerCtx.Done()
	}()
	return nil
}

func (s *workerServer) Stop(context.Context) error {
	s.mu.Lock()
	cancel := s.cancel
	s.mu.Unlock()
	if cancel != nil {
		cancel()
	}
	<-s.done
	return nil
}

func buildApp(server *workerServer) *kratos.App {
	return kratos.New(
		kratos.Name("lifecycle-example"),
		kratos.Version("1.0.0"),
		kratos.Server(server),
	)
}

func main() {
	app := buildApp(newWorkerServer())
	fmt.Println("run lifecycle-example; send SIGTERM to stop")
	if err := app.Run(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

示例只在 main 的最终出口打印错误并退出,确保退出只发生一次。库和 handler 不调用 paniclog.Fatalos.Exit

13. 测试与诊断

usecase 用内存 fake 测成功、NotFound、冲突、deadline 和 cancel;transport 用生成 client 发真实 HTTP/gRPC 请求,验证 codec、middleware、error reason 与 header。生命周期测试并发运行 App,调用 Stop 后等待退出,并使用 goleak 或等价断言检查后台协程。

go test ./...
go test -race ./...
go test -run TestArticleService -count=100 ./...
go vet ./...
curl -fsS http://127.0.0.1:8000/healthz
grpcurl -plaintext 127.0.0.1:9000 grpc.health.v1.Health/Check

诊断一次慢请求要关联 trace ID、transport method、Kratos reason、剩余 deadline、重试次数、实例 ID、下游端点和池等待时间。日志字段有界且脱敏;指标关注请求量、错误、延迟、饱和度、拒绝、连接和注册状态,不能把 user ID 放进 label。

14. 安全、部署和生产清单

生产跨主机使用 TLS,敏感内部环境使用 mTLS 并验证服务身份。认证只证明主体,授权按方法和具体资源判断。限制 body、消息、metadata、并发 stream 和解压后大小;日志不得记录 token、cookie、完整请求体和数据库秘密。管理、健康和调试端点使用独立监听或网络策略。

容器以非 root、只读根文件系统运行,配置与密钥只读挂载;readiness 表示是否愿意接流量,liveness 只判断进程能否恢复,不能把全部非关键下游变成硬失败。发布先令实例 not-ready 并注销,等待发现传播,再有界停止 server;超时后才强制退出。长 stream 要有最大年龄和重连游标。

上线前固定 Go、Kratos、生成器和 proto 工具版本,执行契约、竞态、故障注入与容量测试。升级 Kratos 先阅读 changelog,在预发布环境验证 middleware 顺序、错误编码、resolver、优雅关闭和旧客户端兼容。框架提供统一扩展点,但越多自定义 codec、Suite 和内部 wrapper,升级成本越高;只封装项目确实需要稳定的边界。


系列导航与关联阅读

官方资料

本文依据 Go 官方规范、标准库文档和 Go 官方博客重新梳理;正文与示例由 WR BLOG 编写。