今天遇到了一个 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
方法内打断点, 查看是否调用了方法以及两次调用传的参数是否一致步骤简单来说, 就是自上而下, 先从外层找问题, 当发现外层一切正常, 再向里边找, 就像剥洋葱一样, 一层一层, 直到定位到问题所在.