via:
​​​https://codeburst.io/a-comprehensive-guide-to-slices-in-golang-bacebfe46669​

作者:Radhakishan Surwase

四哥水平有限,如有翻译或理解错误,烦请帮忙指出,感谢!

作者在这篇文章中列举了关于 slice 的各种操作方法,四哥之前的文章也写过这块知识 ​​非懂不可的Slice(一)​​​和 ​​非懂不可的Slice(二)​​,一起跟着作者来温习下。



在这篇文章里,我们将温习 Go 语言里面一种非常重要的数据结构 - slice,它为我们提供了处理和管理数据的方式。slice 是基于动态数组构建的,可以根据需要动态地扩容或者缩小。

  • slice 可以使用内置 append() 函数实现快速且高效地扩容。
  • 你还可以通过分割底层内存的一部分来减少 slice 的大小。
  • slice 的索引操作、迭代和垃圾回收都是非常高效的,因为底层是连续的内存块。

slice 组成

  • 切片不存储数据,仅仅用于描述底层数组的一部分。
  • 切片是包含三个属性的结构体,分别是指向底层数组的指针、数组长度和数组容量。
  • 这种数据结构类似于描述符的概念。

Go slice 全面指南_指针

  • 指针:指针指向可以通过 slice 访问到的第一个元素。这里需要注意的是,指向的不必是底层数组的第一个元素。
  • 长度:长度是数组中存在的元素总数。
  • 容量:容量表示可以扩展的最大大小。

使用 length 声明 slice

声明 slice 时,如果仅仅指定长度,则容量和长度是一样的。

Go slice 全面指南_java_02

// Declaring a slice by length. Create a slice of int. 
// Contains a length and capacity of 5 elements.
slice := make([]int, 5)
fmt.Println(len(slice)) // Print 5
fmt.Println(cap(slice)) // Print 5

使用 length 和 capacity 声明 slice

声明 slice 如果分别指定 length 和 capacity,则长度之外、容量以内的数组元素是不能访问的

/* 
Declaring a slice by length and capacity
Create a slice of integers.
Contains a length of 3 and has a capacity of 5 elements.
*/
slice := make([]int, 3, 5)
fmt.Println(len(slice)) // Print 3
fmt.Println(cap(slice)) // Print 5

Go slice 全面指南_python_03

注意,不能创建 capacity 比 length 小的 slice。

使用字面量创建 slice

使用字面量创建 slice 是一种常见的方式。与创建数组类似,除了不用在 [] 操作符内指定大小。slice 的长度和容量取决于初始化时元素的个数。

// Create a slice of strings. 
// Contains a length and capacity of 5 elements.
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
fmt.Println(len(slice)) //Print 5
fmt.Println(cap(slice)) //Print 5
// Create a slice of integers.
// Contains a length and capacity of 3 elements.
intSlice:= []int{10, 20, 30}
fmt.Println(len(intSlice)) //Print 3
fmt.Println(cap(intSlice)) //Print 3

使用索引位置声明 slice

使用字面量声明 slice 时,可以设置 length 和 capacity,需要做的就是初始化代表长度和容量的索引。下面这段代码就会声明一个长度和容量都是 100 的切片。

slice := []int{99: 88}
fmt.Println(len(slice))
// Print 100
fmt.Println(cap(slice))
// Print 100

Go slice 全面指南_python_04

声明 slice 与 声明数组的不同点

  • 如果在 [] 操作符中指定大小,是创建数组
  • 如果不指定大小,是创建切片

// Create an array of three integers. 
array := [3]int{10, 20, 30}

//Create a slice of integers with a length and capacity of three.
slice := []int{10, 20, 30}

声明一个 nil slice

  • slice 的零值是 nil。
  • nil slice 的长度和容量都是 0 且不指向任何底层数组。

// Create a nil slice of integers. 
var slice []int32
fmt.Println(slice == nil)
//This line will print true
fmt.Println(len(slice))
// This line will print 0
fmt.Println(cap(slice))
// This line will print 0

Go slice 全面指南_指针_05

声明一个空 slice

你也可以通过使用初始化声明切片来创建空切片。

