由 go orm 引发的探索
前言
今天遇到了一个 bug, 是 golang 的orm
导致的. 使用了gorm
框架. 通过实现Scan
与Value
可以将数据库中的 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
方法的从属从指针类型改为值类型就可以解决这个问题.
此时我恍然大悟, 想起了之前的方法集的概念.
- 指针类型拥有 值/指针 的方法
- 值类型只拥有值类型的方法
也就是说, 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, 满心激动的查了下去. 直到最终发现问题的时候, 我懵逼了.
之前我哥就和我说, 查问题要从表现去推测. 而这次就是直接奔着底层去了, 结果做了很多无用功.
我回想了一下, 当时正确的检查步骤应该是:
- 在
Scan
方法内打断点, 查看是否调用了方法以及两次调用传的参数是否一致 - 当发现调用方法且参数一致时, 就直接到了最后一步并最终找到指针的问题
- 若没有调用方法或参数不一致时, 再往调用的地方去找
步骤简单来说, 就是自上而下, 先从外层找问题, 当发现外层一切正常, 再向里边找, 就像剥洋葱一样, 一层一层, 直到定位到问题所在.