Go 语言中的 nil 切片 vs 非 nil 切片 vs 空切片

Posted

技术标签:

【中文标题】Go 语言中的 nil 切片 vs 非 nil 切片 vs 空切片【英文标题】:nil slices vs non-nil slices vs empty slices in Go language 【发布时间】:2017-11-02 11:06:15 【问题描述】:

我是 Go 编程的新手。我在 Go 编程书籍中读到 slice 由三部分组成:指向数组的指针、长度和容量。

我对 nil 切片(切片没有可指向的底层数组,len = 0,cap=0)、只有 len = 0、cap = 0 的非 nil 切片和空切片感到困惑。

谁能告诉 nil 和 empty slices 是否相同? 如果两者都不同,那么请说说这两者之间的区别是什么?

如何判断一个切片是否为空?

另外,指针在长度和容量为零的非零切片中持有什么值?

【问题讨论】:

【参考方案1】:

可观察的行为

nil 和空切片(容量为 0)不一样,但它们的可观察行为是一样的。我的意思是:

您可以将它们传递给内置的 len()cap() 函数 您可以for range 覆盖它们(将是 0 次迭代) 您可以对它们进行切片(不违反Spec: Slice expressions 中列出的限制;因此结果也将是一个空切片) 由于它们的长度为 0,因此您无法更改它们的内容(附加值会创建新的切片值)

看这个简单的例子(一个nil切片和2个非nil空切片):

var s1 []int         // nil slice
s2 := []int        // non-nil, empty slice
s3 := make([]int, 0) // non-nil, empty slice

fmt.Println("s1", len(s1), cap(s1), s1 == nil, s1[:], s1[:] == nil)
fmt.Println("s2", len(s2), cap(s2), s2 == nil, s2[:], s2[:] == nil)
fmt.Println("s3", len(s3), cap(s3), s3 == nil, s3[:], s3[:] == nil)

for range s1 
for range s2 
for range s3 

输出(在Go Playground上试试):

s1 0 0 true [] true
s2 0 0 false [] false
s3 0 0 false [] false

(请注意,切片nil 切片会产生nil 切片,切片非nil 切片会产生非nil 切片。)

您只能通过将切片值与预先声明的标识符nil 进行比较来区分它们,它们在其他方面的行为相同。

要判断一个切片是否为空,只需将其长度与0:len(s) == 0 进行比较。不管是nil 切片还是非nil 切片,它是否具有正容量也没关系;如果没有元素,则为空。

s := make([]int, 0, 100)
fmt.Println("Empty:", len(s) == 0, ", but capacity:", cap(s))

打印(在Go Playground 上试用):

Empty: true , but capacity: 100

引擎盖下

切片值由reflect.SliceHeader中定义的结构体表示:

type SliceHeader struct 
    Data uintptr
    Len  int
    Cap  int

如果是nil 切片,则此结构将具有其零值,即其所有字段都将为零值,即:0

拥有一个容量和长度都等于0LenCap 的非nil 字段肯定是0,但Data 指针可能不是。 不会不是,这就是它与nil 切片的区别所在。它将指向一个大小为零的底层数组。

请注意,Go 规范允许大小为 0 的不同类型的值具有相同的内存地址。 Spec: System considerations: Size and alignment guarantees:

如果结构或数组类型不包含大小大于零的字段(或元素),则它的大小为零。 两个不同的零大小变量在内存中可能具有相同的地址。

让我们检查一下。为此,我们调用 unsafe 包的帮助,并“获取”我们切片值的 reflect.SliceHeader 结构“视图”:

var s1 []int
s2 := []int
s3 := make([]int, 0)

fmt.Printf("s1 (addr: %p): %+8v\n",
    &s1, *(*reflect.SliceHeader)(unsafe.Pointer(&s1)))
fmt.Printf("s2 (addr: %p): %+8v\n",
    &s2, *(*reflect.SliceHeader)(unsafe.Pointer(&s2)))
fmt.Printf("s3 (addr: %p): %+8v\n",
    &s3, *(*reflect.SliceHeader)(unsafe.Pointer(&s3)))

输出(在Go Playground 上试试):

s1 (addr: 0x1040a130): Data:       0 Len:       0 Cap:       0
s2 (addr: 0x1040a140): Data: 1535812 Len:       0 Cap:       0
s3 (addr: 0x1040a150): Data: 1535812 Len:       0 Cap:       0

我们看到了什么?

所有片(片头)都有不同的内存地址 nil 切片具有 0 数据指针 s2s3 切片确实有相同的数据指针,共享/指向相同的 0 大小的内存值

【讨论】:

你能解释一下究竟什么是 0 大小的内存值吗?我的意思是,对于空切片,底层数组分配了内存但没有元素,或者它下面没有分配内存并且切片结构中的数据指针包含任何随机地址? @SurbhiVyas 内存地址没有任何意义,因为它指向的地方不包含数据,或者更确切地说,指向的区域不被读取,因为它只保留了 0 个字节(即:没有)。它是大小为 0 的不同类型变量的特殊保留地址。这是一个实现细节,你不应该担心它或做任何事情。 感谢您的完整解释。 这里要注意的重要一点,如果提供了一个 nil 值,go 的 append 函数不会恐慌。从语义上讲,它将这样的输入视为一个 empty 切片并按原样附加到它上面。这避免了用户代码中的大量样板零检查。【参考方案2】:

从字面意义上来说,切片可以为零:

var n []int
n == nil // true

这是唯一简单的情况。 “空”切片的概念没有明确定义:带有len(s) == 0 的切片s 肯定是空的,无论其容量如何。 最明智的做法是:忽略底层实现,永远不需要知道切片在内部是如何表示的。重要的是切片的定义行为。

如何判断一个切片是否为空?

“切片s 是空的”最合理的定义是一个不包含任何元素的切片,它转换为len(s) == 0。此定义适用于 nil 以及非 nil 切片。

谁能告诉 nil 和 empty slices 是否相同?如果两者都不同,那么请告诉它们两者之间的区别是什么?

从技术上讲,nil 切片和非 nil 切片是不同的(一个是 == nil,另一个是 != nil),但是这种区别通常无关紧要,因为您可以 append 对 nil 切片和 len 和nil 切片的上限返回 0

var n []int
len(n) == cap(n) == 0 // true
n = append(n, 123)
len(n) == 1 // true

阅读 Go 中的零值以获取更多信息。一个零切片就像一个零通道或一个零映射:它是未初始化的。您可以通过makeing 或文字进行初始化。如上所述,没有理由考虑底层表示。

另外,指针在长度和容量为零的非零切片中持有什么值?

这是一个实现细节,可能因编译器而异,甚至因版本而异。没有人需要知道这一点来编写正确且可移植的 Go 程序。

【讨论】:

对于最后一个问题,根据the spec,“使用 make 创建的切片始终分配一个新的隐藏数组,返回的切片值指向该数组。”【参考方案3】:
var s1 []int         // nil slice
s2 := []int        // non-nil, empty slice
s3 := make([]int, 0) // non-nil, empty slice

警告,如果处理 JSON,nil 切片将编码为 null 而不是 [],这可能会破坏一些 (javascript) 客户端尝试迭代(没有 null 检查)不可迭代的 null。

【讨论】:

以上是关于Go 语言中的 nil 切片 vs 非 nil 切片 vs 空切片的主要内容,如果未能解决你的问题,请参考以下文章

go语言之nil

GO切片

go 数组切片

Go 语言入门三部曲:能看懂 Go 语言

Go语言 nil:空值/零值

Nil Channels Always Block(Go语言中空管道总是阻塞)