479 lines
16 KiB
Markdown
479 lines
16 KiB
Markdown
|
|
---
|
|||
|
|
tags: [gRPC, Protobuf, Go, protoc, Code Generation, proto3]
|
|||
|
|
create time: 2026-05-11 16:41
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# Service 定义与代码生成
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
`.proto` 文件的最终目的是定义 Service —— 告诉 gRPC 有哪些远程可调用的 API。Protobuf 编译器会将你的 service 定义翻译成各个语言的 client stub 和 server interface。理解这个从文本到可执行代码的过程,是调试"gRPC 报错说找不到方法"的前提。
|
|||
|
|
|
|||
|
|
> [!question] 为什么需要代码生成?
|
|||
|
|
> Protobuf 不是动态语言。所有类型、字段、方法都在编译期确定,这意味着你没法在运行时通过字符串来调用一个 RPC——必须用生成的 Stub。好处是强类型检查能在编码阶段就发现错误,坏处是你改了 proto 文件就得重新生成代码并编译。
|
|||
|
|
|
|||
|
|
## Proto3 语法速览
|
|||
|
|
|
|||
|
|
Service 由 message 组成,所以先快速过一遍 proto3 的核心语法。这是写 proto 文件时每天都要用的基础知识。
|
|||
|
|
|
|||
|
|
### 消息(Message)与字段类型
|
|||
|
|
|
|||
|
|
```protobuf
|
|||
|
|
syntax = "proto3";
|
|||
|
|
package user.v1;
|
|||
|
|
|
|||
|
|
message User {
|
|||
|
|
string name = 1; // 变长字符串,UTF-8 编码
|
|||
|
|
int64 id = 2; // 64 位有符号整数
|
|||
|
|
bool active = 3; // 布尔值
|
|||
|
|
float balance = 4; // 32 位浮点数
|
|||
|
|
bytes avatar = 5; // 原始字节(如图片数据)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**常用标量类型一览:**
|
|||
|
|
|
|||
|
|
| 声明类型 | 对应 Go 类型 | 说明 |
|
|||
|
|
|----------|-------------|------|
|
|||
|
|
| `double` / `float` | `float64` / `float32` | 浮点数 |
|
|||
|
|
| `int64` / `uint64` | `int64` / `uint64` | 大整数 |
|
|||
|
|
| `int32` / `uint32` | `int32` / `uint32` | 普通整数(用 varint 编码,小值更紧凑) |
|
|||
|
|
| `bool` | `bool` | 布尔 |
|
|||
|
|
| `string` | `string` | UTF-8 字符串(必须用 string,bytes 存原始二进制) |
|
|||
|
|
| `bytes` | `[]byte` | 任意字节序列 |
|
|||
|
|
|
|||
|
|
> [!tip] 零值语义
|
|||
|
|
> proto3 没有 `optional` 标记的字段永远有零值——`string` 是 `""`,`int` 是 `0`,`bool` 是 `false`。你无法区分"字段没设置"和"字段设为零值"。如果需要检测字段是否存在,可以用 `google.protobuf.BoolValue` 包装类型,或者启用 `optional` 关键字(proto3 扩展语法)。
|
|||
|
|
|
|||
|
|
### 枚举(Enum)
|
|||
|
|
|
|||
|
|
```protobuf
|
|||
|
|
enum UserRole {
|
|||
|
|
ROLE_UNKNOWN = 0; // proto3 要求第一个值是 0,且必须是唯一的 zero value
|
|||
|
|
ROLE_ADMIN = 1;
|
|||
|
|
ROLE_USER = 2;
|
|||
|
|
ROLE_GUEST = 3;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message CreateUserRequest {
|
|||
|
|
string name = 1;
|
|||
|
|
UserRole role = 2; // 用枚举替代魔法数字
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!warning] enum 的零值陷阱
|
|||
|
|
> proto3 中如果收到未知枚举值,它会被当作零值处理而非报错。这意味着服务端可以优雅地忽略客户端传来的新版本枚举值——这是 proto 向后兼容的设计之一。
|
|||
|
|
|
|||
|
|
### Oneof(多选一)
|
|||
|
|
|
|||
|
|
当多个字段互斥时使用 `oneof`,它比用单独字段节省内存,因为底层只有一个字段在存储。
|
|||
|
|
|
|||
|
|
```protobuf
|
|||
|
|
message PaymentMethod {
|
|||
|
|
oneof method {
|
|||
|
|
string credit_card = 1; // Visa/MC number
|
|||
|
|
string paypal_email = 2; // PayPal 账户
|
|||
|
|
AlipayAccount alipay = 3; // 自定义 message
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
只能设置 oneof 中的**一个**字段。设置新字段会清除前一个的值。
|
|||
|
|
|
|||
|
|
### Map Fields
|
|||
|
|
|
|||
|
|
```protobuf
|
|||
|
|
message UserMetadata {
|
|||
|
|
map<string, string> tags = 1; // user -> tag mappings
|
|||
|
|
map<int64, string> department_map = 2; // dept ID -> name
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
内部实现是一个哈希表。空 map 序列化为空,不会省略。
|
|||
|
|
|
|||
|
|
### Reserved 字段
|
|||
|
|
|
|||
|
|
当你删除或重命名某个字段时,用 `reserved` 占位防止其他人重用同一 field number:
|
|||
|
|
|
|||
|
|
```protobuf
|
|||
|
|
message User {
|
|||
|
|
reserved 3, 5 to 8; // 保留 field numbers 3, 5, 6, 7, 8
|
|||
|
|
reserved "old_name", "temp"; // 也保留字段名
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Well-Known Types
|
|||
|
|
|
|||
|
|
Protobuf 内置了一组通用类型,import 后可直接使用:
|
|||
|
|
|
|||
|
|
```protobuf
|
|||
|
|
import "google/protobuf/timestamp.proto";
|
|||
|
|
import "google/protobuf/wrappers.proto";
|
|||
|
|
import "google/protobuf/struct.proto";
|
|||
|
|
|
|||
|
|
message Event {
|
|||
|
|
google.protobuf.Timestamp created_at = 1; // RFC 3339 时间戳
|
|||
|
|
google.protobuf.StringValue display_name = 2; // *string,用于 detect missing
|
|||
|
|
google.protobuf.Struct metadata = 3; // JSON-like 任意结构
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
常用 well-known type 速查:
|
|||
|
|
|
|||
|
|
| Well-Known Type | 对应 Go 类型 |
|
|||
|
|
|----------------|-------------|
|
|||
|
|
| `Timestamp` | `time.Time` |
|
|||
|
|
| `Duration` | `time.Duration` |
|
|||
|
|
| `StringValue` | `*string` |
|
|||
|
|
| `Int32Value` / `Int64Value` | `*int32` / `*int64` |
|
|||
|
|
| `BoolValue` | `*bool` |
|
|||
|
|
| `Any` | `any` / `[]byte` |
|
|||
|
|
|
|||
|
|
## Service 定义语法
|
|||
|
|
|
|||
|
|
Service 定义使用 `service` 关键字包裹一组 `rpc` 方法:
|
|||
|
|
|
|||
|
|
```protobuf
|
|||
|
|
syntax = "proto3";
|
|||
|
|
package user.v1;
|
|||
|
|
option go_package = "example.com/proto/user/v1;userpb";
|
|||
|
|
|
|||
|
|
message CreateUserRequest {
|
|||
|
|
string name = 1;
|
|||
|
|
string email = 2;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message CreateUserResponse {
|
|||
|
|
int64 id = 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
service UserService {
|
|||
|
|
// Unary: 普通请求响应
|
|||
|
|
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
|
|||
|
|
|
|||
|
|
// Server Streaming: 一次请求,多个响应
|
|||
|
|
rpc ListUsers(ListUsersRequest) returns (stream ListUsersResponse);
|
|||
|
|
|
|||
|
|
// Client Streaming: 多次请求,一个响应
|
|||
|
|
rpc UploadAvatar(stream AvatarChunk) returns (AvatarResult);
|
|||
|
|
|
|||
|
|
// Bidirectional Streaming: 双方都流式
|
|||
|
|
rpc WatchUsers(stream WatchRequest) returns (stream WatchEvent);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**语法要点:**
|
|||
|
|
- `stream` 关键字出现在参数侧表示该方向是流式
|
|||
|
|
- `stream` 可以出现在 request 侧、response 侧,或两侧都有
|
|||
|
|
- 每个 proto 文件可以有**多个** service 定义(但最佳实践建议一个文件一个 service,保持高内聚)
|
|||
|
|
|
|||
|
|
## Option 系统
|
|||
|
|
|
|||
|
|
### 必填项
|
|||
|
|
|
|||
|
|
| Option | 说明 | 示例 |
|
|||
|
|
|--------|------|------|
|
|||
|
|
| `syntax` | 语法版本,proto3 是当前标准 | `syntax = "proto3";` |
|
|||
|
|
| `package` | 命名空间,防止跨项目命名冲突 | `package user.v1;` |
|
|||
|
|
| `go_package` | Go 输出路径和包名 | `option go_package = "...";` |
|
|||
|
|
|
|||
|
|
### go_package 详解
|
|||
|
|
|
|||
|
|
这是 Go 开发者最容易踩坑的地方。它的格式是 `"模块路径/生成文件存放路径;包名"`。
|
|||
|
|
|
|||
|
|
```protobuf
|
|||
|
|
// ✅ 正确:module path 是 github.com/myorg/services
|
|||
|
|
// 生成的文件放在 gen/proto/user/v1/ 目录下
|
|||
|
|
// 包名为 userpb
|
|||
|
|
option go_package = "github.com/myorg/services/gen/proto/user/v1;userpb";
|
|||
|
|
|
|||
|
|
// ❌ 错误:go_package 中的路径与实际 import 不匹配
|
|||
|
|
// 会导致 Go 编译器报 symbol undefined
|
|||
|
|
option go_package = "user/v1;userpb";
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!tip] go_package 拆解
|
|||
|
|
> ```
|
|||
|
|
> option go_package = "导入路径/子路径;包名";
|
|||
|
|
> // ↑ 生成文件相对 module root 的路径 ↑ Go package name
|
|||
|
|
> // 生成文件的 import path 将是 module root + 前半段
|
|||
|
|
> ```
|
|||
|
|
>
|
|||
|
|
> 例如:`go_package = "github.com/myorg/api/user/v1;userpb"`,如果当前 `.proto` 所在目录与 `v1` 对齐,则生成的 `_pb.go` 的 import path 为 `github.com/myorg/api/user/v1`,文件中 `package userpb`。
|
|||
|
|
|
|||
|
|
### 其他语言路径配置
|
|||
|
|
|
|||
|
|
```protobuf
|
|||
|
|
// 如需支持多语言输出,补全对应的 option
|
|||
|
|
option java_package = "com.example.user.v1";
|
|||
|
|
option java_multiple_files = true; // 每个 message 单独一个 Java 文件
|
|||
|
|
|
|||
|
|
option py_generic_services = false; // Python 是否生成 service 基类
|
|||
|
|
|
|||
|
|
option php_namespace = "Example\\User\\V1";
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!note] 如果只开发 Go 服务,其他语言的 option 可以不填,减少维护负担。
|
|||
|
|
|
|||
|
|
### 标记已废弃字段
|
|||
|
|
|
|||
|
|
```protobuf
|
|||
|
|
message OldUserProto {
|
|||
|
|
string old_field = 1 [deprecated = true]; // 前端可用 @deprecated 注解识别
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### optimize_for(性能调优选项)
|
|||
|
|
|
|||
|
|
对于消息体很大的场景,可以用 `optimize_for` 控制代码生成的策略:
|
|||
|
|
|
|||
|
|
```protobuf
|
|||
|
|
option optimize_for = SPEED; // 默认:生成的序列化/反序列化代码最快
|
|||
|
|
// option optimize_for = CODE_SIZE; // 优化生成代码大小(使用 lite runtime)
|
|||
|
|
// option optimize_for = LITE_RUNTIME; // 生成依赖 lite protobuf runtime 的代码
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- **`SPEED`**(默认):生成完整的序列化和反序列化代码,速度最优
|
|||
|
|
- **`CODE_SIZE`**:使用反射式编解码,减小生成的代码体积,适合嵌入式环境
|
|||
|
|
- **`LITE_RUNTIME`**:类似 CODE_SIZE,但保留部分直接编解码逻辑,折中方案
|
|||
|
|
|
|||
|
|
大多数 Web 微服务不需要改这个选项——默认的 SPEED 就是最好的选择。
|
|||
|
|
|
|||
|
|
### Custom Options 简介
|
|||
|
|
|
|||
|
|
通过 `extend google.protobuf.MessageOptions` 可以定义自定义 option,被 gRPC Gateway、Protoc Gen OpenAPI 等工具链借用。了解即可,涉及 proto 元数据反射,属于进阶话题。
|
|||
|
|
|
|||
|
|
## Proto 编译流程
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
A[".proto 源文件"] --> B["protoc 编译器"]
|
|||
|
|
B --> C["protoc-gen-go — Go struct 定义"]
|
|||
|
|
B --> D["protoc-gen-go-grpc — Client Stub + Server Interface"]
|
|||
|
|
C --> E["Go 代码编译"]
|
|||
|
|
D --> E
|
|||
|
|
E --> F["可执行程序"]
|
|||
|
|
|
|||
|
|
style A fill:#FFD43B
|
|||
|
|
style B fill:#00B6BC,color:#fff
|
|||
|
|
style F fill:#4FC08D,color:#fff
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
核心流程就是三步:写 `.proto` → `protoc` 生成 Go 代码 → 正常 `go build`。
|
|||
|
|
|
|||
|
|
### 典型的项目目录结构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
proto/
|
|||
|
|
├── buf.gen.yaml # buf 代码生成配置(如果用 buf)
|
|||
|
|
├── user/
|
|||
|
|
│ └── v1/
|
|||
|
|
│ ├── user.proto # Service + Message 定义
|
|||
|
|
│ └── error.proto # 错误码定义
|
|||
|
|
├── google/ # third_party 依赖(来自 grpc-ecosystem/grpc-gateway)
|
|||
|
|
└── Makefile # 自动化生成脚本
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!tip] 推荐的两种代码生成方式
|
|||
|
|
>
|
|||
|
|
> 1. **Makefile + protoc**:最传统的做法,用 shell 变量管理 `-I` 路径
|
|||
|
|
> 2. **buf**:现代 proto 编译工具,自动处理依赖管理和 plugin 版本,推荐新项目使用
|
|||
|
|
>
|
|||
|
|
> 具体用法参见 [[hhs/gRPC/6. 工程实践篇/17-protoc 工具链与 Makefile]]
|
|||
|
|
|
|||
|
|
## Go Stub 解析
|
|||
|
|
|
|||
|
|
运行 protoc 后,每个 `.proto` 文件至少生成两个文件:
|
|||
|
|
|
|||
|
|
| 文件 | 内容 |
|
|||
|
|
|------|------|
|
|||
|
|
| `xxx_pb.go` | Message 的 struct 定义(如 `CreateUserRequest{}`) |
|
|||
|
|
| `xxx_grpc.pb.go` | Client interface + Server interface + Register 函数 |
|
|||
|
|
|
|||
|
|
### `_grpc.pb.go` 中的关键符号
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// --- 客户端 ---
|
|||
|
|
type UserServiceClient interface {
|
|||
|
|
CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserResponse, error)
|
|||
|
|
ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (UserService_ListUsersClient, error)
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient
|
|||
|
|
|
|||
|
|
// --- 服务端 ---
|
|||
|
|
type UserServiceServer interface {
|
|||
|
|
CreateUser(context.Context, *CreateUserRequest) (*CreateUserResponse, error)
|
|||
|
|
ListUsers(*ListUsersRequest, UserService_ListUsersServer) error
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 你必须嵌入这个来拿到零值安全的方法实现
|
|||
|
|
type UnimplementedUserServiceServer struct{}
|
|||
|
|
|
|||
|
|
// --- 注册 ---
|
|||
|
|
func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer)
|
|||
|
|
|
|||
|
|
// --- ServiceDesc ---
|
|||
|
|
var UserService_ServiceDesc = grpc.ServiceDesc{
|
|||
|
|
ServiceName: "user.v1.UserService",
|
|||
|
|
MethodType: grpc.Unary,
|
|||
|
|
MethodName: "CreateUser",
|
|||
|
|
Handler: ...,
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### `_pb.go` 中的 Message
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type CreateUserRequest struct {
|
|||
|
|
nameState impl.MessageState
|
|||
|
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
|||
|
|
Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"`
|
|||
|
|
}
|
|||
|
|
// + getter methods: GetName(), GetEmail()
|
|||
|
|
// + JSON marshaler/unmarshaler
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!tip] Getter 方法
|
|||
|
|
> Protobuf Go 生成器会为每个字段生成 `GetXxx()` getter方法。即使字段本身是 public 的(大写),你也应该优先用 getter——某些字段未来可能会改为 internal 实现,getter 能保护你的代码不受影响。
|
|||
|
|
|
|||
|
|
### 实战:客户端调用示例
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 构建客户端并发起请求
|
|||
|
|
conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|||
|
|
defer conn.Close()
|
|||
|
|
|
|||
|
|
client := pb.NewUserServiceClient(conn)
|
|||
|
|
|
|||
|
|
resp, err := client.CreateUser(context.Background(), &pb.CreateUserRequest{
|
|||
|
|
Name: "Alice",
|
|||
|
|
Email: "alice@example.com",
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
log.Fatal(err)
|
|||
|
|
}
|
|||
|
|
fmt.Printf("created user with id=%d\n", resp.Id)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 代码生成命令速查
|
|||
|
|
|
|||
|
|
### protoc 基本用法
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 最简形式(generated files 放入 output dir)
|
|||
|
|
protoc \
|
|||
|
|
--go_out=. \
|
|||
|
|
--go-grpc_out=. \
|
|||
|
|
*.proto
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### source_relative 模式(推荐)
|
|||
|
|
|
|||
|
|
生成的文件与原 `.proto` 放在同目录下,更符合 Go 习惯:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
protoc \
|
|||
|
|
--go_opt=paths=source_relative \
|
|||
|
|
--go-grpc_opt=paths=source_relative \
|
|||
|
|
-I . \
|
|||
|
|
*.proto
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 多目录 / 带 import 的场景
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
protoc \
|
|||
|
|
--go_opt=paths=source_relative \
|
|||
|
|
--go-grpc_opt=paths=source_relative \
|
|||
|
|
-I proto/ \
|
|||
|
|
-I third_party/googleapis/ \
|
|||
|
|
proto/user/v1/*.proto
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 集成到 go generate
|
|||
|
|
|
|||
|
|
在项目的入口文件顶部添加注释指令:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
//go:generate protoc \
|
|||
|
|
// --go_opt=paths=source_relative \
|
|||
|
|
// --go-grpc_opt=paths=source_relative \
|
|||
|
|
// -I . \
|
|||
|
|
// ./proto/**/*.proto
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
然后在终端只需一行:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
go generate ./...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!tip] 为什么推荐 go generate?
|
|||
|
|
> 相比手写 protoc 命令,`go generate` 让代码生成变成 Go 工作流的一部分,不再需要额外记忆复杂的命令行参数。结合 Makefile 或 Task 更稳定。参见 [[hhs/gRPC/6. 工程实践篇/17-protoc 工具链与 Makefile]]。
|
|||
|
|
|
|||
|
|
## 实践要点:自动生成 vs 手写
|
|||
|
|
|
|||
|
|
### 自动生成(由 protoc 产出,不要手动修改)
|
|||
|
|
|
|||
|
|
| 文件 | 内容 |
|
|||
|
|
|------|------|
|
|||
|
|
| `xxx_pb.go` | Message struct 定义 + getter 方法(如 `CreateUserRequest{}`、`GetName()`) |
|
|||
|
|
| `xxx_grpc.pb.go` | Client Stub interface + `NewUserServiceClient()` |
|
|||
|
|
| `xxx_grpc.pb.go` | Server Interface(如 `UserServiceServer{ CreateUser(...) }`) |
|
|||
|
|
| `xxx_grpc.pb.go` | Register 函数 + ServiceDesc 元数据 |
|
|||
|
|
| `xxx_grpc.pb.go` | `UnimplementedUserServiceServer` 零值安全基类 |
|
|||
|
|
|
|||
|
|
这些文件**每次 `.proto` 变更后重新生成即可覆盖**。如果发现有 bug,修复源头 `.proto` 后重新生成,而不是直接改生成的文件。
|
|||
|
|
|
|||
|
|
### 开发者手写的部分
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// ① 服务端:实现生成的 Server Interface
|
|||
|
|
type server struct {
|
|||
|
|
v1.UnimplementedUserServiceServer // ← 嵌入自动生成的基类
|
|||
|
|
store *UserStore // ← 自己的依赖注入
|
|||
|
|
}
|
|||
|
|
func (s *server) CreateUser(ctx, req *v1.CreateUserRequest) (*v1.CreateUserResponse, error) {
|
|||
|
|
// ← 填充业务逻辑(数据库操作、验证、权限等)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ② 客户端:使用生成的 Client Stub 发起调用
|
|||
|
|
client := v1.NewUserServiceClient(conn) // ← 用生成的函数创建
|
|||
|
|
resp, _ := client.CreateUser(ctx, &v1.CreateUserRequest{Name: "Alice"})
|
|||
|
|
|
|||
|
|
// ③ Server 启动注册
|
|||
|
|
v1.RegisterUserServiceServer(s, &myServer{})
|
|||
|
|
|
|||
|
|
// ④ 中间件、拦截器、错误映射、健康检查等——全部自行实现
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!important] 核心原则
|
|||
|
|
> **所有业务代码都应假设生成的 `.pb.go` / `_grpc.pb.go` 随时会被重新生成覆盖。** 不自行定义与 proto 同名的 struct;类型统一从生成的包 import;不修改任何生成的文件。
|
|||
|
|
|
|||
|
|
## 常见错误排查
|
|||
|
|
|
|||
|
|
| 错误信息 | 原因 | 解决方法 |
|
|||
|
|
|----------|------|----------|
|
|||
|
|
| `symbol undefined` | `go_package` 路径不正确 | 检查 `go_package` 中的 module 路径是否匹配当前项目的 `go.mod` |
|
|||
|
|
| `service not found` | 没有运行 `protoc-gen-go-grpc` | 确认 `go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest` 且 `$GOPATH/bin` 在 PATH 中 |
|
|||
|
|
| `mismatched version` | `protoc` CLI 版本与 plugin 版本不匹配 | 升级 plugin 到最新版,或降级 protoc。两者不需要严格一致,但不要跨太多代 |
|
|||
|
|
| `unknown syntax` | `.proto` 缺少 `syntax = "proto3"` | 添加该行或在文件开头加上 `syntax = "proto3";` |
|
|||
|
|
| `import not found` | `-I` 导入路径不对 | 检查 `import "..."` 声明与 `-I` 指定的 search path 是否对齐 |
|
|||
|
|
| `already defined` | 同一个 message/service 被定义了两次 | 检查是否有重复 import,或多个 proto 文件导出到了同一目标目录 |
|
|||
|
|
| `go_package mismatch` | 生成文件所在的包路径不符合 Go module 约定 | 将 `go_package` 调整为相对于 module root 的正确路径 |
|
|||
|
|
|
|||
|
|
> [!tip] 快速诊断顺序
|
|||
|
|
> 1. `protoc --version` 看版本
|
|||
|
|
> 2. `which protoc-gen-go-grpc` 确认 plugin 可执行
|
|||
|
|
> 3. `grep go_package *.proto` 检查模块路径
|
|||
|
|
> 4. 跑 `go generate` 而非手动 protoc——Makefile 更稳定
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[hhs/gRPC/2. gRPC 核心篇/05-RPC 调用模式总览]]
|
|||
|
|
- [[hhs/gRPC/2. gRPC 核心篇/07-HTTP2 传输原理]]
|
|||
|
|
- [[hhs/gRPC/6. 工程实践篇/17-protoc 工具链与 Makefile]]
|