这是帖子的摘录;完整的帖子可以在这里找到:https://victoriametrics.com/blog/go-slice/
新开发人员通常认为切片非常容易获得,只是一个与常规数组相比可以改变大小的动态数组。但老实说,当谈到它们如何改变大小时,它比看起来更棘手。
所以,假设我们有一个切片变量 a,并将其分配给另一个变量 b。现在,a 和 b 都指向同一个底层数组。如果您对切片 a 进行任何更改,您也会在 b 中看到这些更改。
但情况并非总是如此。
a 和 b 之间的联系并不是那么牢固,在 go 中,你不能指望 a 中出现的每一个变化都会出现在 b 中。
经验丰富的 go 开发人员将切片视为指向数组的指针,但有一个问题:该指针可能会在没有通知的情况下发生更改,如果您不完全理解切片的工作原理,这会使切片变得棘手。在本次讨论中,我们将涵盖从基础知识到切片如何增长以及它们如何在内存中分配的所有内容。
在我们深入了解细节之前,我建议先检查一下数组是如何工作的。
切片的结构如何
一旦声明了具有特定长度的数组,该长度就会作为其类型的一部分“锁定”。例如,[1024]byte 的数组与 [512]byte 的数组是完全不同的类型。
现在,切片比数组更灵活,因为它们基本上是数组顶部的一层。它们可以动态调整大小,并且您可以使用append()添加更多元素。
创建切片的方法有很多种:
// a is a nil slice var a []byte // slice literal b := []byte{1, 2, 3} // slice from an array c := b[1:3] // slice with make d := make([]byte, 1, 3) // slice with new e := *new([]byte)
最后一个并不常见,但它是合法的语法。
与数组不同,数组中的 len 和 cap 是常量并且始终相等,而切片则不同。在数组中,go 编译器提前知道长度和容量,甚至将其烘焙到 go 汇编代码中。
但是对于切片,len 和 cap 是动态的,这意味着它们可以在运行时更改。
切片实际上只是描述底层数组“切片”的一种方式。
例如,如果您有一个像 [1:3] 这样的切片,它从索引 1 开始,在索引 3 之前结束,因此长度为 3 - 1 = 2。
func main() { array := [6]int{0, 1, 2, 3, 4, 5} slice := array[1:3] fmt.println(slice, len(slice), cap(slice)) } // output: // [1 2] 2 5
上面的情况可以用下图来表示。
切片的长度就是其中有多少个元素。在本例中,我们有 2 个元素 [1, 2]。上限基本上是从切片开头到底层数组结尾的元素数量。
上面对容量的定义有点不准确,我们会在增长部分讨论它。
由于切片指向底层数组,因此对切片所做的任何更改也会更改底层数组。
“我通过 len 和 cap 函数知道切片的长度和容量,但如何找出切片实际开始的位置?”
让我向您展示 3 种通过查看其内部表示来查找切片开头的方法。
您可以使用 println 来获取切片的原始值,而不是使用 fmt.println:
func main() { array := [6]byte{0, 1, 2, 3, 4, 5} slice := array[1:3] println("array:", &array) println("slice:", slice, len(slice), cap(slice)) } // output: // array: 0x1400004e6f2 // slice: [2/5]0x1400004e6f3 2 5
从该输出中,您可以看到切片底层数组的地址与原始数组的地址不同,这很奇怪,对吧?
让我们在下图中形象化这一点。
如果您已经查看了之前有关数组的文章,您将了解元素如何存储在数组中。真正发生的是切片直接指向数组[1]。
证明这一点的第二种方法是使用 unsafe.slicedata 获取指向切片底层数组的指针:
func main() { array := [6]byte{0, 1, 2, 3, 4, 5} slice := array[1:3] arrptr := unsafe.slicedata(slice) println("array[1]:", &array[1]) println("slice.array:", arrptr) } // output: // array[1]: 0x1400004e6f3 // slice.array: 0x1400004e6f3
当您将切片传递给 unsafe.slicedata 时,它会进行一些检查以确定要返回的内容:
- 如果切片的容量大于 0,则该函数返回指向切片第一个元素的指针(在本例中为 array[1])。
- 如果切片为零,则函数仅返回 nil。
- 如果切片不为零但容量为零(空切片),则该函数会给您一个指针,但它指向“未指定的内存地址”。
您可以在 go 文档中找到所有这些内容。
“‘这个指针指向未指定的内存地址’是什么意思?”
有点断章取义,不过满足一下我们的好奇心吧:)
在 go 中,您可以使用大小为零的类型,例如 struct{} 或 [0]int。当 go 运行时为这些类型分配内存时,它不会为每个类型分配唯一的内存地址,而是返回一个名为 zerobase 的特殊变量的地址。
您可能已经明白了,对吧?
我们前面提到的“未指定”内存就是这个零基地址。
func main() { var a struct{} fmt.printf("struct{}: %pn", &a) var b [0]int fmt.printf("[0]int: %pn", &b) fmt.println("unsafe.slicedata([]int{}):", unsafe.slicedata([]int{})) } // output: // struct{}: 0x104f24900 // [0]int: 0x104f24900 // unsafe.slicedata([]int{}): 0x104f24900
很酷,对吧?就像我们刚刚揭开了 go 所隐藏的一个小秘密。
让我们继续第三条路。
在幕后,切片只是一个具有三个字段的结构:array - 指向底层数组的指针,len - 切片的长度,cap - 切片的容量。
type slice struct { array unsafe.pointer len int cap int }
这也是我们计算切片起点的第三种方法的设置。当我们这样做时,我们将证明上面的结构确实是切片内部工作的方式。
type sliceheader struct { array unsafe.pointer len int cap int } func main() { array := [6]byte{0, 1, 2, 3, 4, 5} slice := array[1:3] println("slice", slice) header := (*sliceheader)(unsafe.pointer(&slice)) println("sliceheader:", header.array, header.len, header.cap) } // output: // slice [2/5]0x1400004e6f3 // sliceheader: 0x1400004e6f3 2 5
输出正是我们所期望的,我们通常将这个内部结构称为切片头(sliceheader)。 reflect包中还有一个reflect.sliceheader,但它已被弃用。
现在我们已经掌握了切片的结构,是时候深入了解它们的实际行为了。
切片如何生长
之前,我提到“上限基本上是基础数组的长度,从切片的第一个元素开始到该数组的末尾。”这并不完全准确,只是正确的在那个特定的例子中。
例如,当您使用切片操作创建新切片时,有一个选项可以指定切片的容量:
func main() { array := [6]int{0, 1, 2, 3, 4, 5} slice := array[1:3:4] println(slice) } // Output: // [2/3]0x1400004e718
默认情况下,如果切片操作中不指定第三个参数,则从切片后的切片或切片数组的长度中获取容量。
在此示例中,切片的容量设置为原始数组的索引 4(不包含索引,如长度)。
那么,让我们重新定义切片容量的真正含义。
“切片的容量是它在需要增长之前可以容纳的最大元素数量。”
如果你不断向切片添加元素并且它超出了其当前容量,go 将自动创建一个更大的数组,复制元素,并将该新数组用作切片的基础数组。
完整的帖子可以在这里找到:https://victoriametrics.com/blog/go-slice/
以上就是Go 中的切片:变大或回家的详细内容,更多请关注php中文网其它相关文章!