用一个 Go 服务器同时支持 REST、gRPC、gRPC-Web 和 Connect 客户端。本文使用claude code编写
什么是 vanguard-go?
vanguard-go 是 ConnectRPC 团队开发的一个 Go 中间件库,核心功能是协议转码(Transcoding):它能让你只写一套 RPC 服务实现,就同时支持多种客户端协议:
| 客户端协议 | 是否支持 |
|---|---|
| Connect Protocol | ✅ |
| gRPC | ✅ |
| gRPC-Web | ✅ |
| REST + JSON(HTTP 转码) | ✅ |
与 gRPC-Gateway 不同,vanguard 直接作为 net/http 中间件运行,无需额外的代理进程,性能更高,集成更简单。
核心概念
理解这三个概念就掌握了 vanguard 的精髓:
Service(服务):对一个 Protobuf RPC 服务的配置包装,包含服务的 schema(用于协议转换)和实际的 HTTP 处理器。
Transcoder(转码器):将一组 Service 包装成 http.Handler,自动处理所有协议转换逻辑,也充当路由器。
HTTP Transcoding Annotations(HTTP 转码注解):在 .proto 文件里用 google.api.http 注解将 RPC 方法映射到 RESTful 路径,这是支持 REST 客户端的关键。
快速开始
1. 安装依赖
go get connectrpc.com/vanguard
go get connectrpc.com/connect
如果你使用 gRPC-Go 服务器,还需要:
go get connectrpc.com/vanguard/vanguardgrpc
2. 定义 Protobuf 服务
这是一个图书馆服务的例子(proto/library/v1/library.proto):
syntax = "proto3";
package library.v1;
import "google/api/annotations.proto";
option go_package = "example/gen/library/v1;libraryv1";
// 书籍消息
message Book {
string name = 1; // 格式: shelves/{shelf}/books/{book}
string title = 2;
string author = 3;
}
// 获取书籍请求
message GetBookRequest {
string name = 1;
}
// 创建书籍请求
message CreateBookRequest {
string parent = 1; // 格式: shelves/{shelf}
Book book = 2;
}
// 书籍列表请求
message ListBooksRequest {
string parent = 1;
int32 page_size = 2;
}
message ListBooksResponse {
repeated Book books = 1;
}
// 定义服务,关键是 google.api.http 注解
service LibraryService {
// 获取书籍 - 映射到 GET /v1/{name=shelves/*/books/*}
rpc GetBook(GetBookRequest) returns (Book) {
option (google.api.http) = {
get: "/v1/{name=shelves/*/books/*}"
};
}
// 创建书籍 - 映射到 POST /v1/{parent=shelves/*}/books
rpc CreateBook(CreateBookRequest) returns (Book) {
option (google.api.http) = {
post: "/v1/{parent=shelves/*}/books"
body: "book"
};
}
// 列出书籍 - 映射到 GET /v1/{parent=shelves/*}/books
rpc ListBooks(ListBooksRequest) returns (ListBooksResponse) {
option (google.api.http) = {
get: "/v1/{parent=shelves/*}/books"
};
}
}
说明:
google.api.http注解告诉 vanguard 如何将 REST 请求路由到对应的 RPC 方法。路径模板中的{name=shelves/*/books/*}会自动将 URL 路径段提取到 Protobuf 字段中。
3. 生成代码
使用 buf 工具(推荐)或 protoc 生成 Go 代码:
buf.gen.yaml 配置:
version: v2
plugins:
- local: [go, tool, protoc-gen-go]
out: gen
opt: paths=source_relative
- local: [go, tool, protoc-gen-connect-go]
out: gen
opt:
- paths=source_relative
- simple
buf generate
生成后你会得到:
gen/library/v1/library.pb.go— Protobuf 消息类型gen/library/v1/libraryv1connect/library.connect.go— Connect 服务接口和工厂函数
4. 实现服务逻辑
// server/library_service.go
package server
import (
"context"
"fmt"
"connectrpc.com/connect"
libraryv1 "example/gen/library/v1"
)
// LibraryServiceImpl 实现了 LibraryService 接口
type LibraryServiceImpl struct {
// 实际应用中这里放数据库连接等
books map[string]*libraryv1.Book
}
func NewLibraryServiceImpl() *LibraryServiceImpl {
return &LibraryServiceImpl{
books: map[string]*libraryv1.Book{
"shelves/top/books/1": {
Name: "shelves/top/books/1",
Title: "三体",
Author: "刘慈欣",
},
},
}
}
// GetBook 获取单本书
func (s *LibraryServiceImpl) GetBook(
ctx context.Context,
req *connect.Request[libraryv1.GetBookRequest],
) (*connect.Response[libraryv1.Book], error) {
book, ok := s.books[req.Msg.Name]
if !ok {
return nil, connect.NewError(connect.CodeNotFound,
fmt.Errorf("book %q not found", req.Msg.Name))
}
return connect.NewResponse(book), nil
}
// CreateBook 创建新书
func (s *LibraryServiceImpl) CreateBook(
ctx context.Context,
req *connect.Request[libraryv1.CreateBookRequest],
) (*connect.Response[libraryv1.Book], error) {
book := req.Msg.Book
name := fmt.Sprintf("%s/books/%d", req.Msg.Parent, len(s.books)+1)
book.Name = name
s.books[name] = book
return connect.NewResponse(book), nil
}
// ListBooks 列出书籍
func (s *LibraryServiceImpl) ListBooks(
ctx context.Context,
req *connect.Request[libraryv1.ListBooksRequest],
) (*connect.Response[libraryv1.ListBooksResponse], error) {
var books []*libraryv1.Book
for _, b := range s.books {
books = append(books, b)
}
return connect.NewResponse(&libraryv1.ListBooksResponse{Books: books}), nil
}
5. 用 vanguard 包装服务(核心步骤)
// main.go
package main
import (
"log"
"net/http"
"connectrpc.com/vanguard"
"example/gen/library/v1/libraryv1connect"
"example/server"
)
func main() {
impl := server.NewLibraryServiceImpl()
// 步骤 1:用 Connect 创建处理器,返回 (路径, http.Handler)
path, handler := libraryv1connect.NewLibraryServiceHandler(impl)
// 步骤 2:创建 vanguard.Service,这是关键的包装步骤
// vanguard 会自动从已注册的 Protobuf schema 中读取 HTTP 注解
svc := vanguard.NewService(path, handler)
// 步骤 3:创建 Transcoder,它实现了 http.Handler
transcoder, err := vanguard.NewTranscoder([]*vanguard.Service{svc})
if err != nil {
log.Fatalf("创建 transcoder 失败: %v", err)
}
// 步骤 4:启动服务器
// 现在这个服务器同时支持 Connect、gRPC、gRPC-Web 和 REST 请求!
log.Println("服务器启动在 :8080")
if err := http.ListenAndServe(":8080", transcoder); err != nil {
log.Fatalf("服务器错误: %v", err)
}
}
就这么简单!三步完成多协议支持。
使用场景详解
场景一:Connect 服务器 + REST 支持(最常见)
上面的快速开始已经覆盖了这个场景。启动后,你可以用 REST 方式访问:
# 创建书籍(REST POST)
curl -X POST http://localhost:8080/v1/shelves/top/books \
-H "Content-Type: application/json" \
-d '{"title": "流浪地球", "author": "刘慈欣"}'
# 获取书籍(REST GET)
curl http://localhost:8080/v1/shelves/top/books/1
# 列出书籍(REST GET)
curl http://localhost:8080/v1/shelves/top/books
同时,gRPC 和 Connect 客户端也能正常工作,无需任何额外配置。
场景二:为现有 gRPC-Go 服务添加多协议支持
如果你已有使用 google.golang.org/grpc 构建的服务,使用 vanguardgrpc 子包:
package main
import (
"log"
"net/http"
"connectrpc.com/vanguard"
"connectrpc.com/vanguard/vanguardgrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/encoding"
"google.golang.org/protobuf/encoding/protojson"
libraryv1 "example/gen/library/v1"
"example/server"
)
func main() {
// 可选:为 gRPC 服务器注册 JSON 编解码器
encoding.RegisterCodec(vanguardgrpc.NewCodec(&vanguard.JSONCodec{
MarshalOptions: protojson.MarshalOptions{EmitUnpopulated: true},
UnmarshalOptions: protojson.UnmarshalOptions{DiscardUnknown: true},
}))
// 创建标准 gRPC 服务器
grpcServer := grpc.NewServer()
impl := server.NewLibraryServiceImpl()
libraryv1.RegisterLibraryServiceServer(grpcServer, impl)
// 用 vanguard 包装 gRPC 服务器
// 这会自动发现 grpcServer 中所有已注册的服务
transcoder, err := vanguardgrpc.NewTranscoder(grpcServer)
if err != nil {
log.Fatalf("创建 transcoder 失败: %v", err)
}
// 启动 HTTP/2 服务器(gRPC 需要 HTTP/2)
log.Println("服务器启动在 :8080")
if err := http.ListenAndServeTLS(":8080", "cert.pem", "key.pem", transcoder); err != nil {
log.Fatalf("服务器错误: %v", err)
}
}
场景三:反向代理模式(代理旧版 REST 服务)
vanguard 还能将 RPC 请求代理转发到后端 REST 服务,适合渐进式迁移:
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
"connectrpc.com/vanguard"
"example/gen/library/v1/libraryv1connect"
)
func main() {
// 后端旧版 REST 服务地址
backendURL, _ := url.Parse("http://legacy-backend:9090")
proxy := httputil.NewSingleHostReverseProxy(backendURL)
// 将 Connect/gRPC 请求转码后代理到 REST 后端
svc := vanguard.NewService(
libraryv1connect.LibraryServiceName,
proxy, // 使用反向代理作为 handler
)
transcoder, err := vanguard.NewTranscoder([]*vanguard.Service{svc})
if err != nil {
log.Fatalf("创建 transcoder 失败: %v", err)
}
log.Println("代理服务器启动在 :8080")
http.ListenAndServe(":8080", transcoder)
}
在这个场景下,客户端发来的 gRPC 或 Connect 请求,会被 vanguard 转码成 REST 请求,再转发到旧版后端。
高级配置
限制某个服务支持的协议
默认情况下,vanguard 会为每个服务启用所有协议。你可以通过 ServiceOption 限制:
import "connectrpc.com/vanguard"
svc := vanguard.NewService(
path, handler,
// 只允许 Connect 和 REST,拒绝原生 gRPC 请求
vanguard.WithTargetProtocols(
vanguard.ProtocolConnect,
vanguard.ProtocolGRPCWeb,
),
// 只允许 JSON 格式,不允许 Protobuf 二进制
vanguard.WithTargetCodecs("json"),
)
使用动态 Schema(不依赖代码生成)
当 Protobuf schema 在运行时才能确定时,用 NewServiceWithSchema:
import (
"google.golang.org/protobuf/reflect/protoreflect"
"connectrpc.com/vanguard"
)
// schema 是通过反射 API 或服务发现动态获取的
var schema protoreflect.ServiceDescriptor = getSchemaFromSomewhere()
svc := vanguard.NewServiceWithSchema(schema, handler)
添加全局 Transcoder 选项
transcoder, err := vanguard.NewTranscoder(
services,
// 统一设置所有服务的未知字段处理策略
vanguard.WithUnknownRequestFields(vanguard.UnknownFieldHandlingDiscard),
)
与其他 HTTP 中间件组合
vanguard 的 Transcoder 实现了 http.Handler,可以直接套用任何标准中间件:
// 例如添加日志、CORS、鉴权等中间件
handler := loggingMiddleware(
corsMiddleware(
authMiddleware(transcoder),
),
)
http.ListenAndServe(":8080", handler)
HTTP 转码注解速查
| 场景 | 注解写法 |
|---|---|
| GET 请求,路径参数 | get: "/v1/{name=shelves/*}" |
| POST 请求,Body 为某字段 | post: "/v1/items" + body: "item" |
| POST 请求,Body 为整个请求 | post: "/v1/items" + body: "*" |
| PUT 请求,更新资源 | put: "/v1/{book.name=shelves/*/books/*}" + body: "book" |
| DELETE 请求 | delete: "/v1/{name=shelves/*/books/*}" |
| 额外路径绑定 | 在注解中加 additional_bindings { get: "/v2/..." } |
路径参数提取示例:
// URL: /v1/shelves/science/books/42
// 会将 name 字段设为 "shelves/science/books/42"
rpc GetBook(GetBookRequest) returns (Book) {
option (google.api.http) = {
get: "/v1/{name=shelves/*/books/*}"
};
}
// Query 参数会自动映射到其他未被路径占用的字段
// URL: /v1/shelves/top/books?page_size=10&page_token=abc
// page_size 和 page_token 会自动从 query string 中解析
rpc ListBooks(ListBooksRequest) returns (ListBooksResponse) {
option (google.api.http) = {
get: "/v1/{parent=shelves/*}/books"
};
}
完整项目结构示例
my-service/
├── proto/
│ └── library/v1/
│ └── library.proto # 定义服务和 HTTP 注解
├── gen/
│ └── library/v1/
│ ├── library.pb.go # 生成的消息类型
│ └── libraryv1connect/
│ └── library.connect.go # 生成的 Connect 接口
├── server/
│ └── library_service.go # 业务逻辑实现
├── main.go # 服务入口,配置 vanguard
├── buf.gen.yaml # buf 代码生成配置
└── go.mod
常见问题
Q: 为什么 vanguard.NewService 返回错误说找不到服务 schema?
A: vanguard 需要服务的 Protobuf 描述符已注册到 Go 的 Protobuf 全局注册表中。使用 protoc-gen-go 生成的代码会在 init() 函数中自动注册。确保你 import 了生成的 *.pb.go 文件所在的包,即使不直接使用它。
Q: 不写 HTTP 注解,REST 客户端还能用吗?
A: 不能。vanguard 的 REST 路由依赖 google.api.http 注解。不过 Connect 和 gRPC 客户端不需要注解就能工作。
Q: vanguard 和 gRPC-Gateway 有什么区别?
A: gRPC-Gateway 是一个独立的代理进程,需要在 gRPC 服务前面额外部署。vanguard 是一个 Go 库,作为中间件直接嵌入你的服务进程,无需额外的网络跳转,延迟更低,部署更简单。
Q: 支持流式 RPC 吗?
A: Connect 协议的流式 RPC 完全支持。对于 REST 场景,单次 RPC(Unary)映射良好,流式需要 SSE(Server-Sent Events)支持,具体取决于注解配置。