前言:
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 []string
sliceInit := []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 []string
if err,result := QueryAll();err== nil{
list = result.Rows
}
ToJson(map[string]interface{}{
"list": list,
})
有没有看出什么问题?如果 QueryAll 返回错误的话,那么 list 永远不会被初始化,则很有可能在转换时被解析为 null 而不是 []。
如下代码:
package main
import (
"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
而造成 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 函数这种初始化方式算做“冗余”写法。
原文发布时间为:2018-09-5
本文作者:绅士喵