【Go】Go数据类型详解—数组与切片

1. 前言

今天需要学习的是Go语言当中的数组与切片数据类型。很多编程语言当中都有数组这样的数据类型,Go当中的切片类型本质上也是对 数组的引用。但是在了解如何定义使用数组与切片之前,我们需要思考为什么要引入数组这样的数据结构。

1.1 为什么需要数组

❓ 现在有一个需求:对一个班级的学生做统一管理,能够方便的打印班级全部学生的姓名

我们通过以前的知识只能写出这样的一段代码:

go 复制代码
var name1 = "zhangsan"
var name2 = "lisi"
var name3 = "wangwu"
fmt.Println(name1)
fmt.Println(name2)
fmt.Println(name3)

可想而知如果学生人数更多,代码量也会变得更为庞大,因此就出现了数组这样 连续存储结构 的数据类型,我们就可以定义一种数据类型就能对整个班级姓名做增删改查统一管理!

2. 数组的定义与使用

2.1 数组声明与访问

2.1.1 数组声明

数组的声明语法如下:

go 复制代码
// var 数组名 [数组长度]元素类型
var stuNameArr [32]string

其中注意点如下:

  • 数组长度必须显示指定
  • 数组内部存储元素类型必须统一
  • 数组元素如果为基本类型且声明未赋值时默认为零值,比如string类型则为""、int为0
2.1.2 数组访问

数组访问则可以通过[] + 下标直接进行索引访问(下标从0开始)

go 复制代码
// var 数组名 [数组长度]元素类型
var stuNameArr [32]string
stuNameArr[0] = "zhangsan"
stuNameArr[1] = "lisi"
fmt.Println(stuNameArr[0], stuNameArr[1])

