cheatsheet

  • 流程控制:
    • for 循环:for i := 0; i < 10; i++ {
    • for range 循环:for i, v := range pow {,可用 _ 代替 iv
    • while 循环:for i < 10 {
    • 紧凑的 if:if v := math.Pow(x, n); v < lim {
  • 结构体:
    • 访问结构体成员或结指针指向的结构体成员:p.X
    • 结构体赋值:v1 = Vertex{1, 2}v2 = Vertex{X: 1/}// Y:0 隐式赋予
  • 切片:
    • 声明切片:[]bool{true, true, false}a := make([]bool, 0, 3)
    • 向切片增加元素:append(a, true, false)
  • 映射:
    • 初始化一个 string 到 int 的空映射:m := make(map[string]int)
    • 删除映射的元素:delete(m, key)
    • 检测某个键是否存在:elem, ok = m[key]
  • 函数和方法:
    • 定义函数:func split(sum int) (x, y int) {
    • 定义结构体的方法:func (v Vertex) Abs() float64 {

入门教程

A Tour of Go

Go 的变量声明方式

第一眼看到,觉得很怪异。不过看了这篇 关于 Go 语法声明的文章,觉得挺有意思。

Go 的声明语句是为了和自然语言(英语)保持一致:

1
2
3
x int       // x is int
p *int // p is pointer of int
a [3]int // a is array[3] of int

这主要针对的是 C 的指针,特别是加入了函数指针,一切都复杂起来:

1
int (*(*fp)(int (*)(int, int), int))(int, int)

这段代码定义了一个函数指针 fp,其接收两个参数,第一个是一个函数指针(类型是 int (*)(int, int),接收两个 int 并返回 int),第二个参数是 intfp 返回一个函数指针,类型是最外层的 int (*(...))(int, int),表示接收两个 int 并返回 int)。

如果用 Go 改写,将会变成:

1
f := func(func (int, int) int, int) func(int, int) int

按照从左往右以 函数接收 xx 参数,返回 xx 的形式解读,可以知道:f 是一个函数,其接收两个参数,第一个参数是一个函数(func (int, int) int,接收。两个 int 并返回 int),第二个参数是一个 int。返回类型是一个函数 func(int, int) int,表示接收两个 int 并返回一个 int。

这里仅对文章核心作出解释,英文原文有更循序渐进的解释。

Go 语言代码风格

go fmt <filename>.go 永远滴神!

虽然 go fmt 的风格是用 tab,还是八个空格的 tab,但至少有一个官方排版方案,所以比 C++、Java、Python 各种民间规范更能让人接受。

Go 的指针

指针和引用的功能是类似的,所以很多语言语言只实现了指针(如 C、Go),或只实现了引用(如 Java、Python)。如果二者都实现了,可能开发者也偏向于使用单一的一种(如 C++ STL中基本都是使用指针)。

Go 只实现了指针。但是不同的是,它的指针结构体有点意思:(*p).X 可以简写为 p.X

这种写法,使得结构体指针访问成员可以写成 p.X,这种写法反而更像是引用。所以,Go 虽然使用的是指针,但其语法也借鉴了引用的优点。

Go 接口 interface

没学过 Java 的我看 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
package main

import (
"fmt"
)

type Phone interface {
call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}

func main() {
var phone Phone

phone = new(NokiaPhone)
phone.call()

phone = new(IPhone)
phone.call()
}

C++ 没有接口的概念,Java 有,可参考 Java 接口

如果用 C++ 的方式理解,interface 可以裂解为一个“父类”,它声明了很多“虚函数”,由“子类”靠重载进行实现嘛!所以也就出现 interface “接口类型可以被赋值”这种听起来匪夷所思的事情,其实也就是把子类值赋给了父类变量,之后便可以调用父类的成员函数了。

这也是 Go 为了弥补没有类而产生的语法吧。

顺便一提,main 函数不使用 interface 写法也是可以的:

1
2
3
4
5
6
7
func main() {
phone1 := new(NokiaPhone)
phone1.call()

phone2 := new(IPhone)
phone2.call()
}

空接口 empty interface

接口还不止于此:一个不包含任何方法的接口被称为空接口 empty interface。

空接口可保存任何类型的值(因为每个类型都至少实现了零个方法)。

空接口被用来处理未知类型的值。例如,fmt.Print 可接受类型为 interface{} 的任意数量的参数。

类型断言和类型选择 type assertion and switch

所以又接着产生了类型断言 type assertion 和类型选择 type switch

类型断言用于断言这个 interface 里到底是什么东西。其语法及对应输出如下:

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

import "fmt"

func main() {
var i interface{} = "hello"

s := i.(string)
fmt.Println(s)
// hello

s, ok := i.(string)
fmt.Println(s, ok)
// hello true

f, ok := i.(float64)
fmt.Println(f, ok)
// 0 false

f = i.(float64)
fmt.Println(f)
/*
panic: interface conversion: interface {} is string, not float64

goroutine 1 [running]:
main.main()
/tmp/sandbox906950040/prog.go:17 +0x1fe
*/
}

类型选择就是综合了类型断言和 switch

1
2
3
4
5
6
7
8
switch v := i.(type) {
case int:
// v 的类型为 int
case float64:
// v 的类型为 float64
default:
// 没有匹配,v 与 i 的类型相同
}

常用接口 Stringer

fmt 包中定义的 Stringer 是最普遍的接口之一。

type Stringer interface {
String() string
}
Stringer 是一个可以用字符串描述自己的类型。fmt 包(还有很多包)都通过此接口来打印值。

类似于 Java 的 toString(),定义了这个函数以后就可以调用 fmt 输出了。

Go 并发

Go 多线程

Go 开多线程也太香了吧,直接 go <function> 就可以了。

Go 线程同步:信道

Go 的线程同步使用的是信道 channel,类似于《操作系统——精髓与设计原理》里进程同步的 消息传递 方法。

无缓冲区信道

默认的信道是无缓冲区的,这种情况下采用的是“阻塞发送、阻塞接收”的方式:发送方和接收方先准备好的一方会被阻塞,直至另一方也准备好了。

创建一个无缓冲区的 int 信道可以使用 c := make(chan int)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 将求数组和问题分配到两个进程完成(分别计算前 3 个和、后 3 个和)
package main

import "fmt"

func sum(a []int, c chan int) {
s := 0
for _, v := range a {
s += v
}
c <- s // 将和送入 c
}

func main() {
a := []int{7, 2, 8, -9, 4, 0}

c := make(chan int)
go sum(a[:len(a)/2], c)
go sum(a[len(a)/2:], c)
x, y := <-c, <-c // 从 c 中接收

fmt.Println(x, y, x+y)
}

c 无缓冲区,因此在主线程执行 x, y := <-c, <-c 之前就准备好的 c<-ssum 会被阻塞。

对于无缓冲区的信道,这么写会报死锁:

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

import "fmt"

func main() {
ch := make(chan int)
ch <- 1
fmt.Println(<-ch)
}

/*
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
/tmp/sandbox780334017/prog.go:7 +0x59

Program exited.
*/

有缓冲区信道

下面这种使用缓冲区信道,则是:当缓冲区满时阻塞发送方、当缓冲区空时阻塞接收方。又像是生产者、消费者问题模型了。

缓冲区大小为 n 的 int 信道定义方法为 ch := make(chan int, n)。顺便一提,无缓冲区的代码也可以用 ch := make(chan int, 0) 定义。

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

import "fmt"

func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}

有意思的是,如果加两行,也会报死锁:

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

import "fmt"

func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
ch <- 3
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}

/*
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
/tmp/sandbox780334017/prog.go:7 +0x59

Program exited.
*/

源源不断从信道接收值

发送者可通过 close(c) 关闭一个信道,表示没有需要发送的值了。

接收者可以使用 v, ok := <-ch 判断信道是否被关闭:若没有值可以接收且信道已被关闭,那么执行后 ok 会被设置为 false

循环 for i := range c 会不断从信道接收值,直到它被关闭。

注意:

  1. 信道关闭后,之前传进缓冲区的值仍可被接收;
  2. 信道关闭并没有值可以接收后,再次接收会接收到零值(如果信道没有关闭,该线程会被阻塞);
  3. 只有发送者才能关闭信道,而接收者不能。因为向一个已经关闭的信道发送数据会引发程序恐慌 (panic)。
  4. 关闭信道不是必需操作。只有在需要终止一个 range 循环等情况下需要关闭。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 万能的斐波那契数列
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)
}
}

select 选择最先就绪的执行

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

import (
"fmt"
"time"
)

func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    .
.
tick.
.
.
tick.
.
.
tick.
.
.
tick.
.
.
BOOM!

互斥锁

互斥方案就类似于《操作系统——精髓与设计原理》里进程同步的信号量了。

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"))
}