kratos学习笔记

框架结构

https://go-kratos.dev/blog/go-project-layout/

个人理解:

  • Data 层
    数据层,类似 DDD 的 Repo 层,主要包含数据操作(如 DB、外部接口 等),实现 Biz 的 Repo 接口。
    这里去掉了 DDD 的 防腐层,而内化到了 Data 层。
  • Biz 层
    业务逻辑层,类似 DDD 的 Service 层,完成 User Case。
    Biz 层实际上已经内化了 DDD 的 Domain 层(值对象、实体和聚合根)。
  • Service 层
    服务层,类似 DDD 的 Application 层,实现API的定义(跟 Server 对接,实现服务暴露),主要处理 DTO 转 DO,不应包含复杂的业务逻辑。
  • Server 层
    把 Service 注入到 HTTP 或 RPC 实例。

Server -> Service -> Biz -> Data,每一层的依赖都通过 Wire 自动注入。

顺便贴一张之前做 Kratos 整合 Nacos 时梳理的图以供参照:


image.png

国际化

错误的国际化已经封装在了 error_reason.proto 里,但很多时候,一些正常的输出也需要做国际化,这时可以用 https://github.com/nicksnyder/go-i18n 来处理。

用法:

# common/i18n/zh/zh.go 里定义翻译内容
var TranslateToml = map[string]string{
   ...
   "time_duration": "{{.d}}天{{.h}}时{{.m}}分{{.s}}秒",
}
# 逻辑里使用
i18n.Localize(ctx, "time_duration", func(option *i18n.LocalizeOptions) {
   option.MsgParams = map[string]interface{}{
      "d": days,
      "h": hours,
      "m": minutes,
      "s": seconds,
   }
})

使用 Validator 做入参校验
入参校验可以做在 biz 层,可以是 Do 里,也可以是在函数里定义的局部 struct,纯粹用于入参校验。

部分疑难杂症
关于错误处理
因为 Proto 里已经提供了 Error 封装,所以,哪一层抛出的 Error,最好在那一层及时封装处理。

而实际上,大部分 Error 封装都应该发生在 Biz 层。

Xorm更新时忽略结构体中零值字段的问题
比如,费用中心中,在余额支付时,如果扣款刚好为0,则 balance=0 放在 Account 结构体,再执行 session.Update 时是会被忽略的。

这时候,只能传递 Map 进去。

用JSON序列化和反序列化实现对象数据拷贝时时间类型转字符串的处理
当原类型中有字段类型为 time.Time 时,反序列化为 string 时会因不匹配而丢失,比如:

src 为指向 struct 的指针类型,其中 struct 的 CreatedAt 字段为 time.Time 类型;dst 为指向 struct 的指针类型,其中 struct 的 CreatedAt 字段为 string 类型。

可通过反射分别判断原类型和目标类型,再进行转换时的处理:

func Copy(src interface{}, dst interface{}) error {
    var json = jsoniter.ConfigCompatibleWithStandardLibrary
    marshal, err := json.Marshal(src)
 
    if err != nil {
        return err
    }
    err = json.Unmarshal(marshal, &dst)
    if err != nil {
        return err
    }
 
    // 通过反射设置类型不相同的字段
    srcVal, dstVal := reflect.ValueOf(src), reflect.ValueOf(dst)
    srcValKind, dstValKind := srcVal.Kind(), dstVal.Kind()
    srcKey, dstKey := reflect.TypeOf(src), reflect.TypeOf(dst)
    // 如果是指针 则钻取指向的数据类型
    if srcValKind == reflect.Ptr {
        srcVal = srcVal.Elem()
        srcValKind = srcVal.Kind()
    }
    if dstValKind == reflect.Ptr {
        dstVal = dstVal.Elem()
        dstValKind = dstVal.Kind()
    }
    if srcKey.Kind() == reflect.Ptr {
        srcKey = srcKey.Elem()
    }
    if dstKey.Kind() == reflect.Ptr {
        dstKey = dstKey.Elem()
    }
 
    if srcValKind == reflect.Struct && dstValKind == reflect.Struct {
        for i := 0; i < srcVal.NumField(); i++ {
            // src 和 dst 的字段一样
            dstField, ok := dstKey.FieldByName(srcKey.Field(i).Name)
            // time.Time 转 string
            if ok && srcKey.Field(i).Type.String() == "time.Time" && dstField.Type.String() == "string" {
                dstVal.FieldByIndex(dstField.Index).SetString(srcVal.Field(i).Interface().(time.Time).Format("2006-01-02 15:04:05"))
            } else {
                continue
            }
        }
    }
 
    return nil
}

