• Golang 数组和切片


    1. Golang 数组和切片

    1.1. 数组

    数组初始化方式常用的有 3 种,至于其它的用的很少,就不用管了,常用方式如下:

    var a[4]intb := [4]int{2, 4}
    c := [...]int{2, 4}
    
    • 1
    • 2

    Go 数组是值类型,赋值和传参会复制整个数组数据,为了避免数据复制,可以使用数组指针:

    func test(x *[2]int) {
    	x[1] += 1
    }
    
    func main() {
    	a := [2]int{2, 3}
    	test(&a)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    最后需要区分指针数组和数组指针的区别:

    func main() {
    	x, y := 1, 2
    	a := [...]*int{&x, &y} // 元素为指针的指针数组
    	p := &a                // 存储数组地址的指针
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Go 的数组,其实我们用的不多,一般大家都用切片,所以对于数组,掌握上述知识就可以了,其它关于数组的知识,需要用的时候,查阅相关资料即可。

    1.2. 切片

    1.2.1. 概念

    切片出现的原因也是因为数组的可操作性不高。切片的长度是不固定的,可以追加数据,可以理解切片是一个动态数组,切片的底层是一个结构体。

    type slice struct { 
    	array unsafe.Pointer 
    	len int 
    	cap int 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    切片类型 (slice) 本身并不是动态数组或数组指针。它内部通过指针引用底层数组,设定相关属性将操作限定在指定范围内。当需要时,会申请更大的内存,将当前数据复制过去,以实现类似动态数组的功能。

    1.2.2. 切片创建

    可直接创建切片对象,无需预先准备数组。因为是引用类型,须使用 make 函数或显式初始化语句,它会自动完成底层数组内存分配。

    普通格式:

    var 切片名 [] 数据类型
    
    • 1

    自动推导类型创建切片:

    切片名 := [] 类型{}
    
    • 1

    make 函数创建切片:长度是已经初始化的空间,容量是已经开辟的空间,包括已经初始化的空间和空闲的空间。

    // 长度是不能大于容量的,容量可以省略不写,不写时候就默认和长度的值一样
    切片名称 := make ([] 切片类型,长度 容量)
    
    // 返回切片的容量使用 cap, 返回切片的长度使用 len
    fmt.Println(cap(切片名称))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    // 演示
    s1 := make([]int, 3, 5)    // 指定 len、cap, 底层数组初始化为零值
    s2 := make([]int, 3)       // 省略 cap, 和 len 相等
    s3 := []int{10, 20, 5: 30} // 按初始化元素分配底层数组,并设置 len、cap, 设置索引 5 的数据为 30
    
    • 1
    • 2
    • 3
    • 4

    1.2.3. 切片初始化

    三种创建格式,都是可以通过 append 向切片添加数据的,初始化格式:

    // 普通格式创建的切片
    切片名 [索引] =// 自动推导类型创建的切片
    切片名 := [] 类型{数据 1, 数据 2, 数据 3}
    
    // make 函数方式创建的切片可以通过 append 和循环初始化
    切片名称 = append(切片名称,数据 1, 数据 2...)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    // 演示
    s1 := make([]int, 4, 6) // 由于 `len = 4`, 所以后面 2 个暂时访问不到,但是容量还是在,数组里面每个变量都是 0。
    s2 := []int{10,20,30,40,50,60}
    
    • 1
    • 2
    • 3

    1.2.4. append 函数

    • append 函数是向切片的末尾 slice(len) 添加数据
    • 如果添加的内容超出了切片初始定义的容量,切片会自动扩容
    • 扩容机制是:上一次的容量 * 2
    • 如果超过 1024 字节,每次扩容上一次的 1/4
    • append 每次扩容都是一个新的内存,和原来的无关联,所以如果是通过参数传递的方式,使用 append 添加数据,但是不会影响到原切片的数据,原因就是 append 每次拓展都是一个新的空间,指向的内存不再是原切片。

    1.2.5. copy 函数

    • 把切片 2 的数据 (0 索引到 len-1) 赋值到切片 1 中。
    • 注意:如果切片 1 的容量不够,则不赋值剩余的数据。如果切片 1 的数据比切片 2 的多,从切片 2 复制的数据是有多少,复制多少。
    • 总结:copy 只是复制索引相对应的数据,如果长度不够,不会覆盖原来的数据。

    格式:

    copy(切片 1, 切片 2)
    
    • 1

    演示:

    // 从切片 2 复制到切片 1, 但是切片 2 的数据比切片 1 的多,所以,最终只是复制了一部分,也就是索引相对应的数据
    func main() {
    	slice := []int{1, 2, 3}
    	slice2 := []int{4, 5, 6, 7, 8, 9}
    	copy(slice, slice2)
    	fmt.Println(slice) // [4 5 6]
    }
    
    // 从切片 1 复制到切片 1, 但是切片 1 的数据比切片 2 的少,所以,最终只是复制了一部分,也就是索引相对应的数据
    func main() {
    	slice := []int{1, 2, 3}
    	slice2 := []int{4, 5, 6, 7, 8, 9}
    	copy(slice2, slice)
    	fmt.Println(slice2) // [1 2 3 7 8 9]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    还可直接从字符串中复制数据到 []byte:

    func main() {
    	b := make([]byte, 3)
    	n := copy(b, "abcde")
    	fmt.Println(n, b)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    1.2.6. 切片截取

    切片截取就是从切片中获取指定的数据。如果初始化切片时,没有指定切片的容量,切片容量是跟随原切片的。

    切片截取的操作:

    操作含义
    s[n]切片 s 中索引位置为 n 的项
    s[:]从切片 s 的索引位置 0len(s)-1 处所获得的切片
    s[low:]从切片 s 的索引位置 low 到 len(s)-1 处所获得的切片
    s[:high]从切片 s 的索引位置 0 到 high 处所获得的切片,len=high
    s[low:high]从切片 s 的索引位置 low 到 high 处所获得的切片,len=high-low
    s[low:high:max]从切片 s 的索引位置 low 到 high 处所获得的切片,len=high-low, cap=max-low
    len(s)切片 s 的长度,总是 <=cap(s)
    cap(s)切片 s 的容量,总是 >=len(s)
    /**
    	第一个值:截取的起始索引
    	第二个值:截取的终止索引(不包括该值)
    	第三个值:用来计算切片的容量,可以省略,默认和长度一样
    	容量 = 第三个值 - 第一个值
    	长度 = 第二个值 - 第一个值
    */
    newSlice := slice[0:3:3] // 切片的操作符 `s[i:j:k]`, `j` 和 `k` 是个开区间。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    1.2.7. 切片值的修改

    切片截取后返回新切片,对新切片的值进行修改,会影响原来的切片。

    原因:切片截取后新的切片,不会给新的切片是指向了原来的切片,没有给新的切片开辟新的空间,所以对于新的切片操作会影响到原来的切片。

    1.2.8. nil 和空切片

    nil 切片的指针指向 nil, 表示一个不存在的切片:

    var slice []int
    
    • 1

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

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

    需要说明的一点:不管是使用 nil 切片还是空切片,对其调用内置函数 append, lencap 的效果都是一样的。然后切片只能和 nil 判等,不支持切片判等。

    1.2.9. 切片扩容

    Go 切片扩容策略:如果切片的容量小 1024 个元素,于是扩容的时候就翻倍增加容量。上面那个例子也验证了这一情况,总容量从原来的 4 个翻倍到现在的 8 个。一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25, 即每次增加原来容量的 1/4。下面我们看一种情况,当扩容时没有新建一个新的数组的情况,这里容易出问题:

    func main() {
    	array := [4]int{10, 20, 30, 40}
    	slice := array[0:2]           // 10 20
    	newSlice := append(slice, 50) // 10 20 50
    	newSlice[1] += 10             // 10 30 50
    	// 这里 slice=[10 30], array=[10 30 50 40], 入坑!! ! 
    	fmt.Printf("slice = %v\n", slice)       // [10 30]
    	fmt.Printf("array = %v\n", array)       // [10 30 50 40]
    	fmt.Printf("newSlice = %v\n", newSlice) // [10 30 50]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    slicenewSlicearray 底层共用一个数组,当修改 newSlice[1] 时,因为底层数据被修改,其它也都被修改了,这样非常容易产生莫名的 Bug!

    1.2.10. 切片遍历

    遍历和数组一样可以使用普通的 for 循环和 range 遍历得到。

    // 演示
    func main() {
    	slice := []int{1, 2, 3, 4, 5}
    	for i := 0; i < len(slice); i++ {
    		fmt.Print(slice[i])
    	}
    	
    	for _, v := range slice {
    		fmt.Println(v)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    如果用 range 的方式去遍历一个切片,拿到的 Value 其实是切片里面的值拷贝,每次打印 Value 的地址都不变,所以仅修改 Value 的值,是不会改变 Slice 中的数据,这点切记!! !

    1.2.11. 切片作为函数参数

    切片可以做为函数的参数,但是在函数中修改切片的值,会影响到原切片。

    因为切片的底层是结构体,结构体里有个参数 Pointer, Pointer 会指向切片的内存地址,使用的是浅拷贝方式,所以会影响到原切片值。

    func main() {
    	slice := []int{1, 2, 3, 4, 5}
    	SliceDemo10(slice)
    }
    
    func SliceDemo10(slice []int) {
    	for _, v := range slice {
    		fmt.Println(v)
    	}
    	slice = append(slice, 5, 6, 7)
    	fmt.Println(slice)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  • 相关阅读:
    java框架-springmvc
    【AI Agent系列】【MetaGPT多智能体学习】4. 基于MetaGPT的Team组件开发你的第一个智能体团队
    【云原生kubernetes从入门到实践系列教程 ] 二.docker操作
    创建js对象的几种方式
    Ubuntu 1804 ModuleNotFoundError: No module named ‘OpenSSL‘ in Python
    [Java学习笔记] Java异常机制(也许是全网最独特视角)
    Vue--整合SVG Icon图标--方法/实例
    Web自动化测试入门 : 前端页面的组成分析详解
    [题] 差分矩阵 #差分
    Mac 安装 boost(bjam)
  • 原文地址:https://blog.csdn.net/wan212000/article/details/126120835