跳到主要内容

开发流程概述

在 Go 语言中开发 gRPC 服务的完整流程:

  1. 定义 proto 文件 - 使用 Protocol Buffers 定义服务接口和消息类型
  2. 生成 Go 代码 - 使用 protoc 编译器生成 Go 语言的客户端和服务端代码
  3. 实现服务接口 - 实现生成的 gRPC 服务接口
  4. 注册服务 - 将服务注册到 gRPC 服务器
  5. 启动服务器 - 启动 gRPC 服务器并监听端口

定义 proto 文件

在项目中创建 proto 文件,放在 grpc/proto/ 目录下。例如 user.proto

// 指定使用的 Protocol Buffers 语法版本,proto3 是当前推荐使用的版本
syntax = "proto3";

// 指定生成的 Go 代码的包路径和包名
// 格式:完整包路径;包名
// 第一部分是 Go 模块的完整导入路径(需要替换为你实际的模块路径)
// 分号后的 pb 是生成的 Go 代码的包名
// 这个选项是必需的,否则无法生成 Go 代码
option go_package = "github.com/example/user-service/grpc/pb;pb";

// 定义 proto 文件的命名空间,用于避免命名冲突
// 使用点号分隔的层次结构,使用反向域名格式(如 com.example.service.v1)
// 这个包名会出现在生成的代码中,用于区分不同的服务
package user.v1;

// CreateUserRequest 创建用户请求
// message 定义数据结构,类似于 Go 中的结构体
// 字段格式:类型 字段名 = 字段编号;
// 字段编号:每个字段必须有唯一的编号(1-536870911)
// 1-15 编号占用 1 字节,16-2047 占用 2 字节,建议常用字段使用 1-15
// 编号一旦使用不能更改(向后兼容性)
message CreateUserRequest {
string username = 1; // 用户名
string email = 2; // 邮箱
string phone = 3; // 电话
}

// CreateUserResponse 创建用户响应
message CreateUserResponse {
int64 id = 1; // 用户ID
string username = 2; // 用户名
string email = 3; // 邮箱
string message = 4; // 响应消息
}

// GetUserListRequest 获取用户列表请求
message GetUserListRequest {
int32 page = 1; // 页码,从1开始
int32 page_size = 2; // 每页数量
string keyword = 3; // 搜索关键词(可选)
}

// User 用户信息
message User {
int64 id = 1; // 用户ID
string username = 2; // 用户名
string email = 3; // 邮箱
string phone = 4; // 电话
int64 created_at = 5; // 创建时间(时间戳)
}

// GetUserListResponse 获取用户列表响应
message GetUserListResponse {
repeated User users = 1; // 用户列表,repeated 表示数组/列表类型
int32 total = 2; // 总数
int32 page = 3; // 当前页码
int32 page_size = 4; // 每页数量
}

// UserService 用户服务
// service 定义 gRPC 服务接口
// RPC 方法类型:
// - 一元 RPC(Unary RPC):rpc Method(Request) returns (Response);
// 客户端发送一个请求,服务端返回一个响应,最常用的 RPC 类型
// - 服务端流式 RPC:rpc Method(Request) returns (stream Response);
// 客户端发送一个请求,服务端返回一个流
// - 客户端流式 RPC:rpc Method(stream Request) returns (Response);
// 客户端发送一个流,服务端返回一个响应
// - 双向流式 RPC:rpc Method(stream Request) returns (stream Response);
// 客户端和服务端都可以发送流
service UserService {
// CreateUser 创建用户
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);

// GetUserList 获取用户列表
rpc GetUserList(GetUserListRequest) returns (GetUserListResponse);
}

生成 Go 代码

使用命令行生成

可以直接使用 protoc 命令生成代码:

# 创建输出目录
mkdir -p grpc/pb
mkdir -p docs

# 生成代码
protoc --proto_path=grpc/proto \
--go_out=paths=source_relative:grpc/pb \
--go-grpc_out=paths=source_relative:grpc/pb \
--openapi_out=fq_schema_naming=true,default_response=false:docs \
grpc/proto/user.proto

