美文网首页
kratos学习笔记

kratos学习笔记

作者: ZplD | 来源:发表于2023-10-30 15:50 被阅读0次

    框架结构

    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/

    相关文章

      网友评论

          本文标题:kratos学习笔记

          本文链接:https://www.haomeiwen.com/subject/ylbbidtx.html