ReZero's Utopia.

Go Tutorial

Word count: 2.6kReading time: 12 min
2021/04/06 Share
  1. 和 C、Java、JavaScript 之类的语言不同,Go 的 for 语句后面的三个构成部分外没有小括号, 大括号 { } 则是必须的。

  2. C 的 while 在 Go 中叫做 for, 初始化语句和后置语句是可选的, 如果把循环条件也去掉就变成了 死循环 。当使用 for 循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。

1
2
3
4
5
6
7
8
9
for sum < 1000 {
sum += sum
}
// dead loop
for {}
// v 不用可直接省略
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
  1. if Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的. 同 for 一样, if 语句可以在条件表达式前执行一个简单的语句。该语句声明的变量作用域仅在 if(以及相连的else) 之内.
1
2
3
4
5
6
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
}
return lim
}
  1. Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 除非以 fallthrough (强制执行下一个分支,不会判断case)语句结束,否则分支会自动终止。 case 无需为常量,且取值不必为整数。甚至可为类型
1
2
3
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
  1. defer 栈,推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。

  2. Go 拥有指针。指针保存了值的内存地址。类型 *T 是指向 T 类型值的指针。其零值为 nil。& 操作符会生成一个指向其操作数的指针。* 操作符表示指针指向的底层值。

  3. 如果我们有一个指向结构体的指针 p,那么可以通过 (*p).X 来访问其字段 X。不过这么写太啰嗦了,所以语言也允许我们使用隐式间接引用,直接写 p.X 就可以。

1
2
3
4
5
6
7
8
9
10
11
type Vertex struct {
X, Y int
}

var (
v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
p = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
v2 = Vertex{X: 1} // Y:0 被隐式地赋予
v3 = Vertex{} // X:0 Y:0
// {1 2} &{1 2} {1 0} {0 0}
)
  1. 更改切片的元素会修改其底层数组中对应的元素。切片的第一个参数会改变 cap(是从它的第一个元素开始数,到其底层数组元素末尾的个数),这会影响下次切片只能在当次切片的基础上切,已切除的无法再动。切片可以用内建函数 make 来创建,这也是你创建动态数组的方式。append 添加元素
1
2
3
4
5
6
7
8
names := []struct {
i int
b bool
}{ {2, true},}
b := names[1:3]
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
s = append(s, 2, 3, 4)

  1. map 映射的零值为 nil
1
2
3
4
5
6
7
8
9
10
11
12
var m map[string]Vertex

func main() {
m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}
fmt.Println(m["Bell Labs"])
//若 key 在 m 中,ok 为 true ;否则,ok 为 false, elem 为对应值或者零值
elem, ok = m[key]
}

  1. 函数作为参数。闭包。结构体方法与函数参数为结构体并无区别
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
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}

// 闭包

func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}

func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}
  1. 就是接收者的类型定义和方法声明必须在同一包内;不能为内建类型声明方法
1
2
3
4
5
6
7
8
9
10
11
12
13
type MyFloat float64

func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}

func main() {
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
}
  1. 指针
1
2
3
4
5
6
7
8
9
10
11
//比较前两个程序,你大概会注意到带指针参数的函数必须接受一个指针:
var v Vertex
ScaleFunc(v, 5) // 编译错误!
ScaleFunc(&v, 5) // OK

//而以指针为接收者的方法被调用时,接收者既能为值又能为指针:
var v Vertex
v.Scale(5) // OK
p := &v
p.Scale(10) // OK
对于语句 v.Scale(5),即便 v 是个值而非指针,带指针接收者的方法也能被直接调用。 也就是说,由于 Scale 方法有一个指针接收者,为方便起见,Go 会将语句 v.Scale(5) 解释为 (&v).Scale(5)。
  1. IO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

func main() {
r := strings.NewReader("Hello, Reader!")

b := make([]byte, 8)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}

