Appearance
gRPC-Gateway 与 REST 集成
gRPC 非常适合内部服务间通信,但浏览器、移动端和第三方调用方往往更习惯 RESTful API。gRPC-Gateway 解决了这个矛盾:它在 proto 文件中用注解声明 HTTP 映射,自动生成反向代理代码,让同一个 gRPC 服务同时对外提供 RESTful 接口。本篇从插件安装、注解配置、代码生成到双端口服务运行,完整讲解 gRPC-Gateway 的生产级用法。
一、gRPC-Gateway 简介
1. 为什么需要 gRPC-Gateway
┌──────────┐ REST/JSON ┌──────────────────┐ gRPC/Protobuf ┌──────────┐
│ 浏览器 │ ──────────────────> │ gRPC-Gateway │ ───────────────────> │ gRPC │
│ 移动端 │ <────────────────── │ (反向代理) │ <─────────────────── │ 服务端 │
│ 第三方 │ │ 自动生成 │ │ │
└──────────┘ └──────────────────┘ └──────────┘gRPC-Gateway 的核心价值:
- 一份 proto,两种协议:业务逻辑只写一次 gRPC 实现,REST 接口自动生成。
- 强类型契约:REST 接口的请求/响应结构由 proto 定义,不会与 gRPC 不一致。
- 自动文档:配合 protoc-gen-openapiv2 可生成 Swagger/OpenAPI 文档。
- 零手写转换:不需要手动写 HTTP handler 再调 gRPC client,代理代码全自动生成。
2. 工作原理
gRPC-Gateway 生成的代码是一个 HTTP server,它接收 RESTful 请求后:
- 解析 HTTP 路径参数、query string、body。
- 将其组装成 Protobuf 消息。
- 通过 gRPC client 调用真正的 gRPC 服务。
- 将 gRPC 响应序列化为 JSON 返回给 HTTP 客户端。
- 将 gRPC 状态码映射为 HTTP 状态码。
二、安装 protoc-gen-grpc-gateway 插件
bash
# 安装 gRPC-Gateway 插件
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
# 安装 OpenAPI/Swagger 文档生成插件
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
# 确认安装成功
protoc-gen-grpc-gateway --version
protoc-gen-openapiv2 --version此外还需要引入 gRPC-Gateway 的 proto 依赖:
bash
# 方式一:go get 获取 proto 依赖
go get github.com/grpc-ecosystem/grpc-gateway/v2
# 方式二:手动下载 google/api 注解 proto 文件
# 从 https://github.com/googleapis/googleapis 获取 google/api/http.proto 和 annotations.protogo
package main
import (
"fmt"
"os/exec"
)
func main() {
// 检查插件是否安装
plugins := []string{"protoc-gen-grpc-gateway", "protoc-gen-openapiv2", "protoc", "protoc-gen-go", "protoc-gen-go-grpc"}
for _, p := range plugins {
cmd := exec.Command(p, "--version")
output, err := cmd.Output()
if err != nil {
fmt.Printf("[MISSING] %s\n", p)
continue
}
fmt.Printf("[OK] %s: %s", p, string(output))
}
}三、配置 google.api.http 注解
gRPC-Gateway 通过 google.api.http 注解声明 gRPC 方法与 HTTP 路由的映射关系。
protobuf
// user.proto
syntax = "proto3";
package user.v1;
option go_package = "github.com/example/user-service/api/v1;userv1";
import "google/api/annotations.proto";
message User {
int64 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
message CreateUserRequest {
string name = 1;
string email = 2;
int32 age = 3;
}
message GetUserRequest {
int64 id = 1;
}
message ListUsersRequest {
int32 page = 1;
int32 page_size = 2;
}
message ListUsersResponse {
repeated User users = 1;
int32 total = 2;
}
message DeleteUserRequest {
int64 id = 1;
}
service UserService {
// POST /v1/users -> CreateUser
rpc CreateUser(CreateUserRequest) returns (User) {
option (google.api.http) = {
post: "/v1/users"
body: "*"
};
}
// GET /v1/users/{id} -> GetUser
rpc GetUser(GetUserRequest) returns (User) {
option (google.api.http) = {
get: "/v1/users/{id}"
};
}
// GET /v1/users -> ListUsers
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse) {
option (google.api.http) = {
get: "/v1/users"
};
}
// PATCH /v1/users/{id} -> UpdateUser (部分更新)
rpc UpdateUser(CreateUserRequest) returns (User) {
option (google.api.http) = {
patch: "/v1/users/{id}"
body: "*"
};
}
// DELETE /v1/users/{id} -> DeleteUser
rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
delete: "/v1/users/{id}"
};
}
}注解语法详解:
post/get/put/patch/delete:HTTP 方法 + 路径模板。{id}:路径参数,自动映射到请求消息的同名字段。body: "*":整个 HTTP body 作为请求消息(常用于 POST/PUT)。body: "user":HTTP body 映射到请求消息的user字段。- 不指定
body:仅路径参数和 query string 被使用。
路径参数与 query 参数的映射规则
protobuf
// 路径参数 {id} 映射到 GetUserRequest.id
// query 参数 ?page=1&page_size=10 映射到 ListUsersRequest.page 和 page_size
message SearchUsersRequest {
string keyword = 1; // -> ?keyword=xxx (query)
int32 page = 2; // -> ?page=1 (query)
int32 page_size = 3; // -> ?page_size=10 (query)
string sort = 4; // -> ?sort=name (query)
}
rpc SearchUsers(SearchUsersRequest) returns (ListUsersResponse) {
option (google.api.http) = {
get: "/v1/users/search/{keyword}" // keyword 既是路径参数又映射到字段
};
}
// GET /v1/users/search/alice?page=1&page_size=10&sort=name四、生成反向代理代码
bash
# 完整的 protoc 命令,生成 Go 消息代码 + gRPC 服务代码 + Gateway 代理代码 + OpenAPI 文档
protoc \
-I . \
-I third_party/googleapis \
--go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
--grpc-gateway_out=. --grpc-gateway_opt=paths=source_relative \
--openapiv2_out=./docs/openapi \
--openapiv2_opt=generate_unbound_methods=true \
api/v1/user.proto生成的文件:
user.pb.go:Protobuf 消息结构体。user_grpc.pb.go:gRPC 服务端接口和客户端 stub。user.pb.gw.go:Gateway 反向代理代码,包含RegisterUserServiceHandlerServer和RegisterUserServiceHandlerFromEndpoint。docs/openapi/user.swagger.json:OpenAPI/Swagger 文档。
go
package main
import (
"fmt"
"os/exec"
)
func main() {
// 演示代码生成命令的结构
cmd := exec.Command("protoc", "--version")
output, _ := cmd.Output()
fmt.Printf("protoc version: %s", output)
// 实际生成命令(注释展示,需在项目目录执行):
generateCmd := `protoc \
-I . \
-I third_party/googleapis \
--go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
--grpc-gateway_out=. --grpc-gateway_opt=paths=source_relative \
api/v1/user.proto`
fmt.Println("Generate command:")
fmt.Println(generateCmd)
// 常用的 grpc-gateway_opt 选项:
opts := []string{
"paths=source_relative # 生成到与 proto 相同的相对路径",
"logtostderr=true # 日志输出到 stderr",
"generate_unbound_methods=true # 为没有 http 注解的方法也生成代理",
"use_request_context=true # 使用 request context",
"allow_patch_feature=true # 支持 PATCH 方法的 FieldMask",
}
fmt.Println("\ngrpc-gateway options:")
for _, o := range opts {
fmt.Printf(" %s\n", o)
}
}五、运行 gRPC-Gateway 服务
Gateway 提供两种注册方式:直接注册服务端实现(进程内),或通过 gRPC endpoint 连接(独立进程)。
方式一:进程内直接注册(推荐简单场景)
go
package main
import (
"context"
"fmt"
"log"
"net/http"
"google.golang.org/grpc"
)
// 模拟生成的 Gateway 注册函数签名
// 实际由 protoc-gen-grpc-gateway 生成
// func RegisterUserServiceHandlerServer(ctx, mux, server UserServiceServer) error
// 模拟一个 gRPC 服务实现
type mockUserServer struct{}
func (s *mockUserServer) mustEmbedUnimplemented() {}
// main 中直接在同一进程内运行 Gateway + gRPC 服务
func main() {
// 方式一:Gateway 和 gRPC handler 在同一进程
// 适合开发环境或简单服务
ctx := context.Background()
mux := http.NewServeMux()
// 实际代码:
// err := userv1.RegisterUserServiceHandlerServer(ctx, mux, &userServiceImpl{})
// if err != nil { log.Fatal(err) }
fmt.Println("Gateway server (in-process) would listen on :8080")
log.Println(http.ListenAndServe(":8080", mux))
}方式二:通过 gRPC endpoint 连接(推荐生产环境)
go
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// 生产环境推荐:Gateway 和 gRPC 服务分开运行
// Gateway 作为独立进程,通过 gRPC 连接到后端服务
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
mux := http.NewServeMux()
// gRPC 服务地址
grpcAddr := "127.0.0.1:50051"
// 方式 A:注册时自动建立 gRPC 连接
// opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
// err := userv1.RegisterUserServiceHandlerFromEndpoint(ctx, mux, grpcAddr, opts)
// if err != nil { log.Fatal(err) }
// 方式 B:传入已建立的 ClientConn
conn, err := grpc.NewClient(grpcAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// err = userv1.RegisterUserServiceHandler(ctx, mux, conn)
// if err != nil { log.Fatal(err) }
fmt.Printf("Gateway proxy -> %s, listening on :8080\n", grpcAddr)
_ = http.ListenAndServe
_ = time.Second
}六、Swagger/OpenAPI 文档生成
go
package main
import (
"fmt"
"os/exec"
)
func main() {
// 生成 OpenAPI v2 文档(Swagger)
// protoc-gen-openapiv2 生成 .swagger.json 文件
cmd := `protoc \
-I . -I third_party/googleapis \
--openapiv2_out=./docs/swagger \
--openapiv2_opt=logtostderr=true \
--openapiv2_opt=generate_unbound_methods=true \
--openapiv2_opt=allow_delete_body=true \
api/v1/user.proto`
fmt.Println("OpenAPI generation command:")
fmt.Println(cmd)
// 常用 openapiv2 选项
opts := map[string]string{
"generate_unbound_methods": "为无 http 注解的方法也生成文档",
"allow_delete_body": "允许 DELETE 请求携带 body",
"simple_operation_ids": "用方法名作为 operationId(简化)",
"openapi_naming_strategy": "命名策略:fqn/legacy/simple",
"enum_type": "枚举类型:int32/string/as_string",
}
fmt.Println("\nOpenAPI options:")
for k, v := range opts {
fmt.Printf(" %-30s %s\n", k, v)
}
// 生成后可以用 swagger-ui 或 redoc 查看
fmt.Println("\n启动 Swagger UI:")
fmt.Println(" docker run -p 8081:8080 -e SWAGGER_JSON=/docs/user.swagger.json -v $(pwd)/docs/swagger:/docs swaggerapi/swagger-ui")
_ = exec.Command
}在 proto 中添加 Swagger 元信息
protobuf
// user.proto 中添加 Swagger 注解
import "protoc-gen-openapiv2/options/annotations.proto";
import "protoc-gen-openapiv2/options/openapiv2.proto";
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
info: {
title: "User Service API";
version: "1.0.0";
description: "User management service with gRPC + REST";
contact: {
name: "API Support";
email: "api@example.com";
};
};
schemes: HTTPS;
consumes: "application/json";
produces: "application/json";
};
message User {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
example: '{"id": 1, "name": "alice", "email": "alice@example.com", "age": 30}'
};
int64 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}七、gRPC + REST 双端口服务
生产环境最常见的模式:一个进程同时监听 gRPC 端口和 REST 端口。
go
package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// 模拟 gRPC 服务实现
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int32 `json:"age"`
}
type UserServiceServer interface {
CreateUser(ctx context.Context, name, email string, age int32) (*User, error)
GetUser(ctx context.Context, id int64) (*User, error)
}
type userServiceImpl struct {
users map[int64]*User
next int64
}
func (s *userServiceImpl) CreateUser(ctx context.Context, name, email string, age int32) (*User, error) {
s.next++
u := &User{ID: s.next, Name: name, Email: email, Age: age}
s.users[u.ID] = u
return u, nil
}
func (s *userServiceImpl) GetUser(ctx context.Context, id int64) (*User, error) {
u, ok := s.users[id]
if !ok {
return nil, fmt.Errorf("user %d not found", id)
}
return u, nil
}
// gRPC 服务监听
func runGRPCServer(addr string) *grpc.Server {
lis, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("gRPC listen: %v", err)
}
server := grpc.NewServer()
// userv1.RegisterUserServiceServer(server, &userServiceImpl{...})
go func() {
log.Printf("gRPC server listening on %s", lis.Addr())
if err := server.Serve(lis); err != nil {
log.Printf("gRPC serve: %v", err)
}
}()
return server
}
// Gateway HTTP 监听
func runGatewayServer(httpAddr, grpcAddr string) *http.Server {
ctx := context.Background()
mux := http.NewServeMux()
// 实际代码:
// opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
// err := userv1.RegisterUserServiceHandlerFromEndpoint(ctx, mux, grpcAddr, opts)
// if err != nil { log.Fatal(err) }
// 演示:手动注册一些路由
mux.HandleFunc("/v1/users", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
fmt.Fprintf(w, `{"users": [], "total": 0}`)
case http.MethodPost:
fmt.Fprintf(w, `{"id": 1, "name": "alice", "email": "alice@example.com", "age": 30}`)
}
})
mux.HandleFunc("/v1/users/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
fmt.Fprintf(w, `{"id": 1, "name": "alice", "email": "alice@example.com", "age": 30}`)
case http.MethodDelete:
w.WriteHeader(http.StatusNoContent)
}
})
server := &http.Server{
Addr: httpAddr,
Handler: mux,
}
go func() {
log.Printf("Gateway (REST) server listening on %s", httpAddr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("gateway serve: %v", err)
}
}()
return server
}
func main() {
grpcAddr := ":50051"
httpAddr := ":8080"
// 启动 gRPC 服务
grpcServer := runGRPCServer(grpcAddr)
// 启动 Gateway HTTP 服务
httpServer := runGatewayServer(httpAddr, grpcAddr)
// 优雅退出
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down...")
grpcServer.GracefulStop()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
httpServer.Shutdown(shutdownCtx)
log.Println("servers stopped")
_ = insecure.NewCredentials
_ = userServiceImpl{users: map[int64]*User{}, next: 0}
_ = UserServiceServer(&userServiceImpl{})
}八、自定义 HTTP 响应头
gRPC-Gateway 支持通过 metadata 自定义 HTTP 响应头和状态码。
go
package main
import (
"context"
"fmt"
"net/http"
"strings"
"google.golang.org/grpc/metadata"
)
// 服务端通过 metadata 设置响应头
func serverSetHeaders(ctx context.Context) context.Context {
// 设置自定义响应头
header := metadata.Pairs(
"x-custom-header", "custom-value",
"x-rate-limit-remaining", "42",
"x-version", "1.0.0",
)
// 注意:以 "x-http-" 前缀开头的 metadata key,
// Gateway 会将其作为 HTTP header 返回
// 也可以用 grpc.SetHeader(ctx, header) 设置
// 设置 Location header(用于 201 Created)
ctx = metadata.AppendToOutgoingContext(ctx, "x-http-code", "201")
ctx = metadata.AppendToOutgoingContext(ctx, "Location", "/v1/users/42")
return ctx
}
// Gateway 端自定义 header 处理
type headerMiddleware struct {
handler http.Handler
}
func (h *headerMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 添加 CORS 头
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Request-ID")
w.Header().Set("Access-Control-Max-Age", "3600")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
// 添加通用响应头
w.Header().Set("X-Request-ID", generateRequestID(r))
w.Header().Set("X-Response-Time", "fast")
h.handler.ServeHTTP(w, r)
}
func generateRequestID(r *http.Request) string {
if id := r.Header.Get("X-Request-ID"); id != "" {
return id
}
return fmt.Sprintf("req-%d", r.ContentLength)
}
// 自定义错误响应格式
type errorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Details any `json:"details,omitempty"`
}
func customErrorHandler(w http.ResponseWriter, err error) {
// 将 gRPC 错误转为自定义 JSON 格式
// 实际项目中可以用 httperror 库或自定义
msg := err.Error()
code := http.StatusInternalServerError
if strings.Contains(msg, "not found") {
code = http.StatusNotFound
} else if strings.Contains(msg, "invalid") {
code = http.StatusBadRequest
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
fmt.Fprintf(w, `{"code": %d, "message": "%s"}`, code, msg)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/v1/users", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Custom-Header", "from-gateway")
fmt.Fprintf(w, `{"id": 1, "name": "alice"}`)
})
// 包装中间件
handler := &headerMiddleware{handler: mux}
fmt.Println("Gateway with custom headers on :8080")
_ = http.ListenAndServe(":8080", handler)
_ = context.Background
_ = serverSetHeaders
}九、文件上传支持
gRPC-Gateway 对文件上传有两种方式:base64 编码到 JSON body,或 multipart/form-data。
protobuf
// 方式一:base64 编码(小文件)
message UploadAvatarRequest {
int64 user_id = 1;
bytes avatar_data = 2; // base64 编码的图片数据
string filename = 3;
string content_type = 4;
}
service UserService {
rpc UploadAvatar(UploadAvatarRequest) returns (UploadResponse) {
option (google.api.http) = {
post: "/v1/users/{user_id}/avatar"
body: "*"
};
}
}
// 方式二:multipart/form-data(大文件,需要额外配置)
// 使用 grpc-gateway 的 multipart 支持go
package main
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
)
// 处理文件上传的 HTTP handler
// gRPC-Gateway 对 multipart 的原生支持有限,通常需要自定义 handler
func uploadHandler(w http.ResponseWriter, r *http.Request) {
// 限制上传大小(10MB)
r.ParseMultipartForm(10 << 20)
file, header, err := r.FormFile("avatar")
if err != nil {
http.Error(w, "failed to read file: "+err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
// 读取文件内容
data, err := io.ReadAll(file)
if err != nil {
http.Error(w, "failed to read file data", http.StatusInternalServerError)
return
}
// 验证文件类型
allowedTypes := map[string]bool{
"image/jpeg": true,
"image/png": true,
"image/gif": true,
}
contentType := header.Header.Get("Content-Type")
if !allowedTypes[contentType] {
http.Error(w, "unsupported file type: "+contentType, http.StatusBadRequest)
return
}
// 验证文件大小
maxSize := int64(5 << 20) // 5MB
if header.Size > maxSize {
http.Error(w, "file too large (max 5MB)", http.StatusBadRequest)
return
}
// 保存文件
filename := fmt.Sprintf("avatar-%d%s", header.Size, filepath.Ext(header.Filename))
// 实际项目中保存到对象存储(如 S3/OSS)
err = os.WriteFile(filename, data, 0644)
if err != nil {
http.Error(w, "failed to save file", http.StatusInternalServerError)
return
}
// 返回 JSON 响应
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, `{
"filename": "%s",
"size": %d,
"content_type": "%s",
"url": "/uploads/%s"
}`, header.Filename, header.Size, contentType, filename)
}
// base64 方式的处理(适合小文件,通过 gRPC-Gateway 自动处理)
func base64UploadDemo() {
// 客户端发送:
// POST /v1/users/1/avatar
// Content-Type: application/json
// {"user_id": 1, "avatar_data": "<base64>", "filename": "a.png", "content_type": "image/png"}
// gRPC-Gateway 会自动将 JSON 的 avatar_data 字段 base64 解码为 bytes
// 演示 base64 编码
sampleData := []byte("fake-image-data")
encoded := base64.StdEncoding.EncodeToString(sampleData)
fmt.Printf("base64 encoded: %s\n", encoded)
decoded, _ := base64.StdEncoding.DecodeString(encoded)
fmt.Printf("base64 decoded: %s\n", string(decoded))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/v1/users/", func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/avatar") && r.Method == http.MethodPost {
uploadHandler(w, r)
return
}
http.NotFound(w, r)
})
fmt.Println("File upload server on :8080")
fmt.Println("Test: curl -F 'avatar=@photo.jpg' http://localhost:8080/v1/users/1/avatar")
base64UploadDemo()
_ = http.ListenAndServe
}十、完整示例:用户服务的 gRPC + REST 双端口实现
go
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// === 数据模型 ===
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int32 `json:"age"`
}
type UserStore struct {
mu sync.RWMutex
users map[int64]*User
next int64
}
func NewUserStore() *UserStore {
return &UserStore{users: make(map[int64]*User), next: 1}
}
func (s *UserStore) Create(name, email string, age int32) *User {
s.mu.Lock()
defer s.mu.Unlock()
s.next++
u := &User{ID: s.next, Name: name, Email: email, Age: age}
s.users[u.ID] = u
return u
}
func (s *UserStore) Get(id int64) (*User, error) {
s.mu.RLock()
defer s.mu.RUnlock()
u, ok := s.users[id]
if !ok {
return nil, status.Errorf(codes.NotFound, "user %d not found", id)
}
return u, nil
}
func (s *UserStore) List(page, pageSize int32) ([]*User, int32) {
s.mu.RLock()
defer s.mu.RUnlock()
all := make([]*User, 0, len(s.users))
for _, u := range s.users {
all = append(all, u)
}
total := int32(len(all))
start := (page - 1) * pageSize
if start >= total {
return []*User{}, total
}
end := start + pageSize
if end > total {
end = total
}
return all[start:end], total
}
func (s *UserStore) Delete(id int64) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.users[id]; !ok {
return status.Errorf(codes.NotFound, "user %d not found", id)
}
delete(s.users, id)
return nil
}
// === gRPC 服务实现 ===
type userServiceServer struct {
store *UserStore
}
func (s *userServiceServer) CreateUser(ctx context.Context, name, email string, age int32) (*User, error) {
if name == "" {
return nil, status.Error(codes.InvalidArgument, "name is required")
}
user := s.store.Create(name, email, age)
// 通过 metadata 设置 Location header(Gateway 会透传)
_ = metadata.NewIncomingContext(ctx, metadata.Pairs("Location", fmt.Sprintf("/v1/users/%d", user.ID)))
return user, nil
}
func (s *userServiceServer) GetUser(ctx context.Context, id int64) (*User, error) {
return s.store.Get(id)
}
func (s *userServiceServer) ListUsers(ctx context.Context, page, pageSize int32) ([]*User, int32) {
return s.store.List(page, pageSize)
}
func (s *userServiceServer) DeleteUser(ctx context.Context, id int64) error {
return s.store.Delete(id)
}
// === gRPC 服务端 ===
func runGRPC(addr string, store *UserStore) *grpc.Server {
lis, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("gRPC listen: %v", err)
}
server := grpc.NewServer()
// 实际项目:userv1.RegisterUserServiceServer(server, &userServiceServer{store})
svc := &userServiceServer{store: store}
_ = svc
go func() {
log.Printf("gRPC server on %s", lis.Addr())
if err := server.Serve(lis); err != nil {
log.Printf("gRPC serve: %v", err)
}
}()
return server
}
// === REST Gateway(手动实现路由,模拟 gRPC-Gateway 生成的代理)===
type listUsersResponse struct {
Users []*User `json:"users"`
Total int32 `json:"total"`
}
func runGateway(httpAddr string, store *UserStore) *http.Server {
mux := http.NewServeMux()
// POST /v1/users -> CreateUser
mux.HandleFunc("/v1/users", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodPost:
var req struct {
Name string `json:"name"`
Email string `json:"email"`
Age int32 `json:"age"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeGRPCError(w, codes.InvalidArgument, "invalid JSON body")
return
}
if req.Name == "" {
writeGRPCError(w, codes.InvalidArgument, "name is required")
return
}
user := store.Create(req.Name, req.Email, req.Age)
w.Header().Set("Location", fmt.Sprintf("/v1/users/%d", user.ID))
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
case http.MethodGet:
page := parseIntDefault(r, "page", 1)
pageSize := parseIntDefault(r, "page_size", 10)
users, total := store.List(page, pageSize)
json.NewEncoder(w).Encode(listUsersResponse{Users: users, Total: total})
default:
writeGRPCError(w, codes.Unimplemented, "method not allowed")
}
})
// /v1/users/{id}
mux.HandleFunc("/v1/users/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
path := strings.TrimPrefix(r.URL.Path, "/v1/users/")
id, err := strconv.ParseInt(path, 10, 64)
if err != nil {
writeGRPCError(w, codes.InvalidArgument, "invalid user id")
return
}
switch r.Method {
case http.MethodGet:
user, err := store.Get(id)
if err != nil {
writeGRPCErrorFromStatus(w, err)
return
}
json.NewEncoder(w).Encode(user)
case http.MethodDelete:
if err := store.Delete(id); err != nil {
writeGRPCErrorFromStatus(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
default:
writeGRPCError(w, codes.Unimplemented, "method not allowed")
}
})
// 健康检查
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "ok")
})
// CORS 中间件
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
mux.ServeHTTP(w, r)
})
server := &http.Server{Addr: httpAddr, Handler: handler}
go func() {
log.Printf("REST Gateway on %s", httpAddr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("gateway: %v", err)
}
}()
return server
}
// === 辅助函数 ===
func parseIntDefault(r *http.Request, key string, def int32) int32 {
v := r.URL.Query().Get(key)
if v == "" {
return def
}
n, err := strconv.ParseInt(v, 10, 32)
if err != nil {
return def
}
return int32(n)
}
func writeGRPCError(w http.ResponseWriter, code codes.Code, msg string) {
httpCode := grpcCodeToHTTP(code)
w.WriteHeader(httpCode)
json.NewEncoder(w).Encode(map[string]interface{}{
"code": code.String(),
"message": msg,
})
}
func writeGRPCErrorFromStatus(w http.ResponseWriter, err error) {
st, ok := status.FromError(err)
if !ok {
writeGRPCError(w, codes.Unknown, err.Error())
return
}
writeGRPCError(w, st.Code(), st.Message())
}
func grpcCodeToHTTP(code codes.Code) int {
switch code {
case codes.OK:
return 200
case codes.InvalidArgument, codes.FailedPrecondition, codes.OutOfRange:
return 400
case codes.Unauthenticated:
return 401
case codes.PermissionDenied:
return 403
case codes.NotFound:
return 404
case codes.AlreadyExists, codes.Aborted:
return 409
case codes.ResourceExhausted:
return 429
case codes.Unimplemented:
return 501
case codes.Internal, codes.Unknown, codes.DataLoss:
return 500
case codes.Unavailable:
return 503
case codes.DeadlineExceeded:
return 504
default:
return 500
}
}
// === main ===
func main() {
store := NewUserStore()
// 预置数据
store.Create("alice", "alice@example.com", 30)
store.Create("bob", "bob@example.com", 25)
grpcAddr := ":50051"
httpAddr := ":8080"
grpcServer := runGRPC(grpcAddr, store)
httpServer := runGateway(httpAddr, store)
// 优雅退出
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down...")
grpcServer.GracefulStop()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
httpServer.Shutdown(ctx)
log.Println("stopped")
_ = insecure.NewCredentials
}运行后可以用 curl 测试 REST 接口:
bash
# 创建用户
curl -X POST http://localhost:8080/v1/users \
-H "Content-Type: application/json" \
-d '{"name":"charlie","email":"charlie@example.com","age":28}'
# 获取用户
curl http://localhost:8080/v1/users/1
# 列表
curl http://localhost:8080/v1/users?page=1&page_size=10
# 删除
curl -X DELETE http://localhost:8080/v1/users/2十一、小结
本篇深入 gRPC-Gateway 与 REST 集成:
- 工作原理:Gateway 是自动生成的反向代理,将 RESTful 请求转为 gRPC 调用,响应序列化为 JSON。
- 插件安装:
protoc-gen-grpc-gateway生成代理代码,protoc-gen-openapiv2生成 Swagger 文档。 - HTTP 注解:
google.api.http声明方法、路径、body 映射规则,路径参数{id}自动绑定到请求字段。 - 代码生成:一条 protoc 命令同时生成 pb.go、grpc.pb.go、pb.gw.go 和 swagger.json。
- 服务运行:进程内注册(简单)和 endpoint 连接(生产推荐)两种模式。
- 双端口服务:gRPC(:50051)+ REST(:8080)在同一进程运行,是生产标配。
- OpenAPI 文档:通过注解添加 Swagger 元信息,配合 swagger-ui 提供交互式文档。
- 自定义响应头:通过 metadata 透传 HTTP header,或用中间件添加 CORS 等。
- 文件上传:base64 适合小文件,multipart/form-data 适合大文件(需自定义 handler)。
- 完整示例:用户服务展示了双端口、CORS、错误映射、健康检查的完整实现。
下一篇将学习 gRPC 安全与 TLS,包括证书配置、mTLS 双向认证、Token/JWT 认证和授权拦截器。