由 go orm 引發的探索

前言

今天遇到了一個 bug, 是 golang 的orm導致的. 使用了gorm框架. 通過實現ScanValue可以將數據庫中的 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方法的從屬從指針類型改爲值類型就可以解決這個問題.

此時我恍然大悟, 想起了之前的方法集的概念.

  1. 指針類型擁有 值/指針 的方法
  2. 值類型只擁有值類型的方法

也就是說, go 在底層是使用值類型來調用的, 所以拿不到指針方法, 故而報錯.

看到這裏, 如果你也遇到同樣的問題, 將Value方法從屬改爲值類型就可以解決了. 以下內容是我手賤之後的另一個愚蠢記錄, 可跳過.

另一個問題

此時我以爲我已經深得精髓, 解決方法很簡單, 將兩個方法的從屬都改爲值類型就好了嘛. 修改後, 插入數據果然沒有問題了, 但是當我查詢的時候, 發現了另一個問題, Content對象沒有賦值, 是空的.

當時我一臉懵逼, 沒有找到問題所在, 我做了什麼? 於是, 我就開始了打斷點之路:

image-20200828233430062

我發現它走到這裏, 調用了Scan方法, 那麼, dest 又是個什麼對象呢?

image-20200828233606565

於是, 我又找到了這個賦值的地方, 將類型打印出來後, 是:

**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, 滿心激動的查了下去. 直到最終發現問題的時候, 我懵逼了.

之前我哥就和我說, 查問題要從表現去推測. 而這次就是直接奔着底層去了, 結果做了很多無用功.

我回想了一下, 當時正確的檢查步驟應該是:

  1. Scan方法內打斷點, 查看是否調用了方法以及兩次調用傳的參數是否一致
  2. 當發現調用方法且參數一致時, 就直接到了最後一步並最終找到指針的問題
  3. 若沒有調用方法或參數不一致時, 再往調用的地方去找

步驟簡單來說, 就是自上而下, 先從外層找問題, 當發現外層一切正常, 再向裏邊找, 就像剝洋蔥一樣, 一層一層, 直到定位到問題所在.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章