数组
数组是同一数据类型元素的集合。在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组长度不能发生变化
。
基本语法:
var 数组变量名 [元素数量]T
//定义一个长度为3元素类型为int的数组a
var a [3]int
数组的声明
var a [3]int64
var b [2]bool
var c [10]string
注意事项
-
数组的长度必须是常量,并且长度是数组类型的一部分.
-
数组支持索引访问
a[1]
、c[7]
,索引的合法范围:0
~len(array)-1
,不支持负数索引。
代码展示
func f1() {
//声明数组
var a [3]int
var b [2]bool
var c [3]string
fmt.Printf("%#v", a) //数组初始值为int类型的初始值:[0 0 0]
fmt.Printf("%#v", b) //数组初始值为bool类型的初始值:[false false]
fmt.Printf("%#v", c) //数组初始值为string类型的初始值:空字符串
}
//运行结果
[3]int{0, 0, 0}
[2]bool{false, false}
[3]string{"", "", ""}
数组初始化
方式1
初始化数组时可以使用初始化列表设置数组元素的值。
//方式1:初始化数组
var array1 [3]int
var array2 = [3]int{1, 2}
var array3 = [3]string{"北京", "上海", "广州"}
fmt.Println(array1) //[0 0 0]
fmt.Println(array2) //[1 2 0]
fmt.Println(array3) //[北京 上海 广州]
//运行结果
[0 0 0]
[1 2 0]
[北京 上海 广州]
方式2
刚开始无法确定数组长度时,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度,数组长度用[...]
:
//方式2:初始化数组
var array4 = [...]int{1, 2, 3, 4, 5}
var array5 = [...]bool{false, true}
var array6 = [...]string{"山西", "北京", "上海", "天津"}
fmt.Printf("数组的类型为:%T,数组的值为:%v\n", array4, array4)
fmt.Printf("数组的类型为:%T,数组的值为:%v\n", array5, array5)
fmt.Printf("数组的类型为:%T,数组的值为:%v\n", array6, array6)
//运行结果:
数组的类型为:[5]int,数组的值为:[1 2 3 4 5]
数组的类型为:[2]bool,数组的值为:[false true]
数组的类型为:[4]string,数组的值为:[山西 北京 上海 天津]
方式3
指定索引值初始化数组:
//方式3:初始化数组
array7 := [...]int{1: 3, 5: 7} //指定索引为1的值为3,索引为5的值为7
fmt.Println(array7) //[0 3 0 0 0 7]
fmt.Printf("%T\n", array7) //[6]int
//运行结果:
[0 3 0 0 0 7]
[6]int
数组的遍历
方式1
//方法1:for循环遍历数组
array8 := [...]int{1, 2, 3, 4, 5}
for i := 0; i < len(array8); i++ {
fmt.Println(array8[i])
}
//运行结果
1
2
3
4
5
方式2
//方式2:for range遍历数组
array9 := [...]string{"北京", "上海", "天津", "广州"}
for index, value := range array9 {
fmt.Printf("索引为:%v, 值为:%v\n", index, value)
}
//运行结果
索引为:0, 值为:北京
索引为:1, 值为:上海
索引为:2, 值为:天津
索引为:3, 值为:广州
多维数据
这里以二维数组为例,三维数组、四维数组类似。
二维数组的定义
//二维数组定义
array10 := [3][2]string{ //array10中有3个[2]string的元素
{"上海", "北京"},
{"天津", "山西"},
{"广西", "杭州"}, //最外层的花括号换行,这里必须加逗号
}
fmt.Println(array10) //[[上海 北京] [天津 山西] [广西 杭州]]
//运行结果:
[[上海 北京] [天津 山西] [广西 杭州]]
二维数组的遍历
方式1:索引遍历
//方式1:for 循环遍历二维数组
array11 := [3][2]string{ //array10中有3个[2]string的元素
{"上海", "北京"},
{"天津", "山西"},
{"广西", "杭州"}, //最外层的花括号换行,这里必须加逗号
}
for i := 0; i < len(array11); i++ {
for j := 0; j < len(array11[i]); j++ {
fmt.Println(array11[i][j])
}
}
//运行结果
上海
北京
天津
山西
广西
杭州
方式2:for range遍历
//方式2: for range遍历二维数组
array12 := [3][2]string{ //array10中有3个[2]string的元素
{"上海", "北京"},
{"天津", "山西"},
{"广西", "杭州"},
}
for _, v := range array12 {
fmt.Println(v) //取出内嵌的二维数组
for _, v1 := range v {
fmt.Println(v1)
}
}
//运行结果:
上海
北京
天津
山西
广西
杭州
注意:
多维数组只有第一层可以使用[...]
来让编译器推导数组长度。例如:
//支持的写法
a := [...][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
//不支持多维数组的内层使用...
b := [3][...]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
数组是值类型
赋值、函数传参都是拷贝,修改副本不影响原值。
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]]
}
Go语言中全部都是值拷贝(深拷贝),Go语言是通过传递指针实现修改原来的值。(后面讲指针细说)
练习
//练习1:求数组元素的和
//方式1:
array13 := [...]int{1, 3, 5, 7, 8}
sum := 0
for i := 0; i < len(array13); i++ {
sum += array13[i]
}
fmt.Printf("方式1数组元素和为: %d\n", sum)
//方式2:
array14 := [...]int{1, 3, 5, 7, 8}
sum1 := 0
for _, value := range array14 {
sum1 += value
}
fmt.Printf("方式2数组元素和为: %d\n", sum1)
// 运行结果:
方式1数组元素和为: 24
方式2数组元素和为: 24
-----------------------------------------------------------------------------------------------------------------------------------
// 练习2:求数组中元素和为8的元素的下标(索引)
// 1 + 7 = 8;索引(0,3)
// 3 + 5 = 8;索引(1,2)
// 1.拿到数组中的每一个元素 :遍历数组
// 2.找到元素和为8的那两个元素 : 数学运算和比较运算
// 3.把符合要求的索引打印出来 : fmt.Println()
array15 := [...]int{1, 3, 5, 7, 8, 10}
for i := 0; i < len(array15); i++ {
for j := i + 1; j < len(array15); j++ {
if array15[i]+array15[j] == 8 {
fmt.Println(i, j)
}
}
}
//运行结果
0 3
1 2
切片
切片(slice)是一个拥有相同类型元素的可变长度序列。他是基于数组类型做的一层封装。非常灵活,支持自动扩容。
切片内部结构包含地址
、长度
、容量
。切片一般用于快速操作一块数据集合。
切片声明
var s []int // 未初始化,值为nil
举个例子:
//声明切片类型
var s1 []string //声明一个字符串切片
var s2 = []int{} //声明一个int切片,并初始化
var s3 = []bool{false, true} //声明一个布尔切片并初始化
// var s4 = []bool{false, true} //声明一个布尔切片并初始化
fmt.Println(s1) //[]
fmt.Println(s2) //[]
fmt.Println(s3) //[false true]
fmt.Println(s1 == nil) // true 切片未初始化,值为nil
fmt.Println(s2 == nil) //false 切片一经初始化,值就不是nil
fmt.Println(s3 == nil) //false 切片一经初始化,值就不是nil
//fmt.Println(s3 == s4) // 切片为引用类型,不支持直接比较,只能和nil比较
//运行结果:
[]
[]
[false true]
true
false
false
切片拥有自己的长度和容量,我们可以使用内置的len()
函数求长度,使用内置的cap()
函数求切片的容量。
切片的原理
切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。
举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7}
,切片s1 := a[:5]
,相应示意图如下。
切片s2 := a[3:6]
,相应示意图如下:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lhogBYaQ-1656579613776)(/Users/alblue/Documents/slice_02.png)]
切片表达式
切片表达式中的数字都是指的 索引 !!!
切片表达式从字符串、数组、指向数组或切片的指针构造字符串或切片。它有两种变体:一种指定low和high两个索引界限值的简单形式,另一种是除了low和high索引界限值外还指定容量的完整的形式。
切片简短表达式
切片的底层就是一个数组,所以我们可以基于数组通过切片表达式得到切片。
func f8() {
//切片简短表达式
array := [5]int{1, 2, 3, 4, 5}
s5 := array[1:3] //s5 := array[low:high]
fmt.Printf("s5:%v len(s5):%v cap(s5):%v 内存地址:%p\n", s5, len(s5), cap(s5), s5)
fmt.Printf("底层数组的内存地址:%p\n", s5) //比较下切片的内存地址和底层数组的内存地址一样,证实了切片引用数组
}
//运行结果:
s5:[2 3] len(s5):2 cap(s5):4 内存地址:0xc0000160f8
底层数组的内存地址:0xc0000160f8
为了方便起见,可以省略切片表达式中的任何索引。省略了low
则默认为0;省略了high
则默认为切片操作数的长度:
a[2:] // 等同于 a[2:len(a)]
a[:3] // 等同于 a[0:3]
a[:] // 等同于 a[0:len(a)]
注意:
对字符串和数组:0 <= low < high <= len
对切片:0 <= low < high <= cap
func f9() {
//数组和切片的索引上限
array := [5]int{1, 2, 3, 4, 5}
s6 := array[1:3] //数组的索引上限为len(array)
fmt.Printf("s6:%v len(s6):%v cap(s6):%v\n", s6, len(s6), cap(s6))
s7 := s6[3:4] //s6切片的索引上限就是cap(s6),而不是len(s6)
fmt.Printf("s7:%v len(s7):%v cap(s7):%v\n", s7, len(s7), cap(s7))
}
//运行结果:
s6:[2 3] len(s6):2 cap(s6):4
s7:[5] len(s7):1 cap(s7):1
切片完整表达式
对于数组,指向数组的指针,或切片a(注意不能是字符串)支持完整切片表达式:
a[low : high : max]
func f10() {
//默认切片的容量是从切片的开始索引到数组的最后
// max: 影响切片的容量
// max: 想象成high能取到最大值
//最终切片容量: max-low
array := [5]int{1, 2, 3, 4, 5}
// array[low:high,max]
s8 := array[1:2:3] // 0<= low <=high <=max <=cap(array)
fmt.Println(s8, len(s8), cap(s8)) //[2] 1 2
}
//运行结果:
[2] 1 2
字面量初始化
/字面量初始化{花括号}
func f11() {
s9 := []int{1, 2, 3, 4, 5, 6}
fmt.Println(s9) //[1 2 3 4 5 6]
s10 := []int{20: 3} //指定具体索引对应的值
fmt.Println(s10)
}
//运行结果:
[1 2 3 4 5 6]
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3]
make初始化切片
切片声明之后需要使用内置的make函数做初始化!! !
我们上面都是基于数组创建的切片,如果动态创建一个切片,我们需要使用内置make()
函数,格式如下:
make([]T,size,cap)
其中:
- T:切片的元素类型
- size:切片中元素的数量
- cap:切片的容量
//使用make函数初始化
func f12() {
// make([]T,len,cap) cap省略时,cap=len
// s := make([]int,2) //len=cap=2
// s:= make([]int,2,4) // len=2;cap=4
s11 := make([]int, 2, 4)
fmt.Println(s11, len(s11), cap(s11)) // [0 0] 2 4
fmt.Println(s11 == nil) //false 只要切片初始化了,值就不是nil
s12 := make([]int, 0, 15) //一次把内存申请到位
//s12[2] = 3 //会报错,因为初始化长度为0,会出现“panic: runtime error: index out of range [2] with length 0”
fmt.Println(s12, len(s12), cap(s12))
}
//运行结果:
[0 0] 2 4
false
[] 0 15
// 如果你确定一个切片中最终要存储的元素个数,那么你最好一次把内存申请到位
切片的复制
使用内置的copy函数完成复制。
func f13() {
s13 := []int{1, 2, 3, 4, 5}
s14 := make([]int, len(s13)) //直接按目标切片的长度初始化
copy(s14, s13) //把s13的值拷贝到s14中
fmt.Println(s14) // [1 2 3 4 5]
s14[1] = 200
fmt.Println(s14) // [1 200 3 4 5]
fmt.Println(s13) //copy为值复制,改变一个切片元素的值,不会影响另一个。
fmt.Printf("s13的内存地址:%p\n", s13)
fmt.Printf("s14的内存地址:%p\n", s14) //会发现两个内存地址不一样,所以改变一个,不会影响另一个。
}
//运行 结果:
[1 2 3 4 5]
[1 200 3 4 5]
[1 2 3 4 5]
s13的内存地址:0xc000196030
s14的内存地址:0xc000196060
使用copy函数要注意,事先初始化好切片的长度
func copyDemo() {
a := []int{1, 2, 3}
// var b = make([]int, 0, len(a))
b := make([]int, 0)
copy(b, a) // 把切片a中的值拷贝到切片b中
fmt.Println(b) // 报错,因为b切片初始化的长度为0,无法增加元素
}
等号复制
下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。
//等号复制
func f14() {
s15 := []int{1, 2, 3, 4, 5}
s16 := s15 //将s15直接赋值给s16,公用一个底层数组
fmt.Println(s16)
s16[1] = 200
fmt.Println(s16) // [1 200 3 4 5]
fmt.Println(s15) // [1 200 3 4 5]
fmt.Printf("s15的内存地址:%p\n", s15)
fmt.Printf("s16的内存地址:%p\n", s16) //会发现两个内存地址一样,所以改变一个,会影响另一个。
}
//运行结果:
[1 2 3 4 5]
[1 200 3 4 5]
[1 200 3 4 5]
s15的内存地址:0xc0000160f0
s16的内存地址:0xc0000160f0
切片遍历
和遍历数组一样,支持索引遍历
和for range
遍历。
//切片遍历
func f15() {
//索引遍历
s17 := []int{1, 2, 34, 5, 6}
for i := 0; i < len(s17); i++ {
fmt.Println(s17[i])
}
fmt.Println("----------------------------------------------")
//for range循环
s18 := []int{2, 4, 6, 8}
for _, value := range s18 {
fmt.Println(value)
}
}
//运行结果:
1
2
34
5
6
----------------------------------------------
2
4
6
8
append函数
使用append函数时必须接收返回值!!!
Go语言的内建函数append()
可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。
//append函数
func f16() {
s19 := []string{"北京"}
// apend函数可能触发切片的扩容
// 切片扩容之后会有一个新的底层数组,需要更新变量s
s19 = append(s19, "上海")
fmt.Println(s19) //["北京" "上海"]
//可以增加一个切片
s20 := []string{"我", "是", "小"}
s19 = append(s19, s20...) //...表示将s20元素拆开一个一个元素增加
fmt.Println(s19) //[北京 上海 我 是 小]
//零值切片可以直接在append中使用
var s3 []int //nil
fmt.Println(s3 == nil) //true
s3 = append(s3, 1)
s3 = append(s3, 2, 3, 4)
fmt.Println(s3)
}
//运行结果:
[北京 上海]
[北京 上海 我 是 小]
true
[1 2 3 4]
append触发扩容
// appendDemo2 使用append函数触发扩容
// 导致意想不到的事情发生
func appendDemo2() {
var s = []string{"北京"}
_ = append(s, "上海", "广州", "深圳")
fmt.Println(s) // [北京]
}
// appendDemo3 append函数导致切片扩容示例
func appendDemo3() {
var s = []string{"北京"}
fmt.Println(len(s), cap(s)) // len = cap = 1
s = append(s, "上海", "广州", "深圳")
fmt.Println(len(s), cap(s)) // 4 4
fmt.Println(s) // [北京]
}
删除切片的元素
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:
// deleteSlice 删除切片中的元素
func deleteSlice(idx int) {
idx = 1
var s = []int{1, 2, 3}
s = append(s[:idx], s[idx+1:]...)
fmt.Println(s)
}
练习题
1.请写出下面代码的输出结果。
//练习题
func f17() {
var a = make([]string, 5, 10)
for i := 0; i < 10; i++ {
a = append(a, fmt.Sprintf("%v", i))
}
fmt.Println(a)
fmt.Println(len(a)) //刚开始5个空元素,执行for循环后,增加10个元素,所以是15个元素
fmt.Println(cap(a)) //超出初始化容量,触发自动扩容,具体数值和扩容策略有关
}
//运行结果:
[ 0 1 2 3 4 5 6 7 8 9] 15 20