Files
cs-note/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式.md
T
2026-05-24 11:42:38 +08:00

288 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [gRPC, Go, Server, Best Practice]
create time: 2026-05-15 10:00
---
# Unimplemented 零-Stub 模式
## 概述
`UnimplementedXxxServer` 是 `protoc-gen-go` 为每个 gRPC service 自动生成的"空实现"结构体。通过将其嵌入自定义 Service 结构体,开发者只需编写实际需要的方法,未覆写的方法自动返回 `codes.Unimplemented` 错误——这是一种兼顾开发便利性与接口契约安全的 Go 语言惯用模式。
> [!question] 如果没有这个机制会怎样?
> 假设你实现了三个 RPC method,后来 proto 新增了一个。**编译器不会报错**——你的新方法永远不会被调用,直到线上某个 client 发出请求后才暴露出 Bug。这就是"静默失败"陷阱。
核心思想:**把运行时 Bug 提前到编译期发现**。
## Protobuf 生成的代码结构
当你定义一个 proto service:
```proto
service UserService {
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc GetUser(GetUserRequest) returns (GetUserResponse);
}
```
protoc 生成三类关键代码:
| 生成物 | 作用 |
|--------|------|
| `UserServiceServer` interface | 你必须满足的方法集合 |
| `UnimplementedUserServiceServer` struct | 所有方法都返回 `codes.Unimplemented` 错误的默认实现 |
| `RegisterUserServiceServer()` 函数 | 将你的实现注册到 server 分发表 |
重点看 `UserServiceServer` interface 的定义:
```go
type UserServiceServer interface {
CreateUser(context.Context, *CreateUserRequest) (*CreateUserResponse, error)
GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)
mustEmbedUnimplementedUserServiceServer() // ← 封闭性守卫
}
```
`mustEmbedUnimplementedUserServiceServer()` 是一个无参空方法。它让 interface 成为**封闭类型**——任何想满足此接口的类型都必须显式嵌入 `UnimplementedUserServiceServer`,否则编译期直接拦截。这是 Go 社区著名的"封闭接口"(unexported method trick)设计手法,在 protobuf 场景下是有意为之。
> [!note] 为什么叫 "closed set of methods"?
> Go 的 interface 是 implicit(隐式实现),正常情况下你可以随时"默默"实现一个新接口。但 protobuf 通过注入一个只能由生成的 struct 提供的方法,把这个隐式契约变成了**显式嵌入**——你必须主动写下一行 `pb.UnimplementedUserServiceServer`,这种"被迫直面"就是安全感的来源。
## 嵌入用法
嵌入后只需实现需要的业务方法:
```go
type userService struct {
pb.UnimplementedUserServiceServer // 自动获得默认 Unimplemented 响应
}
func (s *userService) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
// 只写实际需要的业务逻辑
if err := s.store.Create(ctx, req); err != nil {
return nil, status.Errorf(codes.Internal, "create user failed: %v", err)
}
return &pb.CreateUserResponse{Id: generatedId}, nil
}
// GetUser 不写 — UnimplementedUserServiceServer 自动返回:
// status.Error(codes.Unimplemented, "method GetUser not implemented")
```
### Proto 新增 Method → 编译期告警
这是该模式最大的价值。**先回答一个疑惑**:proto 新增了 `DeleteUser`,重新运行 protoc 后 `UnimplementedUserServiceServer` 自身也会多出对应的默认实现,你的嵌入结构体照样能编译通过——那为什么说有风险前置?
原因在于两个实际场景:
**场景 A:protoc 重新生成后忘记更新依赖**
如果你在同一份代码库里只改了 proto 文件,却忘了对所有 micro-service 跑 protoc,那些没更新的 service 就会报编译错误:
```
cannot use &userService{} as UserServiceServer:
missing method DeleteUser in receiver type userService
```
CI 的编译检查会直接拦住——**某个服务漏跑 protoc**,而不是等到线上被调用的时候才发现这个方法从未被实现。
**场景 B:忘记嵌入 → 退化回普通接口**
如果不 embed `UnimplementedXxxServer`,proto 新增 method 时编译器照样不拦你。新方法永远不会被调用,直到线上某个 client 触发了它——这就退化成了"没这个机制之前"的样子。这也是为什么"正确嵌入"是这套模式生效的前提。
---
> [!question] 那"强制接口契约意识"怎么理解?
>
> 正确用法下,Proto 新增 method 后,`UnimplementedXxxServer` 也会跟着长出新的兜底实现,编译是**通过的**。但这正是设计者想要的效果:
> - **不强制你立刻实现**(不影响正常请求)
> - **但强制你在 protoc 重新生成的那一刻"看到"变更**
> - 从此时起你有两次决策机会:① 跑 protoc 时意识到契约变了;② 决定是否要把 `codes.Unimplemented` 替换成真实逻辑
> - 对比非类型安全语言(Java 等),新方法可以永远不被实现且永远不被发现——Go 版本至少让每次 proto 变更都变得**可追踪**
## 嵌入策略对比
不同语言的 gRPC 框架提供了类似的保底机制,但 Go 的嵌入方式有其独特优势:
| 维度 | Go(嵌入) | Java / C++(继承基类) | Python(抽象基类) |
|------|-----------|----------------------|-------------------|
| **语法** | `struct` 嵌入 | 继承 `ServiceImplBase` | 继承 `Servicer` + `@abc.abstractmethod` |
| **单继承限制** | 无(可嵌多个类型) | 有 | 有 |
| **方法优先级控制** | 嵌入顺序决定 | 虚函数重写 | MRO 决定 |
| **扩展性** | 可同时混入 store、logger 等依赖 | 只能通过构造函数传递 | 同上 |
Go 版本的优势在于**嵌入比继承更灵活**——可以同时嵌入多个类型、控制方法优先级(离当前结构体越近优先匹配),并且没有单继承限制。
## Streaming RPC 下的行为
上面的例子都是 unary RPC。对于 streaming RPC,behavior 完全一致:
```go
type MessageService struct {
pb.UnimplementedMessageServiceServer
}
// 只实现 stream-send 的场景
func (s *MessageService) StreamMessages(req *StreamRequest, ss grpc.ServerStream) error {
for i := 0; i < req.Count; i++ {
if err := ss.SendMsg(&StreamResponse{Data: fmt.Sprintf("msg-%d", i)}); err != nil {
return status.Errorf(codes.Internal, "send failed: %v", err)
}
}
return nil
}
// BidirectionalStreaming 不写 → 自动返回 codes.Unimplemented
```
关键点:
- UnimplementedStub 对 unary、server-streaming、client-streaming、bidirectional-streaming **统一处理**
- Streaming handler 的错误返回值仍然是 `error`,未覆写时同样走 `codes.Unimplemented`
- 如果你在 CI 中有编译检查,streaming 方法的遗漏也能被捕获
## 执行流程图解
```mermaid
flowchart TD
Client["Client RPC Call"] --> Lookup["Server dispatch table"]
Lookup --> Found{"Method 是否被覆写?"}
Found -->|"是"| Handler["Handler 执行业务逻辑"]
Found -->|"否"| Fallback["UnimplementedStub<br/>codes.Unimplemented"]
Handler --> Resp["Response to Client"]
Fallback --> Resp
ProtoChanged["Proto 新增 Method"] --> GenCode["Protoc 重新生成"]
GenCode --> AllUpdated["所有 service 同步更新<br/>Embed 自动继承新方法,编译通过"]
GenCode --> PartialUpdate["部分 service 漏跑 protoc<br/>CI 拦截 ✅"]
style Client fill:#E3F2FD,stroke:#1976D2
style Handler fill:#A8E6CF,stroke:#2E7D32
style Fallback fill:#FFB3BA,stroke:#C62828
style AllUpdated fill:#FFF3E0,color:#000,stroke:#E65100
style PartialUpdate fill:#FFF3E0,color:#000,stroke:#E65100
```
## 实际使用中的注意事项
> [!warning] 不要手动实现 Unimplemented 方法
>
> 如果在自己的结构体中也定义了同名 method(比如不小心写了 `MustEmbedUnimplementedUserServiceServer()`),它会覆盖嵌入类型的空方法。虽然功能上不影响(interface 检查只看是否存在),但语义混乱,应避免。
> [!warning] embed vs pointer embed 的选择
>
> gRPC 生成的 `UnimplementedXxxServer` **值类型和指针类型都可以嵌入**,但效果不同:
>
> | 嵌入方式 | `*userService` 是否满足接口 | 说明 |
> |---------|---------------------------|------|
> | `pb.UnimplementedXxxServer`(值) | `*userService` ✅ | 推荐,指针接收者可调用值方法 |
> | `*pb.UnimplementedXxxServer`(指针) | `*userService` ✅ 且 `userService` ❌ | 仅指针满足接口 |
>
> **最佳实践**: 大多数 gRPC server 方法签名是 `func (s *Service) Method(...)`,所以嵌入值类型的 `Unimplemented` 即可同时支持两种调用方式。
> [!danger] 嵌入顺序决定方法优先级
>
> Go 的结构体嵌入具有**层级可达性**——当多个嵌入类型都有同名方法时,编译器选择距离当前结构体最近的那个。如果不小心嵌入了错误的类型,你的实现可能永远不会被调用:
>
> ```go
> type userService struct {
> pb.UnimplementedUserServiceServer
> someOtherEmbeddedStruct // ← 如果有同名方法,这里优先!
> }
> ```
>
> 养成**将 Unimplemented 嵌入放在最后**的习惯:
>
> ```go
> type userService struct {
> *UserStore // 业务依赖先放前面
> logger // 工具类
> pb.UnimplementedUserServiceServer // 永远放最后
> }
> ```
## Server 集成示例
下面展示 UnimplementedStub 在整个服务链路中的位置(核心逻辑已简化):
```go
package server
import (
"context"
"fmt"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pb "your/proto/package"
)
// UserServiceImpl — 只需实现实际需要的方法
type UserServiceImpl struct {
store *UserStore // 业务依赖
pb.UnimplementedUserServiceServer // 永远放最后
}
func (s *UserServiceImpl) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
user := &model.User{Name: req.GetName(), Email: req.GetEmail()}
if err := s.store.Create(ctx, user); err != nil {
return nil, status.Errorf(codes.Internal, "create failed: %v", err)
}
return &pb.CreateUserResponse{Id: fmt.Sprintf("%d", user.ID)}, nil
}
// GetUser 不写 — 自动返回 codes.Unimplemented
// DeleteUser 不写 — 同上
func NewUserServiceImpl(store *UserStore) *UserServiceImpl {
return &UserServiceImpl{store: store}
}
// Register 注册所有服务到 grpc.Server
func Register(srv *grpc.Server, store *UserStore) {
pb.RegisterUserServiceServer(srv, NewUserServiceImpl(store))
log.Println("UserService registered")
}
```
关键要点:
- `RegisterUserServiceServer()` 内部会校验传入的类型是否实现了 `UserServiceServer` interface
- 由于 `UnimplementedUserServiceServer` 提供了所有方法的默认实现,你只需编写实际需要的 method
- Proto 新增 method → protoc 重新生成 → **变更暴露** → 有意识的决策点(是否把 `codes.Unimplemented` 替换成真实逻辑)
## 常见排错
> [!bug] 忘记 embed → "missing method xxx" 编译错误
>
> 如果你看到类似下面的错误,首先检查是否忘写了嵌入行:
>
> ```
> cannot use &userService{} as UserServiceServer:
> missing method CreateUser in receiver type userService
> ```
>
> 确认嵌入 `pb.UnimplementedXxxServer` 后即可解决。
> [!bug] embed 的是指针而不是值 → `userService` 类型不满足接口
>
> ```go
> type userService struct {
> *pb.UnimplementedUserServiceServer // 指针嵌入
> }
> var _ UserServiceServer = &userService{} // ✅
> var _ UserServiceServer = userService{} // ❌ compile error
> ```
>
> 如果你的某些代码以值类型传 service,建议改用值嵌入。
> [!bug] 自定义方法与嵌入类型冲突
>
> 如果你在结构体上手动定义了 `mustEmbedUnimplementedUserServiceServer()` 或其他与嵌入类型同名的方法,它会在方法解析时**遮蔽**嵌入类型的方法——虽然通常不会影响最终行为,但会造成混淆和潜在 bug。
## 关联笔记
- [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册]]
- [[hhs/gRPC/3. 服务端实现/09-Streaming Handler]]