Channels · Go语言圣经 (studygolang.com)深入理解Go语言中的Channel:用法、特性和最佳实践 - 知乎 (zhihu.com)
如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。 一个可以发送int类型数据的channel一般写为chan int。
使用内置的make函数,我们可以创建一个channel:
go
复制代码
ch := make(chan int) // ch has type 'chan int'
和map类似,channel也对应一个make创建的底层数据结构的引用。当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。 和其它的引用类型一样,channel的零值也是nil。
两个相同类型的channel可以使用 == 运算符比较。如果两个channel引用的是相同的对象,那么比较的结果为真。一个channel也可以和nil进行比较。
一个channel有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都使用<-
运算符。在发送语句中,<-
运算符分割channel和要发送的值。在接收语句中,<-
运算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的。
go
复制代码
ch <- x // a send statement x = <-ch // a receive expression in an assignment statement <-ch // a receive statement; result is discarded
Channel还支持close操作,用于关闭channel,随后对基于该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值的数据。
以最简单方式调用make函数创建的是一个无缓存的channel,但是我们也可以指定第二个整型参数,对应channel的容量。如果channel的容量大于零,那么该channel就是带缓存的channel。
go
复制代码
ch = make(chan int) // unbuffered channel ch = make(chan int, 0) // unbuffered channel ch = make(chan int, 3) // buffered channel with capacity 3
不带缓存的Channels
一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels。
go
复制代码
func main() { done := make(chan struct{}) go func() { log.Println("done") done <- struct{}{} // signal the main goroutine }() <-done // wait for background goroutine to finish }
单方向的Channel
当一个channel作为一个函数参数时,它一般总是被专门用于只发送或者只接收。 为了表明这种意图并防止被滥用,Go语言的类型系统提供了单方向的channel类型,分别用于只发送或只接收的channel
因为关闭操作只用于断言不再向channel发送新的数据,所以只有在发送者所在的goroutine才会调用close函数,因此对一个只接收的channel调用close将是一个编译错误。
go
复制代码
func counter(out chan<- int) { for x := 0; x < 100; x++ { out <- x } close(out) } func squarer(out chan<- int, in <-chan int) { for v := range in { out <- v * v } close(out) } func printer(in <-chan int) { for v := range in { fmt.Println(v) } } func main() { naturals := make(chan int) squares := make(chan int) go counter(naturals) go squarer(squares, naturals) printer(squares) }
带缓存的Channels
带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。
- 创建
go
复制代码
ch = make(chan string, 3)
- 入队
go
复制代码
ch <- "A"
- 出队
go
复制代码
fmt.Println(<-ch) // "A"
- 查容量
go
复制代码
fmt.Println(cap(ch)) // "3"
- 查长度
go
复制代码
fmt.Println(len(ch)) // "2"
goroutines泄漏
go
复制代码
func mirroredQuery() string { responses := make(chan string, 3) go func() { responses <- request("asia.gopl.io") }() go func() { responses <- request("europe.gopl.io") }() go func() { responses <- request("americas.gopl.io") }() return <-responses // return the quickest response } func request(hostname string) (response string) { /* ... */ }
如果我们使用了无缓存的channel,那么两个慢的goroutines将会因为没有人接收而被永远卡住。这种情况,称为goroutines泄漏,这将是一个BUG。和垃圾变量不同,泄漏的goroutines并不会被自动回收,因此确保每个不再需要的goroutine能正常退出是重要的。
goroutines能当成队列使用吗?
答案是否定的!将一个带缓存的channel当作同一个goroutine中的队列使用,虽然语法看似简单,但实际上这是一个错误。Channel和goroutine的调度器机制是紧密相连的,如果没有其他goroutine从channel接收,发送者——或许是整个程序——将会面临永远阻塞的风险。如果只是需要一个简单的队列,使用slice就可以了。
通道的发送和接收特性
- 线程安全对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。channel是线程安全的,多个协程可以同时读写一个channel,而不会发生数据竞争的问题。这是因为Go语言中的channel内部实现了锁机制,保证了多个协程之间对channel的访问是安全的。
- 阻塞式发送和接收当一个协程向一个channel发送数据时,如果channel已经满了,发送操作会被阻塞,直到有其他协程从channel中取走了数据。接收操作同样。这种阻塞式的机制可以保证协程之间的同步和通信。
- 顺序性通过channel发送的数据是按照发送的顺序进行排列的。
- 可以关闭通过关闭channel可以通知其他协程这个channel已经不再使用了。 关闭一个channel之后,其他协程仍然可以从中接收数据,但是不能再向其中发送数据了。关闭channel的操作可以避免内存泄漏等问题。