参数说明

  • --proto_path: 指定 proto 文件的搜索路径,可以指定多个路径用于导入其他 proto 文件
  • --go_out: 指定生成 Go 代码的输出路径和选项
    • paths=source_relative: 生成的代码路径与源文件相对路径保持一致
    • 例如:proto/user.proto 会生成到 pb/proto/user.pb.go
  • --go-grpc_out: 指定生成 gRPC Go 代码的输出路径和选项
    • 会生成 *_grpc.pb.go 文件,包含客户端和服务端接口
  • --openapi_out: 生成 OpenAPI 文档(可选)
    • fq_schema_naming=true: 使用完全限定名称
    • default_response=false: 不包含默认响应
    • 需要安装 protoc-gen-openapi 插件
    • 示例:--openapi_out=fq_schema_naming=true,default_response=false:docs

使用 Makefile(推荐)

在项目的 Makefile 中添加 gRPC 代码生成目标:

# gRPC相关变量
PROTO_DIR := grpc/proto
PROTO_OUT_DIR := grpc/pb
DOCS_DIR := docs
PROTO_FILES := $(shell find $(PROTO_DIR) -name *.proto)

grpc: ## 生成gRPC代码
@mkdir -p $(PROTO_OUT_DIR)
@mkdir -p $(DOCS_DIR)
@for proto in $(PROTO_FILES); do \
echo "processing: $$proto"; \
protoc --proto_path=$(PROTO_DIR) \
--go_out=paths=source_relative:$(PROTO_OUT_DIR) \
--go-grpc_out=paths=source_relative:$(PROTO_OUT_DIR) \
--openapi_out=fq_schema_naming=true,default_response=false:$(DOCS_DIR) \
$$proto || exit 1; \
done
@echo "done..."

执行 make grpc 后会生成以下文件:

  • grpc/pb/user.pb.go:消息类型的 Go 代码
  • grpc/pb/user_grpc.pb.go:gRPC 服务接口代码

实现服务接口

grpc/controller/user.go 中实现服务:

package user

import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/example/user-service/grpc/pb"
userS "github.com/example/user-service/internal/service/user"
)

type UserController struct {
UserService userS.UserService
// 必须嵌入 UnimplementedUserServiceServer,保证向前兼容性
// 如果将来 proto 文件添加新方法,不会导致编译错误
pb.UnimplementedUserServiceServer
}

func NewUserController(s userS.UserService) *UserController {
return &UserController{
UserService: s,
}
}

实现 UserServiceServer 接口中的方法:

// CreateUser 创建用户
// 使用 gRPC 状态码:
// codes.InvalidArgument: 参数无效
// codes.AlreadyExists: 资源已存在
// codes.Internal: 内部服务器错误
// 更多状态码参考:https://grpc.github.io/grpc/core/md_doc_statuscodes.html
func (c *UserController) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
// 参数验证
if req.Username == "" {
return nil, status.Errorf(codes.InvalidArgument, "username is required")
}
if req.Email == "" {
return nil, status.Errorf(codes.InvalidArgument, "email is required")
}

// 调用业务逻辑
user, err := c.UserService.CreateUser(ctx, &userS.CreateUserParams{
Username: req.Username,
Email: req.Email,
Phone: req.Phone,
})
if err != nil {
// 处理业务错误
if err == userS.ErrUserExists {
return nil, status.Errorf(codes.AlreadyExists, "user already exists")
}
// 系统错误
return nil, status.Errorf(codes.Internal, "failed to create user: %v", err)
}

// 返回成功响应
return &pb.CreateUserResponse{
Id: user.ID,
Username: user.Username,
Email: user.Email,
Message: "user created successfully",
}, nil
}

// GetUserList 获取用户列表
func (c *UserController) GetUserList(ctx context.Context, req *pb.GetUserListRequest) (*pb.GetUserListResponse, error) {
// 参数验证和默认值设置
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100 // 限制最大每页数量
}

// 调用业务逻辑
users, total, err := c.UserService.GetUserList(ctx, &userS.GetUserListParams{
Page: int(page),
PageSize: int(pageSize),
Keyword: req.Keyword,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user list: %v", err)
}

// 转换为响应格式
pbUsers := make([]*pb.User, 0, len(users))
for _, u := range users {
pbUsers = append(pbUsers, &pb.User{
Id: u.ID,
Username: u.Username,
Email: u.Email,
Phone: u.Phone,
CreatedAt: u.CreatedAt.Unix(),
})
}

// 返回响应
return &pb.GetUserListResponse{
Users: pbUsers,
Total: int32(total),
Page: int32(page),
PageSize: int32(pageSize),
}, nil
}

注册服务

grpc/router.go 中注册服务:

package grpc

