短信预约-IT技能 免费直播动态提醒
godoudou开发gRPC服务快速上手实现详解
目录 引言 准备 安装gRPC编译器和插件 安装go-doudou 初始化项目 定义服务 生成代码 实现接口 测试服务 总结
引言
go-doudou从v2版本开始已经支持开发gRPC服务。开发流程跟v1版本是一致的,都是先在svc.go文件里的interface里定义方法,然后执行go-doudou代码生成命令生成代码,最后你再写自定义业务逻辑实现接口。go-doudou的学习曲线非常平滑,对新手极其友好,特别是具有其他编程语言开发背景的开发者,比如从Java、Nodejs或是Python转过来的。go-doudou非常易学,但是却能带来极高的生产力。
本文中我将通过一个小demo来向各位同学展示开发流程是什么样的,同时也提供了一些最佳实践。我们将采用go-doudou开发一个通过最大余额法算法解决计算占比不等于100%的问题的gRPC服务,然后通过测试看一下效果。完整代码托管在github:github.com/unionj-clou…
准备
安装go
go-doudou仅支持go 1.16及以上版本。
安装gRPC编译器和插件
安装编译器protoc
安装Protobuf编译器protoc,可参考官方文档,这里贴一下常见操作系统下的安装命令:
$ apt install -y protobuf-compiler
$ protoc --version # 确保安装v3及以上版本
Mac系统,需要先安装Homebrew:
$ brew install protobuf
$ protoc --version # 确保安装v3及以上版本
Windows系统,或者Mac系统安装Homebrew失败,需从github下载安装包,解压后,自行配置环境变量。
Windows系统最新protoc下载地址:github.com/protocolbuf…
Mac系统Intel最新protoc下载地址:github.com/protocolbuf…
其他安装包请在 github releases 里找。
安装插件
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
$ export PATH="$PATH:$(go env GOPATH)/bin"
最新版本号请跳转 grpc.io/docs/langua… 查找。
安装go-doudou
go get -v github.com/unionj-cloud/go-doudou/v2@v2.0.3
如果Go版本 >= 1.17,推荐采用如下命令全局安装go-doudou
命令行工具
go install -v github.com/unionj-cloud/go-doudou/v2@v2.0.3
推荐采用如下命令下载go-doudou作为项目的依赖
go get -v -d github.com/unionj-cloud/go-doudou/v2@v2.0.3
如果遇到410 Gone error
报错,请先执行如下命令,再执行上述的安装命令
安装完成后,如果遇到go-doudou: command not found
报错,请将$HOME/go/bin
配置到~/.bash_profile
文件里,例如:
# .bash_profile
# Get the aliases and functions
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
# User specific environment and startup programs
PATH=$PATH:/usr/local/go/bin
PATH=$PATH:$HOME/go/bin
export PATH
我们可以执行以下命令确认一下安装是否成功:
➜ go-doudou -v
go-doudou version v2.0.3
初始化项目
做完准备工作我们就可以开始coding了。首先我们需要初始化项目。
go-doudou svc init go-stats -m go-doudou-tutorials/go-stats
go-stats是项目的根路径,go-doudou会逐层创建文件夹,底层类似执行unix命令mkdir -p
。-m
可以设置模块名,这里是go-doudou-tutorials/go-stats
go-doudou会生成以下的项目结构。
➜ go-stats git:(master) ✗ tree -L 2
.
├── Dockerfile
├── go.mod
├── svc.go
└── vo
└── vo.go
1 directory, 4 files
svc.go文件里已经声明了一个接口,用于定义方法,go-doudou会通过这些方法提供的信息来生成proto文件里的rpc代码。vo包是用来定义go语言结构体的,go-doudou会扫描整个包里声明的所有结构体来生成proto文件里的message代码。
定义服务
下面我们来看一下svc.go文件。
package service
import (
"context"
"go-doudou-tutorials/go-stats/vo"
)
//go:generate go-doudou svc http -c
//go:generate go-doudou svc grpc
type GoStats interface {
// You can define your service methods as your need. Below is an example.
// You can also add annotations here like @role(admin) to add meta data to routes for
// implementing your own middlewares
PageUsers(ctx context.Context, query vo.PageQuery) (data vo.PageRet, err error)
}
有两个//go:generate
指令,12行和13行分别是用来生成RESTful和gRPC服务代码的,主要是为了方便执行代码生成命令。如果你用goland的话,你可以像下面的截图所示那样操作界面执行命令。
PageUsers是一个示例,我们删掉它写上我们的方法。
package service
import (
"context"
"go-doudou-tutorials/go-stats/vo"
)
//go:generate go-doudou svc http -c
//go:generate go-doudou svc grpc
// GoStats is a demo gRPC service developed by go-doudou
type GoStats interface {
// LargestRemainder implements Largest Remainder Method https://en.wikipedia.org/wiki/Largest_remainder_method
LargestRemainder(ctx context.Context, payload vo.PercentageReqVo) (data []vo.PercentageRespVo, err error)
}
我们还需要在vo包里声明PercentageReqVo和PercentageRespVo这两个vo结构体。需要注意的是,GoStats接口的方法里出现的结构体类型参数必须声明在vo包里,否则go-doudou获取不到字段信息,就无法在proto文件里生成对应的message。
package vo
//go:generate go-doudou name --file $GOFILE
// request vo
type PercentageReqVo struct {
// key value pairs
Data []PercentageVo `json:"data"`
// digit number after dot
Places int `json:"places"`
}
// key value pair
type PercentageVo struct {
// number for something
Value int `json:"value"`
// unique key for distinguishing something
Key string `json:"key"`
}
// result vo
type PercentageRespVo struct {
Value int `json:"value"`
Key string `json:"key"`
Percent float64 `json:"percent"`
// formatted percentage
PercentFormatted string `json:"percentFormatted"`
}
第7行的go-doudou name
命令是go-doudou内置的一个小工具,可以一把生成或更新结构体字段后面的json标签,默认是首字母小写的驼峰命名格式。
生成代码
现在我们可以执行go-doudou svc grpc
命令生成proto文件和gRPC相关的服务端、客户端代码了。执行命令后的项目结构如下所示:
➜ go-stats git:(master) ✗ tree -L 3 -a
.
├── .dockerignore
├── .env
├── .gitignore
├── Dockerfile
├── cmd
│ └── main.go
├── config
│ └── config.go
├── db
│ └── db.go
├── go.mod
├── svc.go
├── svcimpl.go
├── transport
│ └── grpc
│ ├── annotation.go
│ ├── gostats.pb.go
│ ├── gostats.proto
│ └── gostats_grpc.pb.go
└── vo
└── vo.go
13 directories, 16 files
go-doudou为我们生成了一些新的文件夹和文件。
.dockerignore
: 用于打包docker镜像的时候忽略**
syntax = "proto3";
package go_stats;
option go_package = "go-doudou-tutorials/go-stats/transport/grpc";
message LargestRemainderRpcResponse {
repeated PercentageRespVo data = 1 [json_name="data"];
}
// request vo
message PercentageReqVo {
// key value pairs
repeated PercentageVo data = 1 [json_name="data"];
// digit number after dot
int32 places = 2 [json_name="places"];
}
// result vo
message PercentageRespVo {
int32 value = 1 [json_name="value"];
string key = 2 [json_name="key"];
double percent = 3 [json_name="percent"];
// formatted percentage
string percentFormatted = 4 [json_name="percentFormatted"];
}
// key value pair
message PercentageVo {
// number for something
int32 value = 1 [json_name="value"];
// unique key for distinguishing something
string key = 2 [json_name="key"];
}
service GoStatsService {
// LargestRemainder implements Largest Remainder Method https://en.wikipedia.org/wiki/Largest_remainder_method
rpc LargestRemainderRpc(PercentageReqVo) returns (LargestRemainderRpcResponse);
}
如代码所示,所有message里的字段名称都是首字母小写的驼峰命名。其实Protobuf官方给出的规范是下划线分隔单词的蛇形命名。因为我们vo包里的结构体的字段的json标签是首字母小写的驼峰命名,所以我们这里为了保持一致,就没有遵循规范。保持一致的作用是我们后面如果需要将protoc生成的结构体转成vo包里的结构体的时候,可以直接用json序列化反序列化的方式做深拷贝,而无需手工一个个字段去赋值。如果你担心这个办法的性能开销而选择其他办法的话,就无所谓了。
go-doudou在生成proto文件的时候会将方法出参中的除error类型之外的其他类型参数都封装到一个单独的message中,如这里的LargestRemainderRpcResponse。
go-doudou只支持Protobuf v3。
实现接口
下面我们在svcimpl.go文件中编写我们的业务代码。先看一下现在的代码。
package service
import (
"context"
"go-doudou-tutorials/go-stats/config"
pb "go-doudou-tutorials/go-stats/transport/grpc"
)
var _ pb.GoStatsServiceServer = (*GoStatsImpl)(nil)
type GoStatsImpl struct {
pb.UnimplementedGoStatsServiceServer
conf *config.Config
}
func (receiver *GoStatsImpl) LargestRemainderRpc(ctx context.Context, request *pb.PercentageReqVo) (*pb.LargestRemainderRpcResponse, error) {
//TODO implement me
panic("implement me")
}
func NewGoStats(conf *config.Config) *GoStatsImpl {
return &GoStatsImpl{
conf: conf,
}
}
第13行将*GoStatsImpl类型的nil赋值给pb.GoStatsServiceServer接口类型的_
的作用是确保指针类型的GoStatsImpl结构体实例始终都实现pb.GoStatsServiceServer接口。相当于一种约束。
我们可以根据实际需求往GoStatsImpl结构体里声明任何字段来存放各种资源,比如外部服务的客户端,数据库连接,任意的队列或者池等等。这里有一个包级别的工厂方法NewGoStats用来注入各种依赖,创建一个指针类型的GoStatsImpl结构体实例。
接下来我们实现LargestRemainderRpc方法。因为go-doudou不支持grpc-gateway和grpc-web等RESTful转gRPC的转发器,如果需要在一套程序同时支持RESTful服务和gRPC服务,go-doudou提供的解决方案是分别绑定两个端口,启动http服务器和gRPC服务器,复用一套业务逻辑代码,所以如果考虑后期可能需要加上RESTful支持的话,推荐不要直接实现pb.GoStatsServiceServer接口,而是先实现GoStats接口,然后再调用GoStats接口的实现方法去实现pb.GoStatsServiceServer接口。这样就实现了业务逻辑代码的复用。
我个人倾向于不管现在和以后是否需要支持RESTful,都先执行 go-doudou svc http -c
命令生成RESTful相关代码。
现在的项目结构是这样的:
➜ go-stats git:(master) ✗ tree -L 3
.
├── Dockerfile
├── client
│ ├── client.go
│ ├── clientproxy.go
│ └── iclient.go
├── cmd
│ └── main.go
├── config
│ └── config.go
├── db
│ └── db.go
├── go.mod
├── go.sum
├── gostats_openapi3.go
├── gostats_openapi3.json
├── svc.go
├── svcimpl.go
├── transport
│ ├── grpc
│ │ ├── annotation.go
│ │ ├── gostats.pb.go
│ │ ├── gostats.proto
│ │ └── gostats_grpc.pb.go
│ └── httpsrv
│ ├── handler.go
│ ├── handlerimpl.go
│ └── middleware.go
└── vo
└── vo.go
8 directories, 21 files
httpsrv包里是http路由以及http请求解析和返回数据序列化相关的代码。我们可以删掉也可以先留着。我们看一下svcimpl.go文件有什么变化。
package service
import (
"context"
"go-doudou-tutorials/go-stats/config"
pb "go-doudou-tutorials/go-stats/transport/grpc"
"go-doudou-tutorials/go-stats/vo"
"github.com/brianvoe/gofakeit/v6"
)
var _ GoStats = (*GoStatsImpl)(nil)
var _ pb.GoStatsServiceServer = (*GoStatsImpl)(nil)
type GoStatsImpl struct {
pb.UnimplementedGoStatsServiceServer
conf *config.Config
}
func (receiver *GoStatsImpl) LargestRemainderRpc(ctx context.Context, request *pb.PercentageReqVo) (*pb.LargestRemainderRpcResponse, error) {
//TODO implement me
panic("implement me")
}
func NewGoStats(conf *config.Config) *GoStatsImpl {
return &GoStatsImpl{
conf: conf,
}
}
func (receiver *GoStatsImpl) LargestRemainder(ctx context.Context, payload vo.PercentageReqVo) (data []vo.PercentageRespVo, err error) {
var _result struct {
Data []vo.PercentageRespVo
}
_ = gofakeit.Struct(&_result)
return _result.Data, nil
}
第17行和第3642行是新生成的代码。第17行的作用是确保指针类型的GoStatsImpl始终实现GoStats接口。第3642行是需要我们实现的打桩代码。
下面我们编写业务逻辑代码:
func (receiver *GoStatsImpl) LargestRemainder(ctx context.Context, payload vo.PercentageReqVo) (data []vo.PercentageRespVo, err error) {
if len(payload.Data) == 0 {
return
}
input := make([]numberutils.Percentage, 0)
for _, item := range payload.Data {
input = append(input, numberutils.Percentage{
Value: item.Value,
Data: item.Key,
})
}
numberutils.LargestRemainder(input, int32(payload.Places))
for _, item := range input {
data = append(data, vo.PercentageRespVo{
Value: item.Value,
Key: item.Data.(string),
Percent: item.Percent,
PercentFormatted: item.PercentFormatted,
})
}
return
}
go-doudou在 github.com/unionj-cloud/go-doudou/v2/toolkit/numberutils
包里提供了一个工具函数LargestRemainder。此处略过具体的算法实现。
现在我们可以通过复用LargestRemainder方法里的代码实现LargestRemainderRpc方法。
func (receiver *GoStatsImpl) LargestRemainderRpc(ctx context.Context, request *pb.PercentageReqVo) (*pb.LargestRemainderRpcResponse, error) {
var payload vo.PercentageReqVo
copier.DeepCopy(request, &payload)
data, err := receiver.LargestRemainder(ctx, payload)
if err != nil {
return nil, err
}
var ret pb.LargestRemainderRpcResponse
err = copier.DeepCopy(data, &ret.Data)
if err != nil {
return nil, err
}
return &ret, nil
}
我们无需手工编写pb.PercentageReqVo转vo.PercentageReqVo和[]vo.PercentageRespVo转[]*pb.PercentageRespVo的代码,直接用 github.com/unionj-cloud/go-doudou/v2/toolkit/copier
包里的DeepCopy函数做深拷贝即可。
测试服务
因为我们生成了新的代码,导入了新的依赖,所以我们需要再执行一下 go mod tidy
。然后我们启动服务,测试一下效果。
➜ go-stats git:(master) ✗ go mod tidy && go run cmd/main.go
2022/11/23 13:18:13 maxprocs: Leaving GOMAXPROCS=16: CPU quota undefined
_ _
| | | |
__ _ ___ ______ __| | ___ _ _ __| | ___ _ _
/ _` | / _ \ |______| / _` | / _ \ | | | | / _` | / _ \ | | | |
| (_| || (_) | | (_| || (_) || |_| || (_| || (_) || |_| |
__, | ___/ __,_| ___/ __,_| __,_| ___/ __,_|
__/ |
|___/
2022-11-23 13:18:13 INF ================ Registered Services ================
2022-11-23 13:18:13 INF +------------------------------------------+----------------------+
2022-11-23 13:18:13 INF | SERVICE | RPC |
2022-11-23 13:18:13 INF +------------------------------------------+----------------------+
2022-11-23 13:18:13 INF | go_stats.GoStatsService | LargestRemainderRpc |
2022-11-23 13:18:13 INF | grpc.reflection.v1alpha.ServerReflection | ServerReflectionInfo |
2022-11-23 13:18:13 INF +------------------------------------------+----------------------+
2022-11-23 13:18:13 INF ===================================================
2022-11-23 13:18:13 INF Grpc server is listening at [::]:50051
2022-11-23 13:18:13 INF Grpc server started in 1.001238ms
我个人倾向于采用 evans 去帮助测试和调试gRPC服务。
➜ go-stats git:(master) ✗ evans -r repl -p 50051
______
| ____|
| |__ __ __ __ _ _ __ ___
| __| \ \ / / / _. | | '_ \ / __|
| |____ \ V / | (_| | | | | | __ \
|______| _/ __,_| |_| |_| |___/
more expressive universal gRPC client
go_stats.GoStatsService@127.0.0.1:50051> show service
+----------------+---------------------+-----------------+-----------------------------+
| SERVICE | RPC | REQUEST TYPE | RESPONSE TYPE |
+----------------+---------------------+-----------------+-----------------------------+
| GoStatsService | LargestRemainderRpc | PercentageReqVo | LargestRemainderRpcResponse |
+----------------+---------------------+-----------------+-----------------------------+
go_stats.GoStatsService@127.0.0.1:50051> service GoStatsService
go_stats.GoStatsService@127.0.0.1:50051> call LargestRemainderRpc
<repeated> data::value (TYPE_INT32) => 20
<repeated> data::key (TYPE_STRING) => apple
<repeated> data::value (TYPE_INT32) => 30
<repeated> data::key (TYPE_STRING) => banana
<repeated> data::value (TYPE_INT32) => 40
<repeated> data::key (TYPE_STRING) => pear
<repeated> data::value (TYPE_INT32) =>
places (TYPE_INT32) => 2
{
"data": [ { "key": "apple", "percent": 22.22, "percentFormatted": "22.22%", "value": 20 }, { "key": "banana", "percent": 33.33, "percentFormatted": "33.33%", "value": 30 }, { "key": "pear", "percent": 44.45, "percentFormatted": "44.45%", "value": 40 } ]
}
我们输入apple 20kg,banana 30kg,pear 40kg和保留2位小数,然后得到了我们期望的结果:22.22 + 33.33 + 44.45 = 100。
总结
本文我们学到了采用go-doudou微服务框架开发gRPC服务的基本技能,同时我们需要知道go-doudou不仅可以帮助开发者轻松地开发gRPC服务,它还包含了一套完善的服务治理方案帮助开发者打造完整的微服务系统。go-doudou虽然开源时间不长,但是非常有发展潜力,希望越来越多的开发者可以加入进来,更多关于go doudou开发gRPC服务的资料请关注编程网其它相关文章!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341