// o_build_awesomeProject
// n = 8 err = <nil> b = [72 101 108 108 111 44 32 82]
// b[:n] = "Hello, R"
// n = 6 err = <nil> b = [101 97 100 101 114 33 32 82]
// b[:n] = "eader!"
// n = 0 err = EOF b = [101 97 100 101 114 33 32 82]
// b[:n] = ""

  1. goroutine : f, x, y 和 z 的求值发生在当前的 Go 程中,而 f 的执行发生在新的 Go 程中。Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。sync 包提供了这种能力,不过在 Go 中并不经常用到,因为还有其它的办法(见下一页)。
1
2
3
4
5
go f(x, y, z)


ch <- v // 将 v 发送至信道 ch。
v := <-ch // 从 ch 接收值并赋予 v。
  1. 默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。
    以下示例对切片中的数进行求和,将任务分配给两个 Go 程。一旦两个 Go 程完成了它们的计算,它就能算出最终的结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从 c 中接收


// 第二个参数可作为缓冲,仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区满时,接受方会阻塞。

ch := make(chan int, 1)
ch <- 1
ch <- 2
//fatal error: all goroutines are asleep - deadlock!
// goroutine 1 [chan send]:
  1. 注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。
  1. select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。其它分支都没有准备好时,default 分支就会执行。为了在尝试发送或者接收时不发生阻塞,可使用 default 分支:
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


func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
default:
fmt.Println("waiting.")
time.Sleep(50 * time.Millisecond)
}
}
}

func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}

tips: 二叉树检测相同序列

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
// Walk 步进 tree t 将所有的值从 tree 发送到 channel ch。
func Walk(t *tree.Tree, ch chan int){
if t.Left != nil{
Walk(t.Left, ch)
}
ch <- t.Value
if t.Right !=nil{
Walk(t.Right, ch)
}

}

// Same 检测树 t1 和 t2 是否含有相同的值。
func Same(t1, t2 *tree.Tree) bool {
total := 10
ch1, ch2 := make(chan int, total), make(chan int, total)
go Walk(t1, ch1)
go Walk(t2, ch2)
for i:=0; i < 10; i++ {
if <-ch1 != <-ch2 {
return false
}
}
return true
}
  1. sync.Mutex
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 {
v map[string]int
mux sync.Mutex
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
c.v[key]++
c.mux.Unlock()
}

// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
defer c.mux.Unlock()
return c.v[key]
}

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

time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
  1. summary crawler
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package main

import (
"fmt"
)

type Fetcher interface {
// Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。
Fetch(url string) (body string, urls []string, err error)
}

// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
func Crawl(url string, depth int, fetcher Fetcher) {
// TODO: 并行的抓取 URL。
// TODO: 不重复抓取页面。
// 下面并没有实现上面两种情况:
if depth <= 0 {
return
}
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
for _, u := range urls {
Crawl(u, depth-1, fetcher)
}
return
}

func main() {
Crawl("https://golang.org/", 4, fetcher)
}

// fakeFetcher 是返回若干结果的 Fetcher。
type fakeFetcher map[string]*fakeResult

type fakeResult struct {
body string
urls []string
}

func (f fakeFetcher) Fetch(url string) (string, []string, error) {
if res, ok := f[url]; ok {
return res.body, res.urls, nil
}
return "", nil, fmt.Errorf("not found: %s", url)
}

// fetcher 是填充后的 fakeFetcher。
var fetcher = fakeFetcher{
"https://golang.org/": &fakeResult{
"The Go Programming Language",
[]string{
"https://golang.org/pkg/",
"https://golang.org/cmd/",
},
},
"https://golang.org/pkg/": &fakeResult{
"Packages",
[]string{
"https://golang.org/",
"https://golang.org/cmd/",
"https://golang.org/pkg/fmt/",
"https://golang.org/pkg/os/",
},
},
"https://golang.org/pkg/fmt/": &fakeResult{
"Package fmt",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
"https://golang.org/pkg/os/": &fakeResult{
"Package os",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
}
CATALOG