复习顺序:
-
网络通信:为什么两个服务需要互相发送数据?
-
序列化:什么是 JSON,为什么还需要 Protobuf?
-
Proto:为什么要专门定义一个
.proto文件? -
RPC:什么是远程过程调用?
-
gRPC:它到底解决什么问题?
-
将知识与 Go + Kratos 项目串联
微服务
- 第一章:理解最底层的问题------两个程序如何通信?
-
- [1. 如果两个功能都在同一个 Go 程序里](#1. 如果两个功能都在同一个 Go 程序里)
- [2. 但是两个服务在不同服务器上呢?](#2. 但是两个服务在不同服务器上呢?)
- [第二章:什么是序列化?为什么需要 Protobuf?](#第二章:什么是序列化?为什么需要 Protobuf?)
-
- [1. 你熟悉的 JSON 就是一种序列化格式](#1. 你熟悉的 JSON 就是一种序列化格式)
- [2. Protobuf 解决什么问题?](#2. Protobuf 解决什么问题?)
- [第三章:Proto 又是什么?](#第三章:Proto 又是什么?)
-
- [1. `.proto` 是数据结构的定义文件](#1.
.proto是数据结构的定义文件) - [2. 为什么一定要有 `= 1`、`= 2`?](#2. 为什么一定要有
= 1、= 2?) - [3. Proto 和 Protobuf 的准确关系](#3. Proto 和 Protobuf 的准确关系)
- [1. `.proto` 是数据结构的定义文件](#1.
- [第四章:RPC 是什么?为什么需要它?](#第四章:RPC 是什么?为什么需要它?)
-
- [1. RPC 的本质是远程函数调用](#1. RPC 的本质是远程函数调用)
- [2. 那么 gRPC 又是什么?](#2. 那么 gRPC 又是什么?)
- [第五章:gRPC、HTTP、Protobuf 到底是什么关系?](#第五章:gRPC、HTTP、Protobuf 到底是什么关系?)
- [第六章:用一份 `.proto` 把三种概念串起来](#第六章:用一份
.proto把三种概念串起来) -
- [第一步:开发者先定义 `.proto`](#第一步:开发者先定义
.proto) - [第二步:通过生成器生成 Go 文件](#第二步:通过生成器生成 Go 文件)
- [第三步:Go 服务调用远程商品服务](#第三步:Go 服务调用远程商品服务)
- [第四步:Kratos 的 HTTP 生成代码又起什么作用?](#第四步:Kratos 的 HTTP 生成代码又起什么作用?)
- 知识框架
- [第一步:开发者先定义 `.proto`](#第一步:开发者先定义
第一章:理解最底层的问题------两个程序如何通信?
假设你正在开发一个商品管理系统。
系统中有两个服务:
- 订单服务 OrderService:负责用户下单。
- 商品服务 ProductService:负责管理商品信息和价格。
用户购买一台服务器,订单服务必须先向商品服务查询售价。

1. 如果两个功能都在同一个 Go 程序里
事情非常简单:
go
func GetProductPrice(id int64) int64 {
return 100
}
func main() {
price := GetProductPrice(1001)
fmt.Println(price)
}
直接调用函数就行。
因为 GetProductPrice() 和 main() 在同一个程序中,能够按照 Go 的调用规则完成参数传递和结果返回。
2. 但是两个服务在不同服务器上呢?
假设:
txt
订单服务:10.0.0.1:8080
商品服务:10.0.0.2:9000
此时订单服务不能直接调用另一个进程中的普通 Go 函数。
因为两者有独立的进程地址空间,甚至运行在不同机器上。
所以需要一种通信机制:
- 把请求参数转换成能够传输的数据。
- 通过网络发送到商品服务。
- 商品服务接收并解析数据。
- 执行查询商品的业务逻辑。
- 把结果通过网络传回订单服务。
这就是后面所有知识的出发点。
第二章:什么是序列化?为什么需要 Protobuf?
这是理解 Protobuf 的第一道门槛。
假设 Go 中有个商品:
go
# 结构体
type Product struct {
ID int64
Name string
Price int64
}
# 实例
product := Product{
ID: 1001,
Name: "云服务器",
Price: 100,
}
现在你想把 product 发送给另一台服务器。
问题来了:
网络发送的是字节数据,不是 Go 语言中的结构体对象本身。
于是需要两个操作:

序列化就是把程序中的数据转换成适合存储或传输的字节表示;反序列化就是反过来。
1. 你熟悉的 JSON 就是一种序列化格式
例如:
json
{
"id": 1001,
"name": "云服务器",
"price": 100
}
它有一个显著特点:人能够直接阅读。
Go 使用:
go
data, err := json.Marshal(product)
就能把对象编码成 JSON 字节。
接收方使用 json.Unmarshal() 还原到 Go 结构体。
那么问题来了:既然 JSON 已经能够完成通信,为什么还要发明 Protobuf?
2. Protobuf 解决什么问题?
Protobuf 的全称是 Protocol Buffers,是 Google 推出的结构化数据序列化技术。
它特别适合服务之间高频率传输结构化数据。
和 JSON 相比:
| 对比项 | JSON | Protobuf(二进制格式) |
|---|---|---|
| 数据表示 | 可读文本 | 紧凑二进制 |
| 数据体积 | 通常较大 | 通常较小 |
| 编码解码 | 通常较简单 | 通常更高效 |
| 是否方便人类阅读 | 是 | 否 |
| 数据结构约定 | 可用 Schema,但非必需 | 依赖消息 Schema |
| 跨语言支持 | 是 | 是 |
这里的"小"和"快"是通常情况下的优势,不意味着任何数据和程序中 Protobuf 都一定更快。
举个例子:
JSON 可能会反复传输 "price" 这个字段名称。
而 Protobuf 的二进制编码可以通过字段编号来标识该字段,避免每次传输完整名称。

所以你可以先这样记:
Protobuf 主要负责高效地表达、编码、解码结构化消息。它本身不负责把数据发送到另一台机器。
它不是一个网络通信框架。
第三章:Proto 又是什么?
现在遇到一个新问题。
假设我们收到这样的 Protobuf 二进制数据:
txt
08 64
你怎么知道这里面的 100 是商品价格,还是商品数量,或者用户年龄?
仅凭这两个字节,你无法得知它的业务含义。
因此,发送方与接收方必须提前约定数据结构。
这就是 .proto 文件的用途。
1. .proto 是数据结构的定义文件
我们写:
proto
syntax = "proto3";
message Product {
int64 id = 1;
string name = 2;
int64 price = 3;
}
逐行理解:
| 语法 | 含义 |
|---|---|
syntax = "proto3" |
使用 proto3 语法 |
message Product |
定义名为 Product 的消息类型 |
int64 id = 1 |
id 是整数类型,字段编号为 1 |
string name = 2 |
name 是字符串类型,字段编号为 2 |
int64 price = 3 |
price 是整数类型,字段编号为 3 |
这很像 Go 中的:
type Product struct {
ID int64
Name string
Price int64
}
但一个重要区别是:
Go 结构体主要给 Go 程序使用;.proto 定义的消息可以被 Go、Java、Python、C++ 等不同语言共同理解。
例如,一个 Go 订单服务可以和 Java 商品服务使用相同的 Protobuf 消息定义。
2. 为什么一定要有 = 1、= 2?
这是初学者最容易忽略的一点。
go
int64 id = 1;
这里的 1:
- 不是赋值。
- 不是默认值。
- 不是字段位置的随意标记。
- 它是 Protobuf 编码中识别该字段的重要编号。
所以它一旦投入使用,通常就不能随意更改。删除字段时也应保留其编号,防止未来误用。
3. Proto 和 Protobuf 的准确关系
这里有一个术语上的细节。
严格来讲,Proto 通常是开发人员对 .proto 文件或其定义语法的简称;Protobuf 是整个技术体系的名称,包含定义语言、代码生成、运行时和序列化格式。
因此,两者不是完全平级、互不相关的两项技术。
你可以把它们理解为:
.proto:设计好数据结构与服务接口的源文件。- Protobuf 编译器和运行时:根据定义生成代码,以及编码、解码消息的工具。
- Protobuf 二进制格式:消息实际传输或存储的一种形式。
这三者共同组成你日常所说的 Protobuf 技术体系。
第四章:RPC 是什么?为什么需要它?
现在我们已经知道:
.proto可以定义数据结构。- Protobuf 可以把结构化消息转换为二进制数据。
- 网络可以把字节传给另一台服务器。
但还有一个问题:
订单服务怎样让商品服务执行指定的函数?
这就需要理解 RPC。
1. RPC 的本质是远程函数调用
RPC 全称:Remote Procedure Call,中文叫远程过程调用。
简单来说:
RPC 让一个程序可以像调用本地函数一样,调用另一个进程或另一台服务器提供的方法。
例如,订单服务想调用商品服务:
go
price := productService.GetPrice(1001)
表面上看只是普通函数调用。
实际上可能发生了以下过程:

需要特别注意:RPC 并没有把远程函数真的变成本地函数。它只是隐藏了一部分通信细节。
所以 RPC 仍然可能遇到网络超时、服务不可用、连接断开等问题。
2. 那么 gRPC 又是什么?
RPC 是一种调用方式或设计思想。
gRPC 则是实现 RPC 的一个具体框架。
你可以把关系记成:
RPC(远程调用的思想)
│
├── gRPC
├── Apache Thrift
└── 其他 RPC 框架
切记! 因为是要远程调用,所以需要生成两份。如:
gRPC 通常根据同一份 proto,生成客户端调用代码,以及服务端接口、注册和分发代码。双方使用各自需要的部分,服务端业务逻辑仍由开发者实现。
gRPC 主要帮你处理:
- 客户端调用远程服务的方法。
- 发送请求和接收响应。
- 对接序列化与反序列化机制。
- 服务端请求分发。
- 超时、取消、状态码和流式通信等机制。
gRPC 默认使用 Protobuf 定义服务接口和消息,但并不是只能使用 Protobuf。
第五章:gRPC、HTTP、Protobuf 到底是什么关系?
这里我必须纠正一个特别容易形成的误区:
gRPC 和 HTTP 不是完全对立的两种底层协议。
gRPC 通常就是运行在 HTTP/2 之上的。
你可以从下面的通信协议分层理解:

因此,最容易理解的区别是:
普通 REST 风格接口通常强调"访问资源":
GET /v1/products/1001
gRPC 则强调"调用服务方法":
ProductService.GetProduct(
id = "1001"
)
但两者都涉及网络通信。
gRPC 的能力也不限于普通的一次请求、一次响应。它支持服务端流、客户端流、双向流,一共四种 RPC 调用形式。
你目前开发 Kratos 业务时,先掌握最常见的 **一元 RPC(Unary RPC)**就足够了,即一个请求对应一个响应。
第六章:用一份 .proto 把三种概念串起来
到这里,我们终于可以完整理解 Kratos 项目中的文件了。
假设现在需要提供查询商品接口。
第一步:开发者先定义 .proto
syntax = "proto3";
package product.v1;
option go_package = "example.com/shop/api/product/v1;v1";
service ProductService {
rpc GetProduct(GetProductRequest)
returns (GetProductReply);
}
message GetProductRequest {
int64 id = 1;
}
message GetProductReply {
int64 id = 1;
string name = 2;
int64 price = 3;
}
这里需要区分两个概念:
message 是数据结构,service 是服务接口。
例如:
message GetProductRequest {
int64 id = 1;
}
定义的是请求参数的数据结构。
而:
service ProductService {
rpc GetProduct(GetProductRequest)
returns (GetProductReply);
}
定义的是一个远程方法,接受 GetProductRequest,返回 GetProductReply。
第二步:通过生成器生成 Go 文件
例如:
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
product.proto
这是原生生成 Go 消息与 gRPC 代码的示意命令,假设相关插件已安装、输入文件位于当前目录。
它会生成:
product.pb.go
product_grpc.pb.go
你可以理解为:
protoc-gen-go:根据消息定义生成 Go 类型和 Protobuf 支持代码。protoc-gen-go-grpc:根据服务定义生成 gRPC 客户端和服务端接口。
而 Kratos 再提供 HTTP 生成插件,让同样的服务接口定义可以建立 HTTP 映射。
第三步:Go 服务调用远程商品服务
订单服务中:
client := v1.NewProductServiceClient(conn)
reply, err := client.GetProduct(
ctx,
&v1.GetProductRequest{
Id: 1001,
},
)
if err != nil {
return err
}
fmt.Println(reply.Name)
这里 conn 是已经建立好的 gRPC 客户端连接。
只看 client.GetProduct() 这一行,你会觉得是普通方法调用。
但底层实际是:
client.GetProduct()
|
v
gRPC 客户端生成代码
|
v
Protobuf 编码请求
|
v
通过 HTTP/2 传输
|
v
商品服务的 gRPC Server
|
v
解码请求 + 分发到业务方法
|
v
ProductService.GetProduct()
|
v
返回响应
所以现在再回头看:
product.pb.go 是数据结构与 Protobuf 支持。
product_grpc.pb.go 是 gRPC 客户端、服务接口与服务端绑定。
至于数据库查询、业务校验、事务等逻辑,是开发者在业务实现中完成的。
第四步:Kratos 的 HTTP 生成代码又起什么作用?
如果你希望前端也能通过 HTTP 请求访问商品,就可以在 RPC 上添加:
import "google/api/annotations.proto";
service ProductService {
rpc GetProduct(GetProductRequest)
returns (GetProductReply) {
option (google.api.http) = {
get: "/v1/products/{id}"
};
}
}
然后使用 Kratos HTTP 生成插件生成 product_http.pb.go。
最终两种请求:

而且 Kratos 默认 HTTP 场景主要使用 JSON,gRPC 场景一般使用 Protobuf 二进制。两者可以共享同一套 Protobuf 消息类型和业务实现。
知识框架
| 概念 | 必须记住的本质 |
|---|---|
| 网络通信 | 不同进程通过网络交换字节 |
| 序列化 | 把程序数据转换为可传输的字节 |
| Proto | 用统一语言描述消息结构和服务接口 |
| Protobuf | 消息定义、代码生成、序列化技术体系 |
| RPC | 让程序调用远程服务的方法 |
| gRPC | 实现远程调用的框架,通常使用 Protobuf |
| Kratos | 为 Go 微服务提供 HTTP、gRPC 等基础设施的框架 |
其实亲自用 Go 写一个最简单的 gRPC 客户端和服务端,最直观。
可以先不用 Kratos 框架,只用原生 gRPC。
可以更直观的看到 .proto 怎么变成 .pb.go、客户端怎么调用服务端、Protobuf 在哪个环节编码和解码。