go学习记录——第四天

今儿继续学go吧。

数组

这个一看到第一反应就是python的,感觉go就是受到了python的影响。

声明和初始化

从定义上看和python的差不多,相同的唯一类型的一组、已编号、长度固定、可通过索引来进行修改

声明格式如下

1
var identifier [len]type

所有元素都会在建立时被自动初始化为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
2
3
var arr1 = new([5]int)

var arr2 [5]int

这两个又有什么区别呢

首先第一种是创建了一个指针数组的指针,指向了一个在堆上分配的数组,所以arr1是一个指针,arr1[0]是一个指针,*arr1[0]才是数组的第一个元素。

第二种是创建了一个数组,在栈上分配内存,arr2是一个数组,arr2[0]是数组的第一个元素。

从使用场景上看想在多个函数之间共享数组或需要动态分配数组大小,可以使用指针(new)。如果大小固定,且只在一个函数内使用,直接声明数组会更好

所以在想把一个数组赋给另一个数组时,需要再做一次数组内存的拷贝

1
2
arr2 := *arr1
arr2[2] = 100

这样两个数组就分别有了不同的值,且赋值后修改 arr2 对 arr1 不会影响

所以在函数中将数组传入参数时,会直接拷贝副本而非对数组本身进行操作。如果想修改原数组就需要使用&引用来传数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
func f(a [3]int) { fmt.Println(a) }
func fp(a *[3]int) { fmt.Println(a) }

func main() {
var ar [3]int
f(ar) // passes a copy of ar
fp(&ar) // passes a pointer to ar
}
/*output
[0 0 0]
&[0 0 0]
*/

还有一种就是生成数组切片然后传递给函数

数组常量

如果数组的值已经提前知道了就可以用数组常量来初始化数组,而不是依次使用[]=来初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

func main() {
var arrAge = [5]int{18, 20, 15, 22, 16}
var arrLazy = [...]int{5, 6, 7, 8, 22}
// var arrLazy = []int{5, 6, 7, 8, 22}
var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}
//var arrKeyValue = []string{3: "Chris", 4: "Ron"}
for i := 0; i < len(arrAge); i++ {
fmt.Printf("Age at %d is %d\n", i, arrAge[i])
}
fmt.Println()

for i := 0; i < len(arrLazy); i++ {
fmt.Printf("Number at %d is %d\n", i, arrLazy[i])
}
fmt.Printf("\n")

for i := 0; i < len(arrKeyValue); i++ {
fmt.Printf("Person at %d is %s\n", i, arrKeyValue[i])
}
}

根据上面的代码示例可以看出有三种办法来初始化

  1. var arrAge = [5]int{18, 20, 15, 22, 16}这种方式是直接初始化数组,数组的长度和元素个数都必须一致。这种办法中我们可以在后续不填充和元素个数一样的,也就是我们只指定左侧开始的部分元素,后续编译器会把没有初始化的元素补0。[10]int {1, 2, 3}这样便会在后续直接补0。

  2. var arrLazy = [...]int{5, 6, 7, 8, 22}这种方式是使用...语法来声明数组,省略了数组的长度,go会根据元素个数来推断数组的长度。忽略后从技术上来看就变成了切片。

  3. var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}这种方式是使用索引来初始化数组,索引必须小于数组的长度,否则会报错。这里的数组长度同样可以写成...

同样我们可以取任意数组常量的地址来作为新实例的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"

func fp(a *[3]int) { fmt.Println(a) }

func main() {
for i := 0; i < 3; i++ {
fp(&[3]int{i, i * i, i * i * i})
}
}
/*output
&[0 0 0]
&[1 1 1]
&[2 4 8]
*/

多维数组

本质上和python差不多,就是写法上不一样,给个例子就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
const (
WIDTH = 1920
HEIGHT = 1080
)

type pixel int
var screen [WIDTH][HEIGHT]pixel

func main() {
for y := 0; y < HEIGHT; y++ {
for x := 0; x < WIDTH; x++ {
screen[x][y] = 0
}
}
}