同时数组也不能越界访问!!!在上述代码中可行索引区域为:0, 32),因此使用stuNameArr\[32时就会出现编译错误,如果编译时检查不出越界则在运行过程中会出现panic错误信息!

2.1.3 数组声明并初始化

除了上述先声明后初始化的方式之外,Go中的数组还支持使用复合字面量的方式进行声明并初始化的过程,语法如下:

go 复制代码
// var 数组名 = [数组长度]元素类型{元素值1, 元素值2...}
var nums = [3]int{1, 2, 3}
fmt.Println(nums, reflect.TypeOf(nums))

⭐ 扩展知识1:上述方式仍然需要我们手动指定数组长度,但是Go当中也提供了...的语法可以让编译器帮我们自动进行数组长度的计算!

go 复制代码
// var 数组名 = [数组长度]元素类型{元素值1, 元素值2...}
var nums = [...]int{
    1,
    2,
    3, // 最后一个逗号不能省略
}
fmt.Println(nums, reflect.TypeOf(nums))

⭐ 扩展知识2:上述初始化方式只能从左到右依次赋值,不够灵活,Go还提供了更灵活的方式进行赋值------使用索引下标进行初始化赋值

go 复制代码
// var 数组名 = [数组长度]元素类型{元素值1, 元素值2...}
var names = [3]string{0: "张三", 2: "李四"}
fmt.Println(names, reflect.TypeOf(names))
2.1.4 数组的迭代

迭代方式一:我们可以通过for循环的方式进行迭代,可以使用len内置函数得到数组的长度

go 复制代码
var stus = [...]string{"zhangsan", "lisi"}
for i := 0; i < len(stus); i++ {
    fmt.Println(stus[i])
}

迭代方式二:Go语言还提供了range迭代收集器的方式进行遍历,其中i为元素下标,v为元素值,当然range关键字只适用于收集器

go 复制代码
var stus = [...]string{"zhangsan", "lisi"}
for i, v := range stus {
    fmt.Println(i, v)
}

2. 切片

Go语言当中的切片是一种极其重要的数据类型,由于数组长度是固定的,因此操作起来十分麻烦,切片可以理解为是一个 动态数组,在开发中使用占比远远大于数组。

2.1 切片的创建方式

切片有如下两种创建方式:

  1. 创建方式1:使用数组的切片语法:arr[1:3]
  2. 创建方式2:使用make函数初始化:var slice = make([]int, len, cap)
2.1.1 创建方式1

首先先来看创建方式1:

go 复制代码
var arr = [3]string{"zhangsan", "lisi", "wangwu"}
var slice = arr[1:3]
fmt.Println(arr, reflect.TypeOf(arr))
fmt.Println(slice, reflect.TypeOf(slice))

运行结果如下图所示:

❗ 注意:数组和切片虽然打印的形式非常类似,但是这是两种不同的数据类型,使用reflect.TypeOf数组的类型为 3string,但是切片的类型为\[\]string

切片语法:[startIndex:endIndex)

  1. 切片取到的区间为左闭右开
  2. 切片得到的元素数量为:endIndex - startIndex
  3. 数组和切片进行切片操作都能得到切片
  4. 当缺省开始位置时比如:endIndex返回从0位置到结束位置,当缺省结束位置时例如startIndex:表示从起始位置到区域末尾,两者都缺省例如:表示整个区间

练习题:

go 复制代码
var arr = [5]int{10, 11, 12, 13, 14}
var s1 = arr[1:4]
fmt.Println(s1, reflect.TypeOf(s1)) 
var s2 = arr[2:5]
fmt.Println(s2, reflect.TypeOf(s2)) 
var s3 = s2[0:2]                    
s3[0] = 1000
fmt.Println(":::", s1, s2, s3)

运行结果如下图所示:

2.1.2 切片底层结构

翻看Go语言的源码就会发现,切片实际上就是一个结构体:内部结构如下:

go 复制代码
// /src/runtime/slice.go
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

各个字段含义如下:

  • array:该切片所引用的底层数组
  • len:切片的长度
  • cap:切片的容量,可用于扩容判断(后面会花大篇幅讲解)
go 复制代码
var arr = [5]int{10, 11, 12, 13, 14}
var s1 = arr[1:4]
var s2 = arr[2:5]
var s3 = s2[0:2]                    

上述代码在内存中的结构是这样的:

  • arr指的就是底层数组所引用的起始位置地址
  • len指的就是当前切片的元素个数
  • cap指的是从引用的起始位置开始还剩余多少已分配空间可供使用
2.1.2.1 练习题1
go 复制代码
var a = [...]int{1, 2, 3, 4, 5, 6}
a1 := a[0:3]
a2 := a[0:5]
a3 := a[1:5]
a4 := a[1:]
a5 := a[:]
a6 := a3[1:2]
fmt.Printf("a1的长度%d,容量%d\n", len(a1), cap(a1))
fmt.Printf("a2的长度%d,容量%d\n", len(a2), cap(a2))
fmt.Printf("a3的长度%d,容量%d\n", len(a3), cap(a3))
fmt.Printf("a4的长度%d,容量%d\n", len(a4), cap(a4))
fmt.Printf("a5的长度%d,容量%d\n", len(a5), cap(a5))
fmt.Printf("a6的长度%d,容量%d\n", len(a6), cap(a6))

运行结果:

2.1.2.2 练习题2
go 复制代码
s1 := []int{1, 2, 3}
s2 := s1[1:]    
s2[1] = 4       
fmt.Println(s1) 

运行结果:

2.1.2.3 练习题3
go 复制代码
var a = []int{1, 2, 3}
b := a
a[0] = 100
fmt.Println(b)

运行结果:

2.1.3 创建方式2(make函数)

跟指针类型类似,切片也是也是一种引用类型:因此声明未赋值时不会开辟空间进行初始化,如下代码是错误的:

go 复制代码
var slice []int
slice[0] = 1

在指针章节我们通过new函数进行初始化并返回一个地址变量,但是在切片当中我们需要使用make函数进行初始化同时指定长度和容量参数

make函数基本语法:var slice = make(切片类型, 长度, 容量)

go 复制代码
a := make([]int, 2) // 此时长度和容量都为2
b := make([]int, 2, 10)
fmt.Println(a, b) // [0, 0], [0, 0]
fmt.Println(len(a), len(b)) // 2 2
fmt.Println(cap(a), cap(b)) // 2 10

上述代码中我们使用make函数初始化a和b,第一行代码其内部构建了一个长度为2的数组,并初始化了一个切片数据类型,长度和容量都为2并指向对应底层数组;第二行代码其内部构建了一个长度为10的数组,并初始化了一个切片数据类型,长度为2容量为10并指向对应底层数组

2.2 切片扩容机制

append函数引入:由于数组长度是固定的,因此如果添加的元素过多就需要重新分配长度更大的数组并进行元素拷贝。而切片作为动态数组的优势就在于可以通过append函数自动进行扩容拷贝,简化了程序员开发成本

2.2.1 append基本用法

基本语法:append(切片, 元素值...)并返回一个新切片

go 复制代码
var emps = make([]string, 3, 5)
emps[0] = "张三"
emps[1] = "李四"
emps[2] = "王五"
fmt.Println(emps) // ["张三", "李四", "王五"]
emps2 := append(emps, "rain")
fmt.Println(emps2) // ["张三", "李四", "王五", "rain"]
emps3 := append(emps2, "eric")
fmt.Println(emps3) // ["张三", "李四", "王五", "rain", "eric"]
// 容量不够时发生二倍扩容
emps4 := append(emps3, "yuan")
fmt.Println(emps4) // ["张三", "李四", "王五", "rain", "eric", "yuan"]
2.2.2 append扩容原理

💡 扩容机制:

  1. 如果当前切片的长度超过容量时,原数组空间不足以进行扩展,此时就会构建一个长度更大的新数组,并拷贝原数组的元素到新数组中,最后构建一个新的切片重新引用新数组并返回
  2. 当元素个数<1024的时候就进行二倍扩容,但是当元素个数>=1024时进行1.25倍扩容
2.2.2.1 练习题1
go 复制代码
// 案例1
a := []int{11, 22, 33}
fmt.Println(len(a), cap(a)) // 3 3
c := append(a, 44)
a[0] = 100
fmt.Println(a) // [100, 22, 33]
fmt.Println(c // [11, 22, 33, 44]

运行结果:

2.2.2.2 练习题2
go 复制代码
// 案例2
a := make([]int, 3, 10)
fmt.Println(a) // [0, 0, 0]
b := append(a, 11, 22) 
fmt.Println(a) // 小心a等于多少? [0, 0, 0]
fmt.Println(b) // [0, 0, 0, 11, 22]
a[0] = 100
fmt.Println(a) // [100, 0, 0]
fmt.Println(b) // [100, 0, 0, 11, 22]

运行结果:

2.2.2.3 练习题3
go 复制代码
// 案例3
l := make([]int, 5, 10)
v1 := append(l, 1)
fmt.Println(v1) // [0, 0, 0, 0, 1]
fmt.Printf("%p\n", &v1)
v2 := append(l, 2) 
fmt.Println(v2) // [0, 0, 0, 0, 1, 2]
fmt.Printf("%p\n", &v2)
fmt.Println(v1) // [0, 0, 0, 0, 2]

运行结果:

2.2.2.4 经典面试题
go 复制代码
arr := [4]int{10, 20, 30, 40}
s1 := arr[0:2] // [10, 20]
s2 := s1       //  // [10, 20]
s3 := append(append(append(s1, 1), 2), 3)
s1[0] = 1000
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(s3)
fmt.Println(arr)

2.3 切片的其余操作

我们已经介绍完了切片当中的append核心操作,该操作用于新增元素,那么切片如果执行插入、删除等操作呢?

❗ 注意:切片类型并没有提供如delete、insert等操作,仅仅只有append一个操作,但是我们可以通过append模拟插入、删除等操作

2.3.1 使用append进行头插
go 复制代码
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
2.3.2 使用append在任意位置插入
go 复制代码
var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
2.3.3 使用append进行删除
go 复制代码
// 从切片中删除元素
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]
相关推荐
福大大架构师每日一题13 小时前
ollama v0.30.7 正式发布:Hermes 桌面端落地,接口、文档、底层依赖全方位优化
golang·log4j
不爱编程的小陈16 小时前
深入解析 Go 网络 I/O 的底层引擎:从 epoll 到 netpoll
服务器·网络·golang
何以解忧,唯有..19 小时前
Go 语言数据类型详解:从基础到复合类型
开发语言·golang·mfc
踏着七彩祥云的小丑19 小时前
Go学习第7天:Map集合 + 递归函数 + 类型转换
开发语言·学习·golang·go
何以解忧,唯有..19 小时前
Go语言变量的声明方式详解
开发语言·后端·golang
寂夜了无痕20 小时前
Go 多版本管理工具G 保姆级安装配置教程
golang·go多版本管理
张忠琳21 小时前
【Go 1.26.4】Golang Slice 深度解析
开发语言·后端·golang
张忠琳1 天前
【Go 1.26.4】Golang Channel 深度解析
开发语言·后端·golang
张忠琳2 天前
【Go 1.26.4】Golang Map 深度解析
开发语言·后端·golang
何以解忧,唯有..2 天前
Go 语言安装与环境配置完整指南
开发语言·后端·golang