前言
今天遇到了一個 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
方法內打斷點, 查看是否調用了方法以及兩次調用傳的參數是否一致 - 當發現調用方法且參數一致時, 就直接到了最後一步並最終找到指針的問題
- 若沒有調用方法或參數不一致時, 再往調用的地方去找
步驟簡單來說, 就是自上而下, 先從外層找問題, 當發現外層一切正常, 再向裏邊找, 就像剝洋蔥一樣, 一層一層, 直到定位到問題所在.