开发流程概述
在 Go 语言中开发 gRPC 服务的完整流程:
- 定义 proto 文件 - 使用 Protocol Buffers 定义服务接口和消息类型
- 生成 Go 代码 - 使用 protoc 编译器生成 Go 语言的客户端和服务端代码
- 实现服务接口 - 实现生成的 gRPC 服务接口
- 注册服务 - 将服务注册到 gRPC 服务器
- 启动服务器 - 启动 gRPC 服务器并监听端口
定义 proto 文件
protoc 文件是使用 Protocol Buffers 语言定义的文件,描述了 gRPC 服务的接口和消息格式。更多内容可以参考Language Guide (proto 3)
在项目中创建 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 较通用的有两种方式:
- gRPC-Gateway:使用
--grpc-gateway_out(通用方案,不依赖特定框架)- 运行时转换:必须同时运行 gRPC 和 HTTP 服务器,HTTP 服务器作为反向代理
- 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.proto 和 google/api/http.proto 文件。可以通过以下方式获取:
方式1:使用 buf 工具管理依赖(推荐)
- 安装 buf 工具:
# macOS
brew install bufbuild/buf/buf
# 或使用 Go 安装
go install github.com/bufbuild/buf/cmd/buf@latest
- 在项目根目录创建
buf.yaml配置文件:
version: v2
modules:
- path: api
excludes:
- third_party
deps:
- buf.build/googleapis/googleapis
- 初始化并下载依赖:
buf mod update
- 在 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 文档
本文参考资料: