GO中片段容量和长度的获取方法

209 阅读8分钟

在Go中,一个片断的长度告诉你它包含多少个元素。它可以通过 len()函数获得。容量是指分片的底层数组的大小,可以通过 cap()函数获得。

阵列和片断的区别

为了更好地理解切片的容量和长度之间的区别,首先,你应该知道数组和切片的区别。

数组

数组是一个有索引的集合,其中有一定的 size 数值相同的 type,声明为。

var name [size]type

Go array

初始化一个数组

var a [4]int // array with zero values
var b [4]int = [4]int{0, 1, 2} // partially initialized array
var c [4]int = [4]int{1, 2, 3, 4} // array initialization
d := [...]int{5, 6, 7, 0} // ... - means that array size equals the number of elements in the array literal

fmt.Printf("a: length: %d, capacity: %d, data: %v\n", len(a), cap(a), a)
fmt.Printf("b: length: %d, capacity: %d, data: %v\n", len(b), cap(b), b)
fmt.Printf("c: length: %d, capacity: %d, data: %v\n", len(c), cap(c), c)
fmt.Printf("d: length: %d, capacity: %d, data: %v\n", len(d), cap(d), d)

输出

a: length: 4, capacity: 4, data: [0 0 0 0]
b: length: 4, capacity: 4, data: [0 1 2 0]
c: length: 4, capacity: 4, data: [1 2 3 4]
d: length: 4, capacity: 4, data: [5 6 7 0]

数组的属性

  • 数组有一个固定的大小,不能调整大小。片断可以被调整大小。
  • 数组的类型包括其大小。[4]int 数组的类型与[5]int 不同,它们不能被比较。
  • var name [size]type 初始化一个数组会创建一个类型为typesize 元素的集合,每个元素都是给定type零值
  • 数组是按值传递的。这意味着当你把一个数组赋值给另一个数组时,你将对其内容进行一个新的复制。
var a [4]int = [4]int{1, 2, 3, 4}
b := a
a[1] = 999
fmt.Println(a)
fmt.Println(b)

输出

[1 999 3 4]
[1 2 3 4]

分片

一个片断声明为:

var name []type

是一个数据结构,描述了一个数组的一个部分,有三个属性:

Go slice

  • ptr - 一个指向底层数组的指针
  • len - 分片的长度 - 分片中元素的数量
  • cap - 分片的容量 - 底层数组的长度,这也是分片的最大长度(直到它增长)。

一个片断不是一个数组。它描述了存储在ptr 指针下的底层数组的一个部分。

初始化一个分片

var a []int // nil slice
b := []int{0, 1, 2, 3} // slice initialized with specified array
c := make([]int, 4) // slice of size 4 initialized with zero-valued array of size 4
d := make([]int, 4, 5) // slice of size 4 initialized with zero-valued array of size 5

fmt.Printf("a: length: %d, capacity: %d, pointer to underlying array: %p, data: %v, is nil: %t\n", len(a), cap(a), a, a, a == nil)
fmt.Printf("b: length: %d, capacity: %d, pointer to underlying array: %p, data: %v, is nil: %t\n", len(b), cap(b), b, b, b == nil)
fmt.Printf("c: length: %d, capacity: %d, pointer to underlying array: %p, data: %v, is nil: %t\n", len(c), cap(c), c, c, c == nil)
fmt.Printf("d: length: %d, capacity: %d, pointer to underlying array: %p, data: %v, is nil: %t\n", len(d), cap(d), d, d, d == nil)

输出

a: length: 0, capacity: 0, pointer to underlying array: 0x0, data: [], is nil: true
b: length: 4, capacity: 4, pointer to underlying array: 0xc00001e060, data: [0 1 2 3], is nil: false
c: length: 4, capacity: 4, pointer to underlying array: 0xc00001e080, data: [0 0 0 0], is nil: false
d: length: 4, capacity: 5, pointer to underlying array: 0xc000016180, data: [0 0 0 0], is nil: false

正如我们在输出中看到的,var a []int ,创建了一个nil slice--一个长度和容量都等于0的slice,并且没有底层数组。

Go nil slice

用指定的数组初始化一个片断,即b := []int{0, 1, 2, 3} ,创建一个新的片断,其容量和长度取自底层数组。

Slice initialized with specified array

一个片断也可以通过内置的 make()函数来初始化一个片断,该函数将片断的类型作为第一个参数,长度作为第二个参数。得到的分片的容量等于长度,底层数组被初始化为零值

Slice initialized with make(Type, len)

也有一个替代版本的 make()函数的另一个版本,它有三个参数:第一个参数是切片的类型,第二个参数是长度,第三个参数是容量。通过这种方式,你可以创建一个容量大于长度的分片。

