Go-Wind gRPC 服务器从入门到精通

Go-Wind gRPC 服务器从入门到精通

本教程面向初学者,基于 transport/grpc/server/server.go,循序渐进地讲解如何使用 Go-Wind 插件库搭建 gRPC 服务器------从最简单的服务定义开始,逐步深入到拦截器(Interceptor)机制,最终构建一个生产级的 gRPC 微服务。


目录

  • [1. 简介](#1. 简介 "#1-%E7%AE%80%E4%BB%8B")
  • [2. 前置知识:gRPC 基础](#2. 前置知识:gRPC 基础 "#2-%E5%89%8D%E7%BD%AE%E7%9F%A5%E8%AF%86grpc-%E5%9F%BA%E7%A1%80")
  • [3. 核心概念](#3. 核心概念 "#3-%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5")
  • [4. 快速开始:第一个 gRPC 服务](#4. 快速开始:第一个 gRPC 服务 "#4-%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B%E7%AC%AC%E4%B8%80%E4%B8%AA-grpc-%E6%9C%8D%E5%8A%A1")
  • [5. 服务注册](#5. 服务注册 "#5-%E6%9C%8D%E5%8A%A1%E6%B3%A8%E5%86%8C")
  • [6. 拦截器(Interceptor)入门](#6. 拦截器(Interceptor)入门 "#6-%E6%8B%A6%E6%88%AA%E5%99%A8interceptor%E5%85%A5%E9%97%A8")
  • [7. 内置拦截器详解](#7. 内置拦截器详解 "#7-%E5%86%85%E7%BD%AE%E6%8B%A6%E6%88%AA%E5%99%A8%E8%AF%A6%E8%A7%A3")
    • [7.1 recovery ------ 异常恢复](#7.1 recovery —— 异常恢复 "#71-recovery--%E5%BC%82%E5%B8%B8%E6%81%A2%E5%A4%8D")
    • [7.2 logging ------ 访问日志](#7.2 logging —— 访问日志 "#72-logging--%E8%AE%BF%E9%97%AE%E6%97%A5%E5%BF%97")
    • [7.3 requestid ------ 请求追踪](#7.3 requestid —— 请求追踪 "#73-requestid--%E8%AF%B7%E6%B1%82%E8%BF%BD%E8%B8%AA")
    • [7.4 timeout ------ 超时控制](#7.4 timeout —— 超时控制 "#74-timeout--%E8%B6%85%E6%97%B6%E6%8E%A7%E5%88%B6")
    • [7.5 ratelimit ------ 限流](#7.5 ratelimit —— 限流 "#75-ratelimit--%E9%99%90%E6%B5%81")
    • [7.6 authn ------ 身份认证](#7.6 authn —— 身份认证 "#76-authn--%E8%BA%AB%E4%BB%BD%E8%AE%A4%E8%AF%81")
  • [8. 编写自定义拦截器](#8. 编写自定义拦截器 "#8-%E7%BC%96%E5%86%99%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8B%A6%E6%88%AA%E5%99%A8")
  • [9. 拦截器组合:Chain](#9. 拦截器组合:Chain "#9-%E6%8B%A6%E6%88%AA%E5%99%A8%E7%BB%84%E5%90%88chain")
  • [10. 流式 RPC 与 Stream 拦截器](#10. 流式 RPC 与 Stream 拦截器 "#10-%E6%B5%81%E5%BC%8F-rpc-%E4%B8%8E-stream-%E6%8B%A6%E6%88%AA%E5%99%A8")
  • [11. gRPC 客户端](#11. gRPC 客户端 "#11-grpc-%E5%AE%A2%E6%88%B7%E7%AB%AF")
  • [12. 生产级完整示例](#12. 生产级完整示例 "#12-%E7%94%9F%E4%BA%A7%E7%BA%A7%E5%AE%8C%E6%95%B4%E7%A4%BA%E4%BE%8B")
  • [13. 常见问题](#13. 常见问题 "#13-%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98")

1. 简介

transport/grpc/server 是 Go-Wind 插件库提供的 gRPC 服务器封装层 。它在原生 google.golang.org/grpc 的基础上,提供了:

特性 说明
统一抽象 实现 transport.Server 接口,与 HTTP 服务器使用方式一致
拦截器链 内置 Use() / UseStream() 方法,优雅地管理一元和流拦截器
优雅关闭 内置基于 context.ContextGracefulStop 支持
丰富中间件 提供 16 个开箱即用的拦截器(recovery、logging、authn 等)

如果你已经熟悉 gRPC 的基本用法,那么上手会非常快------因为底层的 *grpc.ServerServiceDescUnaryServerInterceptor 等类型全部是原生 gRPC 类型。


2. 前置知识:gRPC 基础

在开始之前,你需要了解 gRPC 的几个核心概念:

2.1 什么是 gRPC?

gRPC 是 Google 开源的高性能 RPC 框架,使用 Protocol Buffers(protobuf)作为接口定义语言和序列化格式。

arduino 复制代码
客户端 (gRPC Client)  ──── HTTP/2 + Protobuf ────►  服务端 (gRPC Server)

2.2 一元 RPC vs 流式 RPC

gRPC 支持四种调用模式:

模式 说明 类比
一元 RPC (Unary) 一次请求,一次响应 类似 HTTP 请求
服务端流 一次请求,多次响应 类似 SSE
客户端流 多次请求,一次响应 类似文件上传
双向流 双方都可多次收发 类似 WebSocket

本教程主要聚焦于 一元 RPC,最后会讲解流式 RPC 的拦截器用法。

2.3 你需要准备的工具

bash 复制代码
# 安装 protoc 编译器
# 参考: https://grpc.io/docs/protoc-installation/

# 安装 Go 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

3. 核心概念

3.1 Server(服务器)

Server 是整个封装的入口。它内部持有:

  • addr:监听地址(如 :9000
  • server:底层的 *grpc.Server(延迟初始化)
  • middlewares:一元拦截器链
  • streamMiddleware:流拦截器链

它实现了 transport.Server 接口,提供 Start / Stop / Endpoint 等方法。

3.2 Interceptor(拦截器)

gRPC 的拦截器相当于 HTTP 的中间件------在 RPC 方法调用前后插入自定义逻辑。Go-Wind 将它们定义为类型别名:

go 复制代码
// 一元拦截器(处理普通 RPC)
type Middleware = grpc.UnaryServerInterceptor

// 流拦截器(处理流式 RPC)
type StreamMiddleware = grpc.StreamServerInterceptor

一元拦截器的函数签名如下:

go 复制代码
func(
    ctx context.Context,       // RPC 上下文
    req any,                   // 请求消息
    info *grpc.UnaryServerInfo, // RPC 元信息(方法名等)
    handler grpc.UnaryHandler, // 下一个拦截器或最终 handler
) (resp any, err error)        // 返回响应和错误

3.3 grpc.Server

Go-Wind 的 Server 封装了原生的 *grpc.Server,你可以通过 srv.Server() 获取它,然后用原生 gRPC 的方式注册服务。


4. 快速开始:第一个 gRPC 服务

4.1 定义 proto 文件

创建 proto/hello.proto

protobuf 复制代码
syntax = "proto3";

package hello;
option go_package = "./proto/hello";

// 请求消息
message HelloRequest {
  string name = 1;
}

// 响应消息
message HelloResponse {
  string message = 1;
}

// 服务定义
service HelloService {
  rpc SayHello(HelloRequest) returns (HelloResponse);
}

4.2 生成 Go 代码

bash 复制代码
protoc --go_out=. --go-grpc_out=. proto/hello.proto

4.3 实现服务并启动

go 复制代码
package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "syscall"

    grpcServer "github.com/tx7do/go-wind-plugins/transport/grpc/server"
    "google.golang.org/grpc"
)

// helloServiceImpl 实现 protobuf 生成的 HelloServiceServer 接口
type helloServiceImpl struct {
    pb.UnimplementedHelloServiceServer // 必须嵌入,提供前向兼容
}

// SayHello 实现 RPC 方法
func (s *helloServiceImpl) SayHello(_ context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
    return &pb.HelloResponse{
       Message: "Hello, " + req.GetName() + "!",
    }, nil
}

func main() {
    // 1. 创建 gRPC 服务器
    srv := grpcServer.NewServer(":9000")

    // 2. 注册服务
    pb.RegisterHelloServiceServer(srv.Server(), &helloServiceImpl{})

    // 3. 监听退出信号,实现优雅关闭
    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer stop()

    fmt.Printf("gRPC server listening on %s\n", srv.Endpoint())
    if err := srv.Start(ctx); err != nil {
       fmt.Fprintf(os.Stderr, "server error: %v\n", err)
       os.Exit(1)
    }
    fmt.Println("server stopped")
}

4.4 代码解读

代码 作用
NewServer(":9000") 创建 gRPC 服务器,监听 9000 端口
pb.UnimplementedHelloServiceServer 必须嵌入,为新增方法提供默认实现(返回 Unimplemented 错误)
srv.Server() 获取底层 *grpc.Server,用于注册服务
pb.RegisterHelloServiceServer(...) protobuf 生成的注册函数,将服务实现注册到 gRPC
srv.Start(ctx) 启动服务器并阻塞,收到信号时优雅关闭
srv.Endpoint() 返回访问地址,如 grpc://localhost:9000

初学者提示 :与 HTTP 不同,gRPC 的 NewServer 不需要指定驱动------因为底层就是 google.golang.org/grpc


5. 服务注册

5.1 两种注册方式

方式一:使用 protobuf 生成的注册函数(推荐)

go 复制代码
// protoc-gen-go-grpc 会自动生成这个函数
pb.RegisterHelloServiceServer(srv.Server(), &helloServiceImpl{})

这是最简单的方式,生成的代码会自动处理 ServiceDesc

方式二:手动注册(了解原理)

go 复制代码
srv.RegisterService(&pb.HelloService_ServiceDesc, &helloServiceImpl{})

5.2 注册多个服务

一个 gRPC 服务器可以承载多个服务:

go 复制代码
srv := grpcServer.NewServer(":9000")

// 注册多个服务到同一个服务器
pb.RegisterUserServiceServer(srv.Server(), &userServiceImpl{})
pb.RegisterOrderServiceServer(srv.Server(), &orderServiceImpl{})
pb.RegisterPaymentServiceServer(srv.Server(), &paymentServiceImpl{})

5.3 Server() 的延迟初始化

底层 *grpc.Server 在首次调用 RegisterService()Start() 时才被创建:

go 复制代码
// ensureServer 的逻辑(简化)
func (s *Server) ensureServer() {
    if s.server != nil {
       return // 已创建,跳过
    }
    var opts []grpc.ServerOption
    if len(s.middlewares) > 0 {
       opts = append(opts, grpc.ChainUnaryInterceptor(s.middlewares...))
    }
    s.server = grpc.NewServer(opts...)
}

这意味着:拦截器必须在注册服务或调用 Start 之前通过 Use() 添加


6. 拦截器(Interceptor)入门

6.1 什么是拦截器?

拦截器是 gRPC 版的"中间件"------采用洋葱模型,RPC 调用依次穿过每一层:

css 复制代码
RPC请求 → [recovery] → [requestid] → [logging] → 业务Handler
                                                      ↓
RPC响应 ← [recovery] ← [requestid] ← [logging] ← 业务Handler

每一层拦截器都可以:

  • 在调用 handler 之前 执行逻辑(如鉴权、限流)
  • 在调用 handler 之后 执行逻辑(如记录耗时)
  • 决定 是否 调用 handler(如鉴权失败时直接返回错误)

6.2 注册拦截器的两种方式

方式一:Use 方法(推荐)

go 复制代码
srv := grpcServer.NewServer(":9000")

// 在注册服务之前调用 Use
srv.Use(
    recovery.UnaryInterceptor(),
    logging.UnaryInterceptor(),
)

// 然后注册服务
pb.RegisterHelloServiceServer(srv.Server(), &helloServiceImpl{})

方式二:WithMiddleware 选项(创建时传入)

go 复制代码
srv := grpcServer.NewServer(":9000",
    grpcServer.WithMiddleware(
       recovery.UnaryInterceptor(),
       logging.UnaryInterceptor(),
    ),
)

两种方式效果相同。Use 更灵活(可在创建后按需添加)。

6.3 拦截器顺序很重要!

拦截器按 注册顺序 执行:先注册的最外层(最先被调用)。

go 复制代码
// 正确顺序示例
srv.Use(
    recovery.UnaryInterceptor(),      // 最外层:捕获所有 panic
    requestid.UnaryServerInterceptor(), // 生成请求 ID
    logging.UnaryInterceptor(),        // 记录日志
    authn.UnaryInterceptor(auth),      // 认证(最内层,贴近业务)
)

经验法则recovery 永远放最外层,认证/限流类放最内层。

6.4 拦截器的工作原理

当你调用 srv.Use(m1, m2) 后,在 ensureServer() 时它们会被组装成 gRPC 的拦截器链:

go 复制代码
// ensureServer 中的关键逻辑
if len(s.middlewares) > 0 {
    opts = append(opts, grpc.ChainUnaryInterceptor(s.middlewares...))
}
s.server = grpc.NewServer(opts...)

grpc.ChainUnaryInterceptor 会按顺序链接拦截器:第一个注册的就是最外层。


7. 内置拦截器详解

Go-Wind 在 transport/grpc/middleware/ 下提供了 16 个开箱即用的拦截器:

拦截器 作用
recovery 捕获 panic,返回 Internal 错误
logging 记录 RPC 调用日志
requestid 生成/传播请求 ID
timeout 超时控制
ratelimit 请求限流
authn 身份认证
authz 权限授权
metrics 指标采集
tracing 链路追踪
metadata 元数据传递
errors 统一错误处理
circuitbreaker 熔断保护
retry 请求重试
validate 参数校验
codec 编解码扩展
crypto 加解密

下面详细介绍最常用的 6 个。


7.1 recovery ------ 异常恢复

作用 :捕获 handler 中的 panic,记录日志并返回 gRPC Internal 错误,防止服务器崩溃。

应该始终放在拦截器链的最外层。

go 复制代码
import grpcRecovery "github.com/tx7do/go-wind-plugins/transport/grpc/middleware/recovery"

// 基础用法
srv.Use(grpcRecovery.UnaryInterceptor())

// 高级用法
srv.Use(grpcRecovery.UnaryInterceptor(
    grpcRecovery.WithStackTrace(true),  // 记录堆栈信息(默认开启)
    grpcRecovery.WithLogger(myLogger),  // 自定义 logger
))

工作流程

go 复制代码
// recovery 拦截器的内部逻辑(简化)
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
    defer func() {
       if rvr := recover(); rvr != nil {
          logger.Error(ctx, "panic recovered", "error", rvr, "method", info.FullMethod)
          err = status.Errorf(codes.Internal, "internal server error")
       }
    }()
    return handler(ctx, req) // 如果这里 panic,会被上面的 defer 捕获
}

与 HTTP 的区别 :gRPC 使用 codes.Internal(状态码 13)而非 HTTP 500,错误通过 gRPC status 返回。


7.2 logging ------ 访问日志

作用:记录每个 RPC 调用的方法名、状态码、耗时。

go 复制代码
import "github.com/tx7do/go-wind-plugins/transport/grpc/middleware/logging"

// 基础用法
srv.Use(logging.UnaryInterceptor())

// 跳过健康检查方法的日志
srv.Use(logging.UnaryInterceptor(
    logging.WithSkipMethods(
       "/grpc.health.v1.Health/Check", // gRPC 健康检查的完整方法名
    ),
    logging.WithLogger(myLogger),
))

日志输出示例

ini 复制代码
grpc unary rpc  method=/hello.HelloService/SayHello code=OK latency_ms=2
grpc unary rpc  method=/hello.HelloService/SayHello code=Internal error="panic: ..." latency_ms=1

智能日志级别

  • 状态码 >= Internal (13)Error 级别
  • 状态码 >= NotFound (5)Warn 级别
  • 其他 → Info 级别

提示FullMethod 的格式是 /<包名>.<服务名>/<方法名>,如 /hello.HelloService/SayHello


7.3 requestid ------ 请求追踪

作用:为每个 RPC 生成唯一 ID,通过 gRPC metadata 传播,便于日志关联和链路追踪。

go 复制代码
import "github.com/tx7do/go-wind-plugins/transport/grpc/middleware/requestid"

// 服务端:提取或生成请求 ID
srv.Use(requestid.UnaryServerInterceptor())

在 handler 中获取请求 ID

go 复制代码
func (s *helloServiceImpl) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
    id := requestid.FromContext(ctx)
    fmt.Printf("handling request: %s\n", id)
    return &pb.HelloResponse{Message: "Hello, " + req.GetName() + "!"}, nil
}

请求 ID 的传播机制

python 复制代码
客户端                        服务端
  │                             │
  │  metadata:                  │  提取 x-request-id
  │  x-request-id: abc123  ──►  │  ↓
  │                             │  注入到 ctx
  │                             │  ↓
  │                             │  handler 通过 FromContext(ctx) 获取
  • 客户端通过 metadata 发送 x-request-id
  • 如果没有,服务端自动生成一个 32 字符的随机 hex ID
  • ID 存入 context,下游 handler 可用 FromContext() 获取

客户端也需要安装对应拦截器才能自动传播(详见第 11 节)。


7.4 timeout ------ 超时控制

作用:为没有设置 deadline 的 RPC 请求添加默认超时,防止慢请求拖垮服务器。

go 复制代码
import (
    "time"
    "github.com/tx7do/go-wind-plugins/transport/grpc/middleware/timeout"
)

// 所有 RPC 默认 30 秒超时
srv.Use(timeout.UnaryServerInterceptor(30 * time.Second))

行为说明

  • 如果客户端已通过 context.WithDeadline 设置了超时,则 尊重客户端的设置
  • 如果没有设置,服务端会添加默认超时
  • 超时后返回 codes.DeadlineExceeded 错误

跳过特定方法(如长时间运行的 RPC):

go 复制代码
srv.Use(timeout.UnaryServerInterceptor(30*time.Second,
    timeout.WithSkipFunc(func(method string) bool {
       return method == "/hello.HelloService/LongRunningTask"
    }),
))

gRPC 超时机制:gRPC 原生支持通过 context 传递 deadline。这个拦截器的作用是为"忘记设置 deadline"的请求兜底。


7.5 ratelimit ------ 限流

作用 :控制 RPC 调用速率,防止服务被压垮。超限时返回 codes.ResourceExhausted

go 复制代码
import (
    "github.com/tx7do/go-wind-plugins/ratelimit/tokenbucket"
    grpcRatelimit "github.com/tx7do/go-wind-plugins/transport/grpc/middleware/ratelimit"
)

// 创建令牌桶限流器:100 QPS,突发容量 200
limiter, _ := tokenbucket.New(100, 200)

// 拒绝模式(默认):超限直接返回错误
srv.Use(grpcRatelimit.UnaryInterceptor(limiter))

// 等待模式:超限后排队等待
srv.Use(grpcRatelimit.UnaryInterceptor(limiter, grpcRatelimit.WithWait()))

跳过健康检查方法

go 复制代码
srv.Use(grpcRatelimit.UnaryInterceptor(limiter,
    grpcRatelimit.WithSkipMethods("/grpc.health.v1.Health/Check"),
))

与 HTTP ratelimit 的区别 :gRPC 使用方法名(info.FullMethod)而非 URL 路径来做跳过判断。


7.6 authn ------ 身份认证

作用:验证 RPC 调用者的身份(如 JWT Token),将认证结果注入 context 供下游使用。

需要配合 security/authn 模块的具体实现(如 JWT):

go 复制代码
import (
    grpcAuthn "github.com/tx7do/go-wind-plugins/transport/grpc/middleware/authn"
    jwtAuthn "github.com/tx7do/go-wind-plugins/security/authn/jwt"
)

// 1. 创建 JWT 认证器
authenticator, _ := jwtAuthn.NewAuthenticator(jwtAuthn.WithKey([]byte("my-secret")))

// 2. 应用认证拦截器
srv.Use(grpcAuthn.UnaryInterceptor(authenticator))

跳过特定方法(如健康检查不需要认证):

go 复制代码
srv.Use(grpcAuthn.UnaryInterceptor(authenticator,
    grpcAuthn.WithSkipMethods("/grpc.health.v1.Health/Check"),
))

在 handler 中获取认证信息

go 复制代码
import engine "github.com/tx7do/go-wind-plugins/security/authn"

func (s *userServiceImpl) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    claims, ok := engine.AuthClaimsFromContext(ctx)
    if !ok {
       return nil, status.Error(codes.Unauthenticated, "no claims")
    }
    // 使用 claims 中的用户信息...
    return &pb.User{Id: req.GetId(), Name: "alice"}, nil
}

行为说明

  • 从 gRPC metadata 中提取 authorization
  • 验证失败返回 codes.Unauthenticated不会调用 handler
  • 验证成功将 claims 注入 context

8. 编写自定义拦截器

8.1 最简单的拦截器

拦截器就是 grpc.UnaryServerInterceptor。下面写一个记录方法名的拦截器:

go 复制代码
// 定义拦截器
func logMethodInterceptor(
    ctx context.Context,
    req any,
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (any, error) {
    fmt.Printf("calling method: %s\n", info.FullMethod)
    return handler(ctx, req) // 调用下一个拦截器或最终 handler
}

// 使用
srv.Use(logMethodInterceptor)

8.2 带配置的拦截器(函数式选项风格)

仿照内置拦截器,用 functional options 模式编写:

go 复制代码
// myValidator 拦截器

type Option func(*options)

type options struct {
    strict bool
}

func WithStrictMode(s bool) Option {
    return func(o *options) { o.strict = s }
}

func UnaryInterceptor(opts ...Option) grpc.UnaryServerInterceptor {
    cfg := &options{strict: false}
    for _, opt := range opts {
       opt(cfg)
    }

    return func(
       ctx context.Context,
       req any,
       info *grpc.UnaryServerInfo,
       handler grpc.UnaryHandler,
    ) (any, error) {
       // 在调用 handler 之前做参数校验
       if validator, ok := req.(interface{ Validate() error }); ok {
          if err := validator.Validate(); err != nil {
             return nil, status.Error(codes.InvalidArgument, err.Error())
          }
       }
       // 调用业务 handler
       return handler(ctx, req)
    }
}

8.3 短路拦截器(不调用 handler)

鉴权失败时直接返回,不执行后续逻辑:

go 复制代码
func requireAPIKey(validKey string) grpc.UnaryServerInterceptor {
    return func(
       ctx context.Context,
       req any,
       info *grpc.UnaryServerInfo,
       handler grpc.UnaryHandler,
    ) (any, error) {
       md, ok := metadata.FromIncomingContext(ctx)
       if !ok {
          return nil, status.Error(codes.Unauthenticated, "no metadata")
       }
       keys := md.Get("x-api-key")
       if len(keys) == 0 || keys[0] != validKey {
          return nil, status.Error(codes.Unauthenticated, "invalid API key")
          // ↑ 直接返回,不调用 handler
       }
       return handler(ctx, req) // 验证通过,继续
    }
}

9. 拦截器组合:Chain

Chain 函数可以将多个拦截器打包成一个,便于复用:

go 复制代码
// 定义一组通用拦截器
commonInterceptors := grpcServer.Chain(
    recovery.UnaryInterceptor(),
    requestid.UnaryServerInterceptor(),
    logging.UnaryInterceptor(),
)

// 应用到服务器
srv.Use(commonInterceptors)

Chain 的实现逻辑(从后往前包裹,和 HTTP 版的 Chain 类似):

go 复制代码
func Chain(middlewares ...Middleware) Middleware {
    switch len(middlewares) {
    case 0:
       return nil
    case 1:
       return middlewares[0]
    }
    return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
       h := handler
       for i := len(middlewares) - 1; i >= 0; i-- {
          interceptor, next := middlewares[i], h
          h = func(ctx context.Context, req any) (any, error) {
             return interceptor(ctx, req, info, next)
          }
       }
       return h(ctx, req)
    }
}

使用场景 :当多个服务器需要共享相同的拦截器集合时,用 Chain 避免重复。


10. 流式 RPC 与 Stream 拦截器

10.1 流式 RPC 简介

在 proto 中定义流式 RPC:

protobuf 复制代码
service ChatService {
  // 服务端流:客户端发一次,服务端返回多次
  rpc Subscribe(SubscribeRequest) returns (stream Event);

  // 双向流:双方都可多次收发
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

10.2 Stream 拦截器

流式 RPC 需要使用 StreamMiddleware(即 grpc.StreamServerInterceptor):

go 复制代码
// 注册流拦截器
srv.UseStream(
    recovery.StreamInterceptor(),
    logging.StreamInterceptor(),
)

或通过选项:

go 复制代码
srv := grpcServer.NewServer(":9000",
    grpcServer.WithStreamMiddleware(
       recovery.StreamInterceptor(),
       logging.StreamInterceptor(),
    ),
)

10.3 Stream 拦截器的特殊之处

Stream 拦截器的签名与一元拦截器不同:

go 复制代码
func(
    srv any,                       // 服务实现
    ss grpc.ServerStream,          // 服务端流(用于收发消息)
    info *grpc.StreamServerInfo,   // 流信息
    handler grpc.StreamHandler,    // 下一个处理器
) error

关键区别

  • 一元拦截器直接拿到 req 和返回 resp
  • Stream 拦截器操作的是 grpc.ServerStream,需要通过它来收发消息
  • Stream 拦截器如果要修改 context,需要包装 ServerStream(因为 context 是只读的)
go 复制代码
// Stream 拦截器修改 context 的通用模式
return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
    // 创建带新值的 context
    ctx := ss.Context()
    newCtx := context.WithValue(ctx, myKey{}, "my-value")

    // 包装 ServerStream 以替换 context
    wrapped := &wrappedServerStream{ServerStream: ss, ctx: newCtx}
    return handler(srv, wrapped)
}

// wrappedServerStream 包装类
type wrappedServerStream struct {
    grpc.ServerStream
    ctx context.Context
}

func (w *wrappedServerStream) Context() context.Context {
    return w.ctx
}

初学者提示:如果你刚开始用 gRPC,可以暂时只关注一元 RPC 和一元拦截器,等需要流式 RPC 时再研究 Stream 拦截器。


11. gRPC 客户端

Go-Wind 也封装了 gRPC 客户端,位于 transport/grpc/client

11.1 基本用法

go 复制代码
import (
    "context"
    "fmt"

    grpcClient "github.com/tx7do/go-wind-plugins/transport/grpc/client"
)

func main() {
    // 创建客户端
    c := grpcClient.NewClient("localhost:9000")

    // 建立连接
    if err := c.Dial(context.Background()); err != nil {
       panic(err)
    }
    defer c.Close()

    // 使用 protobuf 生成的客户端调用服务
    helloClient := pb.NewHelloServiceClient(c.Conn())
    resp, err := helloClient.SayHello(context.Background(), &pb.HelloRequest{Name: "World"})
    if err != nil {
       panic(err)
    }
    fmt.Println(resp.GetMessage())
}

11.2 客户端拦截器

客户端也支持拦截器,常用于 请求追踪超时控制

go 复制代码
import (
    "time"
    grpcClient "github.com/tx7do/go-wind-plugins/transport/grpc/client"
    "github.com/tx7do/go-wind-plugins/transport/grpc/middleware/requestid"
    grpcTimeout "github.com/tx7do/go-wind-plugins/transport/grpc/middleware/timeout"
)

c := grpcClient.NewClient("localhost:9000",
    grpcClient.WithMiddleware(
       requestid.UnaryClientInterceptor(),              // 自动注入请求 ID
       grpcTimeout.UnaryClientInterceptor(10*time.Second), // 客户端超时
    ),
)

11.3 TLS 配置

go 复制代码
import (
    "google.golang.org/grpc/credentials"
)

creds, _ := credentials.NewClientTLSFromFile("cert.pem", "")
c := grpcClient.NewClient("localhost:9000",
    grpcClient.WithTransportCredentials(creds),
)

// 如果不设置任何凭证,默认使用 insecure(明文)
// 也可以显式指定:
c := grpcClient.NewClient("localhost:9000",
    grpcClient.WithInsecure(),
)

11.4 客户端选项一览

选项 说明
WithConn(conn) 注入已建立的连接
WithDialOption(opts...) 透传原生 grpc.DialOption
WithTransportCredentials(creds) 设置 TLS 凭证
WithInsecure() 明文传输
WithMiddleware(mws...) 一元客户端拦截器
WithStreamMiddleware(mws...) 流客户端拦截器

12. 生产级完整示例

下面这个示例整合了前面学到的所有内容,是一个接近生产环境的 gRPC 服务:

go 复制代码
package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/tx7do/go-wind-plugins/ratelimit/tokenbucket"
    grpcAuthn "github.com/tx7do/go-wind-plugins/transport/grpc/middleware/authn"
    "github.com/tx7do/go-wind-plugins/transport/grpc/middleware/logging"
    "github.com/tx7do/go-wind-plugins/transport/grpc/middleware/ratelimit"
    grpcRecovery "github.com/tx7do/go-wind-plugins/transport/grpc/middleware/recovery"
    "github.com/tx7do/go-wind-plugins/transport/grpc/middleware/requestid"
    grpcTimeout "github.com/tx7do/go-wind-plugins/transport/grpc/middleware/timeout"
    grpcServer "github.com/tx7do/go-wind-plugins/transport/grpc/server"
    jwtAuthn "github.com/tx7do/go-wind-plugins/security/authn/jwt"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

// helloServiceImpl 实现服务
type helloServiceImpl struct {
    pb.UnimplementedHelloServiceServer
}

func (s *helloServiceImpl) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
    // 获取请求 ID(来自 requestid 拦截器)
    requestID := requestid.FromContext(ctx)

    // 获取认证信息(来自 authn 拦截器,可选)
    // claims, _ := engine.AuthClaimsFromContext(ctx)

    return &pb.HelloResponse{
       Message: fmt.Sprintf("Hello, %s! (request: %s)", req.GetName(), requestID),
    }, nil
}

func main() {
    // 创建限流器
    limiter, _ := tokenbucket.New(100, 200)

    // 创建 JWT 认证器
    authenticator, _ := jwtAuthn.NewAuthenticator(
       jwtAuthn.WithKey([]byte("my-secret-key")),
    )

    srv := grpcServer.NewServer(":9000",
       grpcServer.WithTimeout(30*time.Second), // 优雅关闭超时
    )

    // 拦截器链(顺序很重要!)
    srv.Use(
       grpcRecovery.UnaryInterceptor(),                     // 1. 最外层:捕获 panic
       requestid.UnaryServerInterceptor(),                  // 2. 生成请求 ID
       logging.UnaryInterceptor(                            // 3. 访问日志
          logging.WithSkipMethods("/grpc.health.v1.Health/Check"),
       ),
       grpcTimeout.UnaryServerInterceptor(30*time.Second),  // 4. 超时控制
       ratelimit.UnaryInterceptor(limiter,                  // 5. 限流
          ratelimit.WithSkipMethods("/grpc.health.v1.Health/Check"),
       ),
       authn.UnaryInterceptor(authenticator,                // 6. 认证(最内层)
          authn.WithSkipMethods("/grpc.health.v1.Health/Check"),
       ),
    )

    // 注册服务
    pb.RegisterHelloServiceServer(srv.Server(), &helloServiceImpl{})

    // 优雅关闭
    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer stop()

    fmt.Printf("gRPC server listening on %s\n", srv.Endpoint())
    if err := srv.Start(ctx); err != nil {
       fmt.Fprintf(os.Stderr, "server error: %v\n", err)
       os.Exit(1)
    }
    fmt.Println("server stopped gracefully")
}

测试(使用 grpcurl):

bash 复制代码
# 健康检查(跳过认证)
grpcurl -plaintext localhost:9000 list

# 调用服务(需要带 token)
grpcurl -plaintext \
  -H "authorization: Bearer <your-jwt-token>" \
  -d '{"name":"World"}' \
  localhost:9000 hello.HelloService/SayHello

13. 常见问题

Q1: 服务方法返回 "Unimplemented" 错误?

原因 :服务实现没有嵌入 UnimplementedXxxServer

解决

go 复制代码
type myServiceImpl struct {
    pb.UnimplementedMyServiceServer // ← 必须嵌入!
}

这是 protobuf 的前向兼容设计------当 proto 文件新增方法但你的代码还没实现时,会返回 Unimplemented 而非编译错误。

Q2: 拦截器没生效?

检查清单

  1. Use() 必须在 RegisterService()Start() 之前调用
  2. 确认拦截器包已正确 import
  3. 检查拦截器顺序是否合理(如 recovery 必须在最外层)
go 复制代码
// ✅ 正确顺序
srv.Use(recovery.UnaryInterceptor(), logging.UnaryInterceptor())
pb.RegisterHelloServiceServer(srv.Server(), &impl{}) // Use 之后

// ❌ 错误顺序
pb.RegisterHelloServiceServer(srv.Server(), &impl{}) // 此时 grpc.Server 已创建
srv.Use(recovery.UnaryInterceptor())                 // 太晚了!不会生效

Q3: 如何获取实际监听端口(用于 :0 随机端口)?

go 复制代码
srv := grpcServer.NewServer(":0")
// 启动后
fmt.Println(srv.Endpoint()) // 输出: grpc://localhost:54321

Q4: 如何只对部分方法跳过认证/限流?

所有内置拦截器都支持 WithSkipMethods 选项:

go 复制代码
srv.Use(authn.UnaryInterceptor(auth,
    authn.WithSkipMethods(
       "/grpc.health.v1.Health/Check",  // 健康检查不需要认证
       "/hello.PublicService/Health",   // 公开接口
    ),
))

方法的完整格式是 /<包名>.<服务名>/<方法名>

Q5: gRPC 和 HTTP 的拦截器/中间件有什么区别?

方面 HTTP 中间件 gRPC 拦截器
类型签名 func(http.Handler) http.Handler grpc.UnaryServerInterceptor
请求数据 *http.Request any(protobuf 消息)
响应方式 http.ResponseWriter 返回值 (resp, err)
错误处理 HTTP 状态码 gRPC status codes
路由区分 URL 路径 info.FullMethod
流支持 StreamServerInterceptor

Q6: 客户端报 "connection refused"?

排查步骤

  1. 确认服务端已启动且监听正确端口
  2. 确认地址格式正确:localhost:9000(不是 grpc://localhost:9000
  3. 确认 TLS 设置匹配(服务端用 TLS,客户端也要用 TLS)
  4. 如果用 :0 随机端口,通过 srv.Endpoint() 获取实际端口

附录:API 速查表

Server 方法

方法 说明
NewServer(addr, opts...) 创建服务器
Use(mws...) 注册一元拦截器
UseStream(mws...) 注册流拦截器
Server() 获取底层 *grpc.Server
RegisterService(desc, impl) 注册服务
Start(ctx) 启动服务器(阻塞)
Stop(ctx) 停止服务器(优雅关闭)
Endpoint() 获取访问地址
Addr() 获取监听地址

Option 选项

选项 说明
WithServer(srv) 直接设置底层 *grpc.Server
WithMiddleware(mws...) 创建时设置一元拦截器
WithStreamMiddleware(mws...) 创建时设置流拦截器
WithTimeout(d) 设置优雅关闭超时

顶层函数

函数 说明
Chain(mws...) 将多个拦截器组合为一个
FormatEndpoint(host, port) 格式化 gRPC endpoint 字符串

常用 gRPC 状态码

状态码 含义
OK 0 成功
NotFound 5 未找到
InvalidArgument 3 参数错误
Unauthenticated 16 未认证
PermissionDenied 7 无权限
ResourceExhausted 8 资源耗尽(限流)
DeadlineExceeded 4 超时
Internal 13 内部错误

附录:与 HTTP 教程的对照

如果你已经阅读过 HTTP 服务器教程,下面的对照表可以帮助你快速迁移知识:

概念 HTTP gRPC
服务器创建 httpServer.NewServer(addr, WithDriver(...)) grpcServer.NewServer(addr)
中间件类型 func(http.Handler) http.Handler grpc.UnaryServerInterceptor
注册中间件 srv.Use(mws...) srv.Use(mws...)
注册路由/服务 srv.GET(path, handler) pb.RegisterXxxServer(srv.Server(), impl)
启动 srv.Start(ctx) srv.Start(ctx)
错误返回 HTTP 状态码(http.Error gRPC status(status.Error
请求标识 URL 路径 info.FullMethod
上下文传递 r.WithContext(ctx) handler(ctx, req)

下一步学习 :尝试运行 _examples/grpc-basic 目录下的示例代码,动手实践是掌握这些概念的最佳方式!

相关推荐
hunterandroid1 小时前
Hilt 依赖注入:从手动 new 到自动装配
后端
喵个咪1 小时前
Go-Wind GraphQL 服务器从入门到精通
后端·graphql
青青子衿悠悠我心1 小时前
Docker与Kubernetes的十年战争与融合
后端
AI小老六1 小时前
SkillOpt 架构拆解:把 Skill 文本当参数,用执行轨迹训练 Agent
后端·算法·ai编程
云技纵横1 小时前
@Transactional 到底要不要加 rollbackFor?一次数据不一致事故讲清楚
后端·面试
Csvn1 小时前
日志分析进阶 — Logwatch 与 GoAccess 实战
后端
Moment1 小时前
牛逼,NextJs 从 16.3 开始全面拥抱 Agent Native 🥰🥰🥰
前端·后端·面试
Csvn1 小时前
CI/CD 入门 — 用 GitLab CI 构建自动化部署流水线
后端
沸点小助手2 小时前
6月沸点活动获奖名单公示|本周互动话题上新🎊
前端·后端