学习Go语言,就不得不学习协程(Goroutines)和信道(Channels),正是因为有了协程和信道的机制,才使得Go语言对高并发的天然支持,下面就让我们对协程和信道的使用一探究竟吧!


协程(Goroutines)


1. 什么是协程


Goroutines are functions or methods that run concurrently with other functions or methods. Goroutines can be thought of as light weight threads. The cost of creating a Goroutine is tiny when compared to a thread. Hence its common for Go applications to have thousands of Goroutines running concurrently.


从协程的概念中我们可以看出协程是一种函数或者一种方法,它们可以并发的运行。协程可以被看作是轻量级的线程,但是与线程相比,创建一个协程的成本很低,因此在Go语言开发的应用中,经常可以看到成千上万的协程并发执行。


2. 如何使用协程


协程的使用十分简单,只需要在调用函数或者方法时,在前面加上关键字go,就启动了一个新的Go协程(注意:主函数无需加关键字go,它默认运行在一个Go协程上,被称为Go主协程Main Goroutine),下面我们就通过示例演示如何创建一个协程:

package main

import (
    "fmt"
)

func hello() {
    fmt.Println("this is hello function")
}
func main() {
    go hello()
    fmt.Println("this is main function")
}

上述代码中,我们在调用hello()函数时,对其加了go关键字,因此就创建了一个协程,是不是超级简单啊!是的,就是这么简单,但是当你运行程序之后,你会发现代码只输出了“this is main function”这是为什么呢?


这是因为Go语言的协程有两个重要的性质:


  • 启动一个新的协程时,协程的调用会立即返回。与函数不同,程序控制不会去等待 Go 协程执行完毕。在调用 Go 协程之后,程序控制会立即返回到代码的下一行,忽略该协程的任何返回值。
  • 如果希望运行其他 Go 协程,Go 主协程必须继续运行着。如果 Go 主协程终止,则程序终止,于是其他 Go 协程也不会继续运行。


这时你就会想,那么怎样才能等待协程执行完毕呢,各位大佬别急,下面就该轮到信道登场了。所以,抛开信道谈协程,就像是抛开现实谈理想,缺少脚踏实地做支撑,终究是竹篮打水一场空。


信道(Channels)


1. 什么是信道


Channels can be thought as pipes using which Goroutines communicate. Similar to how water flows from one end to another in a pipe, data can be sent from one end and received from the another end using channels.


信道可以看作是协程之间通信的管道,就好像管道中的水可以从一头流动到另一头一样,通过信道,数据也可以从一端发送,从另一端接收。


2. 信道使用的注意点


  • 类型:所有信道都关联了一个类型。信道只能运输这种类型的数据,而运输其他类型的数据都是非法的;
  • 定义:需要像对 map 和切片所做的那样,用 make 来定义信道;
  • 零值:信道的零值为 nil;
  • 发送和接收:信道旁的箭头方向指定了是发送数据还是接收数据,如果箭头指向信道,则是把数据写入信道,反之则为从信道读取数据;
  • 阻塞:信道的发送与接收默认是阻塞的,即当把数据发送到信道时,程序控制会在发送数据的语句处发生阻塞,直到有其它Go协程从信道读取到数据,才会解除阻塞。与此类似,当读取信道的数据时,如果没有其它的协程把数据写入到这个信道,那么读取过程就会一直阻塞着;
  • 单向信道:我们可以通过箭头的指向来创建单向信道,这种信道只能发送或者接收数据。一个双向信道可以转换成唯送信道(Send Only)或者唯收信道(Receive Only),但是反过来不行;
  • 死锁:(对于双向信道)当Go协程给一个信道发送数据时,需要有其他Go协程来接收数据。如果没有的话,程序就会在运行时触发panic,形成死锁。同理,当有Go协程等着从一个信道接收数据时,必须要有其他的 Go协程向该信道写入数据,要不然程序就会触发 panic;
  • 关闭信道:使用close关闭信道;
  • 遍历信道:可以使用for  range来遍历信道;
  • 缓冲信道:在用make函数创建信道时,我们可以传递一个参数来指定缓冲的大小,如ch := make(chan type, capacity),默认不填的情况下capacity的值为0,也就是所谓的无缓冲信道。对于缓冲信道,只在缓冲已满的情况,才会阻塞向缓冲信道(Buffered Channel)发送数据。同样,只有在缓冲为空的时候,才会阻塞从缓冲信道接收数据。


3. 信道的使用示例:

package main

import (  
    "fmt"
    "time"
)

func write(ch chan int) {  
    for i := 0; i < 5; i++ {
        ch <- i
        fmt.Println("successfully wrote", i, "to ch")
    }
    close(ch)
}
func main() {  
    ch := make(chan int, 2)
    go write(ch)
    time.Sleep(2 * time.Second)
    for v := range ch {
        fmt.Println("read value", v,"from ch")
        time.Sleep(2 * time.Second)

    }
}

上面的代码中,我们创建了一个容量为2的缓冲信道,并将其作为参数传给了write协程。write 协程有一个 for 循环,依次向信道 ch 写入 0~4。而缓冲信道的容量为 2,因此 write 协程里立即会向 ch 写入 0 和 1,接下来发生阻塞,直到 ch 内的值被读取。

主协程在休眠了两秒后,使用 for range 循环读取信道ch的值并打印,打印完之后又休眠两秒。该过程会一直进行,直到信道读取完所有的值,并在 write 协程中关闭信道。最终代码执行输出如下:

successfully wrote 0 to ch
successfully wrote 1 to ch
read value 0 from ch
successfully wrote 2 to ch
read value 1 from ch
successfully wrote 3 to ch
read value 2 from ch
successfully wrote 4 to ch
read value 3 from ch
read value 4 from ch

这就是Go语言协程和信道的简单使用方法,后面的文章中我们会学习它们的一些更加复杂的用法。