本文的内容建立在已对 C 和 C++ 熟练掌握的基础之上,主要给出 Go 与 C 和 C++ 之间的不同之处。
Go 中以 Goroutine 为单位并发,类似于线程。Goroutine 使用 go 启动,由 Go 自动调度,且是非阻塞的。
下面是启动 Goroutine 的一个实例。
1 | func greet() { |
由于没有特殊控制,输出的顺序是不一定的。
main() 的执行本身为 main goroutine,main goroutine 有一定的特殊性,其由 Go 自动创建,main() 结束时 main goroutine 结束,并且会结束其他的 goroutine。因此,在执行上面的程序时,有时会因 main routine 结束,导致提前结束 greet() routine,出现下面这种输出。
1 | hello from main() |
通道 chan 用于 goroutine 间的通信。
通道声明使用 var ch chan int,声明了一个传送 int 型的通道 ch。但此时的通道为 nil,需要使用 make 初始化才可以使用,初始化后可以使用 <- 来向通道传送或是接收数据,示例如下。
1 | var tmp int |
使用 make(chan int) 声明的通道没有缓冲区,具有同步的特点,即数据发送端会等待数据取出后再继续执行后续的代码,数据的接收端会等待数据到达再执行后续的代码。例如,下面的代码会触发 panic。
1 | func main() { |
这是因为代码会停在 ch <- x 处等待通道中的 x 被取走,而后续的接收通道数据无法执行,程序陷入死锁。正确的通道使用示例如下。
1 | func send(ch chan string) { |
代码中的 close(ch) 用于在发送完成后关闭通道,关闭后无法再向通道发送数据,但是可以从通道中接收数据直到通道为空,继续接收空通道中的数据会得到空值。
为了使通道可以异步接收与发送数据,可以在声明通道时为通道分配一块缓冲区,如下。
1 | ch := make(chan int, 10) |
上面的代码为 int 型通道分配了 10 块缓冲区。这种方法并非完全不会阻塞,当通道元素填满缓冲区时,向通道中传输数据也会阻塞,直至有数据被取走。有缓冲区的异步通道可以使用 len() 获取当前通道中数据数量,使用 cap() 获取缓冲区容量。
异步通道的使用示例如下。
1 | func get(out chan int) { |
上面的代码中使用了两个异步通道,分别实现 get() 到 add() 的通信和 add() 到 print() 的通信。
这里还涉及到了对缓冲区的遍历,可以使用一般的循环语句 for item, ok := <-ch ...,或是使用 range。
在将通道作为参数传入函数参数时,可以指定其对通道操作的方向,例如下面的两个函数。
1 | func send(out chan<- int) { |
有时,我们在一个函数中需要使用到多个通道,并从中接收数据,比较朴素的做法是循环接收并判断哪个通道里存在数据,示例如下。
1 | func recv(ch1 chan int, ch2 chan in) { |
这种做法的性能较差,Go 中提供了 select 相应多个通道的通信,使用方式类似于 switch,每个 case 对应到一个通道的数据传送或是接收操作,示例如下。
1 | select { |
select 也和 switch 一样,一个 case 执行完之后自动 break。因此,要使用 select 实现上面的多通道接收数据的 recv() 函数,还是要使用循环。
1 | func recv(ch1 chan int, ch2 chan in) { |