import (
"google.golang.org/grpc"
"github.com/example/user-service/grpc/pb"
userC "github.com/example/user-service/grpc/controller"
)

// RegisterGrpcServers 注册所有 gRPC 服务
// RegisterUserServiceServer 是 protoc 自动生成的函数
// 第一个参数是 gRPC 服务器实例,第二个参数是实现服务接口的结构体实例
// 可以注册多个服务,每个服务调用一次注册函数
func RegisterGrpcServers(grpcSrv *grpc.Server) {
pb.RegisterUserServiceServer(grpcSrv, userC.User)
}

启动服务器

cmd/run_server.go 中启动 gRPC 服务器:

package cmd

import (
"context"
"fmt"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"github.com/example/user-service/common/config"
"github.com/example/user-service/handler/routers"
)

func RunServer() {
// 创建 gRPC 服务器实例
grpcSrv := grpc.NewServer()

// 创建 TCP 监听器
listen, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", config.AppConfig.ServerConfig.GrpcPort))
if err != nil {
logger.Fatal(fmt.Sprintf("failed to listen error: %v", err))
}

// 注册 gRPC 服务
routers.RegisterGrpcServers(grpcSrv)

// 注册反射服务(可选,用于 grpcurl 调试)
// 开发环境可以启用反射,方便使用 grpcurl 调试
if config.GetEnvAsBool("DEBUG_MODE", false) {
reflection.Register(grpcSrv)
logger.Info("grpc reflection enabled for debug mode")
}

// 启动服务器,调用 grpcSrv.Serve(listen) 开始监听请求
logger.Info(fmt.Sprintf("grpc server is running on 0.0.0.0:%d", config.AppConfig.ServerConfig.GrpcPort))
go func() {
if err := grpcSrv.Serve(listen); err != nil {
logger.Fatal("grpc server error: %s", err)
}
}()

// 优雅关闭(可选)
// ... 实现优雅关闭逻辑
}

扩展:HTTP 网关和错误码生成

将 gRPC 服务暴露为 HTTP REST API 较通用的有两种方式:

  1. gRPC-Gateway:使用 --grpc-gateway_out(通用方案,不依赖特定框架)
    • 运行时转换:必须同时运行 gRPC 和 HTTP 服务器,HTTP 服务器作为反向代理
  2. protoc-gen-go-http:使用 --go-http_out(Kratos 专用)
    • 编译时生成:按需启用服务器,仅做代码生成,更轻量级

HTTP 网关代码生成(gRPC-Gateway)

gRPC-Gateway 是最常用的 gRPC 到 HTTP 转换方案,不依赖任何特定框架。

核心特点

  • 运行时转换:在运行时进行 gRPC 和 HTTP 之间的协议转换
  • 双服务器架构:必须同时运行 gRPC 服务器和 HTTP 服务器
  • 反向代理模式:HTTP 服务器作为 gRPC 服务的反向代理,接收 HTTP 请求后转换为 gRPC 调用

proto 文件配置

增加使用 google.api.http 注解

import "google/api/annotations.proto";

service UserService {
// CreateUser 创建用户
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {
option (google.api.http) = {
post: "/api/v1/user"
body: "*"
};
}

// GetUserList 获取用户列表
rpc GetUserList(GetUserListRequest) returns (GetUserListResponse) {
option (google.api.http) = {
get: "/api/v1/user"
};
}

// GetUser 获取单个用户
rpc GetUser(GetUserRequest) returns (GetUserResponse) {
option (google.api.http) = {
get: "/api/v1/user/{id}"
};
}
}

安装插件

go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest

准备第三方 proto 文件

gRPC-Gateway 需要 google/api/annotations.protogoogle/api/http.proto 文件。可以通过以下方式获取:

方式1:使用 buf 工具管理依赖(推荐)

  1. 安装 buf 工具:
# macOS
brew install bufbuild/buf/buf

# 或使用 Go 安装
go install github.com/bufbuild/buf/cmd/buf@latest
  1. 在项目根目录创建 buf.yaml 配置文件:
version: v2
modules:
- path: api
excludes:
- third_party
deps:
- buf.build/googleapis/googleapis
  1. 初始化并下载依赖:
buf mod update
  1. 在 Makefile 中使用 buf 生成代码:
