目录

Go学习笔记

Go学习笔记

教程来自 MIT 6.824 推荐的 Online Go tutorial

基础

Go 中也有 Java 中的 package 概念。

比如一个 a.go 文件在 main 目录下,我们就需要申明它为 package main

按照约定,包名会被作为导入路径的最后一个元素。例如,"math/rand" 包中的源码均以 package rand 语句开始。这样方便管理

导入

也和 Java 一样,通过使用 import 来导入你想要导入的包

如果想要分组导入(也是为了方便管理),可以用 () 来将导入的包分为一组:

1
2
3
4
5
6
7
8
import "fmt"
import "math"

// 等价于下面写法:
import (
    "fmt"
    "math"
)

隔离级别

Go 为了简洁,不像其他语言一样使用 privatepublicprotected 来更精细化的区分可见性,这也是因为 Go 中神奇的并没有 class 的概念。

而是直接规定大写字母开头的对其他包可见。

首字母 可见性
大写 对其他包可见(public)
小写 仅包内可见(private)

变量

使用 var 关键字来申明变量,:= 关键词可以直接申明并赋值,但是注意 := 不能用在全局

1
2
3
4
5
var a int
a = 1

// 等价于下面写法:
a := 1

函数

Go 函数写法是这样的:

1
2
3
4
5
6
7
func 函数名(变量名 变量类型, ...) 返回类型 {
    
}

func add (a int, b int) int {
    
}

这种将变量类型写在变量类型后面真的是第一次见,(参考这篇关于 Go 声明语法 的文章,了解为何使用这种类型声明的形式。)

当有连续多个函数的形参的变量类型一样,可以省略前面的所有变量类型,只保留最后一个

1
2
3
func add (a, b int) int {
    
}

Go 中的函数可以返回任意多个值:

1
2
3
func calculate (a, b int) (int int) {
    return a + b, a - b
}

类型转换

Go 中是通过表达式 T(v) 将值 v 转换为类型 T,这个写法倒是和其他语言不一样。

例如:

1
2
i := 1
j := float(i)

for 循环

Go 中没有 while 循环,只有 for 循环,因为一切都是为了简洁优雅。

写法和 C++ 和 Java 中只有省掉一个 () 的区别,以及 Go 中不能省略 {}

1
2
3
4
sum := 0
for i := 0; i < 100; i ++ {
	sum += i
}

如果去掉初始化语句和后置语句,那么用法就和 while 完全一样了:

1
2
3
4
sum := 1
for sum <= 1000 {
    sum += sum
}

如果想要表示出无限循环,就可以直接省略循环条件即可:

1
2
3
4
i := 0
for {
    i ++
}

if 条件判断

Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ),而大括号 { } 则是必须的。

以及 if 语句可以在条件表达式前执行一个简短语句

defer 推迟

defer 语句会将函数推迟到外层函数返回之后执行

推迟调用的函数其参数会立即求值,但直到外层函数返回前,该函数都不会被调用。

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
	defer fmt.Println("world")

	fmt.Println("hello")
}

但其实它的很多作用在于保证某些行为一定会被执行,就像并发/锁中我们用 defer c.mu.Unlock() 来保证最后会释放互斥锁。

推迟调用的函数调用会被压入一个栈中,当外层函数返回时,被推迟调用的函数调用会按照后进先出的顺序调用

指针

Go 中的指针的用法其实和 C++ 一样,只是类型 T 的指针写成 *T,而不是 C++ 中的 T*

空指针为 nil

& 操作符可以获取变量的指针。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "fmt"

func main() {
	i := 1

	p := &i         // 指向 i
	fmt.Println(*p) // 通过指针读取 i 的值
	*p = 2         // 通过指针设置 i 的值
	fmt.Println(i)  // 查看 i 的值
}

结构体

struct 的用法和 C++ 也基本一样

1
2
3
4
type Vertex struct {
    x int
    y int
}

访问结构体中的字段通过 . 来访问

1
2
3
v := Vertex{1, 2}
v.x = 3
fmt.Println(v.x)

