kratos学习笔记

2023-10-30  本文已影响0人  ZplD

框架结构

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

个人理解:

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/

上一篇下一篇

猜你喜欢

热点阅读