golang copier库踩坑 '/u0000'

433 阅读3分钟

写业务代码无非就是数据在各种不同的地方转换,从这个模型转到那个模型, 为了降低心智负担,写代码的时候采用了一个非常好用的库"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最终会来到这样一个地方,因为我是intstring,这边的名称也是convert Int String
应该来对地方了,它先把from的data传到vv断言为int类型,然后先尝试转成rune类型,再int64转回去看v的值是否会发生变化,如果不发生变化说明x转成rune对数据不会产生影响(不会溢出之类的),于是将其转成runestring的形式存储到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'。

解决办法:

  1. 可以将数据格式统一
  2. 可以转换结束后手动更正string的值
  3. 可以在int->string的时候将int类型值写为需要的unicode编码值(不推荐)

一段菜鸟debug的经历,希望能帮助到大家!