将数组传给函数

将数组传递给函数时如果数组较大将消耗很多内存,这时有两种方法可以解决

  • 传递数组的指针
  • 传递数组的切片
    例子中是第一种方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package 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(),该函数可以测量切片的计算容量最大可以为多少,等于切片的长度 + 数组切片之外的长度。

举个例子如果有一个切片scap(s)就是从索引0到len(s)的元素个数,而len(s)是切片的长度。切片的长度不会超过它的容量,也即0<=s<=cap(s)

由于切片是带有索引机制,结合python带索引的东西都是可变的我们可以推理出,切片也可能是可变的。而事实证明切片是可变的,并且是在运行过程中可以改变大小,最小为0,最大为整个数组大小。

如果多个切片引用同一个数组,则他们可以共享数据;因此一个切片和相关数组的其他切片是共享存储的,相反,不同的数组总是代表不同的存储。数组实际上是切片的构建块。

切片的优点是引用,由于不需要额外的内存且比直接使用数组更加高效,所以go中更多的使用切片。

声明和初始化

声明格式如下

1
var identifier []type

这里不需要说明长度,切片在未初始化前长度默认为nil,长度为0

初始化格式如下

1
var slice1 []type = arr1[start:end]

这里表示 slice1 是由数组 arr1start 索引到 end-1 索引的切片,包括 start ,不包括 end

如果写var slice []type = arr[:],则表示 slicearr 的一个切片,等价于 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
2
s == s[:i] + s[i:] // i是一个整数且: 0 <= i <= len(s)
len(s) <= cap(s)

在前面说数组时提到我们可以不指定长度使用...来表明他长度不固定,让编译器自己去识别。那我们忽略掉...就可以得到切片的初始化了。var x = []int{1,2,3}。这样就创建了一个长度为5的数字同时创建了一个相关切片。

切片在内存中实际上是一个有三个域的结构体——指向相关数组的指针、切片长度、切片容量。

7.2 Silence in Memory

从图中看到,y[0] = 3y[1] = 5。切片y[0:4] = [3, 5, 7, 11]。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import "fmt"

func main() {
var arr1 [6]int
var slice1 []int = arr1[2:5] // index 5 niet meegerekend!

// load the array with integers: 0,1,2,3,4,5
for i := 0; i < len(arr1); i++ {
arr1[i] = i
}

// print the slice:
for i := 0; i < len(slice1); i++ {
fmt.Printf("Slice at %d is %d\n", i, slice1[i])
}

fmt.Printf("The length of arr1 is %d\n", len(arr1))
fmt.Printf("The length of slice1 is %d\n", len(slice1))
fmt.Printf("The capacity of slice1 is %d\n", cap(slice1))

// grow the slice:
slice1 = slice1[0:4]
for i := 0; i < len(slice1); i++ {
fmt.Printf("Slice at %d is %d\n", i, slice1[i])
}
fmt.Printf("The length of slice1 is %d\n", len(slice1))
fmt.Printf("The capacity of slice1 is %d\n", cap(slice1))

// grow the slice beyond capacity:
// slice1 = slice1[0:7 ] // panic: runtime error: slice bounds out of range
}
/*output
Slice at 0 is 2
Slice at 1 is 3
Slice at 2 is 4
Slice at 3 is 5
The length of arr1 is 6
The length of slice1 is 4
The capacity of slice1 is 6
Slice at 0 is 2
Slice at 1 is 3
Slice at 2 is 4
Slice at 3 is 5
The length of slice1 is 4
The capacity of slice1 is 6
*/

如果s2是一个切片,我们可以使用s2[1:]来表示将切片向后移一位,而结尾没有后移。而我们使用s2[-1:]就会报错,因为切片不能被重新分片以获得数组的前一个元素。

由于切片本身就是引用,所以绝不可以再使用一个指针去引用索引。

将切片传递给函数

如果一个函数需要调用数组,则我们可以创建一个切片并引用传递函数。

