最近准备使用微服务框架做一套系统,当然不用框架也能做(使用gRPC、API网关和权限校验创建Go微服务 Part 1/2),但人力有限的情况下用框架肯定是更好的选择。显然 go-micro
时代已经过去了,那么我们更容易锁定到国内开源的两个框架,go-zero
和Kratos
,各有千秋。本着没有深入使用就没有发言权的原则,我很可能会各实现一套来进行对比,就学习资源而言,当前Kratos
处于劣势。
关于Kratos
就不过多介绍了,是由Bilibili主导开源的一套微服务框架。本文是对Kratos
使用中问题的一些总结,版本自然是v2
,毕竟v1
官方自己都说了有很多设计缺陷,更重要的是从Bilibili 的主账号里脱离出来了,发展就看社区参与度了。初用起来感觉就是深度绑定了protobuf
,不同于go-zero
同时在其基础上封装了一个.api
文件,全部直接使用.proto
文件进行定义。然后就是使用wire
来处理依赖注入。
官方对目录结构的说明如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
. ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── api // 下面维护了微服务使用的proto文件以及根据它们所生成的go文件 │ └── helloworld │ └── v1 │ ├── error_reason.pb.go │ ├── error_reason.proto │ ├── error_reason.swagger.json │ ├── greeter.pb.go │ ├── greeter.proto │ ├── greeter.swagger.json │ ├── greeter_grpc.pb.go │ └── greeter_http.pb.go ├── cmd // 整个项目启动的入口文件 │ └── server │ ├── main.go │ ├── wire.go // 我们使用wire来维护依赖注入 │ └── wire_gen.go ├── configs // 这里通常维护一些本地调试用的样例配置文件 │ └── config.yaml ├── generate.go ├── go.mod ├── go.sum ├── internal // 该服务所有不对外暴露的代码,通常的业务逻辑都在这下面,使用internal避免错误引用 │ ├── biz // 业务逻辑的组装层,类似 DDD 的 domain 层,data 类似 DDD 的 repo,而 repo 接口在这里定义,使用依赖倒置的原则。 │ │ ├── README.md │ │ ├── biz.go │ │ └── greeter.go │ ├── conf // 内部使用的config的结构定义,使用proto格式生成 │ │ ├── conf.pb.go │ │ └── conf.proto │ ├── data // 业务数据访问,包含 cache、db 等封装,实现了 biz 的 repo 接口。我们可能会把 data 与 dao 混淆在一起,data 偏重业务的含义,它所要做的是将领域对象重新拿出来,我们去掉了 DDD 的 infra层。 │ │ ├── README.md │ │ ├── data.go │ │ └── greeter.go │ ├── server // http和grpc实例的创建和配置 │ │ ├── grpc.go │ │ ├── http.go │ │ └── server.go │ └── service // 实现了 api 定义的服务层,类似 DDD 的 application 层,处理 DTO 到 biz 领域实体的转换(DTO -> DO),同时协同各类 biz 交互,但是不应处理复杂逻辑 │ ├── README.md │ ├── greeter.go │ └── service.go └── third_party // api 依赖的第三方proto ├── README.md ├── google │ └── api │ ├── annotations.proto │ ├── http.proto │ └── httpbody.proto └── validate ├── README.md └── validate.proto |
根据自身业务可以在使用kratos new -r xxx
(或是直接设置环境变量KRATOS_LAYOUT_REPO=xxx-layout.git
)来指定采用自己的项目布局。
问题总结
- 官方默认的 layout 目录已其实已经包含第三方包,但proto 文件仍然会出现红色波浪线,如
import "google/api/annotations.proto";
,以 VS Code 为例,只需要添加如下文件及配置:
.vscode/settings.json
1234567{"protoc": {"options": ["--proto_path=${workspaceRoot}/third_party",]}} - protoc-gen-validate: program not found or is not executable
123git clone https://github.com/envoyproxy/protoc-gen-validatecd protoc-gen-validate/go install . - 在 protobuf文件中添加的验证规则未生效
首先要确保在 protoc 命令中添加参数:
1--validate_out=paths=source_relative,lang=go:. \
其次在grpc.go
或http.go
文件(取决于你使用的服务)中应加入validate.Validator
中间件
1234567import "github.com/go-kratos/kratos/v2/middleware/validate"....http.Middleware(recovery.Recovery(),validate.Validator(),),.... - unsupported Scan, storing driver.Value type []uint8 into type *time.Time
这个错误与 Kratos 无关,顺便记录在此,大概率你不会碰到这个问题,因为对 MySQL的配置一般都会怼上charset=utf8mb4&parseTime=True&loc=Local
,万一出现这个报错,那就是缺少parseTime
所致。 - 成功及错误响应值格式可通过
http.ResponseEncoder
和http.ErrorEncode
进行定制 - Kratos 默认返回的错误消息同时反馈在 HTTP状态码中(在
kratos/v2/error
中还内置了多个方法,如BadRequest
、UnAuthorized
等),设计者应该是觉得HTTP状态码的语义更为前端所熟悉,但目前大多使用axios
库来发起 HTTP请求,这样默认service.interceptors.response.use
的拦截会直接进入error
部分,那返回的消息体岂不是毫无价值了?原因从axios
源码可以找到:
123validateStatus: function validateStatus(status) {return status >= 200 && status < 300;}
一种方式是修改Kratos
中的错误处理或者一律返回200到300之间的状态码,但这便辜负了框架的精妙设计,另一种自然就是从前端下手,比如假定简单粗暴地将500以下的状态码都视为正常,错误消息在response
由前端处理,可以使用如下配置:
123456const service = axios.create({validateStatus: (status)=>{return status < 500},...} - Protobuf和OpenAPI关于变量的使用方式有点烦,如果使用
camelCase
,一切太平,但如果使用snake_case
,就开始麻烦了,你会发现接口本身字段没有问题,但 OpenAPI 开始作妖,文档里会显示为camelCase
,那如何让文档里也显示为snake_case
呢?那就还要再加一个注解,比如:
1string captcha_id = 1 [json_name="captcha_id"];
顺便说一下,如果字段定义与json_name
不统一也会出问题,这种方法的缺点是太烦琐,需要为每个字段添加注解。
经过进一步的探索,在生成时可通过protoc-gen-openapi的参数naming=proto
来配置生成使用Protobuf
文件定义名称的文档字段(此步骤在 Kratos 中并无必要,只需关注下面的一个配置即可,但如果将生成的 openapi.yaml拿到外面使用,比如https://editor.swagger.io/,未加参数会显示为camelCase
)。
12345api:protoc --proto_path=./api \...--openapi_out=fq_schema_naming=true,naming=proto,default_response=false:. \...
但如果使用Kratos直接启动swagger,需要在server/http.go
中添加配置generator.UseJSONNamesForFields(false)
,因其默认值为true
:
12345openAPIhandler := openapiv2.NewHandler(openapiv2.WithGeneratorOptions(generator.UseJSONNamesForFields(false),generator.EnumsAsInts(true)))srv.HandlePrefix("/q/", openAPIhandler)
以上都是针对文档,实际返回字段需要对protojson
添加UseProtoNames
来进行序列化配置,Kratos中已暴露了相关配置(https://github.com/go-kratos/kratos/blob/main/encoding/json/json.go),只需将其加入到 main.go 文件的init()
中即可:
1234567func init() {json.MarshalOptions = protojson.MarshalOptions{EmitUnpopulated: true,UseProtoNames: true,}flag.StringVar(&flagconf, "conf", "../../configs", "config path, eg: -conf config.yaml")} - GRPC 不似 HTTP 那样利于调试,可以考虑使用以下工具:
- 在
make api
时出现添加--experimental_allow_proto3_optional
参数的提示是因为 在 proto中使用了optional
,但在3.15.0中这个特性已经转正,所以可以直接升级解决问题,当然不愿意升级请直接对protoc
命令添加该参数;那我们为什么要添加optional
呢(你还会在网上看到官方不建议使用optional
,并且在3.15.0之前它一直作为experimental
,要额外添加参数运行)?其实这与 Go 语言对零值的处理有关,在 Go 语言中数值、字符串等类型会默认被置为零值,那么问题就来了,我们怎么会知道一个int
类型的变量是未传参还是传参的值为0,这时就要使用引用类型*int
,在 Protobuf 中对应的就是在定义字段时在前面添加optional
。
补充一个知识点,使用 Vue 进行前端开发的同学很多人会使用element-ui
,它也有一个坑,就是在添加clearable
属性后清空字段会默认将v-model
对应的变量(即使它是一个数值类型)设置为空字符串,那问题来了,传一个空字符串给后端明显类型不匹配呀!于是我们又需要再添加一个@clear="status = null"
(假定变量名称为status
)来进行处理。 - POST 等请求如何获取query string,也就说怎么获取到http://mydomain.com/url?query1=param1&query2=param2中query1和 query2的值,一开始我以为要通过http.RequestDecoder进行自定义,其实并不需要:
1234567891011121314151617rpc Post (PostRequest) returns (google.protobuf.Empty){option (google.api.http) = {post: "/url",body: "body",};};...message PostRequest {string query1 = 1;string query2 = 2;Body body = 3;}message Body {....}
这里考虑到请求体可能有多个字段,定义了一个嵌套的Body类型来接收POST请求本身提交的字段 body:”body”中第二个body为 PostRequest 中的自定义字段,如仅一个字段,自然可以直接使用 string 等类型。当然你也可以通过/url/{query1}/{query2}
的方式来进行获取。
单元测试
- m.ctrl.T undefined (type *gomock.Controller has no field or method T)
1github.com/golang/mock v1.6.0 // indirect