Go切片全解析-成都快上网建站

Go切片全解析

Go切片全解析

目录结构:

10年积累的成都做网站、成都网站建设经验,可以快速应对客户对网站的新想法和需求。提供各种问题对应的解决方案。让选择我们的客户得到更好、更有力的网络服务。我虽然不认识你,你也不认识我。但先网站制作后付款的网站建设流程,更有松桃免费网站建设让你可以放心的选择与我们合作。

数组

切片

  • 底层结构
  • 创建
    • 普通声明
    • make方式
  • 截取
    • 边界问题
  • 追加
  • 拓展表达式
  • 扩容机制
  • 切片传递的坑
  • 切片的拷贝
    • 浅拷贝
    • 深拷贝

数组

var n [4]int
fmt.Println(n) //输出:[0 0 0 0]
n[0] = 1
n[3] = 2
fmt.Println(len(n)) //输出: 4
fmt.Println(cap(n)) //输出:4
fmt.Println(n) //输出:[1 0 0 2]

b := n
n[0] = 2
fmt.Println(b) //输出: [1 0 0 2]
b[0] = 3
fmt.Println(n) //输出: [2 0 0 2]

说明:

  • var n [4]int就已经完成了数组的初始化,并且全部赋值为0,长度和容量都为4
  • 把n赋值给b,相当于对n进行copy操作,再把copy后的结果赋值给b,所以n和b是分别属于两个数组,互不影响

切片

底层结构

type slice struct {
 array unsafe.Pointer // 指针指向底层数组
 len   int  // 切片长度
 cap   int  // 底层数组容量
}

创建

声明方式

默认值是nil,初始的长度和容量都为0

var s []int
fmt.Println(cap(s)) // 0
fmt.Println(len(s)) // 0
fmt.Println(s == nil) // true

make创建

make([]interface{}, len, cap)

通过make创建,默认值不为nil,且初始的长度和容量都可指定,不受自动扩容机制干扰,并且当初始的len参数不为0时,会像数组那样自动赋值

a := make([]int, 0, 10)
fmt.Println(len(a)) // 0
fmt.Println(cap(a)) // 10
fmt.Println(a == nil) // false
b := make([]int, 1000)
fmt.Println(len(b)) // 1000
fmt.Println(cap(b)) // 1000
c := make([]int, 5, 10)
fmt.Println(len(c)) // 5
fmt.Println(cap(c)) // 10
fmt.Println(c) // [0 0 0 0 0]

截取

切片可以基于数组和切片来创建,截取的规则是左闭右开

 n := [5]int{1, 2, 3, 4, 5}
 n1 := n[1:]     // 从n数组中截取
 fmt.Println(n1) // [2 3 4 5]
 n2 := n1[1:]    // 从n1切片中截取
 fmt.Println(n2) // [3 4 5]

切片与原数组或切片是共享底层空间的,接着上面例子,把n2的元素修改之后,会影响原切片和数组:

 n2[1] = 6 // 修改n2,会影响原切片和数组
 fmt.Println(n1) // [2 3 6 5]
 fmt.Println(n2) // [3 6 5]
 fmt.Println(n)  // [1 2 3 6 5]

边界问题

  • 1、当n为数组或字符串表达式n[low:high]中low和high的取值关系:

0 <= low <=high <= len(n)

  • 2、当n为切片的时候,表达式n[low:high]中high最大值变成了cap(n),low和high的取值关系:

0 <= low <=high <= cap(n)

不满足以上条件会发送越界panic。

不同点,有边界数组是len(n),切片是cap(n)

追加

内置函数append()用于向切片中追加元素。

 n := make([]int, 0)
 n = append(n, 1)                 // 添加一个元素
 n = append(n, 2, 3, 4)           // 添加多个元素
 n = append(n, []int{5, 6, 7}...) // 添加一个切片
 fmt.Println(n)                   // [1 2 3 4 5 6 7]

当append操作的时候,切片容量如果不够,会触发扩容,接着上面的例子:

 fmt.Println(cap(n)) // 容量等于8
 n = append(n, []int{8, 9, 10}...)
 fmt.Println(cap(n)) // 容量等于16,发生了扩容

