Skip to content

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 请求后:

  1. 解析 HTTP 路径参数、query string、body。
  2. 将其组装成 Protobuf 消息。
  3. 通过 gRPC client 调用真正的 gRPC 服务。
  4. 将 gRPC 响应序列化为 JSON 返回给 HTTP 客户端。
  5. 将 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.proto
go
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.goGateway 反向代理代码,包含 RegisterUserServiceHandlerServerRegisterUserServiceHandlerFromEndpoint
  • 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 认证和授权拦截器。