vault backup: 2026-05-15 16:26:14
This commit is contained in:
@@ -13,8 +13,8 @@ create time: 2026-05-11 16:00
|
||||
|
||||
### 入门
|
||||
|
||||
| # | 笔记 | 读它如果... |
|
||||
|---|------|------------|
|
||||
| # | 笔记 | 读它如果... |
|
||||
| ----------------------------------------------------------------- | ------------------------------ | ---------------------- |
|
||||
| [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/01-Hello World 最小可运行 Server]] | **第一课**:三段式模板,从零搭起一个能跑的 server | 你是 gRPC 新手,想先看到"能跑的代码" |
|
||||
|
||||
### 核心概念
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
---
|
||||
tags: [gRPC, Go, Server, Best Practice]
|
||||
create time: 2026-05-13 14:30
|
||||
create time: 2026-05-15 10:00
|
||||
---
|
||||
|
||||
# Unimplemented 零-Stub 模式
|
||||
|
||||
## 概述
|
||||
|
||||
`UnimplementedXxxServer` 是 protoc-gen-go 为每个 gRPC service 自动生成的"空实现"结构体。通过嵌入它到自定义的 Service 结构体中,开发者只需编写实际需要的方法,其余方法自动获得 `codes.Unimplemented` 默认响应——这是一种兼顾开发便利性与接口契约安全的 Go 语言惯用模式。
|
||||
`UnimplementedXxxServer` 是 `protoc-gen-go` 为每个 gRPC service 自动生成的"空实现"结构体。通过将其嵌入自定义 Service 结构体,开发者只需编写实际需要的方法,未覆写的方法自动返回 `codes.Unimplemented` 错误——这是一种兼顾开发便利性与接口契约安全的 Go 语言惯用模式。
|
||||
|
||||
> [!question] 如果没有这个机制会怎样?
|
||||
> 想象你实现了三个 RPC method,后来 proto 新增了一个,编译器不报错——你的新方法永远不会被调用,直到线上某个 client 发出请求后才暴露出 Bug。这就是"静默失败"陷阱。
|
||||
> 假设你实现了三个 RPC method,后来 proto 新增了一个。**编译器不会报错**——你的新方法永远不会被调用,直到线上某个 client 发出请求后才暴露出 Bug。这就是"静默失败"陷阱。
|
||||
|
||||
核心思想:**把运行时 Bug 提前到编译期发现**。
|
||||
|
||||
## Protobuf 生成的代码结构
|
||||
|
||||
当你写一个 proto service:
|
||||
当你定义一个 proto service:
|
||||
|
||||
```proto
|
||||
service UserService {
|
||||
@@ -23,12 +25,12 @@ service UserService {
|
||||
}
|
||||
```
|
||||
|
||||
protoc 会生成三类关键代码:
|
||||
protoc 生成三类关键代码:
|
||||
|
||||
| 生成物 | 作用 |
|
||||
|--------|------|
|
||||
| `UserServiceServer` interface | 你必须实现的方法集合 |
|
||||
| `UnimplementedUserServiceServer` struct | 所有方法都返回 `Unimplemented` 错误的默认实现 |
|
||||
| `UserServiceServer` interface | 你必须满足的方法集合 |
|
||||
| `UnimplementedUserServiceServer` struct | 所有方法都返回 `codes.Unimplemented` 错误的默认实现 |
|
||||
| `RegisterUserServiceServer()` 函数 | 将你的实现注册到 server 分发表 |
|
||||
|
||||
重点看 `UserServiceServer` interface 的定义:
|
||||
@@ -41,13 +43,14 @@ type UserServiceServer interface {
|
||||
}
|
||||
```
|
||||
|
||||
`mustEmbedUnimplementedUserServiceServer()` 是一个**无参数的空方法**。它的存在让 interface 成为封闭类型——其他任何想满足此接口的类型都必须显式嵌入 `UnimplementedUserServiceServer`,否则编译期直接拦截。这是 Go 社区著名的"封闭接口"(unexported method trick)反模式之一,但在 protobuf 场景下它是有意为之的设计。
|
||||
`mustEmbedUnimplementedUserServiceServer()` 是一个无参空方法。它让 interface 成为**封闭类型**——任何想满足此接口的类型都必须显式嵌入 `UnimplementedUserServiceServer`,否则编译期直接拦截。这是 Go 社区著名的"封闭接口"(unexported method trick)设计手法,在 protobuf 场景下是有意为之。
|
||||
|
||||
## 为什么必须嵌入
|
||||
> [!note] 为什么叫 "closed set of methods"?
|
||||
> Go 的 interface 是 implicit(隐式实现),正常情况下你可以随时"默默"实现一个新接口。但 protobuf 通过注入一个只能由生成的 struct 提供的方法,把这个隐式契约变成了**显式嵌入**——你必须主动写下一行 `pb.UnimplementedUserServiceServer`,这种"被迫直面"就是安全感的来源。
|
||||
|
||||
### 1. 减少样板代码
|
||||
## 嵌入用法
|
||||
|
||||
嵌入后只需要实现你需要的那些方法:
|
||||
嵌入后只需实现需要的业务方法:
|
||||
|
||||
```go
|
||||
type userService struct {
|
||||
@@ -62,89 +65,119 @@ func (s *userService) CreateUser(ctx context.Context, req *pb.CreateUserRequest)
|
||||
return &pb.CreateUserResponse{Id: generatedId}, nil
|
||||
}
|
||||
|
||||
// GetUser can be omitted — UnimplementedUserServiceServer returns the default error:
|
||||
// GetUser 不写 — UnimplementedUserServiceServer 自动返回:
|
||||
// status.Error(codes.Unimplemented, "method GetUser not implemented")
|
||||
```
|
||||
|
||||
### 2. Proto 新增 RPC Method → 编译期告警
|
||||
### Proto 新增 Method → 编译期告警
|
||||
|
||||
这是该模式最大的价值。假设后续你在 proto 里加了一个方法:
|
||||
这是该模式最大的价值。**先回答一个疑惑**:proto 新增了 `DeleteUser`,重新运行 protoc 后 `UnimplementedUserServiceServer` 自身也会多出对应的默认实现,你的嵌入结构体照样能编译通过——那为什么说有风险前置?
|
||||
|
||||
```proto
|
||||
service UserService {
|
||||
rpc CreateUser(...) returns (...);
|
||||
rpc GetUser(...) returns (...);
|
||||
rpc DeleteUser(...) returns (...); // ← 新增
|
||||
}
|
||||
```
|
||||
原因在于两个实际场景:
|
||||
|
||||
重新运行 protoc 后,`UserServiceServer` interface 会多出 `DeleteUser` 签名。**此时编译就会失败**:
|
||||
**场景 A:protoc 重新生成后忘记更新依赖**
|
||||
如果你在同一份代码库里只改了 proto 文件,却忘了对所有 micro-service 跑 protoc,那些没更新的 service 就会报编译错误:
|
||||
|
||||
```
|
||||
cannot use &userService{} as UserServiceServer:
|
||||
missing method DeleteUser in receiver type userService
|
||||
missing method DeleteUser in receiver type userService
|
||||
```
|
||||
|
||||
从"线上偶发 Bug"变成了"提交前就发现"——把风险前置到了编译期。
|
||||
CI 的编译检查会直接拦住——**某个服务漏跑 protoc**,而不是等到线上被调用的时候才发现这个方法从未被实现。
|
||||
|
||||
### 3. 强制接口契约意识
|
||||
**场景 B:忘记嵌入 → 退化回普通接口**
|
||||
如果不 embed `UnimplementedXxxServer`,proto 新增 method 时编译器照样不拦你。新方法永远不会被调用,直到线上某个 client 触发了它——这就退化成了"没这个机制之前"的样子。这也是为什么"正确嵌入"是这套模式生效的前提。
|
||||
|
||||
如果不用 embed,你可以"选择性实现"——只有某些方法、其他全忽略。这会导致服务方和客户端对接口理解不一致。Zero-stub 模式强迫每次新增 method 时都做一次有意识的决策:**实现它**,还是保持 Unimplemented?
|
||||
---
|
||||
|
||||
## 与反射型语言的对比
|
||||
> [!question] 那"强制接口契约意识"怎么理解?
|
||||
>
|
||||
> 正确用法下,Proto 新增 method 后,`UnimplementedXxxServer` 也会跟着长出新的兜底实现,编译是**通过的**。但这正是设计者想要的效果:
|
||||
> - **不强制你立刻实现**(不影响正常请求)
|
||||
> - **但强制你在 protoc 重新生成的那一刻"看到"变更**
|
||||
> - 从此时起你有两次决策机会:① 跑 protoc 时意识到契约变了;② 决定是否要把 `codes.Unimplemented` 替换成真实逻辑
|
||||
> - 对比非类型安全语言(Java 等),新方法可以永远不被实现且永远不被发现——Go 版本至少让每次 proto 变更都变得**可追踪**
|
||||
|
||||
在 Java 等语言中,gRPC 通常要求你继承一个基类(如 `UserServiceImplBase`),效果类似:
|
||||
## 嵌入策略对比
|
||||
|
||||
```java
|
||||
public class UserServiceImpl extends UserServiceImplBase {
|
||||
@Override
|
||||
public void createUser(..., StreamObserver observer) {
|
||||
// 只重写需要的
|
||||
}
|
||||
// getUser 自动走父类的 Unimplemented
|
||||
不同语言的 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
|
||||
```
|
||||
|
||||
Go 版本的优势在于**嵌入比继承更灵活**——你可以同时嵌入多个类型、控制方法优先级(离当前结构体越近优先匹配),并且没有单继承限制。
|
||||
关键点:
|
||||
|
||||
- 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"]
|
||||
Client["Client RPC Call"] --> Lookup["Server dispatch table"]
|
||||
Lookup --> Found{"Method 是否被覆写?"}
|
||||
Found -->|"是"| Handler["Handler 执行业务逻辑"]
|
||||
Found -->|"否"| Fallback["UnimplementedStub<br/>codes.Unimplemented 错误"]
|
||||
Found -->|"否"| Fallback["UnimplementedStub<br/>codes.Unimplemented"]
|
||||
|
||||
Handler --> Resp["Response to Client"]
|
||||
Fallback --> Resp
|
||||
|
||||
ProtoChanged["Proto 新增 Method"] --> GenCode["Protoc 重新生成"]
|
||||
GenCode --> CompileErr["userService 缺少新方法,编译失败 ✅"]
|
||||
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 CompileErr fill:#FFF3E0,color:#000,stroke:#E65100
|
||||
style AllUpdated fill:#FFF3E0,color:#000,stroke:#E65100
|
||||
style PartialUpdate fill:#FFF3E0,color:#000,stroke:#E65100
|
||||
```
|
||||
|
||||
## 实际使用中的注意事项
|
||||
|
||||
> [!warning] 不要手动实现 Unimplemented 方法
|
||||
>
|
||||
> 如果在自己的结构体中也定义了同名 method(比如不小心写了 `MustEmbedUnimplementedUserServiceServer()`),它会覆盖嵌入类型的空方法。虽然功能上不影响(interface 检查只看是否存在),但语义混乱,应避免。
|
||||
|
||||
如果你在自己的结构体中也定义了一个同名 method(比如不小心写了 `func (s *userService) MustEmbedUnimplementedUserServiceServer()`),它会**覆盖**嵌入类型的空方法。虽然功能上不影响(interface 检查只看是否存在),但语义混乱,应避免。
|
||||
|
||||
> [!tip] 如何快速确认某个 Service 的所有方法都被覆盖了?
|
||||
|
||||
可以用 IDE 的 "implement interface" 功能列出未实现的方法,或者写一个简单的 compile test:
|
||||
|
||||
```go
|
||||
var _ pb.UserServiceServer = (*userService)(nil) // 编译期检查
|
||||
```
|
||||
|
||||
加上这一行后,任何遗漏的方法都会在这句编译报错,适合作为 CI 的一部分。
|
||||
|
||||
## 常见陷阱
|
||||
> [!warning] embed vs pointer embed 的选择
|
||||
>
|
||||
> gRPC 生成的 `UnimplementedXxxServer` **值类型和指针类型都可以嵌入**,但效果不同:
|
||||
>
|
||||
> | 嵌入方式 | `*userService` 是否满足接口 | 说明 |
|
||||
> |---------|---------------------------|------|
|
||||
> | `pb.UnimplementedXxxServer`(值) | `*userService` ✅ | 推荐,指针接收者可调用值方法 |
|
||||
> | `*pb.UnimplementedXxxServer`(指针) | `*userService` ✅ 且 `userService` ❌ | 仅指针满足接口 |
|
||||
>
|
||||
> **最佳实践**: 大多数 gRPC server 方法签名是 `func (s *Service) Method(...)`,所以嵌入值类型的 `Unimplemented` 即可同时支持两种调用方式。
|
||||
|
||||
> [!danger] 嵌入顺序决定方法优先级
|
||||
>
|
||||
@@ -157,46 +190,38 @@ var _ pb.UserServiceServer = (*userService)(nil) // 编译期检查
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> 养成**将 Unimplemented 嵌入放在最后**的习惯,避免意外覆盖:
|
||||
> 养成**将 Unimplemented 嵌入放在最后**的习惯:
|
||||
>
|
||||
> ```go
|
||||
> type userService struct {
|
||||
> *UserStore // 业务依赖先放前面
|
||||
> someHelper // 工具类
|
||||
> logger // 工具类
|
||||
> pb.UnimplementedUserServiceServer // 永远放最后
|
||||
> }
|
||||
> ```
|
||||
|
||||
> [!warning] `embed` vs `pointer embed` 的选择
|
||||
>
|
||||
> gRPC 生成的 `UnimplementedXxxServer` **值类型和指针类型都可以嵌入**,但效果不同:
|
||||
>
|
||||
> | 嵌入方式 | `*userService` 是否满足接口 | 说明 |
|
||||
> |---------|---------------------------|------|
|
||||
> | `pb.UnimplementedXxxServer`(值) | `*userService` ✅ | 推荐,指针接收者可调用值方法 |
|
||||
> | `*pb.UnimplementedXxxServer`(指针) | `*userService` ✅ 且 `userService` ❌ | 仅指针满足接口 |
|
||||
>
|
||||
> **最佳实践**: 大多数 gRPC server 方法签名是 `func (s *Service) Method(...)`,所以用指针 receiver。嵌入值类型的 `Unimplemented` 即可同时支持两种调用方式。
|
||||
|
||||
## Server 集成示例
|
||||
|
||||
下面是一个完整的、可直接运行的 Server 搭建片段,展示 UnimplementedStub 在整个链路中的位置:
|
||||
下面展示 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 // 放最后
|
||||
store *UserStore // 业务依赖
|
||||
pb.UnimplementedUserServiceServer // 永远放最后
|
||||
}
|
||||
|
||||
func (s *UserServiceImpl) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
|
||||
@@ -222,9 +247,39 @@ func Register(srv *grpc.Server, store *UserStore) {
|
||||
```
|
||||
|
||||
关键要点:
|
||||
- `RegisterUserServiceServer()` 内部会校验你传入的类型是否实现了 `UserServiceServer` interface
|
||||
|
||||
- `RegisterUserServiceServer()` 内部会校验传入的类型是否实现了 `UserServiceServer` interface
|
||||
- 由于 `UnimplementedUserServiceServer` 提供了所有方法的默认实现,你只需编写实际需要的 method
|
||||
- Proto 新增 method → protoc 重新生成 → 编译失败 → **有意识的决策点**
|
||||
- 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。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
|
||||
Reference in New Issue
Block a user