当一开始容量是8,后面追加了切片[]int{8, 9, 10}之后,容量变成了16。

如果append超过切片的长度会重新生产一个全新的切片,不会覆盖原来的:

 n2 := n[1:4:5]         // 长度等于3,容量等于4
 fmt.Printf("%p\n", n2) // 0xc0000ac068
 n2 = append(n2, 5)
 fmt.Printf("%p\n", n2) // 0xc0000ac068
 n2 = append(n2, 6)
 fmt.Printf("%p\n", n2) // 地址发生改变,0xc0000b8000

拓展表达式

简单表达式生产的新切片与原数组或切片会共享底层数组,虽然避免了copy,但是会带来一定的风险。下面这个例子当新的n1切片append添加元素的时候,覆盖了原来n的索引位置4的值,导致你的程序可能是非预期的,从而产生不良的后果

n := []int{1, 2, 3, 4, 5, 6}
n1 := n[1:4]
fmt.Println(n)       // [1 2 3 4 5 6]
fmt.Println(n1)      // [2 3 4]
n1 = append(n1, 100) // 把n的索引位置4的值从原来的5变成了100
fmt.Println(n)       // [1 2 3 4 100 6]
fmt.Println(n1) // [2 3 4 100]
fmt.Println(len(n[1:4])) // 3
fmt.Println(cap(n[1:4])) // 5
关于容量

n[1:4]的长度是3好理解(4-1),容量为什么是5?

因为切片n[1:4]和切片n是共享底层空间,所以它的容量并不等于他的长度3,根据1等于索引1的位置(等于值2),从值2这个元素开始到末尾元素6,共5个,所以n[1:4]容量是5。

Go 1.2[3]中提供了一种可以限制新切片容量的表达式:

n[low:high:max]

max表示新生成切片的容量,新切片容量等于max-low,表达式中low、high、max关系:

0 <= low <= high <= max <= cap(n)

继续刚才的例子,会用max的值来重新计算容量,而不是共享n的容量,但是n2和n还是共享同一个底层数组

n2 := n[1:4:5]
fmt.Println(cap(n2)) // 4
fmt.Println(n2) // 输出 [2 3 4]
n[3] = 111 
fmt.Println(n2) // 输出 [2 3 111]

扩容机制

关于Go切片的扩容机制,网上文章很多,很多结论是这样的:

结论1:

  • 1、当需要的容量超过原切片容量的两倍时,会使用需要的容量作为新容量。
  • 2、当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量。

结论2:

  • 在结论1的基础上(切片的预估容量阶段),提到了内存对齐,容量计算完了后还要考虑到内存的高效利用,进行内存对齐。
例子
package main

func main() {
    s := []int{1,2}
    s = append(s, 3,4,5)
    println(cap(s)) //输出6
}

由于初始 s 的容量是2,现需要追加3个元素,所以通过 append 一定会触发扩容,并调用 growslice 函数,此时他的入参 cap 大小为2+3=5。通过翻倍原有容量得到 doublecap = 2+2,doublecap 小于 cap 值,所以在第一阶段计算出的期望容量值 newcap=5。在第二阶段中,元素类型大小 intsys.PtrSize 相等,通过 roundupsize 向上取整内存的大小到 capmem = 48 字节,所以新切片的容量newcap 为 48 / 8 = 6 ,成功解释!

在切片 append 操作时,如果底层数组已无可容纳追加元素的空间,则需扩容。扩容并不是在原有底层数组的基础上增加内存空间,而是新分配一块内存空间作为切片的底层数组,并将原有数据和追加数据拷贝至新的内存空间中。

在扩容的容量确定上,相对比较复杂,它与CPU位数、元素大小、是否包含指针、追加个数等都有关系。当我们看完扩容源码逻辑后,发现去纠结它的扩容确切值并没什么必要。

在实际使用中,如果能够确定切片的容量范围,比较合适的做法是:切片初始化时就分配足够的容量空间,在append追加操作时,就不用再考虑扩容带来的性能损耗问题。

切片传递的坑

例子1

有以下例子

