This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式.md
T
2026-05-13 23:43:37 +08:00

233 lines
8.8 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-13 14:30
---
# Unimplemented 零-Stub 模式
## 概述
`UnimplementedXxxServer` 是 protoc-gen-go 为每个 gRPC service 自动生成的"空实现"结构体。通过嵌入它到自定义的 Service 结构体中,开发者只需编写实际需要的方法,其余方法自动获得 `codes.Unimplemented` 默认响应——这是一种兼顾开发便利性与接口契约安全的 Go 语言惯用模式。
> [!question] 如果没有这个机制会怎样?
> 想象你实现了三个 RPC method,后来 proto 新增了一个,编译器不报错——你的新方法永远不会被调用,直到线上某个 client 发出请求后才暴露出 Bug。这就是"静默失败"陷阱。
## Protobuf 生成的代码结构
当你写一个 proto service:
```proto
service UserService {
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc GetUser(GetUserRequest) returns (GetUserResponse);
}
```
protoc 会生成三类关键代码:
| 生成物 | 作用 |
|--------|------|
| `UserServiceServer` interface | 你必须实现的方法集合 |
| `UnimplementedUserServiceServer` struct | 所有方法都返回 `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 场景下它是有意为之的设计。
## 为什么必须嵌入
### 1. 减少样板代码
嵌入后只需要实现你需要的那些方法:
```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 can be omitted — UnimplementedUserServiceServer returns the default error:
// status.Error(codes.Unimplemented, "method GetUser not implemented")
```
### 2. Proto 新增 RPC Method → 编译期告警
这是该模式最大的价值。假设后续你在 proto 里加了一个方法:
```proto
service UserService {
rpc CreateUser(...) returns (...);
rpc GetUser(...) returns (...);
rpc DeleteUser(...) returns (...); // ← 新增
}
```
重新运行 protoc 后,`UserServiceServer` interface 会多出 `DeleteUser` 签名。**此时编译就会失败**:
```
cannot use &userService{} as UserServiceServer:
missing method DeleteUser in receiver type userService
```
从"线上偶发 Bug"变成了"提交前就发现"——把风险前置到了编译期。
### 3. 强制接口契约意识
如果不用 embed,你可以"选择性实现"——只有某些方法、其他全忽略。这会导致服务方和客户端对接口理解不一致。Zero-stub 模式强迫每次新增 method 时都做一次有意识的决策:**实现它**,还是保持 Unimplemented?
## 与反射型语言的对比
在 Java 等语言中,gRPC 通常要求你继承一个基类(如 `UserServiceImplBase`),效果类似:
```java
public class UserServiceImpl extends UserServiceImplBase {
@Override
public void createUser(..., StreamObserver observer) {
// 只重写需要的
}
// getUser 自动走父类的 Unimplemented
}
```
Go 版本的优势在于**嵌入比继承更灵活**——你可以同时嵌入多个类型、控制方法优先级(离当前结构体越近优先匹配),并且没有单继承限制。
## 执行流程图解
```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 --> CompileErr["userService 缺少新方法,编译失败 ✅"]
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
```
## 实际使用中的注意事项
> [!warning] 不要手动实现 Unimplemented 方法
如果你在自己的结构体中也定义了一个同名 method(比如不小心写了 `func (s *userService) MustEmbedUnimplementedUserServiceServer()`),它会**覆盖**嵌入类型的空方法。虽然功能上不影响(interface 检查只看是否存在),但语义混乱,应避免。
> [!tip] 如何快速确认某个 Service 的所有方法都被覆盖了?
可以用 IDE 的 "implement interface" 功能列出未实现的方法,或者写一个简单的 compile test:
```go
var _ pb.UserServiceServer = (*userService)(nil) // 编译期检查
```
加上这一行后,任何遗漏的方法都会在这句编译报错,适合作为 CI 的一部分。
## 常见陷阱
> [!danger] 嵌入顺序决定方法优先级
>
> Go 的结构体嵌入具有**层级可达性**——当多个嵌入类型都有同名方法时,编译器选择距离当前结构体最近的那个。如果不小心嵌入了错误的类型,你的实现可能永远不会被调用:
>
> ```go
> type userService struct {
> pb.UnimplementedUserServiceServer
> someOtherEmbeddedStruct // ← 如果有同名方法,这里优先!
> }
> ```
>
> 养成**将 Unimplemented 嵌入放在最后**的习惯,避免意外覆盖:
>
> ```go
> type userService struct {
> *UserStore // 业务依赖先放前面
> someHelper // 工具类
> 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 在整个链路中的位置:
```go
package server
import (
"context"
"log"
"google.golang.org/grpc"
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 重新生成 → 编译失败 → **有意识的决策点**
## 关联笔记
- [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册]]
- [[hhs/gRPC/3. 服务端实现/09-Streaming Handler]]