go-doudou從v2版本開始已經(jīng)支持開發(fā)gRPC服務(wù)。開發(fā)流程跟v1版本是一致的吃既,都是先在svc.go文件里的interface里定義方法渺杉,然后執(zhí)行g(shù)o-doudou代碼生成命令生成代碼,最后你再寫自定義業(yè)務(wù)邏輯實現(xiàn)接口砚偶。go-doudou的學(xué)習(xí)曲線非常平滑批销,對新手極其友好,特別是具有其他編程語言開發(fā)背景的開發(fā)者染坯,比如從Java均芽、Nodejs或是Python轉(zhuǎn)過來的。go-doudou非常易學(xué)单鹿,但是卻能帶來極高的生產(chǎn)力掀宋。
本文中我將通過一個小demo來向各位同學(xué)展示開發(fā)流程是什么樣的,同時也提供了一些最佳實踐仲锄。我們將采用go-doudou開發(fā)一個通過最大余額法算法解決計算占比不等于100%的問題的gRPC服務(wù)劲妙,然后通過測試看一下效果。完整代碼托管在github儒喊。
準(zhǔn)備
安裝go
go-doudou僅支持go 1.16及以上版本镣奋。
安裝gRPC編譯器和插件
安裝編譯器protoc
安裝Protobuf編譯器protoc,可參考官方文檔怀愧,這里貼一下常見操作系統(tǒng)下的安裝命令:
- Ubuntu系統(tǒng):
$ apt install -y protobuf-compiler
$ protoc --version # 確保安裝v3及以上版本
- Mac系統(tǒng)侨颈,需要先安裝Homebrew:
$ brew install protobuf
$ protoc --version # 確保安裝v3及以上版本
- Windows系統(tǒng),或者M(jìn)ac系統(tǒng)安裝Homebrew失敗芯义,需從github下載安裝包哈垢,解壓后,自行配置環(huán)境變量毕贼。
Windows系統(tǒng)最新protoc下載地址:https://github.com/protocolbuffers/protobuf/releases/download/v21.7/protoc-21.7-win64.zip
Mac系統(tǒng)Intel最新protoc下載地址:https://github.com/protocolbuffers/protobuf/releases/download/v21.7/protoc-21.7-osx-x86_64.zip
其他安裝包請在 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
- 配置環(huán)境變量:
$ export PATH="$PATH:$(go env GOPATH)/bin"
最新版本號請?zhí)D(zhuǎn) https://grpc.io/docs/languages/go/quickstart/ 查找蛤奢。
安裝go-doudou
- 如果Go版本低于1.17
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
報錯陶贼,請先執(zhí)行如下命令,再執(zhí)行上述的安裝命令
export GOSUMDB=off
安裝完成后待秃,如果遇到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
我們可以執(zhí)行以下命令確認(rèn)一下安裝是否成功:
? go-doudou -v
go-doudou version v2.0.3
初始化項目
做完準(zhǔn)備工作我們就可以開始coding了章郁。首先我們需要初始化項目枉氮。
go-doudou svc init go-stats -m go-doudou-tutorials/go-stats
go-stats是項目的根路徑,go-doudou會逐層創(chuàng)建文件夾暖庄,底層類似執(zhí)行unix命令mkdir -p
聊替。-m
可以設(shè)置模塊名,這里是go-doudou-tutorials/go-stats
go-doudou會生成以下的項目結(jié)構(gòu)培廓。
? go-stats git:(master) ? tree -L 2
.
├── Dockerfile
├── go.mod
├── svc.go
└── vo
└── vo.go
1 directory, 4 files
svc.go文件里已經(jīng)聲明了一個接口惹悄,用于定義方法,go-doudou會通過這些方法提供的信息來生成proto文件里的rpc代碼肩钠。vo包是用來定義go語言結(jié)構(gòu)體的泣港,go-doudou會掃描整個包里聲明的所有結(jié)構(gòu)體來生成proto文件里的message代碼。
定義服務(wù)
下面我們來看一下svc.go文件价匠。
/**
* Generated by go-doudou v2.0.3.
* You can edit it as your need.
*/
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
指令当纱,分別是用來生成RESTful和gRPC服務(wù)代碼的,主要是為了方便執(zhí)行代碼生成命令踩窖。如果你用goland的話坡氯,你可以像下面的截圖所示那樣操作界面執(zhí)行命令。
PageUsers是一個示例毙石,我們刪掉它寫上我們的方法廉沮。
/**
* Generated by go-doudou v2.0.3.
* You can edit it as your need.
*/
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結(jié)構(gòu)體。需要注意的是徐矩,GoStats接口的方法里出現(xiàn)的結(jié)構(gòu)體類型參數(shù)必須聲明在vo包里滞时,否則go-doudou獲取不到字段信息,就無法在proto文件里生成對應(yīng)的message滤灯。
/**
* Generated by go-doudou v2.0.3.
* You can edit it as your need.
*/
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內(nèi)置的一個小工具坪稽,可以一把生成或更新結(jié)構(gòu)體字段后面的json標(biāo)簽,默認(rèn)是首字母小寫的駝峰命名格式鳞骤。
生成代碼
現(xiàn)在我們可以執(zhí)行go-doudou svc grpc
命令生成proto文件和gRPC相關(guān)的服務(wù)端窒百、客戶端代碼了。執(zhí)行命令后的項目結(jié)構(gòu)如下所示:
? 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鏡像的時候忽略**/*.local
文件 -
.env
: 配置文件 -
cmd
: 里面有main.go文件 -
config
: 用于將環(huán)境變量映射到config結(jié)構(gòu)體 -
db
: 用于建立數(shù)據(jù)庫連接實例篙梢,我們這個demo用不到 -
svcimpl.go
: 用于編寫自定義的業(yè)務(wù)邏輯,實現(xiàn)GoStats接口 -
transport
: proto文件和gRPC相關(guān)的服務(wù)端美旧、客戶端代碼在這里
在我們實現(xiàn)接口之前渤滞,需要執(zhí)行 go mod tidy
命令來下載依賴贬墩。然后我們啟動程序,先看一下是否一切正常妄呕。
? go-stats git:(master) ? go run cmd/main.go
2022/11/23 11:07:45 maxprocs: Leaving GOMAXPROCS=16: CPU quota undefined
_ _
| | | |
__ _ ___ ______ __| | ___ _ _ __| | ___ _ _
/ _` | / _ \ |______| / _` | / _ \ | | | | / _` | / _ \ | | | |
| (_| || (_) | | (_| || (_) || |_| || (_| || (_) || |_| |
\__, | \___/ \__,_| \___/ \__,_| \__,_| \___/ \__,_|
__/ |
|___/
2022-11-23 11:07:45 INF ================ Registered Services ================
2022-11-23 11:07:45 INF +------------------------------------------+----------------------+
2022-11-23 11:07:45 INF | SERVICE | RPC |
2022-11-23 11:07:45 INF +------------------------------------------+----------------------+
2022-11-23 11:07:45 INF | grpc.reflection.v1alpha.ServerReflection | ServerReflectionInfo |
2022-11-23 11:07:45 INF | go_stats.GoStatsService | LargestRemainderRpc |
2022-11-23 11:07:45 INF +------------------------------------------+----------------------+
2022-11-23 11:07:45 INF ===================================================
2022-11-23 11:07:45 INF Grpc server is listening at [::]:50051
2022-11-23 11:07:45 INF Grpc server started in 1.110168ms
go-doudou依賴了 go.uber.org/automaxprocs
這個包來根據(jù)我們給容器做的資源限制來設(shè)置處理器P的數(shù)量陶舞,所以會有第2行的日志輸出。
我們再看一下transport/grpc/gostats.proto文件绪励。
/**
* Generated by go-doudou v2.0.3.
* Don't edit!
*
* Version No.: v20221123
*/
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官方給出的規(guī)范是下劃線分隔單詞的蛇形命名疏魏。因為我們vo包里的結(jié)構(gòu)體的字段的json標(biāo)簽是首字母小寫的駝峰命名停做,所以我們這里為了保持一致,就沒有遵循規(guī)范蠢护。保持一致的作用是我們后面如果需要將protoc生成的結(jié)構(gòu)體轉(zhuǎn)成vo包里的結(jié)構(gòu)體的時候雅宾,可以直接用json序列化反序列化的方式做深拷貝,而無需手工一個個字段去賦值葵硕。如果你擔(dān)心這個辦法的性能開銷而選擇其他辦法的話眉抬,就無所謂了。
go-doudou在生成proto文件的時候會將方法出參中的除error類型之外的其他類型參數(shù)都封裝到一個單獨的message中懈凹,如這里的LargestRemainderRpcResponse蜀变。
go-doudou只支持Protobuf v3。
實現(xiàn)接口
下面我們在svcimpl.go文件中編寫我們的業(yè)務(wù)代碼介评。先看一下現(xiàn)在的代碼库北。
/**
* Generated by go-doudou v2.0.3.
* You can edit it as your need.
*/
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,
}
}
var _ pb.GoStatsServiceServer = (*GoStatsImpl)(nil)
將*GoStatsImpl類型的nil賦值給pb.GoStatsServiceServer接口類型的_
的作用是確保指針類型的GoStatsImpl結(jié)構(gòu)體實例始終都實現(xiàn)pb.GoStatsServiceServer接口。相當(dāng)于一種約束们陆。
我們可以根據(jù)實際需求往GoStatsImpl結(jié)構(gòu)體里聲明任何字段來存放各種資源寒瓦,比如外部服務(wù)的客戶端,數(shù)據(jù)庫連接坪仇,任意的隊列或者池等等杂腰。這里有一個包級別的工廠方法NewGoStats用來注入各種依賴,創(chuàng)建一個指針類型的GoStatsImpl結(jié)構(gòu)體實例椅文。
接下來我們實現(xiàn)LargestRemainderRpc方法喂很。因為go-doudou不支持grpc-gateway和grpc-web等RESTful轉(zhuǎn)gRPC的轉(zhuǎn)發(fā)器,如果需要在一套程序同時支持RESTful服務(wù)和gRPC服務(wù)皆刺,go-doudou提供的解決方案是分別綁定兩個端口少辣,啟動http服務(wù)器和gRPC服務(wù)器,復(fù)用一套業(yè)務(wù)邏輯代碼羡蛾,所以如果考慮后期可能需要加上RESTful支持的話漓帅,推薦不要直接實現(xiàn)pb.GoStatsServiceServer接口,而是先實現(xiàn)GoStats接口,然后再調(diào)用GoStats接口的實現(xiàn)方法去實現(xiàn)pb.GoStatsServiceServer接口忙干。這樣就實現(xiàn)了業(yè)務(wù)邏輯代碼的復(fù)用屯伞。
我個人傾向于不管現(xiàn)在和以后是否需要支持RESTful,都先執(zhí)行 go-doudou svc http -c
命令生成RESTful相關(guān)代碼豪直。
現(xiàn)在的項目結(jié)構(gòu)是這樣的:
? 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請求解析和返回數(shù)據(jù)序列化相關(guān)的代碼。我們可以刪掉也可以先留著珠移。我們看一下svcimpl.go文件有什么變化弓乙。
/**
* Generated by go-doudou v2.0.3.
* You can edit it as your need.
*/
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
}
var _ GoStats = (*GoStatsImpl)(nil)
和 LargestRemainder
方法是新生成的代碼。var _ GoStats = (*GoStatsImpl)(nil)
的作用是確保指針類型的GoStatsImpl始終實現(xiàn)GoStats接口钧惧。LargestRemainder
方法是需要我們實現(xiàn)的打樁代碼暇韧。
下面我們編寫業(yè)務(wù)邏輯代碼:
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
包里提供了一個工具函數(shù)LargestRemainder。此處略過具體的算法實現(xiàn)浓瞪。
現(xiàn)在我們可以通過復(fù)用LargestRemainder方法里的代碼實現(xiàn)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轉(zhuǎn)vo.PercentageReqVo和[]vo.PercentageRespVo轉(zhuǎn)[]*pb.PercentageRespVo的代碼,直接用 github.com/unionj-cloud/go-doudou/v2/toolkit/copier
包里的DeepCopy函數(shù)做深拷貝即可乾颁。
測試服務(wù)
因為我們生成了新的代碼涂乌,導(dǎo)入了新的依賴,所以我們需要再執(zhí)行一下 go mod tidy
英岭。然后我們啟動服務(wù)湾盒,測試一下效果。
? 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 去幫助測試和調(diào)試gRPC服務(wù)诅妹。
? 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位小數(shù)吭狡,然后得到了我們期望的結(jié)果:22.22 + 33.33 + 44.45 = 100尖殃。
總結(jié)
本文我們學(xué)到了采用go-doudou微服務(wù)框架開發(fā)gRPC服務(wù)的基本技能,同時我們需要知道go-doudou不僅可以幫助開發(fā)者輕松地開發(fā)gRPC服務(wù)划煮,它還包含了一套完善的服務(wù)治理方案幫助開發(fā)者打造完整的微服務(wù)系統(tǒng)送丰。go-doudou雖然開源時間不長,但是非常有發(fā)展?jié)摿Π愦耍M絹碓蕉嗟拈_發(fā)者可以加入進(jìn)來蚪战。