Don’t communicate by sharing memory, share memory by communicating.

介绍

channel是GO语言级别提供的goroutine间的通信方式,channel是类型相关的,需要在声明channel时指定类型, 所以channel只能传递一种类型的值。channel使用make内置方法声明,分为无缓冲(unbuffered)和缓冲(buffered)的通道。 语法上的区别在于声明时的长度,默认是可读可写的,关键字chan后面跟上相应的类型表示仅可读写此类型的值,加上箭头表示只读或只写。 使用箭头 <- 来读写一个channel,无缓冲的channel常用于同步场景,当向一个无缓冲的channel发送消息,另一端无接收, 或者从一个无缓冲的channel接收消息,另一端无发送时,当前goroutine会阻塞。

1
2
3
4
5
6
7
c0 := make(chan int)       // unbuffered
c1 := make(chan int, 0)    // unbuffered
c2 := make(chan string, 1) // buffered

c3 := make(<-chan int) // can only read
c4 := make(chan<- int) // can only write
c5 := make(chan int)   // can write and read

使用注意

  • 向一个nil channel(只声明未赋值)发送/读取数据,会一直阻塞;
  • 关闭一个nil channel,会导致panic;
  • 只能关闭一个双向或者可写的channel;
  • 向一个已经关闭的channel发送数据,会导致panic;
  • 对于同一个channel,多次调用close(),会导致panic;
  • 对一个已关闭的channel写数据,会导致panic,但可以继续读取数据;
  • 当channel关闭并且缓冲区为空时,继续读取数据不会panic,会读到channel对应类型的0值;
1
2
3
4
5
6
7
8
var ch chan int
close(ch) // 关闭一个nil channel, panic

ch = make(chan int, 1)
close(ch)
<-ch // 可读取已经关闭channel
ch <- 1 // 向已经关闭channel写数据,panic
close(ch) // 多次关闭,panic

使用

无缓冲channel

1
2
3
4
5
6
7
func main() {
    c := make(chan int)    
    go func(ch chan int) {
        ch <- 0
    }(c)
    <-c
}

有缓冲channel

1
2
3
4
5
6
func main() {
    c := make(chan int, 10)
    c <- 1
    c <- 2
    fmt.Println(<-c, <-c)
}

有缓冲的channel只有当缓冲区满或者空,写入和读取的时候才会发生阻塞,所以缓冲的channel可以当做简单的队列来使用

如果在channel为空时再读取一次,则会发生死锁

1
2
3
4
5
6
func main() {
    c := make(chan int, 10)
    c <- 1
    c <- 2
    fmt.Println(<-c, <-c, <-c)
}

运行错误信息: fatal error: all goroutines are asleep - deadlock! 死锁的原因在于channel的另一端没有goroutine写入了,当缓冲的channel为空时,主goroutine会一直阻塞在读取,同时也没有其他goroutine可以调度。

读取超时处理

1
2
3
4
5
6
select {
    case <- time.After(time.Second):
        println("read channel timeout")
    case i := <-ch:
        println(i)
}

写入超时处理

1
2
3
4
5
6
select {
    case <- time.After(time.Second):
        println("write channel timeout")
    case ch <- "Hello":
        println("write ok")
}

遍历

for range

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main() {  
    c := make(chan int, 5)
    for i := 1; i <= 5; i++ {
        c <- i
    }
    close(c)

    for i := range c {
        fmt.Println(i)
    }
}

for range 遍历方式是阻塞型遍历方式; 在遍历时,如果channel 没有关闭,那么会一直等待下去,出现 deadlock 的错误;如果在遍历时channel已经关闭,那么在遍历完数据后自动退出遍历。

for select

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main() {
    c1 := make(chan int)
    c2 := make(chan int)
    c3 := make(chan int, 2)
    go func() {
        c1<-1
    }()
    go func() {
        for i := 0; i < 2; i++ {
            c3 <- i
        }
        close(c3)
    }()
    for {
        select {
        case c2 <- 1:
            fmt.Println("write to c2")
        case <-c1:
            fmt.Println("read from c1")
        case _, ok := <-c3: // 使用channel返回的第二个可选参数来判断channel是否关闭
            if !ok {
                fmt.Println("c3 closed")
                return
            }
            fmt.Println("read from c3")
        }
    }
}

select可以处理非阻塞式消息发送、接收及多路选择; select关键字的灵感来源与unix中的I/O多路复用函数select; select中有case代码块,用于channel发送或接收消息,任意一个case代码块准备好时,执行其对应内容; 多个case代码块准备好时,随机选择一个case代码块并执行;所有case代码块都没有准备好,则等待; 还可以有一个default代码块,所有case代码块都没有准备好时执行default代码块;

channel closing principle

  • 不要从接收端关闭channel,
  • 不要在多个并发发送端关闭channel,
  1. 个receivers,一个sender
    1. sender通过关闭data channel 让sender不再发送数据
    2. 不能让任意receivers来关闭data channel
  2. 一个receivers,n个sender
    1. receiver通过关闭一个额外的signal channel通知sender停止发送数据
    2. 不能让receiver关闭data channel,也不能让任意sender来关闭
  3. m个receivers,n个sender
    1. 通过通知一个moderator(仲裁者)关闭额外的signal channel来停止发送和接收
    2. 不能让任意receivers和senders关闭data channel
    3. 也不能让任意receivers和senders通过关闭一个额外的signal channel,可能导致signal channel多次关闭panic

参考