Go channel功能详解

   在golang中,channel属于较为核心的一个功能,尤其在go协程中,channel功能尤为重要。作为goroutine之间通信的一种方式,channel跟Linux系统中的管道/消息队列有很多类似之处。使用channel可以方便地在goroutine之间传递数据,此外,channel还关联了数据类型,如int、string等等,可以决定确定channel中的数据单元。

[TOC]

定义channel

   Channel类型的定义格式如下,包括三种类型的定义。可选的<-代表channel的方向,如果没有指定方向,那么Channel就是双向的,既可以接收数据,也可以发送数据。

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .

chan T          // 可以接收和发送类型为 T 的数据
chan<- float64  // 只可以用来发送 float64 类型的数据
<-chan int      // 只可以用来接收 int 类型的数据

// <-总是优先和最左边的类型结合。
chan<- chan int    // 等价 chan<- (chan int)
chan<- <-chan int  // 等价 chan<- (<-chan int)
<-chan <-chan int  // 等价 <-chan (<-chan int)
chan (<-chan int)

   和slice、map类似,可以使用make关键字来初始化channel。

unbuffered := make(chan int)  //定义无缓冲的整型通道
buffered := make(chan string, 10)  //有缓冲的字符串通道

  上述代码中定义了一个无缓冲的channel和一个有缓冲channel。make的第一个参数需要是关键字chan,之后跟着允许通道交换的数据的类型。如果创建的是一个有缓冲channel,之后还需要在第二个参数指定这个channel的缓冲区的大小。和其他引用类型一样,channel 的空值为 nil ,使用 == 可以对类型相同的 channel 进行比较,只有指向相同对象或同为 nil 时,才返回 true
  无缓冲channel默认会阻塞读取操作,有缓冲channel能部分避免阻塞读取操作。据此特性可以实现很多应用场景,后文将会逐项介绍。

读写channel

buffered <- "Gopher" //向channel buffered发送一个字符串
value := <-buffered //从channel buffered中接受一个字符串

  Go使用操作符->实现channel的读写功能。需要注意的是,在执行读写操作之前必须先初始化此通道,否则会出现永久阻塞的现象。

关闭channel

  使用go内置的close函数可以关闭channel,实际使用中经常使用 defer功能,在程序最后关闭channel。

close(buffered)

channel用处

gorouting通信

  这一点勿需多言,前面介绍channel时就提到这一点,channel的下述特性均基于此项功能实现。

gorouting同步

   对于unbuffered channel,缺省情况下发送和接收会一直阻塞着,直至另一方做好准备。用此特性可以实现gororutine之间的同步功能,而不必使用显式的锁或条件变量。

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // send sum to c
}
func main() {
    s := []int{7, 2, 8, -9, 4, 0}
    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // receive from c
    fmt.Println(x, y, x+y)
}

   上述代码执行结果如下

kefin@localhost:~/gopath/src/iotest $ go run sync.go
-5 17 12

   上述代码是官方提供的例子,x, y := <-c, <-c这行代码会一直处于阻塞状态,直至计算结果发送到channel中。

用于range遍历

package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        time.Sleep(1 * time.Hour)
    }()
    c := make(chan int)
    go func() {
        for i := 0; i < 10; i = i + 1 {
            c <- i
        }
        close(c)
    }()
    for i := range c {
        fmt.Println(i)
    }
    fmt.Println("Finished")
}

   range c产生的迭代值为Channel中发送的值,它会一直迭代知道channel被关闭。上面的例子中如果把close(c)注释掉,程序会一直阻塞在for …… range那一行。代码的执行结果为

kefin@localhost:~/gopath/src/iotest $ go run range.go
0
1
2
3
4
5
6
7
8
9
Finished

配合select使用

   select语句选择一组可能的send操作和receive操作去处理,它类似switch,但是只是用来处理通讯(communication)操作。 它的case可以是send语句,也可以是receive语句,亦或者default。receive语句可以将值赋值给一个或者两个变量,最多允许有一个default case,它可以放在case列表的任何位置,大部分会将它放在最后。

package main

import "fmt"

func fibonacci(c, quit chan int) 
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}
func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

  如果有同时多个case去处理,比如同时有多个channel可以接收数据,那么Go会伪随机的选择一个case处理(pseudo-random)。如果没有case需要处理,则会选择default去处理。如果没有default case,则select语句会阻塞,直到某个case需要处理。需要注意的是,nil channel上的操作会一直被阻塞。如果没有default case,只有nil,那么channel的select会一直被阻塞。

  此外,还可以配合select的超时处理功能,如上所述,没有case需要处理时,select语句就会一直阻塞,此时通常需要设置超时操作来处理超时的情况。 下面这个例子我们会在2秒后往channel c1中发送一个数据,但是select设置为1秒超时,因此我们会打印出timeout 1,而不是result 1。

 package main

 import (
     "fmt"
     "time"
 )

 func main() {
     c1 := make(chan string, 1)
     go func() {
         time.Sleep(time.Second * 2)
         c1 <- "result 1"
     }()
     select {
     case res := <-c1:
         fmt.Println(res)
     case <-time.After(time.Second * 1):
         fmt.Println("timeout 1")
     }
 }

  其实利用的是time.After方法,它返回一个类型为<-chan Time的单向的channel,在指定的时间发送一个当前时间给返回的channel中。执行结果为

kefin@localhost:~/gopath/src/iotest $ go run select_timeout.go
timeout 1

实现Timer和Ticker

  timer是一个定时器,代表未来的一个单一事件,可以设置timer需要等待多长时间,它提供一个Channel,在将来的那个时间那个Channel提供了一个时间值。下面的例子中第二行会阻塞2秒钟左右的时间,直到时间到了才会继续执行。当然如果只想单纯的等待2秒,可以使用time.Sleep(2)来实现。

timer1 := time.NewTimer(time.Second * 2)
<-timer1.C
fmt.Println("Timer 1 expired")

你还可以使用timer.Stop来停止[计时器]

timer2 := time.NewTimer(time.Second)
go func() {
    <-timer2.C
    fmt.Println("Timer 2 expired")
}()
stop2 := timer2.Stop()
if stop2 {
    fmt.Println("Timer 2 stopped")
}

  ticker是一个定时触发的计时器,它会以一个间隔(interval)往Channel发送一个事件(当前时间),而Channel的接收者可以以固定的时间间隔从Channel中读取事件。下面的例子中ticker每500毫秒触发一次,你可以观察输出的时间

ticker := time.NewTicker(time.Millisecond * 500)
go func() {
    for t := range ticker.C {
        fmt.Println("Tick at", t)
    }
}()

同样,ticker也可以通过Stop方法来停止。一旦它停止,接收者不再会从channel中接收数据了。



channel操作注意事项

  • 关闭一个未初始化(nil) 的 channel 或者重复关闭同一个channel均会产生 panic
  • 向一个已关闭的 channel 中发送消息会产生 panic
  • 从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已读出,则会读到类型的零值。
  • 从已关闭的 channel 中读取消息永远不会阻塞,并且会返回 false ,据此可判断 channel 是否关闭
  • 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息

推荐阅读更多精彩内容