由 go orm 引发的探索

时间:2022-07-24
本文章向大家介绍由 go orm 引发的探索,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言

今天遇到了一个 bug, 是 golang 的orm导致的. 使用了gorm框架. 通过实现ScanValue可以将数据库中的 json 内容解析出来, 免除了 字符串再解码的步骤. 当时报错的代码大概是这样的:

type TestContent struct {
 Id int
 Content Content // 数据库中的 json 结构
}

type Content struct {
 Name string
 Age int
}

func (c *Content) Scan(value interface{}) error {
 return json.Unmarshal(value.([]byte), c)
}

func (c *Content) Value() (driver.Value, error) {
 return json.Marshal(c)
}

向数据库插入数据, 调用Create方法时报错了:

[2020-08-28 23:18:25] sql: converting argument $1 type: unsupported type main.Content, a struct

这这这, 什么鬼? 当时我百思不得其所. 经过多次尝试, 我发现将Value方法的从属从指针类型改为值类型就可以解决这个问题.

此时我恍然大悟, 想起了之前的方法集的概念.

  1. 指针类型拥有 值/指针 的方法
  2. 值类型只拥有值类型的方法

也就是说, go 在底层是使用值类型来调用的, 所以拿不到指针方法, 故而报错.

看到这里, 如果你也遇到同样的问题, 将Value方法从属改为值类型就可以解决了. 以下内容是我手贱之后的另一个愚蠢记录, 可跳过.

另一个问题

此时我以为我已经深得精髓, 解决方法很简单, 将两个方法的从属都改为值类型就好了嘛. 修改后, 插入数据果然没有问题了, 但是当我查询的时候, 发现了另一个问题, Content对象没有赋值, 是空的.

当时我一脸懵逼, 没有找到问题所在, 我做了什么? 于是, 我就开始了打断点之路:

我发现它走到这里, 调用了Scan方法, 那么, dest 又是个什么对象呢?

于是, 我又找到了这个赋值的地方, 将类型打印出来后, 是:

**main.Content

是一个二级指针, 这时, 我以为是因为二级指针的问题. 于是我动手写了一段代码来模拟这段操作:

func main(){
  // 这里模拟了当时设置的代码内容
 typeOf := reflect.TypeOf(Content{})
 reflectValue := reflect.New(reflect.PtrTo(typeOf))
 reflectValue.Elem().Set(reflect.ValueOf(&Content{}))
 r := reflectValue.Interface()
 if c, ok := r.(**Content); ok {
  (**c).SetName("1111")
  fmt.Println(fmt.Sprintf("%+v", **c))
 }
}

// 这里, 为了方便测试, 添加了 SetName 方法, 与 Scan 相同
func (nt Content) SetName(name string) {
 nt.Name = name
}

当我看到结果的时候, 发现name依旧没有设置进去. 我了个喵, 什么情况?

然后我开始了疯狂检查的过程, 直到我写下了这段代码之后, 我陷入了沉思:

 content := Content{}
 content.SetName("hh")
 fmt.Println(fmt.Sprintf("%+v", content))

当我发现直接设置都没用的时候, 我知道, 一定是我哪个最简单的地方出错了. 我默默的点起一支烟, 望着眼前的代码发起了呆.

我经过与之前改动的对比, 知道问题一定是出在指针与值类型的转换上.

我我我我的天, 最终我发现我犯了一个多么愚蠢的错误. 使用值类型是无法对其字段进行修改的, 其修改通通是通过值复制进行, 并不会影响原始对象. 而且我右打了断点发现, 方法并不是没有调, 确实是调用了, 只不过因为从属与值而没有对原始对象造成影响.

总结

就在我刚开始查这个问题的时候, 我自认为找到了什么不得了的 bug, 满心激动的查了下去. 直到最终发现问题的时候, 我懵逼了.

之前我哥就和我说, 查问题要从表现去推测. 而这次就是直接奔着底层去了, 结果做了很多无用功.

我回想了一下, 当时正确的检查步骤应该是:

  1. Scan方法内打断点, 查看是否调用了方法以及两次调用传的参数是否一致
  2. 当发现调用方法且参数一致时, 就直接到了最后一步并最终找到指针的问题
  3. 若没有调用方法或参数不一致时, 再往调用的地方去找

步骤简单来说, 就是自上而下, 先从外层找问题, 当发现外层一切正常, 再向里边找, 就像剥洋葱一样, 一层一层, 直到定位到问题所在.