由 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
方法内打断点, 查看是否调用了方法以及两次调用传的参数是否一致 - 当发现调用方法且参数一致时, 就直接到了最后一步并最终找到指针的问题
- 若没有调用方法或参数不一致时, 再往调用的地方去找
步骤简单来说, 就是自上而下, 先从外层找问题, 当发现外层一切正常, 再向里边找, 就像剥洋葱一样, 一层一层, 直到定位到问题所在.
- 每周学点大数据 | No.25二叉搜索树回顾(二)
- RBF神经网络及Python实现(附源码)
- 【干货】计算机视觉实战系列03——用Python做图像处理
- Adaboost从原理到实现(Python)
- Selenium+python自动化22-发送各种类型附件邮件
- Selenium2+python自动化38-显示等待(WebDriverWait)
- 逆元(个人模版)
- Selenium2+python自动化37-爬页面源码(page_source)
- ex_gcd(个人模版)
- Selenium2+python自动化36-判断元素存在
- Java A+B(个人模版)
- TensorFlow实战:SoftMax手写体MNIST识别(Python完整源码)
- set排序(个人模版)
- TSP(个人模版)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- ubuntu20.04设置静态ip地址(包括不同版本)
- LayoutAnimation给ListView中的item设置动态出场效果(实例)
- android studio2.3如何编译动态库的过程详解
- Android RecyclerView设置下拉刷新的实现方法
- Android 动态添加view或item并获取数据的实例
- Centos7实现MySQL基于日志还原数据的示例代码
- Android 三种延迟操作的实现方法
- 基于Android在布局中动态添加view的两种方法(总结)
- Android向node.js编写的服务器发送数据并接收请求
- Android startActivityForResult和setResult的区别
- Linux系统使用Fuser命令的方法
- Android实现地理定位功能
- Android实现在ServiceManager中加入自定义服务的方法详解
- Android studio so库找不到问题解决办法
- Android使用ViewPager实现屏幕滑动效果