go学习记录——第四天
今儿继续学go吧。
数组
这个一看到第一反应就是python的,感觉go就是受到了python的影响。
声明和初始化
从定义上看和python的差不多,相同的唯一类型的一组、已编号、长度固定、可通过索引来进行修改
声明格式如下
1 |
|
所有元素都会在建立时被自动初始化为0
在初始化、打印数组元素、处理元素时都可以用for
循环去做,和python一样
在循环中需要注意如果写成i <= len(arr)
,会导致数组越界,因为len(arr)
是数组的长度,而i
是索引,所以应该写成i < len(arr)
。
循环的话可以有两种写法
第一种是for i := 0; i < len(arr); i++ { }
,这种写法可以保证循环的次数是数组的长度,但是不推荐这种写法,因为不够简洁。
第二种是for i, v := range arr { }
,这种写法可以直接遍历数组的所有元素,i
是索引,v
是元素的值。
go的数组是值类型(和C/C++不同),所以在函数中修改数组元素的值,不会影响到原数组的值。所以可以通过var arr1 = new([5]int)
建立
那么既然go的数组是值类型,那就可以用new()
来创建数组,
1 |
|
这两个又有什么区别呢
首先第一种是创建了一个指针数组的指针,指向了一个在堆上分配的数组,所以arr1
是一个指针,arr1[0]
是一个指针,*arr1[0]
才是数组的第一个元素。
第二种是创建了一个数组,在栈上分配内存,arr2
是一个数组,arr2[0]
是数组的第一个元素。
从使用场景上看想在多个函数之间共享数组或需要动态分配数组大小,可以使用指针(new)。如果大小固定,且只在一个函数内使用,直接声明数组会更好
所以在想把一个数组赋给另一个数组时,需要再做一次数组内存的拷贝
1 |
|
这样两个数组就分别有了不同的值,且赋值后修改 arr2 对 arr1 不会影响
所以在函数中将数组传入参数时,会直接拷贝副本而非对数组本身进行操作。如果想修改原数组就需要使用&
引用来传数组
1 |
|
还有一种就是生成数组切片然后传递给函数
数组常量
如果数组的值已经提前知道了就可以用数组常量来初始化数组,而不是依次使用[]=
来初始化。
1 |
|
根据上面的代码示例可以看出有三种办法来初始化
var arrAge = [5]int{18, 20, 15, 22, 16}
这种方式是直接初始化数组,数组的长度和元素个数都必须一致。这种办法中我们可以在后续不填充和元素个数一样的,也就是我们只指定左侧开始的部分元素,后续编译器会把没有初始化的元素补0。[10]int {1, 2, 3}
这样便会在后续直接补0。var arrLazy = [...]int{5, 6, 7, 8, 22}
这种方式是使用...
语法来声明数组,省略了数组的长度,go会根据元素个数来推断数组的长度。忽略后从技术上来看就变成了切片。var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}
这种方式是使用索引来初始化数组,索引必须小于数组的长度,否则会报错。这里的数组长度同样可以写成...
同样我们可以取任意数组常量的地址来作为新实例的指针
1 |
|
多维数组
本质上和python差不多,就是写法上不一样,给个例子就行了
1 |
|
将数组传给函数
将数组传递给函数时如果数组较大将消耗很多内存,这时有两种方法可以解决
- 传递数组的指针
- 传递数组的切片
例子中是第一种方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package main
import "fmt"
func main() {
array := [3]float64{7.0, 8.5, 9.1}
x := Sum(&array) // Note the explicit address-of operator
// to pass a pointer to the array
fmt.Printf("The sum of the array is: %f", x)
}
func Sum(a *[3]float64) (sum float64) {
for _, v := range a { // derefencing *a to get back to the array is not necessary!
sum += v
}
return
}
切片
切片(slice)是对数组的一组连续片段的引用(该数组为匿名数组,也是相关数组),从名字上看类似于python的切片,但实际上和python的list类似而不是切片。
该片段可能是整个数组,也可能是有起始和终止索引所构成的子集片段。但索引并不包含在整个子集中。
切片还提供了相关数组的动态窗口(可以拿来做滑动窗口和DP好像,确信)
由于切片是自带索引的,所以len()
, cap()
, append()
等操作都可以直接使用。
这里需要说的是cap()
,该函数可以测量切片的计算容量最大可以为多少,等于切片的长度 + 数组切片之外的长度。
举个例子如果有一个切片s
,cap(s)
就是从索引0到len(s)
的元素个数,而len(s)
是切片的长度。切片的长度不会超过它的容量,也即0<=s<=cap(s)
。
由于切片是带有索引机制,结合python带索引的东西都是可变的我们可以推理出,切片也可能是可变的。而事实证明切片是可变的,并且是在运行过程中可以改变大小,最小为0,最大为整个数组大小。
如果多个切片引用同一个数组,则他们可以共享数据;因此一个切片和相关数组的其他切片是共享存储的,相反,不同的数组总是代表不同的存储。数组实际上是切片的构建块。
切片的优点是引用,由于不需要额外的内存且比直接使用数组更加高效,所以go中更多的使用切片。
声明和初始化
声明格式如下
1 |
|
这里不需要说明长度,切片在未初始化前长度默认为nil,长度为0
初始化格式如下
1 |
|
这里表示 slice1
是由数组 arr1
从 start
索引到 end-1
索引的切片,包括 start
,不包括 end
。
如果写var slice []type = arr[:]
,则表示 slice
是 arr
的一个切片,等价于 slice = arr[0:len(arr)]
。
arr[2:]表示从索引2开始到数组末尾的切片,arr[:3]表示从数组开头到索引3-1的切片。
如果想去掉最后一个元素,则为slice = arr[:len(arr)-1]
。
一个由数字 1、2、3 组成的切片可以这么生成:s := [3]int{1,2,3}[:](注:应先用 s := [3]int{1, 2, 3} 生成数组, 再使用 s[:] 转成切片)甚至更简单的 s := []int{1,2,3}。
s2 := s[:] 是用切片组成的切片,拥有相同的元素,但是仍然指向相同的相关数组。
由于前面提到切片的上限是 cap(s)
所以如果继续扩展将会报错。
对每个切片下面的情况是成立的
1 |
|
在前面说数组时提到我们可以不指定长度使用...
来表明他长度不固定,让编译器自己去识别。那我们忽略掉...
就可以得到切片的初始化了。var x = []int{1,2,3}
。这样就创建了一个长度为5的数字同时创建了一个相关切片。
切片在内存中实际上是一个有三个域的结构体——指向相关数组的指针、切片长度、切片容量。