1
2
3
4
5
6
7
8
9
10
11
12
func sum(a []int) int {
s := 0
for i := 0; i < len(a); i++ {
s += a[i]
}
return s
}

func main() {
var arr = [5]int{0, 1, 2, 3, 4}
sum(arr[:])
}

用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
2
make([]int, 50, 100)
new([100]int)[0:50]
使用make()生成切片的内存结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main
import "fmt"

func main() {
var slice1 []int = make([]int, 10)
// load the array/slice:
for i := 0; i < len(slice1); i++ {
slice1[i] = 5 * i
}

// print the slice:
for i := 0; i < len(slice1); i++ {
fmt.Printf("Slice at %d is %d\n", i, slice1[i])
}
fmt.Printf("\nThe length of slice1 is %d\n", len(slice1))
fmt.Printf("The capacity of slice1 is %d\n", cap(slice1))
}
/*output
Slice at 0 is 0
Slice at 1 is 5
Slice at 2 is 10
Slice at 3 is 15
Slice at 4 is 20
Slice at 5 is 25
Slice at 6 is 30
Slice at 7 is 35
Slice at 8 is 40
Slice at 9 is 45

The length of slice1 is 10
The capacity of slice1 is 10
*/

new()和make()的区别

make()是用来创建数组的,new()是用来创建指针的。

new(T)为每个新的类型T分配一片内存,初始化为0并返回类型为*T的内存地址。所以这种方法返回的是 一个指向类型为T值为0的地址的指针 ,适用于值类型如数组和结构体。

make(T) 返回一个类型为T的初始值 ,适用于三种类型,切片、mapchannel

也就是说new()用来分配内存,make()用来初始化。

总结一下就是,由于slicemapchannel都是引用类型,三者都存在对于内存中存在多个组成部分,需要对内存进行初始化后才可以使用,这里就需要make()。而new()是直接获取一个地址,不进行初始化。所以需要使用make()来初始化并获取地址,而非简单使用new()获取地址。

new()和make()的不同

第一幅图

1
2
var p *[]int = new([]int) // *p == nil; with len and cap 0
p := new([]int)

第二幅图

1
p := make([]int, 0)

在第二幅图中,实际上切片已经被初始化,但指向了一个空指针。

上面方法实际上并不实用,更常见的是以下两种

1
2
3
4
5
var v []int = make([]int, 10, 50)



v := make([]int, 10, 50)

这样分配了有50个int值的数组,并且创建了一个长度为10,容量为50的切片v,该切片指向了前10个元素。

多维切片

多维切片和多维数组类似,也是可以由多个一维切片组成,且长度可变。这里也需要用到make()对内层切片进行单独分配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

func main() {
// 创建一个 2x3 的二维切片
matrix := [][]int{
{1, 2, 3},
{4, 5, 6},
}

// 打印二维切片
for i := 0; i < len(matrix); i++ {
for j := 0; j < len(matrix[i]); j++ {
fmt.Printf("%d ", matrix[i][j])
}
fmt.Println() // 换行
}

// 修改某个元素
matrix[1][2] = 10 // 将第二行第三列的值改为 10

// 再次打印二维切片
fmt.Println("修改后的二维切片:")
for _, row := range matrix {
fmt.Println(row)
}
}

这里发现go和python通过循环对多维数组进行操作有点类似,下面进行对比

1
2
3
4
5
6
for i := 0; i < len(matrix); i++ {
for j := 0; j < len(matrix[i]); j++ {
fmt.Printf("%d ", matrix[i][j])
}
fmt.Println()
}
1
2
3
4
5
6
7
8
9
matrix = [
[1, 2, 3],
[4, 5, 6]
]

for i in range(len(matrix)):
for j in range(len(matrix[i])):
print(matrix[i][j], end=' ')
print()

go学习记录——第四天
https://www.lx02918.ltd/2024/08/12/go-study-fourth-day/
作者
Seth
发布于
2024年8月12日
许可协议