工業(yè)設(shè)計(jì)專(zhuān)業(yè)就業(yè)前景怎么樣百度優(yōu)化服務(wù)
文章目錄
- 前言
- 1、mangokit介紹
- 1.1 根據(jù)proto文件生成http路由
- 1.2 根據(jù)proto文件生成響應(yīng)碼
- 1.3 使用wire來(lái)管理依賴(lài)注入
- 2、mangokit實(shí)現(xiàn)
- 2.1 protobuf插件開(kāi)發(fā)
- 2.2 mangokit工具
- 3、使用示例
- 3.1 創(chuàng)建新項(xiàng)目
- 3.2 添加新的proto文件
- 3.3 代碼生成
前言
在使用gin框架開(kāi)發(fā)web應(yīng)用時(shí),需要我們自己手動(dòng)完成請(qǐng)求到結(jié)構(gòu)體的反序列化,以及發(fā)送響應(yīng),如下:
func Handler(ctx *gin.Context) {user := new(User)if err := ctx.ShouldBind(&user); err != nil {...}...resp := serivce()...ctx.Json(http.StatusOk, resp)
}
顯然,這些工作都是多余的,和業(yè)務(wù)無(wú)關(guān)的,每個(gè)handler都需要我們自己處理,非常的麻煩
為了解決這個(gè)問(wèn)題,我們可以使用反射的方式來(lái)字段完成請(qǐng)求數(shù)據(jù)到結(jié)構(gòu)體的映射;對(duì)于響應(yīng),則定義一個(gè)統(tǒng)一的結(jié)構(gòu)體,并且讓handler返回這個(gè)結(jié)構(gòu)體,如下:
type Response struct {R RespValueStatus int
}type RespValue struct {Data interface{} `json:"data"`Codee int `json:"code"`Messagee string `json:"message"`
}func NewResponse(status, code int, message string, data interface{}) *Response {...
}
func Handler(ctx *gin.Context, user *User) *Response {...resp = service()...return NewResponse(http.StatusOk, 0, "success", resp)
}
在注冊(cè)路由時(shí),則需要使用反射來(lái)對(duì)我們的handler進(jìn)行適配,使用反射機(jī)制創(chuàng)建請(qǐng)求參數(shù),然后將數(shù)據(jù)反序列化為對(duì)應(yīng)的結(jié)構(gòu)體,然后調(diào)用我們定義的handler,并且獲取到返回值,調(diào)用ctx.Json來(lái)發(fā)送
這種方式方便了我們的開(kāi)發(fā),但是使用反射會(huì)對(duì)程序帶來(lái)一定的性能損失(但是在這里只是簡(jiǎn)單的使用,性能損失很少),并且使用反射容易出現(xiàn)錯(cuò)誤
最近在使用了bilibili的kratos框架后,給了我一些靈感,我們完全可以使用proto來(lái)定義http的路由,然后生成反序列化的結(jié)構(gòu)代碼,并且可以使用proto來(lái)定義返回錯(cuò)誤碼等。
因此借鑒了kratos的設(shè)計(jì),我實(shí)現(xiàn)了一個(gè)小工具用來(lái)加速我的web開(kāi)發(fā)
github:https://github.com/mangohow/mangokit
?
1、mangokit介紹
mangokit是一個(gè)web項(xiàng)目的管理工具,它的功能如下:
- 根據(jù)預(yù)設(shè)的項(xiàng)目結(jié)構(gòu)創(chuàng)建出一個(gè)web項(xiàng)目,使用已有的代碼框架,減少工作量
- 使用proto來(lái)定義http路由以及錯(cuò)誤碼,使用相關(guān)工具生成代碼,完成自動(dòng)結(jié)構(gòu)體反序列化以及返回值響應(yīng)
- 使用wire來(lái)管理依賴(lài)注入,減少依賴(lài)管理的煩惱
1.1 根據(jù)proto文件生成http路由
proto定義文件如下:
hello.proto
syntax = "proto3";package hello.v1;import "google/api/annotations.proto";option go_package = "api/helloworld/v1;v1";// 定義service
service Hello {rpc SayHello (HelloRequest) returns (HelloReply) {option (google.api.http) = {get: "/hello/:name"};}
}message HelloRequest {string name = 1;
}message HelloResponse {string message = 2;
}
然后使用mangokit命令根據(jù)proto生成gin框架對(duì)應(yīng)的路由處理器:
mangokit generate proto api
生成的文件如下:
hello.pb.go
hello_http_gin.pb.go
其中hello.pb.go
是protoc --go-out生成的,而hello_http_gin.pb.go
是我們自己寫(xiě)的proto插件protoc-gen-go-gin
生成的
hello_http_gin.pb.go
的代碼如下:
// Code generated by protoc-gen-go-gin. DO NOT EDIT.
// versions:
// - protoc-gen-go-gin v1.0.0
// - protoc v3.20.1
// source: helloworld/v1/proto/hello.protopackage v1import ("context""net/http""github.com/mangohow/mangokit/serialize""github.com/mangohow/mangokit/transport/httpwrapper"
)type HelloHTTPService interface {SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}func RegisterHelloHTTPService(server *httpwrapper.Server, svc HelloHTTPService) {server.GET("/hello/:name", _Hello_SayHello_HTTP_Handler(svc))
}func _Hello_SayHello_HTTP_Handler(svc HelloHTTPService) httpwrapper.HandlerFunc {return func(ctx *httpwrapper.Context) error {in := new(HelloRequest)if err := ctx.BindRequest(in); err != nil {return err}value := context.WithValue(context.Background(), "gin-ctx", ctx)reply, err := svc.SayHello(value, in)if err != nil {return err}ctx.JSON(http.StatusOK, serialize.Response{Data: reply})return nil}
}
在上面生成的go代碼中,包含一個(gè)接口的定義,其中包含了我們定義的handler方法
并且提供了RegisterHelloHTTPService
函數(shù)來(lái)注冊(cè)路由,注冊(cè)的路由為_Hello_SayHello_HTTP_Handler
函數(shù),在這個(gè)函數(shù)中有反序列化的代碼,以及響應(yīng)代碼
因此我們只需要實(shí)現(xiàn)HelloHTTPService
中的方法,并且調(diào)用RegisterHelloHTTPService
來(lái)注冊(cè)路由即可,大大的減少了我們的工作量。
這有點(diǎn)類(lèi)似于grpc的方式。
1.2 根據(jù)proto文件生成響應(yīng)碼
有時(shí)候只使用http的狀態(tài)碼是不夠的,比如200表示請(qǐng)求成功,但是雖然請(qǐng)求成功了,還可能出現(xiàn)其它問(wèn)題。
比如一個(gè)登錄的接口,用戶(hù)登錄時(shí)可能出現(xiàn)以下的情況:1、用戶(hù)不存在 2、密碼錯(cuò)誤 3、用戶(hù)被封禁了
因此,我們需要定義相關(guān)的一些響應(yīng)碼來(lái)處理這些情況
proto定義文件如下:
errors.proto
syntax = "proto3";package errors.v1;
import "errors/errors.proto";option go_package = "api/errors/v1;v1";enum ErrorReason {// 設(shè)置缺省錯(cuò)誤碼option (errors.default_code) = 500;Success = 0 [(errors.code) = 200];// 為某個(gè)枚舉單獨(dú)設(shè)置錯(cuò)誤碼UserNotFound = 1 [(errors.code) = 200];UserPasswordIncorrect = 2 [(errors.code) = 200];UserBanned = 3 [(errors.code) = 200];
}
在上面的proto文件中,我們使用enum來(lái)定義響應(yīng)碼,其中包括int
類(lèi)型的響應(yīng)碼,以及返回的http狀態(tài)碼(errors.code)
然后使用mangokit來(lái)生成go代碼:
mangokit generate proto api
生成的文件如下:
errors.pb.go
errors_errors.pb.go
其中errors.pb.go
是protoc --go_out生成的,而errors_errors.pb.go
同樣也是自己編寫(xiě)的proto插件protoc-gen-go-error
生成的
errors_errors.pb.go
中的代碼如下:
// Code generated by protoc-gen-go-error. DO NOT EDIT.
// versions:
// - protoc-gen-go-error v1.0.0
// - protoc v3.20.1
// source: errors/v1/proto/errors.protopackage v1import ("fmt""github.com/mangohow/mangokit/errors"
)func ErrorSuccess(format string, args ...interface{}) errors.Error {return errors.New(int32(ErrorReason_Success), 200, ErrorReason_Success.String(), fmt.Sprintf(format, args...))
}// 為某個(gè)枚舉單獨(dú)設(shè)置錯(cuò)誤碼
func ErrorUserNotFound(format string, args ...interface{}) errors.Error {return errors.New(int32(ErrorReason_UserNotFound), 200, ErrorReason_UserNotFound.String(), fmt.Sprintf(format, args...))
}func ErrorUserPasswordIncorrect(format string, args ...interface{}) errors.Error {return errors.New(int32(ErrorReason_UserPasswordIncorrect), 200, ErrorReason_UserPasswordIncorrect.String(), fmt.Sprintf(format, args...))
}func ErrorUserBanned(format string, args ...interface{}) errors.Error {return errors.New(int32(ErrorReason_UserBanned), 200, ErrorReason_UserBanned.String(), fmt.Sprintf(format, args...))
}
然后我們就可以調(diào)用這些函數(shù)來(lái)生成具體的響應(yīng)碼,減少我們的代碼工作量
1.3 使用wire來(lái)管理依賴(lài)注入
wire是谷歌開(kāi)源的一款依賴(lài)注入工具,相比于其它的反射式的依賴(lài)注入方式,wire采用代碼生成的方式來(lái)完成依賴(lài)注入,代碼運(yùn)行效率更高
代碼如下:
//go:generate wire
//go:build wireinject
// +build wireinjectpackage mainimport ("github.com/google/wire""mangokit_test/internal/conf""mangokit_test/internal/dao""mangokit_test/internal/server""mangokit_test/internal/service""github.com/mangohow/mangokit/transport/httpwrapper""github.com/sirupsen/logrus"
)func newApp(serverConf *conf.Server, dataConf *conf.Data, logger *logrus.Logger) (*httpwrapper.Server, func(), error) {panic(wire.Build(dao.ProviderSet, service.ProviderSet, server.NewHttpServer))
}
根據(jù)上面的代碼,wire即可自動(dòng)生成依賴(lài)創(chuàng)建的代碼:
// Code generated by Wire. DO NOT EDIT.//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinjectpackage mainimport ("mangokit_test/internal/conf""mangokit_test/internal/dao""mangokit_test/internal/server""mangokit_test/internal/service""github.com/mangohow/mangokit/transport/httpwrapper""github.com/sirupsen/logrus"
)// Injectors from wire.go:func newApp(serverConf *conf.Server, dataConf *conf.Data, logger *logrus.Logger) (*httpwrapper.Server, func(), error) {db, cleanup, err := dao.NewFakeMysqlClient(dataConf)if err != nil {return nil, nil, err}greeterDao := dao.NewGreeterDao(db)greeterService := service.NewGreeterService(greeterDao, logger)httpwrapperServer := server.NewHttpServer(serverConf, logger, greeterService)return httpwrapperServer, func() {cleanup()}, nil
}
同樣的mangokit中也添加了相應(yīng)的指令來(lái)生成wire依賴(lài)注入代碼
mangokit generate wire
?
2、mangokit實(shí)現(xiàn)
mangokit主要包含三個(gè)組件:
protoc-gen-go-gin
protoc-gen-go-error
mangokit
protoc-gen-go-gin
用于根據(jù)proto文件中定義的service來(lái)生成gin框架的路由代碼
protoc-gen-go-error
用于根據(jù)proto文件中定義的enum來(lái)生成相應(yīng)的響應(yīng)錯(cuò)誤碼
mangokit中則設(shè)置了多種指令用于管理項(xiàng)目,比如:
- 使用create命令來(lái)生成一個(gè)初始項(xiàng)目結(jié)構(gòu)
- 使用add命令來(lái)添加proto文件、makefile或Dockerfile
- 使用generate命令來(lái)根據(jù)proto文件生成go代碼、生成openapi以及生成wire依賴(lài)注入
2.1 protobuf插件開(kāi)發(fā)
在使用protoc時(shí)可以指定其它的插件用于生成代碼,比如:
--go_out
則會(huì)調(diào)用protoc-gen-go插件來(lái)生成go的代碼--go-grpc_out
則會(huì)調(diào)用protoc-gen-go-grpc插件來(lái)生成grpc的代碼
同樣的,我們可以使用go來(lái)實(shí)現(xiàn)一個(gè)類(lèi)似的插件,從而根據(jù)proto文件來(lái)生成gin框架的代碼以及響應(yīng)碼代碼
工作原理:
在使用 protoc --go-gin_out
時(shí),protoc
會(huì)解析proto
文件,然后生成抽象語(yǔ)法樹(shù)
,并且它會(huì)使用protobuf
將語(yǔ)法樹(shù)
序列化為二進(jìn)制序列
,然后使用標(biāo)準(zhǔn)輸入將二進(jìn)制序列傳入我們的插件中,然后再使用protobuf
進(jìn)行反序列化
,然后我們?cè)谧约旱某绦蛑芯涂梢愿鶕?jù)提供的信息來(lái)生成go代碼,比如:proto
中定義的message、service、enum
等
開(kāi)發(fā)proto插件我們可以使用google.golang.org/protobuf/compiler/protogen
庫(kù)
我們可以參考kratos的代碼來(lái)實(shí)現(xiàn)自己的代碼:
https://github.com/go-kratos/kratos/tree/main/cmd/protoc-gen-go-errors
首先看main函數(shù):
protogen.Options.Run來(lái)運(yùn)行我們的程序
在傳入的匿名函數(shù)中,我們會(huì)接收到protogen.Plugin參數(shù),該參數(shù)中有proto文件中定義的各種結(jié)構(gòu)的詳細(xì)信息
然后我們可以遍歷每個(gè)proto文件來(lái)生成相應(yīng)的代碼,在generateFile中生成代碼
package mainimport ("flag""fmt""google.golang.org/protobuf/compiler/protogen""google.golang.org/protobuf/types/pluginpb"
)var (showVersion = flag.Bool("version", false, "print the version and exit")
)func main() {flag.Parse()if *showVersion {fmt.Printf("protoc-gen-go-gin %v\n", version)return}protogen.Options{ParamFunc: flag.CommandLine.Set,}.Run(func(plugin *protogen.Plugin) error {plugin.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL)for _, f := range plugin.Files {if !f.Generate {continue}generateFile(plugin, f)}return nil})
}
在protogen.File中保存了一個(gè)proto文件中定義的各種結(jié)構(gòu)解析后的信息:
詳細(xì)代碼參考:https://github.com/mangohow/mangokit
代碼編寫(xiě)好之后編譯為二進(jìn)制程序,在使用protoc時(shí)指定插件名稱(chēng),我們的插件一定要以protoc-gen
開(kāi)頭,在指定插件名稱(chēng)時(shí)指定protoc-gen
后面的部分拼接上_out
即可;比如protoc-gen-go-error
,在使用時(shí)為:protoc --go-error_out=. hello.proto
2.2 mangokit工具
mangokit使用cobra命令行工具開(kāi)發(fā),包含以下功能:
- 創(chuàng)建基礎(chǔ)項(xiàng)目:根據(jù)預(yù)設(shè)的項(xiàng)目目錄結(jié)構(gòu)和代碼生成
- 添加文件:包括api proto、error proto、makefile和Dockerfile
- 生成代碼:包括go代碼生成、wire生成和openapi生成
?
3、使用示例
3.1 創(chuàng)建新項(xiàng)目
首先使用mangokit來(lái)創(chuàng)建一個(gè)項(xiàng)目,項(xiàng)目目錄為mangokit-test,go mod名稱(chēng)為mangokit_test
mangokit create mangokit-test mangokit_test
然后執(zhí)行cd mangokit-test && go mod tidy
來(lái)下載依賴(lài)
項(xiàng)目目錄結(jié)構(gòu)如下:
$ tree
.
|-- api
| |-- errors
| | `-- v1
| | |-- errors.pb.go
| | |-- errors_errors.pb.go
| | `-- proto
| | `-- errors.proto
| `-- helloworld
| `-- v1
| |-- greeter.pb.go
| |-- greeter_http_gin.pb.go
| `-- proto
| `-- greeter.proto
|-- cmd
| `-- server
| |-- main.go
| |-- wire.go
| `-- wire_gen.go
|-- configs
| `-- application.yaml
|-- internal
| |-- conf
| | |-- conf.pb.go
| | `-- conf.proto
| |-- dao
| | |-- dao.go
| | |-- data.go
| | `-- userdao.go
| |-- middleware
| |-- model
| | `-- user.go
| |-- server
| | `-- http.go
| `-- service
| |-- helloservice.go
| `-- service.go
|-- pkg
|-- test
|-- third_party
|-- go.mod
|-- go.sum
|-- makefile
|-- Dockerfile
|-- openapi.yaml32 directories, 52 files
api:
api目錄用來(lái)放置proto文件以及根據(jù)proto文件生成的go代碼,通常將.proto文件放在proto文件夾下,而生成的代碼放在它的上一級(jí)目錄,這樣看起來(lái)更清晰一些cmd:
cmd目錄存放了wire注入代碼和main文件configs:
configs目錄用來(lái)放置程序的配置文件internal:
internal用來(lái)存放本項(xiàng)目依賴(lài)的代碼,不會(huì)暴露給其它的項(xiàng)目,其中包括middleware(中間件)
、model(數(shù)據(jù)庫(kù)結(jié)構(gòu)體模型
)、dao(數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)對(duì)象
)、conf(配置信息代碼)
、server(服務(wù)初始化代碼)
、service(service的具體實(shí)現(xiàn)代碼)
pkg:
用來(lái)存放一些共用代碼test:
存放測(cè)試代碼third_party:
其中包含一些使用到的proto的擴(kuò)展文件
在創(chuàng)建項(xiàng)目時(shí)默認(rèn)會(huì)從github拉取一個(gè)預(yù)制的項(xiàng)目結(jié)構(gòu),如果遇到網(wǎng)絡(luò)問(wèn)題導(dǎo)致無(wú)法拉取,則可以使用-r
命令來(lái)指定其它的倉(cāng)庫(kù),比如使用gitee:
mangokit create -r https://gitee.com/mangohow/mangokit-template mangokit-test mangokit_test
3.2 添加新的proto文件
可以使用下面的命令來(lái)添加新的proto文件
# 添加http api
mangokit add api api/helloworld/v1/proto hello.proto
然后就會(huì)在api/helloworld/v1/proto目錄下生成一個(gè)hello.proto文件
syntax = "proto3";package hello.v1;import "google/api/annotations.proto";option go_package = "api/helloworld/v1;v1";service Hello {}
使用下面的命令來(lái)添加error proto
mangokit add error api/errors/v1/proto errorReason.proto
同樣的,在api/errors/v1/proto目錄下生成了errorReason.proto文件
syntax = "proto3";package errorReason.v1;import "errors/errors.proto";option go_package = "api/errors/v1;v1";enum ErrorReason {option (errors.default_code) = 500;Placeholder = 0 [(errors.code) = 0];}
除了添加proto文件,還可以添加預(yù)制的makefile和Dockerfile
3.3 代碼生成
根據(jù)proto生成代碼
# 根據(jù)api目錄下的proto文件生成go代碼
mangokit generate proto api
根據(jù)wire依賴(lài)注入生成代碼:
mangokit generate wire
生成openapi文檔
mangokit generate openapi
生成上面所有的三個(gè)項(xiàng)目
mangokit generate all