// Use make to create an empty slice of integers.
sliceOne := make([]int, 0)
// Use a slice literal to create an empty slice of integers.
sliceTwo := []int{}
fmt.Println(sliceOne == nil) // This will print false
fmt.Println(len(sliceOne)) // This will print 0
fmt.Println(cap(sliceOne)) // This will print 0
fmt.Println(sliceTwo == nil) // This will print false
fmt.Println(len(sliceTwo)) // This will print 0
fmt.Println(cap(sliceTwo)) // This will print 0

Go slice 全面指南_java_06

为任何指定的索引分配值

使用 [] 操作符给单个元素赋值

// Create a slice of integers.
// Contains a length and capacity of 4 elements.
slice := []int{10, 20, 30, 40}
fmt.Println(slice) //This will print [10 20 30 40]
slice[1] = 25 // Change the value of index 1.
fmt.Println(slice) // This will print [10 25 30 40]

Go slice 全面指南_编程语言_07

使用切片操作创建切片

切片之所以称为切片是因为我们可以基于切片的底层数组创建新的切片(译者注:不知道说的是不是真的,准确性有待考察~)。

/* Create a slice of integers. Contains a 
length and capacity of 5 elements.*/
slice := []int{10, 20, 30, 40, 50}
fmt.Println(slice) // Print [10 20 30 40 50]
fmt.Println(len(slice)) // Print 5
fmt.Println(cap(slice)) // Print 5
/* Create a new slice.Contains a length
of 2 and capacity of 4 elements.*/
newSlice := slice[1:3]
fmt.Println(slice) //Print [10 20 30 40 50]
fmt.Println(len(newSlice)) //Print 2
fmt.Println(cap(newSlice)) //Print 4

Go slice 全面指南_python_08

执行完切片操作之后,我们就得到两个共享底层数组的切片,然而两个切片能访问底层数组的范围是不同的。原始切片能访问 5 个数组元素,而新切片只能访问 4 个数组元素。新切片无法访问其指针之前的数组元素,对于新切片而言,这些元素是不存在的。新切片的长度和容量可以用下面提到的公式计算出。

切片的长度和容量是如何计算的?

对底层数组大小为 k 的切片执行切片操作 slice[i:j] 之后,长度和容量分别是:

长度:j - i

容量:k - i

例如,上面这个例子,切片操作之后得到的新切片长度和容量分别是:

长度:3 - 1 = 2

容量:5 - 1 = 4

修改 slice 带来的影响

改变一个切片底层共享数组部分将会影响到另一个切片。

// Create a slice of integers.
// Contains a length and capacity of 5 elements.
slice := []int{10, 20, 30, 40, 50}
// Create a new slice.
// Contains a length of 2 and capacity of 4 elements.
newSlice := slice[1:3]
// Change index 1 of newSlice.
// Change index 2 of the original slice.
newSlice[1] = 35

上面的例子,35 分配给新切片之后,原始切片的元素也会发生改变。

超索引范围访问会报运行时错误

slice 能访问的范围必须小于等于其长度,试图访问超出长度之外的元素将会报运行时错误

// Create a slice of integers.
// Contains a length and capacity of 5 elements.
slice := []int{10, 20, 30, 40, 50}
// Create a new slice.
// Contains a length of 2 and capacity of 4 elements.
newSlice := slice[1:3]
// Change index 3 of newSlice.
// This element does not exist for newSlice.
newSlice[3] = 45

/*
Runtime Exception:
panic: runtime error: index out of range
*/

切片增长

相比于数组,使用 slice 的好处之一就是,切片可以根据自己的需要自由扩容。我们使用的内置函数 append() 会处理相关的所有细节。

  • 使用 append(),需要一个源 slice 和 待追加的值。
  • append() 函数会返回一个新的切片。
  • append() 函数会使新的切片长度增长。
  • 另外一方面,新切片容量是否增长取决于源切片剩余容量和需要追加元素数目。

使用 append() 函数追加一个元素

/*  Create a slice of integers.
Contains a length and capacity of 5 elements.*/
slice := []int{10, 20, 30, 40, 50}

/* Create a new slice.
Contains a length of 2 and capacity of 4 elements.*/
newSlice := slice[1:3]
fmt.Println(len(newSlice)) // Print 2
fmt.Println(cap(newSlice)) // Print 4

/* Allocate a new element from capacity.
Assign the value of 60 to the new element.*/
newSlice = append(newSlice, 60)
fmt.Println(len(newSlice)) // Print 3
fmt.Println(cap(newSlice)) // Print 4