api:
buf generate
# 或使用 protoc,但需要指定 buf 的缓存路径
protoc --proto_path=./api \
--proto_path=$(shell buf export buf.build/googleapis/googleapis --path google/api) \
--go_out=paths=source_relative:./api \
--go-grpc_out=paths=source_relative:./api \
--grpc-gateway_out=paths=source_relative:./api \
$(API_PROTO_FILES)

方式2:手动下载到 third_party 目录

mkdir -p third_party/google/api
# 从 https://github.com/googleapis/googleapis 下载以下文件到 third_party/google/api/ 目录:
# - annotations.proto
# - http.proto
# - httpbody.proto(如果使用 body 参数)

Makefile 配置

api:
protoc --proto_path=./api \
--proto_path=./third_party \
--go_out=paths=source_relative:./api \
--go-grpc_out=paths=source_relative:./api \
--grpc-gateway_out=paths=source_relative:./api \
--openapiv2_out=./api \
$(API_PROTO_FILES)

生成的文件

  • *_pb.gw.go:gRPC-Gateway 生成的 HTTP 网关代码
  • *.swagger.json:OpenAPI/Swagger 文档(如果使用 --openapiv2_out

服务端集成

gRPC-Gateway 需要同时运行 gRPC 服务器和 HTTP 服务器,HTTP 服务器作为 gRPC 服务的反向代理:

import (
"context"
"log"
"net"
"net/http"

"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"

pb "your-module/pb" // 生成的 pb 包
)

func main() {
// 启动 gRPC 服务器
lis, err := net.Listen("tcp", ":9090")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
pb.RegisterUserServiceServer(grpcServer, &userService{}) // 注册 gRPC 服务
go func() {
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}()

// 创建 gRPC-Gateway mux
mux := runtime.NewServeMux()

// 注册 gRPC 服务到 HTTP(通过反向代理)
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
err = pb.RegisterUserServiceHandlerFromEndpoint(
context.Background(),
mux,
"localhost:9090", // gRPC 服务地址
opts,
)
if err != nil {
log.Fatalf("failed to register gateway: %v", err)
}

// 启动 HTTP 服务器
log.Println("HTTP server listening on :8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

工作原理:gRPC-Gateway 在运行时创建一个 HTTP 服务器,接收 RESTful 请求,在运行时将其转换为 gRPC 调用,然后转发给 gRPC 服务器,最后将 gRPC 响应转换回 HTTP 响应。这是一个运行时转换过程,因此必须同时运行 gRPC 和 HTTP 两个服务器。

HTTP 网关代码生成(protoc-gen-go-http)

protoc-gen-go-http 是 Kratos 项目提供的插件,可以单独使用,但生成的代码会依赖 Kratos 的 HTTP 传输层包(github.com/go-kratos/kratos/v2/transport/http)。如果项目不使用 Kratos 框架,建议使用 gRPC-Gateway 方案。

核心特点

  • 编译时生成:在代码生成阶段直接生成 HTTP 处理器代码,而非运行时转换
  • 按需启用服务器:可以选择只启动 gRPC 服务器、只启动 HTTP 服务器,或同时启动两者
  • 更轻量级:不需要额外的网关运行时组件,生成的代码直接处理 HTTP 请求

proto 文件配置

在 proto 文件中使用 google.api.http 注解定义 HTTP 路由:

import "google/api/annotations.proto";

service UserService {
// CreateUser 创建用户
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {
option (google.api.http) = {
post: "/api/v1/user"
body: "*"
};
}

// GetUserList 获取用户列表
rpc GetUserList(GetUserListRequest) returns (GetUserListResponse) {
option (google.api.http) = {
get: "/api/v1/user"
};
}

// GetUser 获取单个用户
rpc GetUser(GetUserRequest) returns (GetUserResponse) {
option (google.api.http) = {
get: "/api/v1/user/{id}"
};
}
}

安装插件

go install github.com/go-kratos/kratos/cmd/protoc-gen-go-http/v2@latest

Makefile 配置

api:
protoc --proto_path=./api \
--proto_path=./third_party \
--go_out=paths=source_relative:./api \
--go-http_out=paths=source_relative:./api \
--go-grpc_out=paths=source_relative:./api \
$(API_PROTO_FILES)

生成的文件

  • *_http.pb.go:HTTP 网关代码,包含 HTTP 路由注册函数
  • 实现了 UserServiceHTTPServer 接口,用于注册 HTTP 路由

服务端集成

protoc-gen-go-http 在编译时生成 HTTP 处理器代码,因此可以按需启用服务器

  • 只启动 HTTP 服务器:如果只需要 HTTP REST API,可以只启动 HTTP 服务器
  • 只启动 gRPC 服务器:如果只需要 gRPC 服务,可以只启动 gRPC 服务器
  • 同时启动两个服务器:如果需要同时提供 HTTP 和 gRPC 接口,可以同时启动

如果使用 Kratos 框架,可以直接使用框架的 HTTP 服务器注册:

import (
"github.com/go-kratos/kratos/v2/transport/http"
pb "your-module/pb"
)

func main() {
// 只启动 HTTP 服务器(按需启用)
httpSrv := http.NewServer(
http.Address(":8080"),
)

// 注册 HTTP 服务(编译时生成的代码)
pb.RegisterUserServiceHTTPServer(httpSrv, &userService{})

// 启动服务器(Kratos 框架会自动处理)
}

如果不使用 Kratos 框架,需要手动集成 Kratos 的 HTTP 传输层包,或者自行实现 HTTP 处理器。

在 Gin 框架中集成 protoc-gen-go-http

方案一:最小依赖 + 适配器(推荐)

引入 Kratos 的 HTTP 传输层包(不引入整个 Kratos 框架),然后创建一个适配器将 Gin 路由转换为调用生成的 HTTP 处理器:

package adapter

import (
"context"
"net/http"

"github.com/gin-gonic/gin"
"github.com/go-kratos/kratos/v2/transport/http"
pb "your-module/pb"
)

// GinToKratosAdapter 将 Gin 的 Context 转换为 Kratos HTTP 的 Context
type GinToKratosAdapter struct {
userService pb.UserServiceHTTPServer
}

func NewGinToKratosAdapter(userService pb.UserServiceHTTPServer) *GinToKratosAdapter {
return &GinToKratosAdapter{
userService: userService,
}
}

// RegisterRoutes 在 Gin 中注册路由
func (a *GinToKratosAdapter) RegisterRoutes(r *gin.Engine) {
// 创建 Kratos HTTP 服务器(仅用于路由注册,不启动)
kratosSrv := http.NewServer(
http.Address(":8080"), // 地址不会被使用
)

// 注册到 Kratos HTTP 服务器(这会生成路由信息)
pb.RegisterUserServiceHTTPServer(kratosSrv, a.userService)

// 从 Kratos 服务器提取路由信息,转换为 Gin 路由
// 注意:这需要访问 Kratos 的内部路由信息,可能需要反射或手动映射

// 手动注册路由(推荐方式)
v1 := r.Group("/api/v1")
{
v1.POST("/user", a.createUser)
v1.GET("/user", a.getUserList)
v1.GET("/user/:id", a.getUser)
}
}

// createUser 处理创建用户请求
func (a *GinToKratosAdapter) createUser(c *gin.Context) {
// 将 Gin Context 转换为 Kratos HTTP Context
kratosCtx := a.ginToKratosContext(c)

// 解析请求体
var req pb.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// 调用生成的 HTTP 处理器
resp, err := a.userService.CreateUser(kratosCtx, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, resp)
}

// getUserList 处理获取用户列表请求
func (a *GinToKratosAdapter) getUserList(c *gin.Context) {
kratosCtx := a.ginToKratosContext(c)

var req pb.GetUserListRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

resp, err := a.userService.GetUserList(kratosCtx, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, resp)
}

// getUser 处理获取单个用户请求
func (a *GinToKratosAdapter) getUser(c *gin.Context) {
kratosCtx := a.ginToKratosContext(c)

id := c.Param("id")
req := &pb.GetUserRequest{Id: id}

resp, err := a.userService.GetUser(kratosCtx, req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, resp)
}

// ginToKratosContext 将 Gin Context 转换为 Kratos HTTP Context
func (a *GinToKratosAdapter) ginToKratosContext(c *gin.Context) context.Context {
ctx := c.Request.Context()
// 可以在这里传递额外的上下文信息
return ctx
}

main.go 中使用:

package main

import (
"github.com/gin-gonic/gin"
pb "your-module/pb"
"your-module/adapter"
userC "your-module/grpc/controller/user"
)

func main() {
r := gin.Default()

// 创建服务实现
userService := userC.NewUserController(...)

// 创建适配器
adapter := adapter.NewGinToKratosAdapter(userService)

// 注册路由
adapter.RegisterRoutes(r)

// 启动 Gin 服务器
r.Run(":8080")
}

方案二:手动实现接口(更灵活)

不依赖 Kratos HTTP 传输层,直接手动实现生成的接口,在 Gin 中注册路由:

package handler

import (
"net/http"

"github.com/gin-gonic/gin"
pb "your-module/pb"
userC "your-module/grpc/controller/user"
)

type UserHTTPHandler struct {
userService *userC.UserController
}

func NewUserHTTPHandler(userService *userC.UserController) *UserHTTPHandler {
return &UserHTTPHandler{
userService: userService,
}
}

// RegisterRoutes 在 Gin 中注册路由
func (h *UserHTTPHandler) RegisterRoutes(r *gin.Engine) {
v1 := r.Group("/api/v1")
{
v1.POST("/user", h.CreateUser)
v1.GET("/user", h.GetUserList)
v1.GET("/user/:id", h.GetUser)
}
}

// CreateUser 创建用户
func (h *UserHTTPHandler) CreateUser(c *gin.Context) {
var req pb.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// 调用 gRPC 服务实现(复用业务逻辑)
resp, err := h.userService.CreateUser(c.Request.Context(), &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, resp)
}

// GetUserList 获取用户列表
func (h *UserHTTPHandler) GetUserList(c *gin.Context) {
var req pb.GetUserListRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

resp, err := h.userService.GetUserList(c.Request.Context(), &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, resp)
}

// GetUser 获取单个用户
func (h *UserHTTPHandler) GetUser(c *gin.Context) {
id := c.Param("id")
req := &pb.GetUserRequest{Id: id}

resp, err := h.userService.GetUser(c.Request.Context(), req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, resp)
}

方案对比

方案优点缺点适用场景
方案一(适配器)可以复用生成的 HTTP 处理器代码需要引入 Kratos HTTP 传输层包,代码较复杂需要利用生成代码的完整功能
方案二(手动实现)不依赖 Kratos,代码更简洁需要手动实现所有路由处理只需要简单的 HTTP 接口,或想完全控制路由逻辑
gRPC-Gateway不依赖特定框架,通用性强运行时转换,需要同时运行两个服务器通用 Go 项目,不需要 Kratos 依赖

Kratos vs gRPC-Gateway 对比

特性protoc-gen-go-http (--go-http_out)gRPC-Gateway (--grpc-gateway_out)
核心区别编译时生成代码,按需启用服务器运行时转换,必须同时运行两个服务器
转换时机编译时生成 HTTP 处理器代码运行时进行协议转换
服务器架构按需启用,可只运行 gRPC 或只运行 HTTP必须同时运行 gRPC 和 HTTP 服务器
代码依赖依赖 Kratos HTTP 传输层包不依赖特定框架
集成方式Kratos 框架自动集成,或手动集成 HTTP 传输层需要手动注册路由,同时运行 gRPC 和 HTTP 服务器
代码生成*_http.pb.go*_pb.gw.go
工作原理直接生成 HTTP 处理器,直接处理 HTTP 请求HTTP 服务器作为 gRPC 服务的反向代理
性能开销更轻量,无运行时转换开销有运行时转换开销
适用场景Kratos 项目或需要 Kratos HTTP 传输层的项目通用 Go 项目

错误码生成(Kratos 框架)

使用 --go-errors_out 可以生成统一的错误码定义。详情可以参考 Kratos 官方文档

安装插件

go install github.com/go-kratos/kratos/cmd/protoc-gen-go-errors/v2@latest

Makefile 配置

api:
protoc --proto_path=./api \
--proto_path=./third_party \
--go_out=paths=source_relative:./api \
--go-http_out=paths=source_relative:./api \
--go-grpc_out=paths=source_relative:./api \
--go-errors_out=paths=source_relative:./api \
$(API_PROTO_FILES)

错误码定义

在 proto 文件中定义错误码:

enum ErrorReason {
option (errors.default_code) = 500;

USER_NOT_FOUND = 0 [(errors.code) = 404];
USER_ALREADY_EXISTS = 1 [(errors.code) = 409];
INVALID_PARAMS = 2 [(errors.code) = 400];
}

扩展:生成前端 TypeScript 代码

可以使用 protoc-gen-ts_proto 从 proto 文件生成前端 TypeScript 类型定义,用于前端项目的类型安全。

提示

关于 protoc-gen-ts_proto 的详细安装、配置和使用说明,请参考 protoc-gen-ts_proto 文档

本文参考资料: