写业务代码无非就是数据在各种不同的地方转换,从这个模型转到那个模型,
为了降低心智负担,写代码的时候采用了一个非常好用的库"github.com/jinzhu/copier",这篇文章主要记录了一次使用copier库踩坑的过程。
改业务代码的时候经常发现前端会显示/u0000这么一个奇怪的玩意
啊,这TM是什么东西啊???看的好不爽啊!下定决心要把它弄掉!
顺着数据流动的链路,debug,它显示明明是“”,里面啥也没有。但每次都能注意到是数据模型之间,会有出现数据类型不一致的情况。
数据data流动:db(varchar) -> dao(int) -> grpc-server(string) -> grpc-cli(string) -> handler(string) -> frontend
应该是数据转换的问题吧,于是我在每个地方,都将len(data)打印出来
发现从grpc-server开始len(data)就是1了,关注到dao中,发现data类型是int值是0
看起来是copier.Copy有问题!
开启debug模式
首先它会来到一个循环,对结构体内部的每一个Field进行copy尝试
在fieldFlags, _ := flgs.BitFlags[name]处打断点,watch name变量
当name等于想要观察的Field的变量名称时watch变量的内容并且F8逐步向下
在!set(toField, fromField, opt.DeepCopy, converters)中发现变量发生了copy
重新debug,再次来到此处然后F7
...
for _, field := range fromTypeFields {
name := field.Name
// Get bit flags for field
fieldFlags, _ := flgs.BitFlags[name]
// Check if we should ignore copying
if (fieldFlags & tagIgnore) != 0 {
continue
}
srcFieldName, destFieldName := getFieldName(name, flgs)
if fromField := source.FieldByName(srcFieldName); fromField.IsValid() && !shouldIgnore(fromField, opt.IgnoreEmpty) {
// process for nested anonymous field
destFieldNotSet := false
if f, ok := dest.Type().FieldByName(destFieldName); ok {
for idx := range f.Index {
destField := dest.FieldByIndex(f.Index[:idx+1])
if destField.Kind() != reflect.Ptr {
continue
}
if !destField.IsNil() {
continue
}
if !destField.CanSet() {
destFieldNotSet = true
break
}
// destField is a nil pointer that can be set
newValue := reflect.New(destField.Type().Elem())
destField.Set(newValue)
}
}
if destFieldNotSet {
break
}
toField := dest.FieldByName(destFieldName)
if toField.IsValid() {
if toField.CanSet() {
if !set(toField, fromField, opt.DeepCopy, converters) {
if err := copier(toField.Addr().Interface(), fromField.Interface(), opt); err != nil {
return err
}
}
if fieldFlags != 0 {
// Note that a copy was made
flgs.BitFlags[name] = fieldFlags | hasCopied
}
}
} else {
// try to set to method
var toMethod reflect.Value
if dest.CanAddr() {
toMethod = dest.Addr().MethodByName(destFieldName)
} else {
toMethod = dest.MethodByName(destFieldName)
}
if toMethod.IsValid() && toMethod.Type().NumIn() == 1 && fromField.Type().AssignableTo(toMethod.Type().In(0)) {
toMethod.Call([]reflect.Value{fromField})
}
}
}
}
...
一路F7最终会来到这样一个地方,因为我是int转string,这边的名称也是convert Int String
应该来对地方了,它先把from的data传到v,v断言为int类型,然后先尝试转成rune类型,再int64转回去看v的值是否会发生变化,如果不发生变化说明x转成rune对数据不会产生影响(不会溢出之类的),于是将其转成rune以string的形式存储到s,后面就是s赋值到to的data的过程
func cvtIntString(v Value, t Type) Value {
s := "\uFFFD"
if x := v.Int(); int64(rune(x)) == x {
s = string(rune(x))
}
return makeString(v.flag.ro(), s, t)
}
所以说了半天,根本原因就是它把int的0变成rune(0)发现“问题不大”,就把它返回了,我们知道golang中字符是unicode编码的所以0就是/u0000也就是NUL。最后做个验证,将data转换前改成97,unicode中是字符'a',一通操作之后,果然在前端显示是'a'。
解决办法:
- 可以将数据格式统一
- 可以转换结束后手动更正string的值
- 可以在int->string的时候将int类型值写为需要的unicode编码值(不推荐)
一段菜鸟debug的经历,希望能帮助到大家!