美文网首页
Go channel功能详解

Go channel功能详解

作者: 北春南秋 | 来源:发表于2019-02-27 20:14 被阅读0次

       在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 都会收到消息

    相关文章

      网友评论

          本文标题:Go channel功能详解

          本文链接:https://www.haomeiwen.com/subject/zdxquqtx.html