Go从入门到放弃之数组、切片
一、数组
数组的声明和初始化
在 Go 语言中,数组是固定长度的、同一类型的数据集合。数组中包含的每个数据项被称为数组元素,一个数组包含的元素个数被称为数组的长度。
在 Go 语言中,你可以通过 []
来标识数组类型,但需要指定长度和元素类型,使用时可以修改数组成员,但是数组大小不可变化。以下是一些常见的数组声明方法:
1 2 3 4 5 | var a [8] byte // 长度为8的数组,每个元素为一个字节 var b [3][3] int // 二维数组(9宫格) var c [3][3][3]float64 // 三维数组(立体的9宫格) var d = [3] int {1, 2, 3} // 声明时初始化 var e = new ([3] string ) // 通过 new 初始化 |
和普通变量赋值一样,数组也可以通过 :=
进行一次性声明和初始化,所有数组元素通过 {}
包裹,然后通过逗号分隔多个元素
1 | a := [5] int {1,2,3,4,5} |
语法糖省略数组长度的声明
1 | a := [...] int {1, 2, 3} |
数组在初始化的时候,如果没有填满,则空位会通过对应的元素类型零值填充
1 2 3 4 5 | a := [5] int {1, 2, 3} fmt.Println(a) //上述代码的打印结果是: [1 2 3 0 0] |
我们还可以初始化指定下标位置的元素值,未设置的位置也会以对应元素类型的零值填充
1 2 3 4 | a := [5] int {1: 3, 3: 7} //这样数组 a 的元素值如下: [0 3 0 7 0] |
数组的长度是该数组类型的一个内置常量,可以用 Go 语言的内置函数 len()
来获取
1 | arrLength := len(arr) |
数组元素的访问和设置
可以使用数组下标来访问 Go 数组中的元素,数组下标默认从 0 开始,len(arr)-1
表示最后一个元素的下标:
1 2 | arr := [5] int {1,2,3,4,5} a1, a2 := arr[0], arr[len(arr) - 1] // 还可以通过下标设置对应索引位置的元素值:arr[0] = 100 |
遍历数组
遍历数组a有以下两种方法
1 2 3 4 5 6 7 8 9 10 11 12 | func main() { var a = [...] string { "北京" , "上海" , "深圳" } // 方法1:for循环遍历 for i := 0; i < len(a); i++ { fmt.Println(a[i]) } // 方法2:for range遍历 for index, value := range a { fmt.Println(index, value) } } |
如果我们不想获取索引值,可以这么做:
1 2 3 | for _, v := range arr { // ... } |
如果只想获取索引值,可以这么做:
1 2 3 | for i := range arr { // ... } |
多维数组
这里以二维数组为例(数组中又嵌套数组)
二维数组的定义
1 2 3 4 5 6 7 8 9 | func main() { a := [3][2] string { { "北京" , "上海" }, { "广州" , "深圳" }, { "成都" , "重庆" }, } fmt.Println(a) //[[北京 上海] [广州 深圳] [成都 重庆]] fmt.Println(a[2][1]) //支持索引取值:重庆 } |
二维数组的遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 | func main() { a := [3][2] string { { "北京" , "上海" }, { "广州" , "深圳" }, { "成都" , "重庆" }, } for _, v1 := range a { for _, v2 := range v1 { fmt.Printf( "%s\t" , v2) } fmt.Println() } } |
注意: 多维数组只有第一层可以使用...
来让编译器推导数组长度。例如:
1 2 3 4 5 6 7 8 9 10 11 12 | //支持的写法 a := [...][2] string { { "北京" , "上海" }, { "广州" , "深圳" }, { "成都" , "重庆" }, } //不支持多维数组的内层使用... b := [3][...] string { { "北京" , "上海" }, { "广州" , "深圳" }, { "成都" , "重庆" }, } |
数组是值类型
数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | func modifyArray(x [3] int ) { x[0] = 100 } func modifyArray2(x [3][2] int ) { x[2][0] = 100 } func main() { a := [3] int {10, 20, 30} modifyArray(a) //在modify中修改的是a的副本x fmt.Println(a) //[10 20 30] b := [3][2] int { {1, 1}, {1, 1}, {1, 1}, } modifyArray2(b) //在modify中修改的是b的副本x fmt.Println(b) //[[1 1] [1 1] [1 1]] } |
数组类型的不足
由于数组类型变量一旦声明后长度就固定了,这意味着我们将不能动态添加元素到数组,如果要这么做的话,需要先创建一个容量更大的数组,然后把老数组的元素都拷贝过来,最后再添加新的元素,如果数组尺寸很大的话,势必会影响程序性能,例如
1 2 3 4 5 6 7 | //这个求和函数只能接受[3]int类型,其他的都不支持<br>func arraySum(x [3]int) int{ sum := 0 for _, v := range x{ sum = sum + v } return sum } |
另外,数组是值类型,这意味着作为参数传递到函数时,传递的是数组的值拷贝,也就是说,会先将数组拷贝给形参,然后在函数体中引用的是形参而不是原来的数组,当我们在函数中对数组元素进行修改时,并不会影响原来的数组,
这种机制带来的另一个负面影响是当数组很大时,值拷贝会降低程序性能。综合以上因素,我们迫切需要一个引用类型的、支持动态添加元素的新「数组」类型,这就是下篇教程将要介绍的切片类型,实际上,我们在 Go 语言中很少使用数组,大多数时候会使用切片取代它。
二、切片
在 Go 语言中,切片是一个新的数据类型,与数组最大的不同在于,切片的类型字面量中只有元素的类型,没有长度
切片定义
声明切片类型的基本语法如下
1 2 3 4 | // name:表示变量名 // T:表示切片中的元素类型 var name []T |
创建切片的方法主要有三种 —— 基于数组、切片和直接创建
基于数组
切片可以基于一个已存在的数组创建。从这个层面来说,数组可以看作是切片的底层数组,而切片则可以看作是数组某个连续片段的引用。切片可以只使用数组的一部分元素或者整个数组来创建,甚至可以创建一个比所基于的数组还要大的切片:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 先定义一个数组 months := [...] string { "January" , "February" , "March" , "April" , "May" , "June" , "July" , "August" , "September" , "October" , "November" , "December" } // 基于数组创建切片 q2 := months[3:6] // 第二季度 summer := months[5:8] // 夏季 fmt.Println(q2) fmt.Println(summer) // 运行结果为: [April May June] [June July August] |
Go 语言支持通过 array[start:end]
这样的方式基于数组生成一个切片,start
表示切片在数组中的下标起点,end
表示切片在数组中的下标终点,两者之间的元素就是切片初始化后的元素集合,通过上面的示例可以看到,和字符串切片一样,这也是个左闭右开的集合,下面几种用法也都是合法的:
1 2 3 4 5 6 7 8 | // 基于 months 的所有元素创建切片(全年) all := months[:] // 基于 months 的前 6 个元素创建切片(上半年) firsthalf := months[:6] // 基于从第 6 个元素开始的后续元素创建切片(下半年) secondhalf := months[6:] |
基于切片
1 2 3 4 5 | firsthalf := months[:6] q1 := firsthalf[:3] // 基于 firsthalf 的前 3 个元素构建新切片q1 := firsthalf[:9] 打印结果是:[January February March April May June July August September]。 因为 firsthalf 的容量是 12,只要选择的范围不超过 firsthalf 的容量,那么这个创建操作就是合法的,所以虽然是基于切片创建切片,但本质上还是基于数组。 |
使用make()函数构造切片
1 2 3 4 5 | T:切片的元素类型 size:切片中元素的数量 cap:切片的容量 make([]T, size, cap) |
示例:
1 2 3 4 5 6 7 8 | func main() { a := make([] int , 2, 10) fmt.Println(a) //[0 0] fmt.Println(len(a)) //2 fmt.Println(cap(a)) //10 } 上面代码中a的内部存储空间已经分配了10个,但实际上只用了2个。 容量并不会影响当前元素的个数,所以len(a)返回2,cap(a)则返回该切片的容量 |
判断切片是否为空
要检查切片是否为空,请始终使用len(s) == 0
来判断,而不应该使用s == nil
来判断。
切片的赋值拷贝
下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意
1 2 3 4 5 6 7 | func main() { s1 := make([] int , 3) //[0 0 0] s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组 s2[0] = 100 fmt.Println(s1) //[100 0 0] fmt.Println(s2) //[100 0 0] } |
切片遍历
切片的遍历方式和数组是一致的,支持索引遍历和for range
遍历
1 2 3 4 5 6 7 8 9 10 11 | func main() { s := [] int {1, 3, 5} for i := 0; i < len(s); i++ { fmt.Println(i, s[i]) } for index, value := range s { fmt.Println(index, value) } } |
动态增加元素
Go语言的内建函数append()
可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)
1 2 3 4 5 6 7 8 9 10 | func main(){ var s [] int s = append(s, 1) // [1] s = append(s, 2, 3, 4) // [1 2 3 4] s2 := [] int {5, 6, 7} s = append(s, s2...) // [1 2 3 4 5 6 7] } // 注意:通过var声明的零值切片可以在append()函数直接使用,无需初始化。 var s [] int s = append(s, 1, 2, 3 |
每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()
函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
举个例子:
1 2 3 4 5 6 7 8 | func main() { //append()添加元素和切片扩容 var numSlice [] int for i := 0; i < 10; i++ { numSlice = append(numSlice, i) fmt.Printf( "%v len:%d cap:%d ptr:%p\n" , numSlice, len(numSlice), cap(numSlice), numSlice) } } |
append()函数还支持一次性追加多个元素。 例如:
1 2 3 4 5 6 7 8 9 | var citySlice [] string // 追加一个元素 citySlice = append(citySlice, "北京" ) // 追加多个元素 citySlice = append(citySlice, "上海" , "广州" , "深圳" ) // 追加切片 a := [] string { "成都" , "重庆" } citySlice = append(citySlice, a...) fmt.Println(citySlice) //[北京 上海 广州 深圳 成都 重庆] |
切片的扩容策略
可以通过查看$GOROOT/src/runtime/slice.go
源码,其中扩容相关代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.len < 1024 { newcap = doublecap } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap { newcap += newcap / 4 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } } |
从上面的代码可以看出以下内容:
- 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
- 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
- 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int
和string
类型的处理方式就不一样。
使用copy()函数复制切片
首先我们来看一个问题:
1 2 3 4 5 6 7 8 9 | func main() { a := [] int {1, 2, 3, 4, 5} b := a fmt.Println(a) //[1 2 3 4 5] fmt.Println(b) //[1 2 3 4 5] b[0] = 1000 fmt.Println(a) //[1000 2 3 4 5] fmt.Println(b) //[1000 2 3 4 5] } |
由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。
Go语言内建的copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()
函数的使用格式如下
1 2 3 4 | srcSlice: 数据来源切片 destSlice: 目标切片 copy(destSlice, srcSlice []T) |
示例:
1 2 3 4 5 6 7 8 9 10 11 | func main() { // copy()复制切片 a := [] int {1, 2, 3, 4, 5} c := make([] int , 5, 5) copy(c, a) //使用copy()函数将切片a中的元素复制到切片c fmt.Println(a) //[1 2 3 4 5] fmt.Println(c) //[1 2 3 4 5] c[0] = 1000 fmt.Println(a) //[1 2 3 4 5] fmt.Println(c) //[1000 2 3 4 5] } |
从切片中删除元素
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素
1 2 3 4 5 6 7 8 | func main() { // 从切片中删除元素 a := [] int {30, 31, 32, 33, 34, 35, 36, 37} // 要删除索引为2的元素 a = append(a[:2], a[3:]...) fmt.Println(a) //[30 31 33 34 35 36 37] }<br> 总结一下就是:要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...) |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具