添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

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