func modifySlice(innerSlice []string) {
    innerSlice[0] = "b"
    innerSlice[1] = "b"
    fmt.Println(innerSlice)
}
 
func main() {
    outerSlice := []string{"a", "a"}
    modifySlice(outerSlice)
    fmt.Print(outerSlice)
}

// 输出如下
[b b]
[b b]

在上面的例子中,切片内容都得到了修改。

例子2

func modifySlice(innerSlice []string) {
	innerSlice = append(innerSlice, "a")
    innerSlice[0] = "b"
    innerSlice[1] = "b"
    fmt.Println(innerSlice)
}
 
func main() {
    outerSlice := []string{"a", "a"}
    modifySlice(outerSlice)
    fmt.Print(outerSlice)
}

// 输出如下
[b b a]
[a a]

说明:

  • 在modifySlice方法中,innerSlice是outerSlice的副本,但是共同引用相同的底层数组,所以在例子1中,切片内容都得到了修改。
  • innerSlice是一个len和cap都相同的切片,当append方法发生时,会进行扩容操作,扩容操作会使得产生一个新的切片,是在原有的数组中进行深拷贝,并且扩大容量。

对代码的细节进行打印再次看一下输出结果

func modifySlice(innerSlice []string) {
	fmt.Println("begin modify")
	innerSlice = append(innerSlice, "a")
	fmt.Printf("%p, %v\n", innerSlice, &innerSlice[0])
	fmt.Println("innerSlice  len:", len(innerSlice), "cap:", cap(innerSlice))
	innerSlice[0] = "b"
	innerSlice[1] = "b"
	fmt.Println(innerSlice)
	fmt.Println("end modify")
}

func main() {
	outerSlice := []string{"a", "a"}
	fmt.Printf("%p, %v\n", outerSlice, &outerSlice[0])
	fmt.Println("outerSlice  len:", len(outerSlice), "cap:", cap(outerSlice))
	modifySlice(outerSlice)
	fmt.Println("outerSlice  len:", len(outerSlice), "cap:", cap(outerSlice))
	fmt.Printf("%p, %v\n", outerSlice, &outerSlice[0])
	fmt.Print(outerSlice)
}
//输出
0xc0000464e0, 0xc0000464e0
outerSlice  len: 2 cap: 2
begin modify
0xc0000, 0xc0000 //地址转换
innerSlice  len: 3 cap: 4 //容量改变
[b b a]
end modify
outerSlice  len: 2 cap: 2
0xc0000464e0, 0xc0000464e0
[a a]

证明了我们的猜想。

例子3

func modifySlice(innerSlice []string) {
  innerSlice = append(innerSlice, "a")
  innerSlice[0] = "b"
  innerSlice[1] = "b"
  fmt.Println(innerSlice)
}

func main() {
  outerSlice := make([]string, 0, 3)
  outerSlice = append(outerSlice, "a", "a")
  modifySlice(outerSlice)
  fmt.Println(outerSlice)
}

//输出
[b b a]
[b b]

说明:

  • 初始化切片的容量为3,所以在innerSlice不会发生扩容操作,但是由于是值传递,innerSlice只是outerSlice的一个副本,当进行append操作的时候,也是对同一个数组进行插入,同时改变innerSlice的长度,但是outerSlice的长度(len字段)并没有发生改变,所以打印出来的还是[b b]

补充一下打印的细节并稍微做点处理

func modifySlice(innerSlice []string) {
	innerSlice = append(innerSlice, "a")
	innerSlice[0] = "b"
	innerSlice[1] = "b"
	for { //不断打印OuterSlice的内存地址以及值
		time.Sleep(time.Second / 10)
		fmt.Printf("%p\n", innerSlice)
		fmt.Println(innerSlice)
	}
}

func main() {
	outerSlice := make([]string, 0, 3) //初始化容量为3长度为0的切片
	outerSlice = append(outerSlice, "a", "a")
	fmt.Printf("outerSlice %p\n", outerSlice) //打印innerSlice初始的内存地址
	go modifySlice(outerSlice) //执行modifySlice
	time.Sleep(time.Second / 5) //等待modifySlice结束
	fmt.Println(outerSlice) //再次打印innerSlice的值
	fmt.Println("outerSlice", len(outerSlice), cap(outerSlice)) //打印innerSlice的长度和容量
	outerSlice = append(outerSlice, "b")
	fmt.Println(outerSlice) ////再次打印innerSlice的值
	fmt.Printf("outerSlice %p\n", outerSlice) //再次打印innerSlice的内存地址
	time.Sleep(time.Second) //等待modify方法的输出
}
//输出
outerSlice 0xc0000c4c60 //outerSlice的初始的内存地址
0xc0000c4c60 //innerSlice的内存地址
[b b a] //modify后的值
[b b]
outerSlice 2 3
[b b b]
outerSlice 0xc0000c4c60 //outerSlice的内存地址没有发生改变
0xc0000c4c60 //innerSlice的内存地址的值没有发生改变
[b b b] //innerSlice的值被覆盖了
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]
0xc0000c4c60
[b b b]

由此可以说明,当append()执行的时候,没有进行扩容的话还是共享同一个数组,但因为是值传递,innerSlice是一个副本,改变的是副本的lenouterSlicelen实际并没有变化,所以输出的值会比innerSlice

切片的拷贝

浅拷贝

通过=操作符拷贝切片,这是浅拷贝。

func main() {
 a := []int{1, 2, 3}
 b := a
 fmt.Println(unsafe.Pointer(&a))  // 0xc00000c030
 fmt.Println(a, &a[0])            // [100 2 3] 0xc00001a078
 fmt.Println(unsafe.Pointer(&b))  // 0xc00000c048
 fmt.Println(b, &b[0])            // [100 2 3] 0xc00001a078
}

通过[:]方式复制切片,同样是浅拷贝。

func main() {
 a := []int{1, 2, 3}
 b := a[:]
 fmt.Println(unsafe.Pointer(&a)) *// 0xc0000a4018*
 fmt.Println(a, &a[0])      *// [1 2 3] 0xc0000b4000*
 fmt.Println(unsafe.Pointer(&b)) *// 0xc0000a4030*
 fmt.Println(b, &b[0])      *// [1 2 3] 0xc0000b4000*
}

深拷贝

深拷贝,需要用到copy()内置函数

func copy(dst, src []Type) int

其返回值代表切片中被拷贝的元素个数

func main() {
 a := []int{1, 2, 3}
 b := make([]int, len(a), len(a))
 copy(b, a)
 fmt.Println(unsafe.Pointer(&a)) *// 0xc00000c030*
 fmt.Println(a, &a[0])      *// [1 2 3] 0xc00001a078*
 fmt.Println(unsafe.Pointer(&b)) *// 0xc00000c048*
 fmt.Println(b, &b[0])      *// [1 2 3] 0xc00001a090*
}

copy 的元素数量与原始切片和目标切片的大小、容量有关系,并且只是往原有的切片进行数据替换,不会产生新的切片

func main() {
	a := []int{1, 2, 3}
	b := []int{-1, -2, -3, -4}
	c := []int{-1, -2}

	fmt.Println(unsafe.Pointer(&b)) //0xc0000040f0
	copy(b, a)
	fmt.Println(unsafe.Pointer(&a)) // 0xc0000040d8
	fmt.Println(a, &a[0])           // [1 2 3] 0xc0000145e8
	fmt.Println(unsafe.Pointer(&b)) // 0xc0000040f0
	fmt.Println(b, &b[0])           // [1 2 3 -4] 0xc0000101e0
	fmt.Println(unsafe.Pointer(&c)) //0xc000004108
	copy(c, a)
	fmt.Println(unsafe.Pointer(&a)) // 0xc0000040d8
	fmt.Println(a, &a[0])           // [1 2 3] 0xc0000145e8
	fmt.Println(unsafe.Pointer(&c)) // 0xc000004108
	fmt.Println(c, &c[0])           // [1 2] 0xc0000129a0
}

名称栏目:Go切片全解析
文章出自:http://kswjz.com/article/dsoisgd.html
扫二维码与项目经理沟通

我们在微信上24小时期待你的声音

解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流