以及理论上,如果使用结构体指针来访问结构体中的字段,就需要写成 (*p).x,但是和 C++ 中额外提供了更简洁的语法糖 p->x 一样,Go 中也可以直接通过 p.x 来让结构体指针访问结构体中的字段

我们也可以只初始化结构体中的部分字段:

1
2
v1 := Vertex{x: 1} // y 会被赋默认值
v2 := Vertex{}     //x, y 都会被赋默认值

数组

类型 T 的数组写为 [n]T,比如 10 个整数的数组可以声明为 var a [10]int

slice 切片

Go 中提供了 slice,来代替 C++ 中的 vector 这种动态数组的功能

  1. 创建切片: 使用 make([]T, len, cap) 函数来创建,如: a := make([]int, 5)

​ 也可以通过使用 make([]T, len) 来创建,默认的 cap 会等于 len

  1. 从数组中创建切片: a[low : high],区间其实是 $[\text{low}, \text{high})$,包含从下标 low 到 high-1 的元素

  2. 切片类似于数组的引用,它的本身并不存储任何元素。所以更改切片的元素会修改底层数组中对应的元素,所有共享底层数组的切片都会观察到这些修改

  3. 切片可以默认忽略下界的 0 和上界的切片长度

比如对于数组 var a [10]int,下面的切片表达式是等价的:

1
2
3
4
a[0:10]
a[:10]
a[0:]
a[:]
  1. 获取切片的长度和容量,通过表达式 len(s)cap(s)

  2. 切片末尾插入新元素: s = append(s, val)

  3. range 遍历:可以遍历切片,每次迭代会返回两个值,第一个值为下标,第二个值是实际的值

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    package main
    
    import "fmt"
    
    func main() {
    	var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
    	for i, v := range pow {
    		fmt.Println(i, v)
    	}
    }
    

    当然我们如果只想要实际的值,可以通过为值赋予 _ 来忽略它:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    package main
    
    import "fmt"
    
    func main() {
    	var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
    	for _, v := range pow {
    		fmt.Println(v)
    	}
    }
    

map 映射

基本和 C++ 中的 map 一样

  1. map 也是通过 make 进行初始化
1
m = make(map[string]int)
  1. 插入或者修改值: m[key] = value
  2. 获取元素: value = m[key]
  3. 删除元素: delete(m, key)

函数闭包

其实有点像 C++ 中的 lambda,只是它默认是引用传递外部函数的所有变量:

方法

方法就是 Go 中实现类似 class 的功能关键字

记住: 方法只是一个带接收者参数的函数

并发

协程

Go协程是Go语言中的线程,也就是 go routine

使用起来非常简单,直接用 go f (x, y, ...) 就可以开一个 go协程去执行函数 f

比如下面代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
	"fmt"
	"time"
)

func say(s string) {
	for i := 0; i < 5; i ++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
}

func main() {
	go say("world")
	say("hello")
}

信道

多线程编程需要对共享数据进行同步,也就是进行 data race,有一个简单的方法就是使用信道。

信道是一种带有类型的管道,直接使用信道操作符 <- 进行值的发送或者接收。(“箭头"方向也就是数据流的方向)

  1. 创建不带缓冲的信道: ch := make(chan int)

  2. 创建带缓冲的信道: ch := make(chan int, n) ,其中 n 是信道缓冲的大小

  3. rangeclose: 通过 for i := range c 可以不断从信道接收值,直到它被关闭,发送者可以通过 close 来关闭一个信道,用来表示没有需要发送的值了。 ​range 其实是一种语法糖,其实我们也可以通过为接受表达式分配第二个参数来知道是否信道被关闭,语法上来看下面两种写法是等价的:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    // range写法
    for i := range c {
    	fmt.Println(i)
    }
    // 等价写法
    for {
    	i, ok := <- c
    	if !ok {
    		break
    	}
    	fmt.Println(i)
    }
    

    注意: 只应该由发送方关闭信道,而不应该由接收方。因为如果向一个已经关闭的信道发送数据会引发程序panic。

    还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须通过关闭信道来通知接收方不再有新的发送的值的时候才有必要关闭,例如终止一个 range 循环。

    示例代码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    package main
    
    import (
    	"fmt"
    )
    
    func fibonacci(n int, c chan int) {
    	x, y := 0, 1
    	for i := 0; i < n; i++ {
    		c <- x
    		x, y = y, x+y
    	}
    	close(c)
    }
    
    func main() {
    	c := make(chan int, 10)
    	go fibonacci(cap(c), c)
    	for i := range c {
    		fmt.Println(i)
    	}
    }
    
  4. select 可以使一个 Go 程可以等待多个通信操作,然后执行某一个未阻塞的分支:

    1
    2
    3
    4
    5
    6
    7
    8
    
    for {
    	select {
    		case i <- c:
    			i ++
    		case j <- quit:
    			break
    	}
    }
    

​ 也可以通过 default 分支来实现:当其他分支都被阻塞时,尝试做点什么

Go 标准库提供了 sync.Mutex 互斥锁类型及其两个方法: LockUnlock

我们可以通过在访问临界区之前调用 Lock 方法,在访问完之后调用 Unlock 方法来保证互斥访问。

我们还可以使用 defer 来保证锁一定会被释放(语义上更简洁,更利于代码理解)

示例代码:

 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
package main

import (
	"fmt"
	"sync"
	"time"
)

// SafeCounter 是并发安全的
type SafeCounter struct {
	mu sync.Mutex
	v map[string]int
}

// Inc 对给定键的计数加一
func (c *SafeCounter) Inc(key string) {
	c.mu.Lock()
	// 锁定使得一次只有一个 Go 协程可以访问映射 c.v。
	c.v[key]++
	c.mu.Unlock()
}

// Value 返回给定键的计数的当前值。
func (c *SafeCounter) Value(key string) int {
	c.mu.Lock()
	// 锁定使得一次只有一个 Go 协程可以访问映射 c.v。
	defer c.mu.Unlock()
	return c.v[key]
}

func main() {
	c := SafeCounter{v: make(map[string]int)}
	for i := 0; i < 1000; i++ {
		go c.Inc("k")
	}

	time.Sleep(time.Second)
	fmt.Println(c.Value("k"))
}

练习题

练习:等价二叉查找树

1. 实现 Walk 函数。

2. 测试 Walk 函数。

函数 tree.New(k) 用于构造一个随机结构的已排序二叉查找树,它保存了值 k, 2k, 3k, …, 10k

创建一个新的信道 ch 并且对其进行步进:

1
go Walk(tree.New(1), ch)

然后从信道中读取并打印 10 个值。应当是数字 1, 2, 3, …, 10.

3.Walk 实现 Same 函数来检测 t1t2 是否存储了相同的值。

4. 测试 Same 函数。

Same(tree.New(1), tree.New(1)) 应当返回 true,而 Same(tree.New(1), tree.New(2)) 应当返回 false

Tree 的文档可在这里找到。

代码实现如下:

 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
package main

import (
	"fmt"

	"golang.org/x/tour/tree"
)

// Walk 遍历树 t,并树中所有的值发送到信道 ch。
func Walk(t *tree.Tree, ch chan int) {
	var walk func(t *tree.Tree)
	walk = func(t *tree.Tree) {
		if t == nil {
			return
		}
		walk(t.Left)
		ch <- t.Value
		walk(t.Right)
	}
	walk(t)
	close(ch)
}

// Same 判断 t1 和 t2 是否包含相同的值。
func Same(t1, t2 *tree.Tree) bool {
	ch1 := make(chan int)
	ch2 := make(chan int)
	go Walk(t1, ch1)
	go Walk(t2, ch2)
	for {
		i, ok1 := <-ch1
		j, ok2 := <-ch2
		if ok1 != ok2 {
			return false
		}
		if ok1 == false {
			break
		}
		if i != j {
			return false
		}
	}
	return true
}

func main() {
	fmt.Println(Same(tree.New(1), tree.New(1)))
	fmt.Println(Same(tree.New(1), tree.New(2)))
}