注:reflect.Elem() 为通过反射获取指针指向的元素类型。

Proto定义中支持传入不同类型的入参 - Any类型 & Struct类型
为了兼容前端传入和数据库中可能存在的多种类型数据(整型、字符串、浮点型等),考虑在 proto 文件中使用一个类似 go interface{} 的类型。

google 的 protobuf 内置了一个 anypb.Any 的 struct,于是 proto 文件中这样设置:

import "google/protobuf/any.proto";
message CommodityAttr {
    ...
    map<string, google.protobuf.Any> props = 5; // 配置项
}

因为通过 json.Marshal/Unmarshal 进行数据拷贝,所以,需要实现 Any 类型的 Unmarshal 自定义:

import (
    "google.golang.org/protobuf/types/known/anypb"
    "google.golang.org/protobuf/types/known/wrapperspb"
)
type any anypb.Any
 
func (x *any) UnmarshalJSON(b []byte) error {
    value := wrapperspb.StringValue{Value: string(b)}
    data, err := anypb.New(&value)
    if err != nil {
        return err
    }
    *x = any{
        TypeUrl: data.TypeUrl,
        Value: data.Value,
    }
    return nil
}
type CommodityAttr struct {
    ...
    Props     map[string]*any   `json:"props"`     // 配置项
}

但因为 Any.Value 是 []byte 类型,后续想把 Any.Value 的值转换为 int 或 string 时发现有个奇怪的字符而导致转换失败。。。

如果参数是一个 Map,则可以用 Struct:


import "google/protobuf/struct.proto";
message CalculateRequest {
    ...
    google.protobuf.Struct opts = 2;
}

go 中:

func (s *CommodityService) Calculate(ctx context.Context, req *pb.CalculateRequest) (*pb.CalculateReply, error) {
    calculate, _, err := s.CommodityUcase.CalculateFee(ctx, req.Id, req.Opts.AsMap()) // 通过 AsMap 方法把 Struct 转换为 map[string]interface{}
    ...
}

关于类型转换
很多时候,我们需要把 interface{} 要转换成 int、bool、string、float...,虽然可以通过类型断言判断进行特定类型的转换处理,但断言需要判断成功与否,否则失败时会抛异常。

后发现,Xorm 中有个 convert 包,提供了很多类型转换方法(实现原理同上,也用到了内置的 strconv 包)。

具体用法比如:

import "xorm.io/xorm/convert"
valInt, valErr := convert.AsUint64(value)
# 而对于 interface{} 转 string,可以直接 convert.AsString,且此方法没有错误返回
str := convert.AsString(data)
# convert.AsString 比类型断言精简多了
str, ok := data.(string)
if ok {
    fmt.Println(str)
}

Xorm 自定义实现 time.Time 空值JSON的问题
空的 time.Time 在 JSON 时会输出 0001-01-01 00:00:00,所以在 Format 之前需要先做空值判断,可以是这样:

// 时间格式化
func TimeFormat(t time.Time, layout string) string {
    if t.IsZero() {
        return ""
    } else {
        return t.Format(layout)
    }
}

但可以尝试更优雅的实现:通过设定一个新的 struct,定义 MarshalJSON 来实现,比如:

import "time"
type CustomTime struct {
    time.Time
}
func (t CustomTime) MarshalJSON() ([]byte, error) {
    if time.Time(t).IsZero() {
        return []byte(`""`), nil
    }
    return []byte(`"` + time.Time(t).Format("2006-01-02 15:04:05") + `"`), nil
}

