cheatsheet
- 流程控制:
- for 循环:
for i := 0; i < 10; i++ {
- for range 循环:
for i, v := range pow {
,可用_
代替i
或v
- while 循环:
for i < 10 {
- 紧凑的 if:
if v := math.Pow(x, n); v < lim {
- for 循环:
- 结构体:
- 访问结构体成员或结指针指向的结构体成员:
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]
- 初始化一个 string 到 int 的空映射:
- 函数和方法:
- 定义函数:
func split(sum int) (x, y int) {
- 定义结构体的方法:
func (v Vertex) Abs() float64 {
- 定义函数:
入门教程
Go 的变量声明方式
第一眼看到,觉得很怪异。不过看了这篇 关于 Go 语法声明的文章,觉得挺有意思。
Go 的声明语句是为了和自然语言(英语)保持一致:
1 | x int // x is int |
这主要针对的是 C 的指针,特别是加入了函数指针,一切都复杂起来:
1 | int (*(*fp)(int (*)(int, int), int))(int, int) |
这段代码定义了一个函数指针 fp
,其接收两个参数,第一个是一个函数指针(类型是 int (*)(int, int)
,接收两个 int 并返回 int),第二个参数是 int
。fp
返回一个函数指针,类型是最外层的 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 | package main |
C++ 没有接口的概念,Java 有,可参考 Java 接口。
如果用 C++ 的方式理解,interface
可以裂解为一个“父类”,它声明了很多“虚函数”,由“子类”靠重载进行实现嘛!所以也就出现 interface
“接口类型可以被赋值”这种听起来匪夷所思的事情,其实也就是把子类值赋给了父类变量,之后便可以调用父类的成员函数了。
这也是 Go 为了弥补没有类而产生的语法吧。
顺便一提,main 函数不使用 interface 写法也是可以的:
1 | func main() { |
空接口 empty interface
接口还不止于此:一个不包含任何方法的接口被称为空接口 empty interface。
空接口可保存任何类型的值(因为每个类型都至少实现了零个方法)。
空接口被用来处理未知类型的值。例如,fmt.Print
可接受类型为 interface{}
的任意数量的参数。
类型断言和类型选择 type assertion and switch
所以又接着产生了类型断言 type assertion
和类型选择 type switch
。
类型断言用于断言这个 interface
里到底是什么东西。其语法及对应输出如下:
1 | package main |
类型选择就是综合了类型断言和 switch
:
1 | switch v := i.(type) { |
常用接口 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 | // 将求数组和问题分配到两个进程完成(分别计算前 3 个和、后 3 个和) |
c
无缓冲区,因此在主线程执行 x, y := <-c, <-c
之前就准备好的 c<-s
的 sum
会被阻塞。
对于无缓冲区的信道,这么写会报死锁:
1 | package main |
有缓冲区信道
下面这种使用缓冲区信道,则是:当缓冲区满时阻塞发送方、当缓冲区空时阻塞接收方。又像是生产者、消费者问题模型了。
缓冲区大小为 n 的 int
信道定义方法为 ch := make(chan int, n)
。顺便一提,无缓冲区的代码也可以用 ch := make(chan int, 0)
定义。
1 | package main |
有意思的是,如果加两行,也会报死锁:
1 | package main |
源源不断从信道接收值
发送者可通过 close(c)
关闭一个信道,表示没有需要发送的值了。
接收者可以使用 v, ok := <-ch
判断信道是否被关闭:若没有值可以接收且信道已被关闭,那么执行后 ok
会被设置为 false
。
循环 for i := range c
会不断从信道接收值,直到它被关闭。
注意:
- 信道关闭后,之前传进缓冲区的值仍可被接收;
- 信道关闭并没有值可以接收后,再次接收会接收到零值(如果信道没有关闭,该线程会被阻塞);
- 只有发送者才能关闭信道,而接收者不能。因为向一个已经关闭的信道发送数据会引发程序恐慌 (panic)。
- 关闭信道不是必需操作。只有在需要终止一个
range
循环等情况下需要关闭。
1 | // 万能的斐波那契数列 |
select 选择最先就绪的执行
select
会阻塞当前线程,直至某个分支可以继续执行,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。
也可以添加 default
,如果当前没有分支可以执行,就不会阻塞当前线程而是执行 default
语句。
1 | package main |
输出:
1 | . |
互斥锁
互斥方案就类似于《操作系统——精髓与设计原理》里进程同步的信号量了。
1 | package main |