当切片底层数组容量不足时,append() 函数将会创建一个新的数组并且将原数组元素拷贝到新的数组,最后分配新值。

使用 append() 增加切片的长度和容量

// Create a slice of integers.
// Contains a length and capacity of 4 elements.
slice := []int{10, 20, 30, 40}
fmt.Println(len(slice)) // Print 4
fmt.Println(cap(slice)) // Print 4

// Append a new value to the slice.
// Assign the value of 50 to the new element.
newSlice := append(slice, 50)
fmt.Println(len(newSlice)) //Print 5
fmt.Println(cap(newSlice)) //Print 8

Go slice 全面指南_指针_09

执行 append() 操作后,原数组元素将会拷贝到新的切片,并且该数组的容量是其原始大小的两倍。append() 操作增加底层数组容量会自动调整的,例如,当原 slice 容量小于 1000 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1000,新 slice 容量变成原来的 1.25 倍。这个算法以后可能会改变。

改变新切片将不会影响到旧切片,因为两个切片的底层数组是不同的。

用一个切片追加另一个切片

内置函数 append() 是一个可变函数,这意味着我们可以一次性可以往切片追加多个值。如果使用 … 操作符,可以将一个切片追加到另一个切片。

// Create two slices each initialized with two integers.
slice1:= []int{1, 2}
slice2 := []int{3, 4}
// Append the two slices together and display the results.
fmt.Println(append(slice1, slice2...))
//Output: [1 2 3 4]

对切片执行索引操作

  • 可以通过执行操作 a[low:high] 来创建新的切片,新切片包含原切片索引为 low 的值,但不包含索引为 high 的值。
  • low 和 high 都是可选的,如果 low 省略,默认就是 0,如果 high 省略,默认就是原切片的长度。

a := [...]int{0, 1, 2, 3} 
// an array
s := a[1:3]
// s == []int{1, 2}
// cap(s) == 3
s = a[:2]
// s == []int{0, 1}
// cap(s) == 4
s = a[2:]
// s == []int{2, 3}
// cap(s) == 2
s = a[:]
// s == []int{0, 1, 2, 3}
// cap(s) == 4

迭代切片

可以使用 range 来迭代切片。

// Create a slice of integers.
// Contains a length and capacity of 4 elements.
slice := []int{10, 20, 30, 40}
// Iterate over each element and display each value.
for index, value := range slice {
fmt.Printf("Index: %d Value: %d\n", index, value)
}
/*
Output:
Index: 0 Value: 10
Index: 1 Value: 20
Index: 2 Value: 30
Index: 3 Value: 40
*/

  • 使用 range 迭代切片,有两个返回值。
  • 第一个值是索引,第二个值是元素的拷贝。
  • 需要注意的是,range 返回的是值的拷贝,而不是引用。

/*
Create a slice of integers.Contains
a length and capacity of 4 elements.
*/
slice := []int{10, 20, 30, 40}
/*
Iterate over each element and display
the value and addresses.
*/
for index, value := range slice {
fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n",
value, &value, &slice[index])
}
/*
Output:
Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100
Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104
Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108
Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C
*/

可以使用 _ 操作符忽略索引:

// Create a slice of integers.
// Contains a length and capacity of 4 elements.
slice := []int{10, 20, 30, 40}
// Iterate over each element and display each value.
for _, value := range slice {
fmt.Printf("Value: %d\n", value)
}
/*
Output:
Value: 10
Value: 20
Value: 30
Value: 40
*/

range 是从头开始迭代切片,如果你想从其中某个元素开始迭代,可以选择 for 语句。

// Create a slice of integers.
// Contains a length and capacity of 4 elements.
slice := []int{10, 20, 30, 40}
// Iterate over each element starting at element 3.
for index := 2; index < len(slice); index++ {
fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}
/*
Output:
Index: 2 Value: 30
Index: 3 Value: 40
*/

总结

在本文中,我们深入探讨了切片的概念,并学习了很多关于切片的其他内容。
切片有以下特点:

  • 切片并不存储数据,而是描述底层数组的一部分;
  • 切片基于底层数组可以自由地扩容或者缩容;
  • 切片的零值是 nil;
  • 可以使用切片字面量或者 make() 函数来创建切片;

希望这篇文章对你有帮助!


如果我的文章对你有所帮助,点赞、转发都是一种支持!

Go slice 全面指南_python_10

Go slice 全面指南_golang_11