本文的内容建立在已对 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) { |