数组
数组介绍
数组可以存放多个同一类型数据。数组也是一种数据类型,在 Go 中,数组是值类型。
数组的快速入门
//使用数组的方式来解决问题
//1.定义一个数组
var hens [7]float64
//2.给数组的每个元素赋值, 元素的下标是从0开始的 0-5
hens[0] = 3.0 //hens数组的第一个元素 hens[0]
hens[1] = 5.0 //hens数组的第2个元素 hens[1]
hens[2] = 1.0
hens[3] = 3.4
hens[4] = 2.0
hens[5] = 50.0
hens[6] = 150.0 //增加一只鸡
//3.遍历数组求出总体重
totalWeight2 := 0.0
for i := 0; i < len(hens); i++ {
totalWeight2 += hens[i]
}
//4.求出平均体重
avgWeight2 := fmt.Sprintf("%.2f", totalWeight2 / float64(len(hens)))
fmt.Printf("totalWeight2=%v avgWeight2=%v", totalWeight2, avgWeight2)
数组定义和内存布局
数组的定义
var 数组名 [数组大小]数据类型
var a [5]int
//赋初值
a[0] = 1 a[1] = 30 ....
💡数组在内存布局
var intArr [3]int //int占8个字节
//当我们定义完数组后,其实数组的各个元素有默认值 0
fmt.Println(intArr)
intArr[0] = 10
intArr[1] = 20
intArr[2] = 30
fmt.Println(intArr)
fmt.Printf("intArr的地址=%p intArr[0] 地址%p intArr[1] 地址%p intArr[2] 地址%p\n",
&intArr, &intArr[0], &intArr[1], &intArr[2])
输出
[0 0 0]
[10 20 30]
intArr的地址=0xc000014168 intArr[0] 地址0xc000014168 intArr[1] 地址0xc000014170 intArr[2] 地址0xc000014178
对上图的说明
- 数组的地址可以通过数组名来获取
&intArr
- 数组的第一个元素的地址,就是数组的首地址
数组的各个元素的地址间隔是依据数组的类型决定,比如 int64 -> 8字节、int32->4字节…
数组的使用
访问数组元素
数组名[下标] 比如:你要使用 a 数组的第三个元素 a[2]//从终端循环输入5个成绩,保存到float64数组,并输出. var score [5]float64 for i := 0; i < len(score); i++ { fmt.Printf("请输入第%d个元素的值\n", i+1) fmt.Scanln(&score[i]) } //变量数组打印 for i := 0; i < len(score); i++ { fmt.Printf("score[%d]=%v\n", i, score[i]) }
几种初始化数组的方式
var numArr01 [3]int = [3]int{1, 2, 3} fmt.Println("numArr01=", numArr01) var numArr02 = [3]int{5, 6, 7} fmt.Println("numArr02=", numArr02) //这里的 [...] 是规定的写法 var numArr03 = [...]int{8, 9, 10} fmt.Println("numArr03=", numArr03) var numArr04 = [...]int{1: 800, 0: 900, 2:999} fmt.Println("numArr04=", numArr04) //这里的 numArr05 是一个切片 var numArr05 = []int{11,22,33} fmt.Println("numArr05=", numArr05) //类型推导 strArr05 := [...]string{1: "tom", 0: "jack", 2:"mary"} fmt.Println("strArr05=", strArr05)
输出
numArr01= [1 2 3] numArr02= [5 6 7] numArr03= [8 9 10] numArr04= [900 800 999] numArr05= [11 22 33] strArr05= [jack tom mary]
数组的遍历
方式 1-常规遍历
score := []int{1,2,3} for i := 0; i < len(score); i++ { fmt.Printf("score[%d]=%v\n", i, score[i]) }
方式 2-for-range 结构遍历
//● 第一个返回值index是数组下标 //● 第二个value是在该下标位置的值 //● 他们都是仅在for循环内部可见的局部变量 //● 遍历数组元素的时候,如果不想使用下标index,可以直接把下标index标为下划线_ //● index和value的名称不是固定的,即程序员可以自行指定,一般命名为index和value for index,value := range array{ ... }
for-range 的案例
//演示for-range遍历数组 heroes := [...]string{"宋江", "吴用", "卢俊义"} for i, v := range heroes { fmt.Printf("i=%v v=%v\n", i , v) }
输出
i=0 v=宋江 i=1 v=吴用 i=2 v=卢俊义
数组使用的注意事项和细节
数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的, 不能动态变化
var arr []int
这时 arr 就是一个 slice 切片- 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用
数组创建后,如果没有赋值,有默认值(零值)
//数组创建后,如果没有赋值,有默认值(零值) //1. 数值(整数系列, 浮点数系列) =>0 //2. 字符串 ==> "" //3. 布尔类型 ==> false var arr01 [3]float32 var arr02 [3]string var arr03 [3]bool fmt.Printf("arr01=%v arr02=%v arr03=%v\n", arr01, arr02, arr03)//arr01=[0 0 0] arr02=[ ] arr03=[false false false]
使用数组的步骤
- 声明数组并开辟空间
- 给数组各个元素赋值(默认零值)
- 使用数组
- 数组的下标是从 0 开始的
- 数组下标必须在指定范围内使用,否则报 panic:数组越界,比如
var arr [5]int
则有效下标为 0-4 - Go 的数组属值类型, 在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响 ```go //值传递方式 func test01(arr [3]int) { arr[0] = 88 fmt.Printf(“test01 arr地址=%p\n”, &arr) fmt.Println(“test01 arr=”, arr) }
func main() { //Go的数组属值类型, 在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响 arr := [3]int{11, 22, 33} test01(arr) fmt.Printf(“main arr地址=%p\n”, &arr) fmt.Println(“main arr=”, arr) }
输出
```go
test01 arr地址=0xc0000ae090
test01 arr= [88 22 33]
main arr地址=0xc0000ae078
main arr= [11 22 33]
- 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式) ```go //引用传递方式 func test02(arr [3]int) { arr[0] = 88 fmt.Printf(“test02 arr地址=%p\n”, &arr) fmt.Printf(“test02 arr指针指向地址=%p\n”, &(arr)) fmt.Println(“test02 arr=”, *arr) }
func main() { arr := [3]int{11, 22, 33} test02(&arr) fmt.Printf(“main arr地址=%p\n”, &arr) fmt.Println(“main arr=”, arr) }
输出
```go
test02 arr地址=0xc000006030
test02 arr指针指向地址=0xc000014168
test02 arr= [88 22 33]
main arr地址=0xc000014168
main arr= [88 22 33]
长度是数组类型的一部分,在传递函数参数时 需要考虑数组的长度
二维数组
使用方式 1: 先声明/定义,再赋值
语法:
var 数组名 [大小][大小]类型
比如:var arr [2][3]int
, 再赋值。var arr2 [2][3]int //以这个为例来分析arr2在内存的布局!! arr2[1][1] = 10 fmt.Println(arr2) fmt.Printf("arr2[0]的地址%p\n", &arr2[0]) fmt.Printf("arr2[1]的地址%p\n", &arr2[1]) fmt.Printf("arr2[0][0]的地址%p\n", &arr2[0][0]) fmt.Printf("arr2[1][0]的地址%p\n", &arr2[1][0])
输出
[[0 0 0] [0 10 0]] arr2[0]的地址0xc0000c0060 arr2[1]的地址0xc0000c0078 arr2[0][0]的地址0xc0000c0060 arr2[1][0]的地址0xc0000c0078
使用方式 2: 直接初始化
声明:
var 数组名 [大小][大小]类型 = [大小][大小]类型{{初值..},{初值..}}
arr3 := [2][3]int{{1,2,3}, {4,5,6}} fmt.Println("arr3=", arr3)
二维数组在声明/定义时也对应有四种写法[和一维数组类似]
- var 数组名 [大小][大小]类型 = [大小][大小]类型{{初值..},{初值..}}
- var 数组名 [大小][大小]类型 = […][大小]类型{{初值..},{初值..}}
- var 数组名 = [大小][大小]类型{{初值..},{初值..}}
var 数组名 = […][大小]类型{{初值..},{初值..}}
二维数组的遍历
双层 for 循环完成遍历
for-range 方式完成遍历func main() { //演示二维数组的遍历 var arr3 = [2][3]int{{1,2,3}, {4,5,6}} //for循环来遍历 for i := 0; i < len(arr3); i++ { for j := 0; j < len(arr3[i]); j++ { fmt.Printf("%v\t", arr3[i][j]) } fmt.Println() } //for-range来遍历二维数组 for i, v := range arr3 { for j, v2 := range v { fmt.Printf("arr3[%v][%v]=%v \t",i, j, v2) } fmt.Println() } }
输出
1 2 3 4 5 6 arr3[0][0]=1 arr3[0][1]=2 arr3[0][2]=3 arr3[1][0]=4 arr3[1][1]=5 arr3[1][2]=6
切片
切片的基本介绍
- 切片的英文是 slice
- 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。
- 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度 len(slice)都一样。
- 切片的长度是可以变化的,因此切片是一个可以动态变化数组。
切片定义的基本语法:
var 切片名 []类型
比如:var a [] int
快速入门
func main() { //演示切片的基本使用 var intArr [5]int = [...]int{11, 22, 33, 44, 55} //声明/定义一个切片 //slice := intArr[1:3] //1. slice 就是切片名 //2. intArr[1:3] 表示 slice 引用到intArr这个数组 //3. 引用intArr数组的起始下标为 1 , 最后的下标为3(但是不包含3) slice := intArr[1:3] fmt.Println("intArr=", intArr) fmt.Println("slice 的元素是 =", slice) // 22, 33 fmt.Println("slice 的元素个数 =", len(slice)) // 2 fmt.Println("slice 的最大容量 =", cap(slice)) // 切片的容量是可以动态变化 一般len<=cap }
输出
intArr= [11 22 33 44 55] slice 的元素是 = [22 33] slice 的元素个数 = 2 slice 的最大容量 = 4
💡切片在内存布局
func main() { var intArr [5]int = [...]int{11, 22, 33, 44, 55} slice := intArr[1:3] fmt.Printf("slice的类型=%T intArr的类型=%T\n", slice, intArr) fmt.Printf("intArr[0]的地址=%p intArr[1]的地址=%p\n", &intArr[0], &intArr[1]) fmt.Printf("slice[0]的地址=%p slice[0==%v]\n", &slice[0], slice[0]) slice[0] = 66//修改元素值 fmt.Println("intArr=", intArr) fmt.Println("slice 的元素是 =", slice) }
输出
slice的类型=[]int intArr的类型=[5]int intArr[0]的地址=0xc0000c0060 intArr[1]的地址=0xc0000c0068 slice[0]的地址=0xc0000c0068 slice[0==22] intArr= [11 66 33 44 55] slice 的元素是 = [66 33]
对应的内存模型
对上图的几点说明
- slice 的确是一个引用类型
- slice 从底层来说,其实就是一个数据结构(struct 结构体)
// src/runtime/slice.go type slice struct { array unsafe.Pointer // 元素指针 len int // 长度 cap int // 容量 }
切片的使用
- 第一种方式:定义一个切片,然后让切片去引用一个已经创建好的数组,比如前面的案例就是这样的。
- 第二种方式:通过 make 来创建切片
- 基本语法:
var 切片名 []type = make([]type, len, [cap])
- 参数说明: type: 就是数据类型 len : 大小 cap :指定切片容量,可选, 如果你分配了 cap,则要求 cap>=len.
输出//演示切片的使用 make var slice []float64 = make([]float64, 5, 10) slice[1] = 10 slice[3] = 20 //对于切片,必须make使用. fmt.Println(slice) fmt.Println("slice的size=", len(slice)) fmt.Println("slice的cap=", cap(slice))
对上面代码的小结:[0 10 0 20 0] slice的size= 5 slice的cap= 10
- 基本语法:
- 通过 make 方式创建切片可以指定切片的大小和容量
- 如果没有给切片的各个元素赋值,那么就会使用默认值
- 通过 make 方式创建的切片对应的数组是由 make 底层维护,对外不可见,即只能通过 slice 去访问各个元素
第 3 种方式:定义一个切片,直接就指定具体数组,使用原理类似 make 的方式
//第3种方式:定义一个切片,直接就指定具体数组,使用原理类似make的方式 var strSlice []string = []string{"tom", "jack", "mary"} fmt.Println("strSlice=", strSlice) fmt.Println("strSlice size=", len(strSlice)) fmt.Println("strSlice cap=", cap(strSlice))
输出
strSlice= [tom jack mary] strSlice size= 3 strSlice cap= 3
切片的遍历
切片的遍历和数组一样,也有两种方式
for 循环常规方式遍历
for-range 结构遍历切片
func main() { //使用常规的for循环遍历切片 var arr [5]int = [...]int{10, 20, 30, 40, 50} slice := arr[1:4] // 20, 30, 40 for i := 0; i < len(slice); i++ { fmt.Printf("slice[%v]=%v ", i, slice[i]) } fmt.Println() //使用for--range 方式遍历切片 for i, v := range slice { fmt.Printf("i=%v v=%v \n", i, v) } }
切片的使用的注意事项和细节讨论
切片初始化时
var slice = arr[startIndex:endIndex]
说明:从 arr 数组下标为 startIndex,取到 下标为 endIndex 的元素(不含 arr[endIndex])- 切片初始化时,仍然不能越界。范围在 [0-len(arr)] 之间,但是可以动态增长.
var slice = arr[0:end]
可以简写var slice = arr[:end]
var slice = arr[start:len(arr)]
可以简写:var slice = arr[start:]
var slice = arr[0:len(arr)]
可以简写:var slice = arr[:]
- cap 是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素
- 切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者 make 一个空间供切片来使用
切片可以继续切片 ```go func main() {
//使用常规的for循环遍历切片 var arr [5]int = […]int{10, 20, 30, 40, 50} slice := arr[1:4] // 20, 30, 40 slice2 := slice[1:2] // slice [ 20, 30, 40] [30] slice2[0] = 100 // 因为arr , slice 和slice2 指向的数据空间是同一个,因此slice2[0]=100,其它的都变化
fmt.Println(“slice2=”, slice2) fmt.Println(“slice=”, slice) fmt.Println(“arr=”, arr)
}
输出
```go
slice2= [100]
slice= [20 100 40]
arr= [10 20 100 40 50]
用 append 内置函数,可以对切片进行动态追加
//用append内置函数,可以对切片进行动态追加 var slice3 []int = []int{100, 200, 300} //通过append直接给slice3追加具体的元素 slice3 = append(slice3, 400, 500, 600) fmt.Println("slice3", slice3) //100, 200, 300,400, 500, 600 //通过append将切片slice3追加给slice3 //slice3... 表示将strss3的元素打散一个个append进slice3中 slice3 = append(slice3, slice3...) // 100, 200, 300,400, 500, 600 100, 200, 300,400, 500, 600 fmt.Println("slice3", slice3)
输出
slice3 [100 200 300 400 500 600] slice3 [100 200 300 400 500 600 100 200 300 400 500 600]
💡切片 append 操作的底层原理分析
- 切片 append 操作的本质就是对数组扩容
- go 底层会创建一下新的数组 newArr(安装扩容后大小)
- 将 slice 原来包含的元素拷贝到新的数组 newArr
- slice 重新引用到 newArr
- 注意 newArr 是在底层来维护的,程序员不可见.
- 切片的拷贝操作
对上面代码的说明://切片使用copy内置函数完成拷贝,举例说明 var slice4 []int = []int{1, 2, 3, 4, 5} var slice5 = make([]int, 10) copy(slice5, slice4) fmt.Println("slice4=", slice4)// 1, 2, 3, 4, 5 fmt.Println("slice5=", slice5) // 1, 2, 3, 4, 5, 0 , 0 ,0,0,0
- copy(para1, para2) 参数的数据类型是切片
- 按照上面的代码来看, slice4 和 slice5 的数据空间是独立,相互不影响,也就是说 slice4[0]= 999,slice5[0] 仍然是 1
关于拷贝的注意事项
var a []int = []int{1,2,3} var slice = make([]int,1) fmt.Println(slice)//[0] //不会报错 可以运行, 最后输出的是 [1] copy(slice,a) fmt.Println(slice)//[1]
切片是引用类型,所以在传递时,遵守引用传递机制。 ```go
func test(slice []int) { slice[0] = 100 //这里修改slice[0],会改变实参 }
func main() { var slice = []int {1, 2, 3, 4} fmt.Println(“slice=”, slice) // [1,2,3,4] test(slice) fmt.Println(“slice=”, slice) // [100, 2, 3, 4] }
<a name="yXrpS"></a>
## string 和 slice
1. string 底层是一个 byte 数组,因此 string 也可以进行切片处理
```go
//string底层是一个byte数组,因此string也可以进行切片处理
str := "hello@abc"
//使用切片获取到 abc
slice := str[6:]
fmt.Println("slice=", slice)//slice= abc
- string 和切片在内存的形式,以 “abcd” 画出内存示意图
string 是不可变的,也就说不能通过
str[0] = 'z'
方式来修改字符串str := "hello@abc" str[0] = 'z' //[编译不会通过,报错,原因是string是不可变]
如果需要修改字符串,可以先将 string -> []byte / 或者 []rune -> 修改 -> 重写转成 string
//如果需要修改字符串,可以先将string -> []byte / 或者 []rune -> 修改 -> 重写转成string // "hello@abc" =>改成 "zello@abc" str := "hello@abc" arr1 := []byte(str) arr1[0] = 'z' str = string(arr1) fmt.Println("str=", str)//str= zello@abc // 细节,我们转成[]byte后,可以处理英文和数字,但是不能处理中文 // 原因是 []byte 字节来处理 ,而一个汉字,是3个字节,因此就会出现乱码 // 解决方法是 将 string 转成 []rune 即可, 因为 []rune是按字符处理,兼容汉字 arr2 := []rune(str) arr2[0] = '中' str = string(arr2) fmt.Println("str=", str)//str= 中ello@abc