Scott's Blog

学则不固, 知则不惑

0%

Go 语法概览 P2

这是 Go 语言之旅的笔记 P2, 包括了指针、结构体、数组、切片、映射和闭包等内容。

指针

指针, 指针保存了值的地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 指针声明
// *int 是指向 int 类型的指针,其初始值(零值)为 nil
var p *int


// & 操作符会生成一个指向其操作数的指针
// 此处生成一个指向整数 42 的指针,赋值给 p
i := 42
p = &i


// 读取指针指向的值,操作也是 *
fmt.Print(*p) // 通过指针读取值
*p = 52 // 通过指针设置值

结构体

结构体 struct,即一组字段 field:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义
type Vertex struct {
X int
Y int
}

// 使用
v = Vertex{1, 2} // 初始化赋值
v.X = 4 // 结构内元素赋值
fmt.Print(v.X) // 取值
fmt.Print(v.Y)

// 结构体的特殊文法:
var (
v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
v2 = Vertex{X: 1} // Y:0 被隐式地赋予
v3 = Vertex{} // X:0 Y:0
p = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)

这里我们第一次看到了 type,可以点击这篇文章了解更多。

使用指针访问结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 访问结构体,需要结构提指针
// 还是以上面定义的结构体 Vertex 为例
v := Vertex{1, 2}


// 创建结构体指针 p
// 赋值,本质是 (*p).X,为了避免繁琐,简化了操作(隐式间接调用)
p := &v
p.X = 1e9
fmt.Println(v)

// 创建结构体的多种方式
var s1 Vertex{}
var s2 Vertex
var s3 *Vertex = new(Vertex)

注意:结构体内部的变量名的大小写,会影响外部访问结构体内部的权限。

结构体占用内存的大小是如何计算的?在 Go 中可以使用 sizeof 函数查看对象占用的对象类型。

1
2
3
4
// 占用 8 字节
unsafe.Sizeof(x: 1)
// 不管字符串多长,都是 16,为什么?
unsafe.Sizeof(x: "scottzhang.pro")

其实 go 语言中的 string 是一个结构体:

1
2
3
4
5
type string struct
{
Data uintptr // 指针,占用8个字节
Len int //长度,64位占用8个字节
}

甚至 slice 也是一个结构体,类似这样:

1
2
3
4
5
type slice struct {
array unsafe.Pointer // 底层数组的地址 8
len int // 长度 8
cap int // 容量 8
} // 永远都是 = 24

数组

1
2
3
4
5
6
7
8
9
10
11
12
13
// 声明形式 [n]T
// 数组的长度是其类型的一部分,因此数组不能改变大小
var a [10]int


// 访问内容的文法与其它语言一致
var a [2]string
a[0] = "Hello"
a[1] = "Scott"

// 声明的同时设置初始值
primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)

切片

切片为数组提供动态大小, 在实践中,切片比数组更常用:

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
// 切片与 Python Pandas 中 iloc 的语法类似
// 语法为 []T, 包括第一个元素,排除最后一个元素
a[low:high]

// 选择下标1-3的元素
a[1:4]

// 一个例子
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]

// 切片只是提供一个视角,所以它并不存储数据
// 但是对切片的修改,会修改底层数据
names := [2]string{
"Scott",
"Sam",
}
names[0] = "XXX"
fmt.Println(names)

// 切片的文法与没有写数组长度的类似
[3]bool{true, true, false} // 数组文法
[]bool{true, true, false} // 切片文法


// 与 numpy, pandas 类似,取同样的值有多种写法
var a [10]int

a[:]
a[0:]
a[:10]
a[0:10]

// 切片本身有属性,如长度和容量
// 长度:包含的元素个数
// 容量: 从它的第一个元素开始数,到其底层数组元素末尾的个数(注意区别 numpy)
// 长度和容量可通过表达式 len(s) 和 cap(s) 来获取
s := []int{2, 3, 5, 7, 11, 13}
// 截取切片使其长度为 0
s = s[:0]
// 拓展其长度
s = s[:4]
// 舍弃前两个值
s = s[2:]

//nil 切片
// 切片的零值是 nil
// nil 切片的长度和容量为 0 且没有底层数组

使用 make 创建切片, make 函数会分配一个元素为零值的数组并返回一个引用了它的切片, 这也是在 Go 中使用动态数组的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用 make
a := make([]int, 5)
b := make([]int, 0, 5)
c := b[:2]
d := c[2:5]