从图中看到,y[0] = 3
且y[1] = 5
。切片y[0:4] = [3, 5, 7, 11]。
1 |
|
如果s2
是一个切片,我们可以使用s2[1:]
来表示将切片向后移一位,而结尾没有后移。而我们使用s2[-1:]
就会报错,因为切片不能被重新分片以获得数组的前一个元素。
由于切片本身就是引用,所以绝不可以再使用一个指针去引用索引。
将切片传递给函数
如果一个函数需要调用数组,则我们可以创建一个切片并引用传递函数。
1 |
|
用make()创建一个切片
当相关数组还没被定义时可以使用make()
来创建一个切片,同时创建好相关数组var slice1 []type = make([]type, len)
。也可以简写为slice1 := make([]type, len)
,这里的len
表示切片的长度也是slice
的初始长度。
举个例子s2 := make([]int, 5)
表示创建一个长度为5的切片,初始值都是0。那么cap(s2) == len(s2) == 5
。
make()
可以接受两个参数,元素类型和切片的元素个数。
如果创建一个slice1
且不想占用整个数组,而是占用以len
为个数,那么只要slice1 = make([]type, len, cap)
就可以了。
make()
的使用方法是func make([]T, len, cap)
,其中len
表示切片的长度,cap
表示切片的容量。cap
为可选。
下面两种方法可以创建相同的切片
1 |
|

1 |
|
new()和make()的区别
make()
是用来创建数组的,new()
是用来创建指针的。
new(T)
为每个新的类型T
分配一片内存,初始化为0
并返回类型为*T
的内存地址。所以这种方法返回的是 一个指向类型为T
值为0
的地址的指针 ,适用于值类型如数组和结构体。
make(T)
返回一个类型为T
的初始值 ,适用于三种类型,切片、map
、channel
。
也就是说new()
用来分配内存,make()
用来初始化。
总结一下就是,由于slice
、map
、channel
都是引用类型,三者都存在对于内存中存在多个组成部分,需要对内存进行初始化后才可以使用,这里就需要make()
。而new()
是直接获取一个地址,不进行初始化。所以需要使用make()
来初始化并获取地址,而非简单使用new()
获取地址。

第一幅图
1 |
|
第二幅图
1 |
|
在第二幅图中,实际上切片已经被初始化,但指向了一个空指针。
上面方法实际上并不实用,更常见的是以下两种
1 |
|
这样分配了有50个int
值的数组,并且创建了一个长度为10,容量为50的切片v
,该切片指向了前10个元素。
多维切片
多维切片和多维数组类似,也是可以由多个一维切片组成,且长度可变。这里也需要用到make()
对内层切片进行单独分配。
1 |
|
这里发现go和python通过循环对多维数组进行操作有点类似,下面进行对比
1 |
|
1 |
|