前言
GoLand 是 Jetbrains 推出的 Golang IDE,在內側階段我就開始使用了,剛出的時候我還在部落格中發表過文章(看了下日期是 16 年年底)。
那時候它還不是很完善,BUG 很多。準確的說也不算 BUG,主要是文法提示上的各種不足,重構功能也很弱。後來我有一段時間沒有寫 Go 代碼,直到它更新為正式版我才差不多又抽出機會繼續寫 Go 代碼了。雖然它已經很完善了,但還是發現它的一個很小但又很明顯的 BUG,不過這個 BUG 卻恰好給我造成了麻煩,所以我才想發文描述一下。
有關零值
When storage is allocated for a variable, either through a declaration or a call of new, or when a new value is created, either through a composite literal or a call of make, and no explicit initialization is provided, the variable or value is given a default value. Each element of such a variable or value is set to the zero value for its type: false for booleans, 0 for numeric types, “” for strings, and nil for pointers, functions, interfaces, slices, channels, and maps. This initialization is done recursively, so for instance each element of an array of structs will have its fields zeroed if no value is specified.
官方文檔中明確指出,Slice(切片)的零值是 nil,在沒有明確初始化的情況下這是顯而易見的,任何 Go 開發人員應該都知道才對。
但是對於 GoLand 而言,聲明並直接初始化的空 Slice 和聲明不初始化 Slice 是一回事。為什麼這麼說呢?如果你在 GoLand 中寫入以下兩句代碼:
var sliceNoInit []stringsliceInit := []string{}
第二行代碼的等號右邊會有波浪線,提示這種寫法可以被 Cleanup。如果你根據 IDE 提示按 Alt +Enter 那麼第二行代碼會被重構成:
var sliceInit []string
你沒有看錯,GoLand 將一個初始化過的 Slice 重構為了未初始化的的 Slice…… GoLand 認為前者是冗餘的寫法。的確,大部分函數對於 nil Slice 和 empty Slice 而言,都是一樣的,例如它們的長度都是 0。不過
nil Slice 的長度為 0,所以長度為 0 的都可以轉變 nil Slice
這樣考慮就有問題了。這有什麼問題嗎?它們兩個長度不都是零嗎?當然有問題,在 len 函數的注釋清晰的寫了這麼一句話:
if v is nil, len(v) is zero
這是什麼意思呢?就是說 len 函數允許參數為 nil,但只要參數為 nil 不管任何類型都會返回 0。所以 nil Slice 的長度為零是一種不嚴禁的說法,應該說調用 len 函數的結果為零。
所以,初始化後空的 Slice 是絕對不等同於沒有初始化的 nil Slice 的。最直接的,它們對 slice == nil 的結果就相反。
帶來的麻煩
在使用某些 JSON 架構的時候,nil Slice 會被解析為 null 而不是 [],如果你有 web 開發經驗就知道這是個多麼嚴重的小問題了。而我恰好就是遇到這個問題了,最後追查原因原來是我手動初始化的空 Slice 被重構成了未初始化 nil Slice,直接導致了這個轉換為 null 的結果。
我們用虛擬碼舉例(不涉及具體架構):
var list []stringif err,result := QueryAll();err== nil{ list = result.Rows}ToJson(map[string]interface{}{ "list": list,})
有沒有看出什麼問題?如果 QueryAll 返回錯誤的話,那麼 list 永遠不會被初始化,則很有可能在轉換時被解析為 null 而不是 []。
如下代碼:
package mainimport ( "encoding/json" "github.com/pkg/errors" "fmt")type result struct { Rows []string Columes []string}func main() { //1、var list []string //2、list := make([]string, 0) list := make([]string, 0) //3、list := []string{} if err, result := QueryAll(); err == nil { list = result.Rows } ss, err := json.Marshal(map[string]interface{}{ "list": list, }) fmt.Printf("ss:%s, error:%v", string(ss), err)}func QueryAll() (error, result) { return errors.New("error"), result{}}
如果用第一種方式初始化:
var list []string
結果為:
ss:{"list":null}, error:<nil>
用第二種和第三種方式初始化:*
list := make([]string, 0)list := []string{}
結果為:
ss:{"list":[]}, error:<nil>
如果你想這麼寫解決這個弊端:
list := []string{}
那麼你會被 GoLand 活活糾正成上面那樣…… 然後帶來錯誤的結果。
真實細節
在 GoLand 的部落格中有一篇文章提到了關於 Slice 的零值,他們是這麼認為的:
a nil slice is functionally equivalent to a zero-length slice, even though it points to nothing
原文在這裡:https://blog.golang.org/slices
而造成 gin 的 JSON 方法將 nil Slice 轉換 null 的原因是 json.Marshal() 就是這麼處理的,也就是說幾乎任何涉及到 JSON 處理的地方都有可能因為 GoLand 團隊認為二者在功能上完全等價而造成問題。
解決辦法
- 進入 Settings -> Editor - Inspections
- 展開 Go - Declaration redundancy
- 將 Empty slice declared via literal 右邊的勾去掉
這麼做可以關掉 GoLand 對直接初始化的空 Slice 的冗餘檢查。
不過有意思的是還有一種以代碼的方式杜絕這種提示,那就是使用 make 函數:
list := make([]string, 0)
實際上使用 make 建立一個長度為 0 的 empty Slice 和不插入一個值的直接初始化這兩者才是等價的…… 但是 GoLand 卻不會將 make 函數這種初始化方式算做“冗餘”寫法。
結束語
本文來自知乎:https://zhuanlan.zhihu.com/p/35777565
剛好今天遇到這個goland警告,順便學習記錄下,侵刪!