go-kratos 使用体会:如何帮助研发提效

Mew151
6 min readMar 10, 2023

--

从 Java 转 Go 也两三年了,除了喜于其较 Java 独特的语言特性和编程上的简洁性,也会由于它不够丰富的类库而带来开发上的不便,比如没有像 Java SpringBoot 这样成熟的工程化框架。

在最初使用 Go 语言做业务开发上时,跟很多新手一样,用最原始的方式搭建项目:新建一个空项目,按照 DDD 的思想分层手动建一些目录,然后 import gin ,这样,就可以进行 http 接口的开发了,其他的一些基础组件,比如像配置解析、日志、链路追踪等,都是用到什么找什么库,东拼西凑把这个项目弄完整。

那么在开发过程中,主要有以下两个痛点:

关于 API 的

1、编写接口文档要研发在部门内的 YApi 平台上一点一点手敲出来,经常会发生代码中的接口定义修改了,但文档没改的情况,导致对接的前端同学在群里反馈说接口又调不通了。

2、facade 层代码要根据接口文档定义来手写,参数校验规则、默认值使用 tag 标记在 DTO 上,有时候校验规则、默认值在 DTO 做修改但又忘记改文档,这时前端同学又按不住了,在群里”吼”,你们接口怎么又调不通啦 🫠

关于错误的

在项目中对 error 的定义和使用一直没有太统一的方式,虽然我们也有自己封装的 error:

type CustomError struct {
Code int
Message string
}

并且将一些通用的 error(比如”参数错误”、”token 失效”等)抽成 func,但总体来说使用起来还是有不清晰的地方。

一开始在定义 error 时大家可能还会稍微想想这个 error 会作用于哪些层(facade or application or domain or infra),是定义在 common 库中,还是放到项目的某个层中,但随着时间久了,再加上开发进度有时很赶,况且总是思考 error 定义到哪也耗精力,于是很多时候对于错误的定义连我们自己封装的 error 库也不用了(毕竟还得编个 Code),直接用标准库的 errors.New(...),凭当时的感觉来决定应该把定义的 error 放到哪。这样造成了两个问题,一是 error 定义比较分散,在一个项目中,几个人同时开发,都不能很直观知道到底有哪些 error,经常是我这边定义了一个,你那边又定义了一个相似的,二是 error 的定义不统一,有的 error 是用封装的库定义的,有的 error 是用标准库定义的,带来的麻烦就是在错误处理解析 error 的时候还得一点点从调用链往上找,看这个 error 到底是哪种 error 🤨

以上说的两个痛点带来的开发体感不是很好,直到前些日子调研了一下 go-kratos 并应用到在做的一个项目中,发现这些问题都被很好的解决了。

那 kratos 是怎么解决的呢?它使用了 一切定义皆 proto 的方式,把 API 定义、接口文档、参数校验规则、默认值、error 定义、配置定义全部放到 proto 文件中,使用 protoc 的各种插件结合 Makefile 定制脚本,一条命令不仅能生成 http / gRPC 两种方式的接口,还能生成参数校验、设置默认值、error 定义的代码,同时也生成了所有接口的文档,只需要一键导入 YApi 就可以了 😀

拿常见的用户登录接口举例,在 proto 中定义:

// 用户登录 ①-1
rpc UserLogin (UserLoginRequest) returns (UserLoginReply) {
option (google.api.http) = {
post: "/api/v1/user/login"
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { ②
parameters: {
headers: {
name: "Authorization";
description: "认证token";
type: STRING,
required: true;
};
};
};
}

// 用户登录请求 ①-2
message UserLoginRequest {
// 用户名 ①-3
string username = 1 [(google.api.field_behavior) = REQUIRED, (validate.rules).string.min_len = 1]; ③-1
// 密码 ①-4
string password = 2 [(google.api.field_behavior) = REQUIRED, (validate.rules).string.min_len = 1]; ③-2
}

① 和 ② 的作用是生成文档,③ 的作用是标注参数校验规则,生成出来的文档非常的漂亮:

而在开发用户登录 application 层的代码时,参数校验只需要一行代码就搞定了:

func (as *ApplicationService) UserLogin(ctx context.Context, req *v1.UserLoginRequest) (*v1.UserLoginReply, error) {
if err := req.ValidateAll(); err != nil {
...
}

...
}

当然,如果参数中有设置默认值,再加一行 req.Default() 就好了。

对于错误的定义也是类似,在 proto 中定义:

enum ErrorReason {
// 设置缺省错误码
option (errors.default_code) = 500;

SERVER_EXCEPTION = 0;

ACCESS_DB_EXCEPTION = 1;
ACCESS_CACHE_EXCEPTION = 2;

INVALID_PARAMETER = 401 [(errors.code) = 400];
BAD_REQUEST = 402 [(errors.code) = 400];

INVALID_TOKEN = 410 [(errors.code) = 401];

USER_NOT_FOUND = 440 [(errors.code) = 404];

}

在业务代码中统一使用 pb.go 文件中生成的 error 定义,比如:

if ... {
return v1.ErrorServerException("describe some reason here")
}

这样下来,通过把各种定义 收拢到一个地方,不仅方便修改、管理和维护,避免改了这个忘了那个的问题,也让研发的精力更聚焦于业务逻辑的实现上,减少了手工编写文档和常规代码的时间。那省下来的时间干什么呢,去喝杯咖啡吧 ☕

--

--

Mew151

十年经验的coolder,记录和分享自己对软件开发的一些心得和感悟