Xorm 中 Update 更新的问题
在操作数据时需注意指定主键,否则可能会全表更新:

_, err := session.ID(order.Id).Update(order)

获取当前时间下个月1日及解析时间字符串的问题
虽然可以巴拉巴拉一通代码,但,也可以有很优雅的方式:

time.Now().AddDate(0, 1, -time.Now().Day()+1)

另外,解析时间需注意用 time.ParseInLocation 并加上 time.Local:

time.ParseInLocation("2006-01-02 15:04:05", "2021-11-29 16:33:55", time.Local)

Validate 中 gtefield 不能用于字符串类型的数值对比
怎么办呢,没办法,只能自己扩展。举个例子:


type req struct {
    StartMonth string `validate:"required,datetime=200601" label:"开始月份"`
    EndMonth string `validate:"required,datetime=200601,gtefield=StartMonth" label:"结束月份"`
}

想要实现 EndMonth >= StartMonth,但 Validator 内置的 gtefield 对字符串类型,实际上是比较长度。。。

这种情况只能自定义校验实现:

import (
    ut "github.com/go-playground/universal-translator"
    v10 "github.com/go-playground/validator/v10"
)
// 定义一个结构体用于校验请求参数
type req struct {
    StartMonth string `validate:"required,datetime=200601" label:"开始月份"`
    EndMonth string `validate:"required,datetime=200601,gteField=StartMonth" label:"结束月份"`
}
err := validator.ValidateData(ctx, &req{
    StartMonth: startMonth,
    EndMonth: endMonth,
}, func(validate *v10.Validate, trans ut.Translator) {
    // 结束月份大于等于开始月份
    gteField := func(fl v10.FieldLevel) bool {
        // 获取当前字段信息(结束月份)
        field := fl.Field()
        kind := field.Kind()
        // 获取关联字段信息(开始月份)
        currentField, currentKind, ok := fl.GetStructFieldOK()
        if !ok || currentKind != kind {
            return false
        }
        // 字符串转换为整数并进行比较
        end, _ := strconv.Atoi(field.String())
        start, _ := strconv.Atoi(currentField.String())
        return end >= start
    }
    // 注册校验器
    _ = validate.RegisterValidation("gteField", gteField)
    // 注册翻译器
    _ = validate.RegisterTranslation("gteField", trans, func(ut ut.Translator) error {
        var text string
        if trans.Locale() == "en" {
            text = "{0} cannot be earlier than the StartMonth"
        } else {
            text = "{0}不能早于开始月份"
        }
        return ut.Add("gteField", text, true)
    }, func(ut ut.Translator, fe v10.FieldError) string {
        t, _ := ut.T("gteField", fe.Field())
        return t
    })
})
if err != nil {
    return nil, err
}

http.Client 发送 HTTPS 请求时报 x509: certificate... 错误

跟HTTPS请求的证书信任有关,可以配置成跳过证书认证:

// 跳过证书认证
transport := &http.Transport{
    TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient := http.Client{Timeout: 30 * time.Second, Transport: transport}
resp, respErr := httpClient.Do(req)

不过这种做法有待商榷,最好是本地有HTTPS证书可以用于请求认证。

相关资料

https://go-kratos.dev/docs/getting-started/examples/

框架文档

【必选】ProtoBuf 官方文档,https://developers.google.com/protocol-buffers/docs/proto3

【必选】Kratos 官方文档,https://go-kratos.dev/docs/

基于 Kratos 的基础框架,Canvas-Kratos-V0.2.x设计与实施

Canvas-Kratos接入文档

【可选】Gin 官方文档,https://gin-gonic.com/zh-cn/docs/

【可选】gRPC 官方文档,https://www.grpc.io/docs/

开发组件

【必选】Xorm 中文文档,https://gobook.io/read/gitea.com/xorm/manual-zh-CN/

【必选】Validator API 文档,https://pkg.go.dev/github.com/go-playground/validator/v10

https://github.com/xxl-job/xxl-job-executor-go/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容