Slice initialized with make(Type, len, cap)

分片的属性

  • 当函数被调用时,切片会自动调整大小。 append()函数被调用时,切片会自动调整大小。这里的调整意味着 append()添加新的元素到分片的末端,如果底层数组没有足够的容量,将分配一个新的数组。该 append()函数总是返回一个新的、更新的片断,所以如果你想调整一个片断的大小,s ,有必要将结果存储在同一个变量s
  • 切片是不能比较的,简单的平等比较a == b ,是不可能的。请看如何比较片子
  • var name []type 初始化一个片子,会创建一个nil 片子,其长度和容量等于0,没有底层数组。请看nil和空片的区别是什么
  • 就像数组(以及Go中的所有东西)一样,分片是通过值传递的。当你把一个片断分配给一个新的变量时,ptr,len, 和cap 被复制,包括ptr 指针,它将指向同一个底层数组。如果你修改了复制的片断,你就修改了同一个共享数组,这使得所有的改变在新旧片断中都是可见的。
var a []int = []int{1, 2, 3, 4}
b := a
a[1] = 999
fmt.Println(a)
fmt.Println(b)

输出

[1 999 3 4]
[1 999 3 4]

长度和容量

你已经知道容量是分片的底层数组的大小,长度是分片元素的数量,但是它们之间的关系是什么?为了更好地理解这个问题,我们来分析一下重新切分和追加的操作。

重新切分

重新切分是一个从现有的数组或阵列中创建一个新片断的操作。要 "切分 "一个数组或 "重新切分 "一个现有的分片,使用一个半开的范围,用冒号隔开两个索引。

var arr [4]int = [4]int{1, 2, 3, 4}
a := arr[1:3]
fmt.Printf("a: length: %d, capacity: %d, data: %v\n", len(a), cap(a), a)

输出:

a: length: 2, capacity: 3, data: [2 3]

我们得到的分片结果是一样的:

var s []int = []int{1, 2, 3, 4}
a := s[1:3]
fmt.Printf("a: length: %d, capacity: %d, data: %v\n", len(a), cap(a), a)

输出:

a: length: 2, capacity: 3, data: [2 3]

Re-slicing - slice a

重新切分一个分片或一个数组会创建一个新的分片,其长度由索引范围给定,容量等于从分片的第一个元素的索引到数组末端的底层数组中的元素数。请看另外两个重新分片操作的例子--对于没有第一个索引的ranges[:3] ,和没有最后一个索引的s[3:]

b := s[:3]
fmt.Printf("b: length: %d, capacity: %d, data: %v\n", len(b), cap(b), b)

输出

b: length: 3, capacity: 4, data: [1 2 3]

Re-slicing - slice b


c := s[3:]
fmt.Printf("c: length: %d, capacity: %d, data: %v\n", len(c), cap(c), c)

输出

c: length: 1, capacity: 1, data: [4]

Re-slicing - slice c

append()函数

追加是对分片最重要的操作之一。由于Go中的数组是不可变的,只有通过 append()函数,我们可以得到一个可变长度的数据集合。然而,正如我们所知,在切片下面仍然使用数组。下面的例子显示了当分片项目的数量超过其容量时会发生什么。

var s []int
for i := 0; i < 10; i++ {
fmt.Printf("length: %d, capacity: %d, address: %p\n", len(s), cap(s), s)
s = append(s, i)
}

输出

length: 0, capacity: 0, address: 0x0
length: 1, capacity: 1, address: 0xc00001c0a0
length: 2, capacity: 2, address: 0xc00001c0b0
length: 3, capacity: 4, address: 0xc00001e080
length: 4, capacity: 4, address: 0xc00001e080
length: 5, capacity: 8, address: 0xc000020140
length: 6, capacity: 8, address: 0xc000020140
length: 7, capacity: 8, address: 0xc000020140
length: 8, capacity: 8, address: 0xc000020140
length: 9, capacity: 16, address: 0xc000026200

正如你在输出中看到的,每当切片的长度超过其容量(底层数组的长度)时,该 append()函数通过分配一个两倍于其大小的新的底层数组并将其所有元素复制到那里来扩展分片。注意,底层数组的指针随着容量的变化而变化。

结论

要理解Go中分片的长度和容量,重要的是要理解分片的工作原理以及分片和数组的区别。分片是建立在数组之上的,提供可变长度的数据集合。它们由三个元素组成--指向底层数组的指针(在下面,分片使用数组作为数据存储),分片的长度,以及容量--底层数组的大小。当一个片断值被传递时,这3个属性被复制,但新的指针总是指向同一个共享数组。该 append()函数使分片可以扩展,创造了一个强大而富有表现力的数据结构,是Go中最常用的结构之一。