/*
分别打印 abcd
a len=5 cap=5 [0 0 0 0 0]
b len=0 cap=5 []
c len=2 cap=5 [0 0]
d len=3 cap=3 [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
// 类似操作 dataframe
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}
/*
_ _ _
_ _ _
_ _ _

*/

// 通过两个下标设置值后得到改变后的图案
board[0][0] = "X"
board[2][2] = "O"
board[1][2] = "X"
board[1][0] = "O"
board[0][2] = "X"
/*
X _ X
O _ X
_ _ O
*/

向切片追加元素:

1
2
3
4
5
6
7
8
9
10
var s []int

// 添加一个空切片
s = append(s, 0)

// 这个切片会按需增长
s = append(s, 1)

// 可以一次性添加多个元素
s = append(s, 2, 3, 4)

for 循环的 range 形式, 类似于 Python 中的 enumrate,但它是一个关键字而不是函数:

映射

1
2
3
4
5
6
7
8
9
10
11
12
13
// 每次迭代都会返回两个值
// 第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
for i, v := range pow {
fmt.Printf(i, v)
}

// 如果你想忽略某个值,可以使用 _
for i, _ := range pow
for _, value := range pow

// 如果你只需要索引
for i := range pow

映射,类似 Python 中的字典,但需要规定 Key 和 Value 的类型,假设值是 struct,则需提前定义:

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
// 声明结构体,用来存值
type Vertex struct {
Lat, Long float64
}

// 声明映射,键是 string,值是 struct
// 映射的零值为 nil 。nil 映射既没有键,也不能添加键。
var m map[string]Vertex

// 初始化结构体
// make 函数会返回给定类型的映射,并将其初始化备用
m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}

// 不使用 make 函数
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}

// 结构体内部,Vertex 可以省略
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}

获取、编辑映射内部的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
m := make(map[string]int)

// 获取值
m["Answer"] = 42
fmt.Println("The value:", m["Answer"])

// 设置值
m["Answer"] = 48
fmt.Println("The value:", m["Answer"])

// 删除值
delete(m, "Answer")
fmt.Println("The value:", m["Answer"])

// 双赋值检测, elem, ok = m[key]
// 若 key 在 m 中,ok 为 true ;否则,ok 为 false。
// 若 key 不在映射中,那么 elem 是该映射元素类型的零值。
// 同样的,当从映射中读取某个不存在的键时,结果是映射的元素类型的零值。
// 若 elem 或 ok 还未声明,你可以使用短变量声明:elem, ok := m[key]
v, ok := m["Answer"]
fmt.Println("The value:", v, "Present?", ok)

闭包

函数值,Go 中函数也可以被传到另一个函数中:

1
2
3
4
5
6
7
8
9
10
// compute 函数接收另外一个函数
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}

// compute 接收了 hypot 作为参数
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(compute(hypot))

函数的闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。

来一步一步理解 Go 中的闭包:

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
49
50
51
52
53
54
55
56
57
// 定义一个普通的函数 send 并调用
func send(message string){
fmt.Println(message)
}
send("hi, scott")

// 定义一个匿名版本的 send 函数并立即调用
// 注意这个函数并没有函数名字
func (message string){
fmt.Println(message)
}("hi, scott")

// 定义一个函数,返回一个函数
/*
func give_me_a_func() |func(string)| <---- 这里规定了返回值
即为一个接收 string 的函数
*/
func give_me_a_func() func(string) {
return func(message string){
fmt.Println(message)
}
}


// 可以把 give_me_a_func() 传给另外一个函数
send_func := give_me_a_func()
send_func("hi scott")

// 理解了上面函数的工作方式后,我们来介绍闭包
// 先定义一个函数, 它返回一个函数(return int)
func incrementor() func() int{
i := 0
return func() int {
i++
return i
}
}

// 现在定义另外一个变量来存储 incrementor 返回的函数
next := incrementor()

// 打印每次调用 next() 返回的值
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3

// 分析
func incrementor() func() int{
i := 0
return func() int {
// 这里 i 看起来应该无法工作的
// 因为 i 是定义在 incrementor 中的
// 但闭包拥有其被创建环境下的作用域
i++
return i
}
}

参考