Go中切片Slice详解

Go中切片Slice详解

首页休闲益智翻刀切片挑战更新时间:2024-05-21
Go中什么是切片

切片是方便、灵活和强大的数组包装器。切片本身不拥有任何数据。它们只是对现有数组的引用。

切片创建方式

T类型元素的切片表示方式为[]T;

package main import ( "fmt" ) func main() { a := [5]int{76, 77, 78, 79, 80} var b []int = a[1:4] //creates a slice from a[1] to a[3] fmt.Println(b) }

如上程序所示,先创建一个长度为5的数组,然后通过索引1-->4表示引用数组a的索引1-->3(左开右闭)的切片;

还有另外种方式创建切片:

package main import ( "fmt" ) func main() { c := []int{6, 7, 8} //creates and array and returns a slice reference fmt.Println(c) }

第8行的代码意思是创建一个数组返回一个切片引用c。

切片修改

切片不拥有自己的任何数据。它只是底层数组的表示形式。对切片所做的任何修改都将反映在基础数组中。

package main import ( "fmt" ) func main() { darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59} dslice := darr[2:5] fmt.Println("array before",darr) for i := range dslice { dslice[i] } fmt.Println("array after",darr) }

在上述程序的第9行中,我们从数组的索引2、3、4创建dslice。for range将这些索引中的值递增1。当我们在 for range之后打印数组时,我们可以看到对切片的更改反映在数组中。程序的输出为:

array before [57 89 90 82 100 78 67 69 59] array after [57 89 91 83 101 78 67 69 59]

当多个切片共享同一个基础数组时,每个切片所做的更改将反映在数组中。

package main import ( "fmt" ) func main() { numa := [3]int{78, 79 ,80} nums1 := numa[:] //creates a slice which contains all elements of the array nums2 := numa[:] fmt.Println("array before change 1",numa) nums1[0] = 100 fmt.Println("array after modification to slice nums1", numa) nums2[1] = 101 fmt.Println("array after modification to slice nums2", numa) }

上面程序输出:

array before change 1 [78 79 80] array after modification to slice nums1 [100 79 80] array after modification to slice nums2 [100 101 80] 切片的长度和容量

切片的长度是切片中的元素数。切片的容量是基础数组中的元素数,从创建切片的索引开始。看下面的demo:

package main import ( "fmt" ) func main() { fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"} fruitslice := fruitarray[1:3] fmt.Printf("length of slice %d capacity %d", len(fruitslice), cap(fruitslice)) //length of fruitslice is 2 and capacity is 6 }

在上面的demo中,fruitslice是从fruitarray的索引1和2创建的。因此,fruitslice的长度为2。

fruitarray的长度为7,fruiteslice是从fruitarray的索引1创建的。因此,fruitslice的容量是从索引1开始的fruitarray中元素的数量,即从orange开始,该值为6。因此,fruiteslice的容量为6,程序打印切片,长度为2,容量为6。

切片可以重新切片到其容量,超出此范围的任何内容都将导致程序抛出运行时错误。

package main import ( "fmt" ) func main() { fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"} fruitslice := fruitarray[1:3] fmt.Printf("length of slice %d capacity %d\n", len(fruitslice), cap(fruitslice)) //length of is 2 and capacity is 6 fruitslice = fruitslice[:cap(fruitslice)] //re-slicing furitslice till its capacity fmt.Println("After re-slicing length is",len(fruitslice), "and capacity is",cap(fruitslice)) }

上面的程序输出:

length of slice 2 capacity 6 After re-slicing length is 6 and capacity is 6 使用make创建切片

func make([]T, len, cap) []T 可用于通过传递类型、长度和容量来创建切片。容量参数是可选的,默认为长度。make 函数创建一个数组并返回对该数组的切片引用。

package main import ( "fmt" ) func main() { i := make([]int, 5, 5) fmt.Println(i) }

使用 make 创建切片时,默认情况下,这些值为零。上述程序将输出 [0 0 0 0 0]。

切片追加

正如我们已经知道的,数组被限制为固定长度,它们的长度不能增加。切片是动态的,可以使用append功能将新元素附加到切片中。追加函数的定义是func append(s []T, x ...T) []T.

x ...函数定义中的 T 表示函数接受参数 x 的可变数量的参数。这些类型的函数称为可变参数函数。

不过,有一个问题可能会困扰您。如果切片由数组支持,并且数组本身是固定长度的,那么切片为什么是动态长度的。那么可想之下发生的事情是,当新元素附加到切片时,会创建一个新数组。现有数组的元素将复制到此新数组,并返回此新数组的新切片引用。新切片的容量现在是旧切片的两倍。很酷:)。以下程序将使事情变得清晰。

package main import ( "fmt" ) func main() { cars := []string{"Ferrari", "Honda", "Ford"} fmt.Println("cars:", cars, "has old length", len(cars), "and capacity", cap(cars)) //capacity of cars is 3 cars = append(cars, "Toyota") fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars)) //capacity of cars is doubled to 6 }

在上述程序中,cars的容量最初为3。我们将一个新元素附加到第10行的cars,并将append(cars, “Toyota”)返回的切片再次赋值给cars。现在cars的容量翻了一番,变成了6辆。上述程序的输出为

cars: [Ferrari Honda Ford] has old length 3 and capacity 3 cars: [Ferrari Honda Ford Toyota] has new length 4 and capacity 6

切片类型的零值为 nil。零切片的长度和容量为 0。可以使用追加函数将值追加到零切片。

package main import ( "fmt" ) func main() { var names []string //zero value of a slice is nil if names == nil { fmt.Println("slice is nil going to append") names = append(names, "John", "Sebastian", "Vinay") fmt.Println("names contents:",names) } }

上面程序输出:

slice is nil going to append names contents: [John Sebastian Vinay]

并且我们可以用...操作将一个切片追加到另一个切片,

package main import ( "fmt" ) func main() { veggies := []string{"potatoes","tomatoes","brinjal"} fruits := []string{"oranges","apples"} food := append(veggies, fruits...) fmt.Println("food:",food) } //food: [potatoes tomatoes brinjal oranges apples]将切片传递给函数

可以将切片视为由struct类型在内部表示。如下:

type slice struct { Length int Capacity int ZerothElement *byte }

切片包含长度、容量和指向数组第0个元素的指针。当切片传递给函数时,即使它是按值传递的,指针变量也会引用相同的基础数组。因此,当切片作为参数传递给函数时,函数内部所做的更改在函数外部也可见。让我们编写一个程序来检查一下。

package main import ( "fmt" ) func subtactOne(numbers []int) { for i := range numbers { numbers[i] -= 2 } } func main() { nos := []int{8, 7, 6} fmt.Println("slice before function call", nos) subtactOne(nos) //function modifies the slice fmt.Println("slice after function call", nos) //modifications are visible outside }

上述程序第17行中的函数调用将切片的每个元素递减2。在函数调用后打印切片时,这些更改是可见的。如果您还记得,这与数组不同,在数组中,对函数内部的数组所做的更改在函数外部不可见。上述程序的输出是,

slice before function call [8 7 6] slice after function call [6 5 4] 多维切片

和数组类似,切片也有多维。

package main import ( "fmt" ) func main() { pls := [][]string { {"C", "C "}, {"JavaScript"}, {"Go", "Rust"}, } for _, v1 := range pls { for _, v2 := range v1 { fmt.Printf("%s ", v2) } fmt.Printf("\n") } } //C C //JavaScript //Go Rust内存优化

切片保存对基础数组的引用。只要切片在内存中,就不能对数组进行垃圾回收。在内存管理方面,这可能是一个问题。假设我们有一个非常大的数组,并且我们只对处理其中的一小部分感兴趣。此后,我们从该数组创建一个切片并开始处理该切片。这里要注意的重要一点是,数组仍将在内存中,因为切片引用了它。

解决此问题的一种方法是使用复制函数func copy(dst,src []T) int来复制该切片。这样我们就可以使用新的切片,并且可以对原始数组进行垃圾回收。

package main import ( "fmt" ) func countries() []string { countries := []string{"USA", "Singapore", "Germany", "India", "Australia"} neededCountries := countries[:len(countries)-2] countriesCpy := make([]string, len(neededCountries)) copy(countriesCpy, neededCountries) //copies neededCountries to countriesCpy return countriesCpy } func main() { countriesNeeded := countries() fmt.Println(countriesNeeded) }

在上述程序的第9行中,needStates := countries[:len(countries)-2] 创建了除最后2个元素之外的countries切片。第11行将neededCountries复制到countriesCpy,并从下一行的函数返回它。现在countries数组可以进行垃圾回收,因为不再需要countries。

切片扩容机制

在Go语言中,切片的扩容是通过 append 函数实现的。当我们向一个切片中添加元素时,如果当前切片的容量不足以存储新的元素,Go会自动为该切片分配一块新的内存空间,然后将原来的数据复制到新分配的内存空间中,并将新的元素添加进去。这个过程被称为切片的扩容。

具体来说,当我们使用 append 函数向一个切片添加元素时,Go会先判断当前切片的容量是否足够存储新元素。如果足够,则直接将新元素添加到切片的末尾;如果不足,则需要进行扩容操作。

在进行扩容操作时,Go会先根据当前切片的长度和容量计算出新的容量。具体的计算方式是:

1.17之前代码的扩容策略可以简述为以下三个规则:

1、当期望容量>两倍的旧容量时,直接使用期望容量作为新切片的容量;

2、如果旧容量< 1024(注意这里单位是元素个数),那么直接翻倍旧容量;

3、如果旧容量> 1024,那么会进入一个循环,每次增加25%直到大于期望容量;

可以看到,原来的go对于切片扩容后的容量判断有一个明显的magic number:1024,在1024之前,增长的系数是2,而1024之后则变为1.25。关于为什么会这么设计,社区的相关讨论1给出了几点理由:

1.如果只选择翻倍的扩容策略,那么对于较大的切片来说,现有的方法可以更好的节省内存。

2.如果只选择每次系数为1.25的扩容策略,那么对于较小的切片来说扩容会很低效。

3.之所以选择一个小于2的系数,在扩容时被释放的内存块会在下一次扩容时更容易被重新利用

到了Go1.18时,又改成不和1024比较了,而是和256比较;并且扩容的增量也有所变化,不再是每次扩容1/4,而是(oldCap 3 * 256)/4;

然后,Go会为新的容量分配一块内存空间,并将原来的数据复制到这个新的内存空间中,最后将新的元素添加到切片的末尾。

需要注意的是,切片的扩容操作可能会导致原来的切片和新的切片共享同一块内存空间。这种情况下,如果修改了原来的切片或新的切片中的数据,都会影响到另一个切片。因此,在使用切片时,需要注意切片的扩容和共享内存的情况,避免引发不必要的错误。

切片拷贝三种方式

1、使用=操作符拷贝切片,这种就是浅拷贝;

2、使用[:]下标的方式复制切片,这种也是浅拷贝;

3、使用Go语言的内置函数copy()进行切片拷贝,这种就是深拷贝。

深浅拷贝都是进行复制,区别在于复制出来的新对象与原来的对象在它们发生改变时,是否会相互影响,本质区别就是复制出来的对象与原对象是否会指向同一个地址。

总结
  1. 切片的创建三种方式;

s := a[1:3]

s := []string{"2", "3"}

s := make([]int, 5, 5)

  1. 切片所做的更改将反映在数组中;
  2. len()得到切片的长度,cap()得到切片容量;
  3. append()函数用于切片追加元素;
  4. 切片传参函数做的修改外部可见;
  5. 通过copy()函数复制切片,可以使原数组进行垃圾回收。
  6. 切片扩容机制。
  7. 切片拷贝。
查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved