Go语言基础 -- 派生/复杂数据类型(数组和切片)

1. 数组(Array)

1.1 Golang中的数组

**Golang中的数组(Array) 和 以往认知的数组有很大不同: **

  1. Golang中的数组是同一种数据类型固定长度的序列。

  2. 数组定义时必须定义数组长度为常量,为数据类型的组成部分,且不可改变,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。

  3. 长度是数组类型的一部分,因此,var a[5] intvar a[10]int是不同的类型。

  4. 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len(Array)-1

    for i := 0; i < len(a); i++ {
    }
    
    for index, v := range a {
    }
    
  5. 访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic

  6. 数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。

  7. 支持 "=="、"!=" 操作符,因为内存总是被初始化过的。

  8. 指针数组 [n]*T,数组指针 *[n]T。

1.2 数组声明

1.2.1 初始化数组的方式

var n [3]int = [3]int{1,2,3}
var n = [3]int{1,2,3}
var n = [...]int{1,2,3}  // 编译器自动判断,大括号中的元素个数,自动开辟对应个数的内存
var n = [3]string{0:"a",2:"b",1:"c"} // 指定索引赋值

1.2.2 一维数组

1. 全局变量声明

package main

import (
	"fmt"
)

var a1 [4]int = [4]int{1, 2, 3, 4}                                           // 定长数组声明
var a2 = [4]int{1, 2, 3, 4}                                                  // 定长数组声明
var a3 = [...]int{1, 2, 3, 4}                                                // 不定长数组声明,不知道将会有多少个元素,用 ... 来表示不定数
var a4 = [5]string{"xiaoming", "wangqiang", "xiaohong", "xiaoai", "xiaobai"} // 通过索引指定元素,未指定的为其元素类型的默认值,string为空字符串
var a5 = [5]string{0: "xiaoming", 2: "wangqiang", 4: "xiaohong"}             // 通过索引指定元素,未指定的为其元素类型的默认值,int为0,
var a6 = [5]int{0: 0, 2: 2, 4: 4}

func main() {
	fmt.Println(a1)
	fmt.Println(a2)
	fmt.Println(a3)
	fmt.Println(a4)
	fmt.Println(a5)
	fmt.Println(a6)
}


// 打印结果:
[1 2 3 4]
[1 2 3 4]
[1 2 3 4]
[xiaoming wangqiang xiaohong xiaoai xiaobai]
[xiaoming  wangqiang  xiaohong]
[0 0 2 0 4]

2. 局部变量声明

package main

import (
	"fmt"
)

func main() {
	a1 := [3]int{1, 2}             // 定长数组声明,为初始化的元素为默认值0
	a2 := [...]int{1, 2, 3, 4}     // 不定长数组声明
	a3 := [5]int{0: 0, 1: 1, 4: 4} // 通过索引指定元素,未指定的元素位置为默认值0
	a4 := [...]struct {
		name string
		age  int
	}{
		{name: "xiaoming", age: 18},
		{name: "xiaohong", age: 16},
	}
	fmt.Println(a1)
	fmt.Println(a2)
	fmt.Println(a3)
	fmt.Println(a4)
}


// 打印结果:
[1 2 0]
[1 2 3 4]
[0 1 0 0 4]
[{xiaoming 18} {xiaohong 16}]

1.2.3 多维数组

1. 全局变量声明

package main

import (
	"fmt"
)

var a1 [2][3]int = [...][3]int{
	{1, 2, 3},
	{4, 5, 6},
}    // 不定长数组定义,只有第一维可以是不定长,第二维必须是定长

func main() {
	fmt.Println(a1)
}


// 打印结果:
[[1 2 3] [4 5 6]]

2. 局部变量声明

package main

import (
	"fmt"
)

func main() {

	a1 := [...][3]int{
		{1, 2, 3},
		{4, 5, 6},
	} // 不定长数组定义,只有第一维可以是不定长,用 ... 来表示,第二维必须是定长
	fmt.Println(a1)
}


// 打印结果:
[[1 2 3] [4 5 6]]

1.3 数组的内存布局

定义数组时的内存布局
img

赋值/修改u元素的内存布局

img

1.4 数组操作方法

1.3.1 修改指定元素

值拷贝行为会造成性能问题,通常会建议使用 slice,或数组指针

1. 值拷贝

package main

import (
	"fmt"
)

func changea1(a1 [2]int) {
	fmt.Printf("传递到函数内  , 值为 %v,指针地址为: %p\n", a1, &a1) // & 为取址运算符
	a1[1] = 100
	fmt.Printf("函数内部修改后, 值为 %v, 指针地址为:%p\n", a1, &a1)
}

func main() {
	// 1. 定义数组 a1
	a1 := [2]int{}
	fmt.Printf("初始化完成    ,值为 %v, 指针地址为:%p\n", a1, &a1)

	// 2. 修改数组中的值
	changea1(a1)
	fmt.Printf("全局变量     ,值为 %v, 指针地址为:%p\n", a1, &a1)
}


// 打印结果:
初始化完成    ,值为 [0 0], 指针地址为:0xc00000a0c0
传递到函数内  , 值为 [0 0],指针地址为: 0xc00000a0f0
函数内部修改后, 值为 [0 100], 指针地址为:0xc00000a0f0
全局变量     ,值为 [0 0], 指针地址为:0xc00000a0c0

// 结论分析:
Golang 中的参数传递区分 值拷贝和指针拷贝,值拷贝不会改变源数据

2. 指针拷贝

package main

import (
	"fmt"
)

func changea1(a1 *[2]int) {    // 这里为指针类型
	fmt.Printf("传递到函数内  , 值为 %v,指针地址为: %p\n", a1, &a1) // & 为取址运算符
	a1[1] = 100
	fmt.Printf("函数内部修改后, 值为 %v, 指针地址为:%p\n", a1, &a1)
}

func main() {
	// 1. 定义数组 a1
	a1 := [2]int{}
	fmt.Printf("初始化完成    ,值为 %v, 指针地址为:%p\n", a1, &a1)

	// 2. 修改数组中的值
	changea1(&a1)  // 这里传递变量的指针
	fmt.Printf("全局变量     ,值为 %v, 指针地址为:%p\n", a1, &a1)
}


// 运行结果
初始化完成    ,值为 [0 0], 指针地址为:0xc00000a0c0
传递到函数内  , 值为 &[0 0],指针地址为: 0xc000006030
函数内部修改后, 值为 &[0 100], 指针地址为:0xc000006030
全局变量     ,值为 [0 100], 指针地址为:0xc00000a0c0

// 结论分析:
Golang 中的参数传递区分 值拷贝和指针拷贝,指针拷贝会改变源数据

1.3.2 获取数组长度和容量

内置函数 len() 获取长度, cap() 获取容量

package main

import (
	"fmt"
)

func main() {
	a1 := [2]int{}
	
	// len(array) 返回数组元素个数,即数组长度  
	// cap(array) 返回数组容量,普通数组为对应的定义长度
	fmt.Println(len(a1), cap(a1))
}


// 打印结果:
2 2

1.3.3 数组遍历

1. 一维数组遍历

package main

import (
	"fmt"
)

func main() {
	a1 := [...]int{1,2,3,4,5,6,7,8}

	for index,value := range a1{
		fmt.Printf("索引为 %v 的元素为:%v\n",index,value)
	}

}


// 打印结果:
索引为 0 的元素为:1
索引为 1 的元素为:2
索引为 2 的元素为:3
索引为 3 的元素为:4
索引为 4 的元素为:5
索引为 5 的元素为:6
索引为 6 的元素为:7
索引为 7 的元素为:8

2. 二维数组 (矩阵) 遍历

package main

import (
	"fmt"
)

func main() {
	a1 := [...][3]int{
		{1, 2, 3},
		{11, 22, 33},
		{111, 222, 333},
	}

	for index1, value1 := range a1 {
		for index2, value2 := range value1 {
			fmt.Printf("二维数组(矩阵)中第%d行 第%d列 的元素为:%v\n", index1+1, index2+1, value2)
		}
		fmt.Printf("二维数组(矩阵)中第%d行所有元素为%d, \n", index1+1, value1)
	}

}


// 输出结果为:
二维数组(矩阵)中第1行 第1列 的元素为:1
二维数组(矩阵)中第1行 第2列 的元素为:2
二维数组(矩阵)中第1行 第3列 的元素为:3
二维数组(矩阵)中第1行所有元素为[1 2 3] 
二维数组(矩阵)中第2行 第1列 的元素为:11
二维数组(矩阵)中第2行 第2列 的元素为:22
二维数组(矩阵)中第2行 第3列 的元素为:33
二维数组(矩阵)中第2行所有元素为[11 22 33] 
二维数组(矩阵)中第3行 第1列 的元素为:111
二维数组(矩阵)中第3行 第2列 的元素为:222
二维数组(矩阵)中第3行 第3列 的元素为:333
二维数组(矩阵)中第3行所有元素为[111 222 333] 

1.3.4 数组拷贝和传参

1. 值拷贝

package main

import (
	"fmt"
)

func changea1(a1 [2]int) {
	fmt.Printf("传递到函数内  , 值为 %v,指针地址为: %p\n", a1, &a1) // & 为取址运算符
	a1[1] = 100
	fmt.Printf("函数内部修改后, 值为 %v, 指针地址为:%p\n", a1, &a1)
}

func main() {
	// 1. 定义数组 a1
	a1 := [2]int{}
	fmt.Printf("初始化完成    ,值为 %v, 指针地址为:%p\n", a1, &a1)

	// 2. 修改数组中的值
	changea1(a1)
	fmt.Printf("全局变量     ,值为 %v, 指针地址为:%p\n", a1, &a1)
}


// 打印结果:
初始化完成    ,值为 [0 0], 指针地址为:0xc00000a0c0
传递到函数内  , 值为 [0 0],指针地址为: 0xc00000a0f0
函数内部修改后, 值为 [0 100], 指针地址为:0xc00000a0f0
全局变量     ,值为 [0 0], 指针地址为:0xc00000a0c0

// 结论分析:
Golang 中的参数传递区分 值拷贝和指针拷贝,值拷贝不会改变源数据

2. 指针拷贝

package main

import (
	"fmt"
)

func changea1(a1 *[2]int) {    // 这里为指针类型
	fmt.Printf("传递到函数内  , 值为 %v,指针地址为: %p\n", a1, &a1) // & 为取址运算符
	a1[1] = 100
	fmt.Printf("函数内部修改后, 值为 %v, 指针地址为:%p\n", a1, &a1)
}

func main() {
	// 1. 定义数组 a1
	a1 := [2]int{}
	fmt.Printf("初始化完成    ,值为 %v, 指针地址为:%p\n", a1, &a1)

	// 2. 修改数组中的值
	changea1(&a1)  // 这里传递变量的指针
	fmt.Printf("全局变量     ,值为 %v, 指针地址为:%p\n", a1, &a1)
}


// 运行结果
初始化完成    ,值为 [0 0], 指针地址为:0xc00000a0c0
传递到函数内  , 值为 &[0 0],指针地址为: 0xc000006030
函数内部修改后, 值为 &[0 100], 指针地址为:0xc000006030
全局变量     ,值为 [0 100], 指针地址为:0xc00000a0c0

// 结论分析:
Golang 中的参数传递区分 值拷贝和指针拷贝,指针拷贝会改变源数据

1.3.5 数组练习题

1. 生成随机数,添加进数组中,并求出数组中所有元素的总和

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func randNumber(randNumList *[5]int) {
	// 1. 设置当前时间为随机种子
	r := rand.New(rand.NewSource(time.Now().UnixNano()))

	// 2. 遍历数组的长度, 避免超出数组的容量
	for i := 0; i < len(randNumList); i++ {
		// 3. 生成一个 100 以内的随机数
		randInt := r.Intn(100)
		fmt.Printf("第 %d 次生成的随机数为 %d\n", i+1, randInt)
		// 4. 将生成的随机数添加到数组中
		randNumList[i] = randInt
	}
}

func main() {

	// 1. 声明存放随机数的数组
	randNumList := [5]int{}

	// 2. 调用生成随机数的函数, 并将随机数添加到数组中
	randNumber(&randNumList)

	// 3. 定义 sum,用来计算总和
	sum := 0

	// 4. 遍历数组, 并计算总和
	for i := 0; i < len(randNumList); i++ {
		sum += randNumList[i]
	}

	// 5. 输出总和
	fmt.Println("数组 randNumList 中元素的总和为: ", sum)
}

2. 找出数组中和为给定值的两个元素的下标,例如数组[1,3,5,8,7],找出两个元素之和等于8的下标分别是(0,4)和(1,2)

package main

import "fmt"

func selectNumber(a1 *[10]int,sum int) {
	for i := 0; i < len(a1); i++ {
		num := sum - a1[i]
		for i1,v1 := range a1{
			if num == v1{
				fmt.Printf("指定元素1:%d,下标为%d  指定元素2:%d,下标为%d\n", a1[i], i, num, i1)
			}
		}
	}
}

func main() {
	a1 := [10]int{1, 5, 3, 7, 8, 4, 9, 6, 2, 0}
	selectNumber(&a1,10)
}


// 打印结果:
指定元素1:1,下标为0  指定元素2:9,下标为6
指定元素1:5,下标为1  指定元素2:5,下标为1
指定元素1:3,下标为2  指定元素2:7,下标为3
指定元素1:7,下标为3  指定元素2:3,下标为2
指定元素1:8,下标为4  指定元素2:2,下标为8
指定元素1:4,下标为5  指定元素2:6,下标为7
指定元素1:9,下标为6  指定元素2:1,下标为0
指定元素1:6,下标为7  指定元素2:4,下标为5
指定元素1:2,下标为8  指定元素2:8,下标为4

1.5 数组使用的注意事项

  1. 数组的地址为 数组中首个元素的地址,第二个元素的地址为第一个元素地址 + 对应数据类型的位数,数组中元素的地址为连续的
  2. 数组是多个相同数据类型的组合,一旦数组 声明 / 定义 了,其长度是固定的,不能动态变化
  3. var a []int 这时 a 就是一个 slice 切片
  4. 数组中的元素可以是任意相同数据类型,包括值类型,引用类型,但是不能混用
  5. 数组创建后,如果没有赋值,有默认值,数值类型默认值为0,字符串类型默认值为""空串,bool类型默认值为false
  6. 使用数组的步骤:1,声明数组并开辟空间 2,给数组各个元素赋值 3,使用数组
  7. 数组的下标是从0开始的
  8. 数组下标必须在指定范围内使用,否则会报错 panic,数组越界
  9. Go 的数组属于值类型,在默认情况下是值传递,因此会进行值拷贝,数组与数组间不会相互影响
  10. 如果在其他函数中,修改原来的数组,可以使用引用传递(指针)
  11. 长度是数组类型的一部分,在传递函数参数时,需要考虑到数组的长度
    img

2. 切片(slice)

2.1 Golang中的切片

需要说明,slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。

  1. 切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
  2. 切片的长度可以改变,因此,切片是一个可变的数组。
  3. 切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。
  4. cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array),其中array是slice引用的数组。
  5. 切片的定义:var 变量名 []类型,比如 var str []string var arr []int
  6. 如果 slice == nil,那么 len、cap 结果都等于 0。

2.2 初始化

对于元素个数不确定的数组可以直接创建切片,避免容量不够产生的麻烦

2.1.1 一维切片

1. 声明空切片(nil切片)

空切片内部是不允许添加元素的

package main

import "fmt"

func main() {

	// 1. 初始化空切片,当前切片为 [] 其长度为0 容量为0
	var s1 []int
    fmt.Println(s1,len(s1),cap(s1))   // [] 0 0 
	s1[0] = 19                       // 空切片内部是不允许添加元素的,所以这里会报错
	as1 := append(s1, 1, 3, 4, 5, 6) // 可以使用 append 来动态扩容数组
	fmt.Println(as1)                 // 扩容后的新切片为 [1 3 4 5 6]

    
	// 2. 初始化空切片,当前切片为 [] 其长度为0 容量为0
	s2 := []int{}
    fmt.Println(s2,len(s2),cap(s2))   // [] 0 0 
    
    
    
    // 3. 初始化空切片,当前切片为 [] 其长度为0 容量为0
	var s3 []int = make([]int, 0, 0)
	s3[0] = 10                    // 空切片内部是不允许添加元素的,所以这里会报错
	as2 := append(s3, 1, 2, 3, 4) // 可以使用 append 来动态扩容数组
	fmt.Println(as2)              // 扩容后的新切片为 [1 2 3 4]
    
}

2. 引用数组切片

package main

import "fmt"

func main() {
	// 1. 创建数组

	a1 := [5]int{1, 3, 4, 7, 2}
	fmt.Println(a1) // 初始化数组: [1 3 4 7 2]

	s1 := a1[:2]
	fmt.Println(s1) // 初始化切片:  [1 3]

	s1[0] = 10                    // 切片的 下标0 改为10
	fmt.Println(s1)               // 修改后的切片: [10 3]
	fmt.Println(a1)               // 原数组也发生了改变:  [10 3 4 7 2]
	as1 := append(s1, 11, 22, 33) // append 扩容 s1
	fmt.Println(as1)              // 扩容后的新切片为 [10 3 11 22 33]
	fmt.Println(a1)               // 原数组也发生了改变 [10 3 11 22 33]
    fmt.Printf("%p, %p, %p", &a1, &s1, &as1)  // 数组, 切片, 扩容后的数组地址也发生了改变  0xc00000e390, 0xc000008048, 0xc000008090
    
}

3. make() 创建切片

package main

import "fmt"

func main() {
	// 创建一个长度为8,容量为10的切片
	var s1 []int = make([]int, 8, 10)
	fmt.Println(s1, len(s1), cap(s1)) // [0 0 0 0 0 0 0 0] 8 10

	// 容量参数不指定,默认和长度一样
	var s2 []int = make([]int, 6)
	fmt.Println(s2, len(s2), cap(s2)) // [0 0 0 0 0 0] 6 6
}

2.2.2. 多维切片

package main

import "fmt"

func main() {
	// 1. 创建多维切片,并初始化
	s1 := [][]int{
		{1, 2, 3},
		{4, 5, 6},
		{7, 8, 9},
	}
	fmt.Println(s1) // [[1 2 3] [4 5 6] [7 8 9]]

	// 2. 引用数组
	a1 := [...][2]int{
		{1, 2},
		{3, 4},
		{5, 6},
	}
	s2 := a1[:2]
	s3 := a1[:2][:1]
	fmt.Println(s2) // [[1 2] [3 4]]
	fmt.Println(s3) //[[1 2]]
}

2.2.3 创建slice对象并自动分配底层数组

package main

import "fmt"

func main() {
    // 通过初始化表达式构造,可使用索引号。
    s1 := []int{0,1,2,3,8:100}
    fmt.Print(s1,len(s1),cap(s1),"\n")    // [0 1 2 3 0 0 0 0 100] 9 9

    // 使用make 创建,指定len,cap值
    s2 := make([]int,6,8)
    fmt.Println(s2,len(s2),cap(s2),"\n")  // [0 0 0 0 0 0] 6 8 

    // 省略cap, 相当于cap = len
    s3 := make([]int,6)
    fmt.Println(s3,len(s3),cap(s3),"\n")  // [0 0 0 0 0 0] 6 6 
}

2.3.4 指针访问切片的底层数组

make 函数允许在运行期,动态指定数组长度,绕开了数组类型必须使用编译期常量的限制,同时还可用指针直接访问底层数组退化成底层数组操作

package main

import "fmt"

func main() {
	s := []int{1,2,3}
	// 通过& 取址符,获取底层数组元素的指针
	p := &s[0]

	// 通过*p 取到该元素的值,并赋值为100
	*p = 100

	fmt.Println(s)   // [100 2 3]
	fmt.Println(*p)  // 100
}

2.3 数组切片的索引取值

1. 切片[]操作的理解

golang slice data[:6:8] 两个冒号的理解

常规slice , data[6:8],从索引为6的开始到索引为8的截止,不包括索引为8的元素(返回切片 [6, 7]),长度len为2, 最大可扩充长度cap为 4

另一种写法: data[:6:8],从索引为0的开始到索引为6的截止,不包括索引为6的元素(返回 [1, 2, 3, 4, 5, 6]), 长度len为6,最大扩充项cap设置为8(data[:6:8]中的8, 指定设置了cap 为8)

a[x:y:z]: 切片内容 [x:y], 切片长度: y-x, 切片容量:z-x

2. 一维切片

var slice0 []int = arr[start:end] // start开始,end-1结束,同样是顾前不顾后,即不包含 索引为end的元素
var slice1 []int = arr[:end]      // :end 表示 0开始 end -1 结束
var slice2 []int = arr[start:]    // start:表示 start开始,到数组最尾部元素
var slice3 []int = arr[:]         // [:] 表示 引用数组的全部

3. 多维切片

// [][]Type, 是指切片中的元素类型为[]T,即切片中的元素的数据类型为切片

var slice0 [][]int = arr[start:end][start:end] // start开始,end-1结束,同样是顾前不顾后,即不包含 索引为end的元素
var slice1 [][]int = arr[:end][:end]           // :end 表示 0开始 end -1 结束
var slice2 [][]int = arr[start:][start:]       // start:表示 start开始,到数组最尾部元素
var slice3 [][]int = arr[:][:]                 // [:] 表示 引用数组的全部

示例:

var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

var slice4 = arr[:len(arr)-1]      // 生成一个去掉了数组最后一个元素的切片

2.3 切片数组的内存图

2.4 常用操作方法

2.4.1 make()

切片初始化方法

package main

import "fmt"

func main() {
	// 创建一个长度为8,容量为10的切片
	var s1 []int = make([]int, 8, 10)
	fmt.Println(s1, len(s1), cap(s1)) // [0 0 0 0 0 0 0 0] 8 10

	// 容量参数不指定,默认和长度一样
	var s2 []int = make([]int, 6)
	fmt.Println(s2, len(s2), cap(s2)) // [0 0 0 0 0 0] 6 6
}

2.4.2 通过索引操作切片

切片的读写操作实际操作的目标是底层数组

1. 修改切片中的元素

// 1. 整型切片
package main

import "fmt"

func main() {
	// 1. 初始化切片
	a1 := [4]int{1,2,3,4}
	s1 := a1[:2]
	fmt.Println(a1)   // 原始数组:  [1 2 3 4]
	fmt.Println(s1)   // 原始切片:  [1 2]

	// 2. 修改切片中的元素
	s1[0] = 10
	fmt.Println(a1)   // 修改后的数组:  [10 2 3 4]
	fmt.Println(s1)   // 修改后的切片:  [10 2]
}

// 2. 结构体切片
package main

import "fmt"

func main() {
	// 1. 创建结构体数组
	a1 := [3]struct {
		name string
		age  int
	}{
		{"小明", 18},
		{"小红", 16},
	}
	fmt.Println(a1) // [{小明 18} {小红 16} { 0}]
	// 2. 创建结构体数组切片
	s1 := a1[:1]
	fmt.Println(s1) // [{小明 18}]

	// 3. 索引修改切片中的元素
	s1[0].name = "小白"
	fmt.Println(s1) // [{小白 18}]
	fmt.Println(a1) // [{小白 18} {小红 16} { 0}]

	// 地址相同,说明操作的是原结构体,而不是拷贝,开辟新空间
	fmt.Printf("%p, %p\n", &a1[0], &s1[0]) // 0xc000100050, 0xc000100050  
}


2. 索引操作多维数组

package main

import "fmt"

func main() {
	// 1. 创建结构体数组
	var stru1 = [3][3]struct {
		name string
		age  int
	}{}
	fmt.Println(stru1) // [[{ 0} { 0} { 0}] [{ 0} { 0} { 0}] [{ 0} { 0} { 0}]]

	// 2. 创建引用结构体数组的切片
	s1 := stru1[:]
	fmt.Println(s1) // [[{ 0} { 0} { 0}] [{ 0} { 0} { 0}] [{ 0} { 0} { 0}]]

	// 3. 通过索引修改切片中的元素
	s1[0][0].name = "小明"
	s1[1][2].name = "小白"
	fmt.Println(s1) // [[{小明 0} { 0} { 0}] [{ 0} { 0} {小白 0}] [{ 0} { 0} { 0}]]
}

2.4.3 切片遍历

package main

import (
    "fmt"
)

func main() {

    data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    slice := data[:]
    for index, value := range slice {
        fmt.Printf("inde : %v , value : %v\n", index, value)
    }

}

2.4.4 copy()

copy(slice1,slice2) :函数 copy 在两个 slice 间复制数据,以slice1 的长度为基础,拷贝slice对应索引位置的元素

案例1:

package main

import (
	"fmt"
)

func main() {
	slice1 := []int{1, 2}
	slice2 := []int{3, 4, 5, 6}
	copy(slice1, slice2)
	fmt.Println(slice1) 	// [3,4]
	fmt.Println(slice2) 	// [3,4,5,6]

	slice3 := []int{1, 2}
	slice4 := []int{3, 4, 5, 6}
	copy(slice4, slice3)
	fmt.Println(slice3) 	// [1,2]
	fmt.Println(slice4) 	// [1,2,5,6]

}

案例2:

package main

import "fmt"

func main() {
	s1 := []int{1, 2, 3, 4, 5}
	s2 := make([]int, 10)
	// 将s1的元素按照下标拷贝到s2
	copy(s2, s1)
	fmt.Println("s1为:", s1)
	fmt.Println("s2为:", s2)

	s3 := []int{11, 22, 33}
	s3 = append(s3, s2...)
	fmt.Println("s3为:", s3)
}

// 运行结果:
s1为: [1 2 3 4 5]
s2为: [1 2 3 4 5 0 0 0 0 0]
s3为: [11 22 33 1 2 3 4 5 0 0 0 0 0]

2.4.5 resizereslice

1. resize 调整切片大小,但是不能超过底层数组的长度

package main

import (
    "fmt"
)

func main() {
    var a = []int{1, 3, 4, 5}
    fmt.Printf("slice a : %v , len(a) : %v\n", a, len(a))
    b := a[1:2]
    fmt.Printf("slice b : %v , len(b) : %v\n", b, len(b))
    c := b[0:3]
    fmt.Printf("slice c : %v , len(c) : %v\n", c, len(c))
}

// 运行结果
slice a : [1 3 4 5] , len(a) : 4
slice b : [3] , len(b) : 1
slice c : [3 4 5] , len(c) : 3

2. reslice 重置数组切片,是可以反复取的

package main

import "fmt"

func main() {
	arr := [8]int{0, 1, 100, 3, 4, 5, 6, 7}
	s2 := arr[:]
	fmt.Println(s2) //[0 1 100 3 4 5 6 7]
	s2 = s2[:5]
	fmt.Println(s2) //[0 1 100 3 4]
	s2 = s2[2:]
	fmt.Println(s2) //[100 3 4]
}

2.4.6 获取内存地址

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	s := make([]byte, 200)
	ptr := unsafe.Pointer(&s[0])  // 0xc0000d4000
	fmt.Println(ptr)
}

2.4.7 手写切片数据结构

package main

import (
	"fmt"
	"unsafe"
)

var ptr uintptr

var s1 = struct {
	addr uintptr
	len  int
	cap  int
}{ptr, 1, 1}

func main() {
	s := *(*[]byte)(unsafe.Pointer(&s1))
	fmt.Println(s)  // []
}

2.5 重点操作方法: append()

2.5.1 append() 基本使用

1. append() 简单操作:向 slice 尾部添加数据,返回新的 slice 对象

package main

import "fmt"

func main() {
	var s1 = []int{1, 2, 3, 4}
	fmt.Println(s1) // [1 2 3 4]

	s2 := append(s1, 5, 6, 7)
	fmt.Println(s2) // [1 2 3 4 5 6 7]

	// s2是切片,需要...,类似打散操作
	s3 := []int{11, 22, 33}
	s3 = append(s3, s2...)
	fmt.Println(s3) // [11 22 33 1 2 3 4 5 6 7]
}

2. 超出原 slice.cap 限制,就会重新分配底层数组,即便原数组并未填满

package main

import "fmt"

func main() {
	a1 := [...]int{1, 2, 3, 4}
	s1 := a1[2:3]   // 切片长度为1
	fmt.Println(s1) // [3]

	// 一次性append 两个值,超出了s.cap的限制
	as1 := append(s1, 111, 222)
	fmt.Println("原始数组: ", a1)    // [1 2 3 4]
	fmt.Println("原始切片: ", s1)    // [3]
	fmt.Println("扩容后的切片: ", as1) // [3 111 222]

	// 对比底层数组起始指针 0xc0000a8070   0xc0000a8060
	fmt.Println("切片起始元素的指针:", &s1[0], "数组起始元素的指针:", &a1[0])
}

2.5.2 slice的扩容现象

从输出结果可以看出,append 后的 s 重新分配了底层数组,并复制数据。如果只追加一个值,则不会超过 s.cap 限制,也就不会重新分配。 
通常以 2 倍容量重新分配底层数组。在大批量添加数据时,建议一次性分配足够大的空间,以减少内存分配和数据复制开销。或初始化足够长的 len 属性
改用索引号进行操作。及时释放不再使用的 slice 对象,避免持有过期数组,造成 GC 无法回收。

2.5.3 一次性分配和扩容执行时间测试

package main

import (
	"fmt"
	"time"
)

func appendDemo() {
	// 初始化切片,长度为10,容量为10
	s := make([]int, 10)

	// 不停给slice扩容,append 方式添加数据
	for i := 0; i < 50000000; i++ {
		s = append(s, i)
	}
}

func fuZhiDemo() {

	// 一次性分配50000000长度,50000000容量,避免了不停扩容的开销时间
	s := make([]int, 50000000)
	for i := 0; i < len(s); i++ {
		s[i] = i
	}
}

func main() {

	startTime := time.Now().UnixNano()
	appendDemo()
	fmt.Println(time.Now().UnixNano() - startTime) // 616130100 单位为纳秒

	startTime1 := time.Now().UnixNano()
	fuZhiDemo()
	fmt.Println(time.Now().UnixNano() - startTime1) // 80416300 单位为纳秒

}

2.5.4 扩容规律

package main

import "fmt"

func main() {
	// 创建一个长度为0,容量为1的切片
	s := make([]int, 0, 3)
	c := cap(s)

	for i := 0; i < 50; i++ {
		s = append(s, i)
		if n := cap(s); n > c {
			fmt.Printf("slice扩容了,扩容前为:%d,扩容后为:%d\n", c, n)
			c = n
		}
	}
}


// 运行结果: slice底层扩容是:成倍数扩容
slice扩容了,扩容前为:3,扩容后为:6
slice扩容了,扩容前为:6,扩容后为:12
slice扩容了,扩容前为:12,扩容后为:24
slice扩容了,扩容前为:24,扩容后为:48
slice扩容了,扩容前为:48,扩容后为:96

注意:应及时将所需数据 copy 到较小的 slice,以便释放超大号底层数组内存

package main

import (
    "fmt"
)

func main() {

    data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    fmt.Println("array data : ", data)
    s1 := data[8:]     // [8,9]
    s2 := data[:5]     // [0, 1, 2, 3, 4]
    fmt.Printf("slice s1 : %v\n", s1)
    fmt.Printf("slice s2 : %v\n", s2)
    copy(s2, s1)     // [0, 1, 2, 3, 4,8,9]
    fmt.Printf("copied slice s1 : %v\n", s1)
    fmt.Printf("copied slice s2 : %v\n", s2)
    fmt.Println("last array data : ", data)

}

// 运行结果,copy操作是,修改底层源数据的指向
array data :  [0 1 2 3 4 5 6 7 8 9]
slice s1 : [8 9]
slice s2 : [0 1 2 3 4]
copied slice s1 : [8 9]
copied slice s2 : [8 9 2 3 4]
last array data :  [8 9 2 3 4 5 6 7 8 9]

2.6 字符串和切片转换

string底层就是一个byte的数组,因此,也可以进行切片操作。

2.6.0 string和slice的关系

  1. string 底层是一个byte数组,因此string也可以进行切片处理
  2. string和切片在内存的形式
    img
  3. string是不可变的,也就是说不可以通过str[0] = "2"的方式修改字符串
  4. 如果需要修改字符串,可以先将string --> []byte(二进制转成byte) // 或者[]rune(有汉字转成rune) 再修改,然后转成string
    img

2.6.1 切片操作字符串

package main

import (
    "fmt"
)

func main() {
    str := "hello world"
    s1 := str[0:5]
    fmt.Println(s1)

    s2 := str[6:]
    fmt.Println(s2)
}

// 运行结果
hello
world

2.6.2 英文字符串切片

**string本身是不可变的,因此要改变string中字符。需要如下操作: **

package main

import "fmt"

func main() {

	str := "hello world"
	// 将字符串转成 byte数组,通过数组的下标,改变其中元素的值
    // 中文需要转成 rune数组
	s := []byte(str)
	s[0] = 'b'
	// 将byte数组 转成字符串
	fmt.Println(string(s))    // bello world
}

2.6.3 中文字符串切片

// 1. byte数组 操作中文字符串
package main

import "fmt"

func main() {
	s := "你好"
	sl := []byte(s)
	s2 := []byte("我")
	copy(sl, s2)
	fmt.Println(string(sl))
}

// 2. rune数组 操作中文字符串
package main

import (
    "fmt"
)

func main() {
    str := "你好,世界!hello world!"
    // 中文需要转成 rune数组
    s := []rune(str) 
    s[3] = '够'
    s[4] = '浪'
    s[12] = 'g'
    s = s[:14]
    str = string(s)
    fmt.Println(str)
}

// 输出结果
你好,够浪!hello go

2.6.4 数组转字符串(或切片)

package main

import (
	"fmt"
	"strings"
)

func main() {
	arr1 := [9]string{"1", "2", "3", "4", "5", "6", "7", "8", "9"}
	fmt.Println(arr1)     // [1 2 3 4 5 6 7 8 9]
	// fmt.Sprint(arr1) 将数组输出成字符串后,脱掉两边的括号
	s1 := strings.Trim(fmt.Sprint(arr1), "[]")
	fmt.Println(s1)      // 1 2 3 4 5 6 7 8 9
	
	// 字符串替换
	s2 := strings.Replace(fmt.Sprint(arr1), "[", "", 1)
	s3 := strings.Replace(s2, "]", "", 1)
	fmt.Println(s3)      // 1 2 3 4 5 6 7 8 9
}

2.7 nil切片

nil 切片被用在很多标准库和内置函数中,描述一个不存在的切片的时候,就需要用到 nil 切片。比如函数在发生异常的时候,返回的切片就是 nil 切片。nil 切片的指针指向 nil。

空切片一般会用来表示一个空的集合。比如数据库查询,一条结果也没有查到,那么就可以返回一个空切片。

如:

silce := make([]int , 0 )
slice := []int{ }

空切片和nil切片的区别

空切片和 nil 切片的区别在于,空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。

不管是使用 nil 切片还是空切片,对其调用内置函数 append,len 和 cap 的效果都是一样的

package main

import (
    "fmt"
)

func main() {
    slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    d1 := slice[6:8]
    fmt.Println(d1, len(d1), cap(d1))
    d2 := slice[:6:8]
    fmt.Println(d2, len(d2), cap(d2))
}

2.8 切片的原理及数组切片如何选

2.8.1 slice底层实现

切片是 Go 中的一种基本的数据结构,使用这种结构可以用来管理数据集合。切片的设计想法是由动态数组概念而来,为了开发者可以更加方便的使一个数据结构可以自动增加和减少。
但是切片本身并不是动态数据或者数组指针。切片常见的操作有 reslice、append、copy。与此同时,切片还具有可索引,可迭代的优秀特性。

2.8.2 切片和数组如何选择

1. 参数传递

在 Go 中,与 C 数组变量隐式作为指针使用不同,Go 中数组是值类型,赋值和函数传参操作都会复制整个数组数据

package main

import "fmt"

func testA2(x [2]int) {
	fmt.Printf("func a2 : %p , %v\n", &x, x)  // func a2 : 0xc0000aa0c0 , [100 200]
}

func main() {
	a1 := [2]int{100, 200}
	var a2 [2]int

	a2 = a1
	fmt.Printf("a1 : %p , %v\n", &a1, a1)   // a1 : 0xc0000aa070 , [100 200]
	fmt.Printf("a2 : %p , %v\n", &a2, a2)   // a2 : 0xc0000aa080 , [100 200]

	testA2(a2)
}

**可以看到,三个内存地址都不同,这也就验证了 Go 中数组赋值和函数传参都是值复制的。那这会导致什么问题呢 **

假想每次传参都用数组,那么每次数组都要被复制一遍。如果数组大小有 100万,在64位机器上就需要花费大约 800W 字节,即 8MB 内存。这样会消耗掉大量的内存。于是乎有人想到,函数传参用数组的指针。

package main

import "fmt"

func main() {
	a1 := [2]int{100, 200}
	
	// 1.传数组指针
	ArrayPoint(&a1) 
	fmt.Printf("a1 point : %p , %v\n", &a1, a1)   // a1 point : 0xc0000aa070 , [100 300]

	s1 := a1[:]
	// 2.传切片指针
	slicePoint(&s1)
	fmt.Printf("s1 point : %p , %v\n", &s1, s1)   // s1 point : 0xc000096060 , [100 400]
    fmt.Printf("a1 point : %p , %v\n", &a1, a1)   // a1 point : 0xc0000aa070 , [100 400]
}

func ArrayPoint(a1 *[2]int) {
	fmt.Printf("a1 func  : %p , %v\n", fa1, *fa1)  // a1 func  : 0xc0000aa070 , [100 200]
	(*a1)[1] += 100
}
func slicePoint(s1 *[]int) {
	fmt.Printf("s1 func  : %p , %v\n", fs1, *fs1)  // s1 func  : 0xc000096060 , [100 300]
	(*s1)[1] += 100
}

2. 结论:

这也就证明了数组指针确实到达了我们想要的效果。现在就算是传入10亿的数组,也只需要再栈上分配一个8个字节的内存给指针就可以了。这样更加高效的利用内存,性能也比之前的好。

不过传指针会有一个弊端,从打印结果可以看到,`a1` 和 `fa1` 指针地址都是同一个,万一原数组的指针指向更改了,那么函数里面的指针指向都会跟着更改。

切片的优势也就表现出来了。用切片传数组参数,既可以达到节约内存的目的,也可以达到合理处理好共享内存的问题。切片的指针和原来数组的指针是不同的。

// 由此我们可以得出结论:
把一个大数组传递给函数会消耗很多内存,采用切片的方式传参可以避免上述问题。切片是引用传递,所以它们不需要使用额外的内存并且比使用数组更有效率。

2.8.3 数组和切片的性能测试

将文件名改为以_test结尾

package main

import "testing"

func array() [1024]int {
    var x [1024]int
    for i := 0; i < len(x); i++ {
        x[i] = i
    }
    return x
}

func slice() []int {
    x := make([]int, 1024)
    for i := 0; i < len(x); i++ {
        x[i] = i
    }
    return x
}

func BenchmarkArray(b *testing.B) {
    for i := 0; i < b.N; i++ {
        array()
    }
}

func BenchmarkSlice(b *testing.B) {
    for i := 0; i < b.N; i++ {
        slice()
    }
}

我们做一次性能测试,并且禁用内联和优化,来观察切片的堆上内存分配的情况。

go test -bench . -benchmem -gcflags "-N -l"

输出结果比较“令人意外”:

BenchmarkArray-4          500000              3637 ns/op               0 B/op          0 alloc s/op
BenchmarkSlice-4          300000              4055 ns/op            8192 B/op          1 alloc s/op

结论:

解释一下上述结果,在测试 Array 的时候,用的是4核,循环次数是500000,平均每次执行时间是3637 ns,每次执行堆上分配内存总量是0,分配次数也是0 。

而切片的结果就“差”一点,同样也是用的是4核,循环次数是300000,平均每次执行时间是4055 ns,但是每次执行一次,堆上分配内存总量是8192,分配次数也是1 。

这样对比看来,并非所有时候都适合用切片代替数组,因为切片底层数组可能会在堆上分配内存,而且小数组在栈上拷贝的消耗也未必比 make 消耗大。

2.8.4 切片的数据结构原理

  1. 切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。
  2. 切片本身是一个只读对象,其工作机制类似数组指针的一种封装。
  3. 切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个与指向数组的动态窗口。
  4. 给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个长度可变的数组。

1. Slice 底层的数据结构定义如下:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

切片的结构体由3部分构成,Pointer 是指向一个数组的指针,len 代表当前切片的长度,cap 是当前切片的容量。cap 总是大于等于 len 的。

2. 得到 slice 的内存地址

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	s := make([]byte, 200)
	ptr := unsafe.Pointer(&s[0])
	fmt.Println(ptr)
}

3. Go 的内存地址中构造一个 slice

构造一个虚拟的结构体,把 slice 的数据结构拼出来

var ptr uintptr

var s1 = struct {
	addr uintptr
	len  int
	cap  int
}{ptr, 1, 1}

func main() {
	s := *(*[]byte)(unsafe.Pointer(&s1))
	fmt.Println(s)
}

当然还有更加直接的方法,在 Go 的反射中就存在一个与之对应的数据结构 SliceHeader我们可以用它来构造一个 slice

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)
var o []byte

func main() {
	sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(&o)))
	sliceHeader.Cap = 1
	sliceHeader.Len = 1
	sliceHeader.Data = 1
	fmt.Println(sliceHeader)   // &{1 1 1}
}

2.8.5 make和切片字面量的底层代码原理

1. make() 创建切片的底层代码原理

func makeslice(et *_type, len, cap int) slice {
    // 根据切片的数据类型,获取切片的最大容量
    maxElements := maxSliceCap(et.size)
    // 比较切片的长度,长度值域应该在[0,maxElements]之间
    if len < 0 || uintptr(len) > maxElements {
        panic(errorString("makeslice: len out of range"))
    }
    // 比较切片的容量,容量值域应该在[len,maxElements]之间
    if cap < len || uintptr(cap) > maxElements {
        panic(errorString("makeslice: cap out of range"))
    }
    // 根据切片的容量申请内存
    p := mallocgc(et.size*uintptr(cap), et, true)
    // 返回申请好内存的切片的首地址
    return slice{p, len, cap}
}

还有一个 int64 的版本,实现原理和上面的是一样的,只不过多了把 int64 转换成 int 这一步罢了。

func makeslice64(et *_type, len64, cap64 int64) slice {
    len := int(len64)
    if int64(len) != len64 {
        panic(errorString("makeslice: len out of range"))
    }

    cap := int(cap64)
    if int64(cap) != cap64 {
        panic(errorString("makeslice: cap out of range"))
    }

    return makeslice(et, len, cap)
}

用 make 函数创建的一个 len = 4, cap = 6 的切片。内存空间申请了6个 int 类型的内存大小。由于 len = 4,所以后面2个暂时访问不到,但是容量还是在的。这时候数组里面每个变量都是0 。

1. 字面量 创建切片的底层代码原理

这里是用字面量创建的一个 len = 6,cap = 6 的切片,这时候数组里面每个元素的值都初始化完成了。需要注意的是 [ ] 里面不要写数组的容量,因为如果写了个数以后就是数组了,而不是切片了。

还有一种简单的字面量创建切片的方法。如上图。如图就 Slice A 创建出了一个 len = 3,cap = 3 的切片。从原数组的第二位元素(0是第一位)开始切,一直切到第四位为止(不包括第五位)。同理,Slice B 创建出了一个 len = 2,cap = 4 的切片。

3. nil 和空切片
nil 切片和空切片也是常用的

var slice []int

2.8.6 切片扩容原理及注意事项

1. 当一个切片的容量满了,就需要扩容了。怎么扩容的策略是什么?

func growslice(et *_type, old slice, cap int) slice {
    if raceenabled {
        callerpc := getcallerpc(unsafe.Pointer(&et))
        racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
    }
    if msanenabled {
        msanread(old.array, uintptr(old.len*int(et.size)))
    }

    if et.size == 0 {
        // 如果新要扩容的容量比原来的容量还要小,这代表要缩容了,那么可以直接报panic了。
        if cap < old.cap {
            panic(errorString("growslice: cap out of range"))
        }

        // 如果当前切片的大小为0,还调用了扩容方法,那么就新生成一个新的容量的切片返回。
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }

  // 这里就是扩容的策略
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            for newcap < cap {
                newcap += newcap / 4
            }
        }
    }

    // 计算新的切片的容量,长度。
    var lenmem, newlenmem, capmem uintptr
    const ptrSize = unsafe.Sizeof((*byte)(nil))
    switch et.size {
    case 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        newcap = int(capmem)
    case ptrSize:
        lenmem = uintptr(old.len) * ptrSize
        newlenmem = uintptr(cap) * ptrSize
        capmem = roundupsize(uintptr(newcap) * ptrSize)
        newcap = int(capmem / ptrSize)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem = roundupsize(uintptr(newcap) * et.size)
        newcap = int(capmem / et.size)
    }

    // 判断非法的值,保证容量是在增加,并且容量不超过最大容量
    if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) {
        panic(errorString("growslice: cap out of range"))
    }

    var p unsafe.Pointer
    if et.kind&kindNoPointers != 0 {
        // 在老的切片后面继续扩充容量
        p = mallocgc(capmem, nil, false)
        // 将 lenmem 这个多个 bytes 从 old.array地址 拷贝到 p 的地址处
        memmove(p, old.array, lenmem)
        // 先将 P 地址加上新的容量得到新切片容量的地址,然后将新切片容量地址后面的 capmem-newlenmem 个 bytes 这块内存初始化。为之后继续 append() 操作腾出空间。
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        // 重新申请新的数组给新切片
        // 重新申请 capmen 这个大的内存地址,并且初始化为0值
        p = mallocgc(capmem, et, true)
        if !writeBarrier.enabled {
            // 如果还不能打开写锁,那么只能把 lenmem 大小的 bytes 字节从 old.array 拷贝到 p 的地址处
            memmove(p, old.array, lenmem)
        } else {
            // 循环拷贝老的切片的值
            for i := uintptr(0); i < lenmem; i += et.size {
                typedmemmove(et, add(p, i), add(old.array, i))
            }
        }
    }
    // 返回最终新切片,容量更新为最新扩容之后的容量
    return slice{p, old.len, newcap}
}

上述就是扩容的实现。主要需要关注的有两点,一个是扩容时候的策略,还有一个就是扩容是生成全新的内存地址还是在原来的地址后追加。

2. 扩容策略

package main

import "fmt"

func main() {
	// 1. append() 一个元素到切片中,原切片的容量不够了,那么此时就需要扩容了,底层会将旧切片的所有元素拷贝到新切片中,并在尾部添加新元素进去,得到新的切片
	slice := []int{10, 20, 30, 40}
	newSlice := append(slice, 50)
	fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))                // Before slice = [10 20 30 40], Pointer = 0xc000004078, len = 4, cap = 4
	fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice)) // Before newSlice = [10 20 30 40 50], Pointer = 0xc000004090, len = 5, cap = 8

	// 2. 修改其中的一个元素,再观察内存地址是否发生改变
	newSlice[1] += 10
	fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))                // After slice = [10 20 30 40], Pointer = 0xc000004078, len = 4, cap = 4
	fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice)) // After newSlice = [10 30 30 40 50], Pointer = 0xc000004090, len = 5, cap = 8
}

从图中我们可以很容易的看出,新的切片和之前的切片已经不同了,因为新的切片更改了一个值,并没有影响到原来的数组,新切片指向的数组是一个全新的数组并且 cap 容量也发生了变化。这之间究竟发生了什么呢?

Go 中切片扩容的策略是这样的:

  1. 如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量。上面那个例子也验证了这一情况,总容量从原来的4个翻倍到现在的8个。
  2. 一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25 ,即每次增加原来容量的四分之一。

注意: 扩容扩大的容量都是针对原来的容量而言的,而不是针对原来数组的长度而言的。

新数组 or 老数组 ?

再谈谈扩容之后的数组一定是新的么?这个不一定,分两种情况。

情况一:

package main

import "fmt"

func main() {
	array := [4]int{10, 20, 30, 40}
	slice := array[0:2]
	newSlice := append(slice, 50)
	fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))                // Before slice = [10 20], Pointer = 0xc000004078, len = 2, cap = 4
	fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice)) // Before newSlice = [10 20 50], Pointer = 0xc000004090, len = 3, cap = 4
	newSlice[1] += 10
	fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))                // After slice = [10 30], Pointer = 0xc000004078, len = 2, cap = 4
	fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice)) // After newSlice = [10 30 50], Pointer = 0xc000004090, len = 3, cap = 4
	fmt.Printf("After array = %v\n", array)                                                                                  // After array = [10 30 50 40]
}

1. 通过打印的结果,我们可以看到,在这种情况下,扩容以后并没有新建一个新的数组,扩容前后的数组都是同一个,这也就导致了新的切片修改了一个值,也影响到了老的切片了。并且 append() 操作也改变了原来数组里面的值。一个 append() 操作影响了这么多地方,如果原数组上有多个切片,那么这些切片都会被影响!无意间就产生了莫名的 bug!

2. 这种情况,由于原数组还有容量可以扩容,所以执行 append() 操作以后,会在原数组上直接操作,所以这种情况下,扩容以后的数组还是指向原来的数组。

3. 这种情况也极容易出现在字面量创建切片时候,第三个参数 cap 传值的时候,如果用字面量创建切片,cap 并不等于指向数组的总容量,那么这种情况就会发生。

一定要注意:

slice := array[1:2:3]

上面这种情况非常危险,极度容易产生 bug 。建议用字面量创建切片的时候,cap 的值一定要保持清醒,避免共享原数组导致的 bug。

情况二:

情况二其实就是在扩容策略里面举的例子,在那个例子中之所以生成了新的切片,是因为原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响原数组。

所以建议尽量避免情况一,尽量使用情况二,避免 bug 产生。

2.8.7 切片拷贝原理

1. slicecopy()

func slicecopy(to, fm slice, width uintptr) int {
    // 如果源切片或者目标切片有一个长度为0,那么就不需要拷贝,直接 return 
    if fm.len == 0 || to.len == 0 {
        return 0
    }
    // n 记录下源切片或者目标切片较短的那一个的长度
    n := fm.len
    if to.len < n {
        n = to.len
    }
    // 如果入参 width = 0,也不需要拷贝了,返回较短的切片的长度
    if width == 0 {
        return n
    }
    // 如果开启了竞争检测
    if raceenabled {
        callerpc := getcallerpc(unsafe.Pointer(&to))
        pc := funcPC(slicecopy)
        racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
        racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
    }
    // 如果开启了 The memory sanitizer (msan)
    if msanenabled {
        msanwrite(to.array, uintptr(n*int(width)))
        msanread(fm.array, uintptr(n*int(width)))
    }

    size := uintptr(n) * width
    if size == 1 { 
        // TODO: is this still worth it with new memmove impl?
        // 如果只有一个元素,那么指针直接转换即可
        *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
    } else {
        // 如果不止一个元素,那么就把 size 个 bytes 从 fm.array 地址开始,拷贝到 to.array 地址之后
        memmove(to.array, fm.array, size)
    }
    return n
}

在这个方法中,slicecopy 方法会把源切片值(即 fm Slice )中的元素复制到目标切片(即 to Slice )中, 并返回被复制的元素个数,copy 的两个类型必须一致。slicecopy 方法最终的复制结果取决于较短的那个切片, 当较短的切片复制完成,整个复制过程就全部完成了。

示例:

package main

import "fmt"

func main() {
	array := []int{10, 20, 30, 40}
	slice := make([]int, 6)
	n := copy(slice, array)
	fmt.Println(n, slice) // 4 [10 20 30 40 0 0]
}

2.slicestringcopy() 这个方法原理和slicecopy() 方法类似,不在赘述了,注释写在代码里面了

func slicestringcopy(to []byte, fm string) int {
    // 如果源切片或者目标切片有一个长度为0,那么就不需要拷贝,直接 return 
    if len(fm) == 0 || len(to) == 0 {
        return 0
    }
    // n 记录下源切片或者目标切片较短的那一个的长度
    n := len(fm)
    if len(to) < n {
        n = len(to)
    }
    // 如果开启了竞争检测
    if raceenabled {
        callerpc := getcallerpc(unsafe.Pointer(&to))
        pc := funcPC(slicestringcopy)
        racewriterangepc(unsafe.Pointer(&to[0]), uintptr(n), callerpc, pc)
    }
    // 如果开启了 The memory sanitizer (msan)
    if msanenabled {
        msanwrite(unsafe.Pointer(&to[0]), uintptr(n))
    }
    // 拷贝字符串至字节数组
    memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n))
    return n
}

示例:

func main() {
    slice := make([]byte, 3)
    n := copy(slice, "abcdef")
    fmt.Println(n,slice)  // 3 [97,98,99]
}

3. 注意:

func main() {
    slice := []int{10, 20, 30, 40}
    for index, value := range slice {
        fmt.Printf("value = %d , value-addr = %x , slice-addr = %x\n", value, &value, &slice[index])
    }
}
// 打印结果:
value = 10 , value-addr = c4200aedf8 , slice-addr = c4200b0320
value = 20 , value-addr = c4200aedf8 , slice-addr = c4200b0328
value = 30 , value-addr = c4200aedf8 , slice-addr = c4200b0330
value = 40 , value-addr = c4200aedf8 , slice-addr = c4200b0338

从上面结果我们可以看到,如果用 range 的方式去遍历一个切片,拿到的 Value 其实是切片里面的值拷贝。所以每次打印 Value 的地址都不变

由于 Value 是值拷贝的,并非引用传递,所以直接改 Value 是达不到更改原切片值的目的的,需要通过 &slice[index] 获取真实的地址

posted @ 2021-08-26 10:53  河图s  阅读(162)  评论(0)    收藏  举报