channel是什么
channel中文翻译为通道,它是Go语言内置的数据类型,使用channel不需要导入任何包,像int/float一样直接使用。它主要用于goroutine之间的消息传递和事件通知。
在Go语言中流传着一句话,就是说不要通过共享内存来通信,而是应该通过通信来共享内存。
Don’t communicate by sharing memory, share memory by communicating.
上面这句话也包含了通信的两种方式:1是通过共享内存 2是通过通信。channel来源灵感要追溯到CSP(communicating sequential process)模型,CSP是用来描述并发系统中进行交互的一种模式。CSP概念最早由Tony Hoare提出,对这个人我们可能很陌生。但说到快速排序算法(Quicksort),你一定不陌生,快速排序太经典了,对Tony Hoare就是快速排序算法的作者。CSP允许使用进程组件来描述系统,各个组件独立运行,它们之间通过消息传递的方式进行通信。
Go语言中使用channel实现CSP思想,整个channel实现只有短短的700行代码,非常精炼,非常值得一读。channel和goroutine的结合,为并发编程提供了优雅的、便利的、 与传统并发控制不同的方案,并产生了在Go中特有的并发模式。
为什么需要channel
channel不是必需的,不用channel也可以完成goroutine之间的消息传递和事件通知,比如通过共享变量的方式。但使用了channel,会大大提升开发的效率,因为channel是并发安全的,channel的设计与goroutine之间完美配合,降低了并发编程的难度,减少了data race产生,大幅提升了生产力,嗯,程序员的福音。
channel基本用法
channel创建
channel有3种类型,分别为只能接收、只能发送、既能接收又能发送。定义方法如下:
func main() {
// readChan只能接收
var readChan <-chan int
// writeChan只能发送
var writeChan chan<- int
// rwChan既能接收又能发送
var rwChan chan int
fmt.Println(readChan == nil) //true
fmt.Println(writeChan == nil) //true
fmt.Println(rwChan == nil) //true
}
channel中的元素对类型没有限制,任意类型都可以,所以元素的类型也可以是chan类型。哪怎么判断<-属于哪个chan呢? <-的匹配规则是总是尽量与它左边的chan结合(the <-operator associates with the leftmost chan possible)。所以writeIntChan是一个只能发送的chan,发送的元素为chan int(双向的int型chan), readIntChan是一个只能接收的chan,发送的元素为chan int, rwIntChan是一个双向的chan, 发送的元素为chan int.
func elemIsChan(){
// chan元素类型也可以是channel
var writeIntCahn chan<- chan int
var readIntChan <- chan chan int
var rwIntChan chan chan int
}
channel定义完成之后,并不能直接使用,需要初始化。上面readChan/writeChan/rwChan都是未被初始化的,它们的值都是nil.
channel用make初始化,不能用new方法。make创建的时候可以传一个数字,表示创建有缓冲区的chan, 如果没有设置,它是无缓冲区的。
func makeChan(){
// 无缓冲区的chan
unbufferedCh:=make(chan int)
// 有缓冲区的chan,可以缓存10个int数据
bufferedCh:=make(chan int,10)
}
向channel中发送数据
往chan中发送一个数据使用“ch<-”,下面的操作往ch发送一个int数据200. ch是一个双向的chan,可以往里面发送数据。 ch2是一个只能发送的ch,也可以往里面发送数据。
func sendDataToChan() {
ch := make(chan int, 1)
ch <- 200
ch2 := make(chan<- int, 1)
ch2 <- 100
}
从channel中取数据
使用<-ch从chan中接收一条数据,ch是一个双向chan或者只读chan.下面的操作从ch读取数据,ch2是一个只读chan,也可以进行读取数据的操作。
chan接收操作,可以返回一个值可以可以返回两个值,第一个值是返回chan中的元素,第二个值是个bool类型,表示是否成功地从chan中读取到了一个值。如果chan已经被关闭而且所有的数据都已经读完,第一个值将是零值。需要注意的是,如果从chan读取到一个零值,可能是sender真实的在发送零值,也有可能是chan被关闭且没有缓存的元素了产生的零值。
func recvDataFromChan() {
// 双向chan
ch := make(chan int, 1)
<-ch
// 只读chan
ch2 := make(<-chan int, 1)
<-ch2
// 只读取数据
_=<-ch2
// 读取数据并想知道ch2是否已关闭
_,_=<-ch2
}
关闭channel
close(ch)直接将一个chan关闭,需要注意的是,如果一个chan未被初始化,也就是没有执行make操作,是不能close的,否则引发panic.还有就是不能重复关闭一个chan,重复关闭一个chan也会产生panic.还有就是不能往一个关闭的chan中发送数据,也会产生panic. 最后一个需要注意的是不能close一个只读的chan,直接编译不会通过。
func closeNilChan() {
var ch chan int
close(ch)
//panic:close of nil channel
}
func closeOnlyReadChan() {
var ch <-chan int
ch = make(<-chan int)
close(ch)
// invalid operation: close(ch) (cannot close receive-only channel)
}
其他操作
Go内置的cap、len都可以操作chan,cap返回chan的容量,len返回chan中缓存的还未被取走的元素数量。还可以在select case中向chan发送数据或从chan中接收数据。
for-range操作chan,从chan读取数据。当ch被close之后,for-rang循环都会结束。
func forRangeChan() {
ch := make(chan int, 1)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
// ch被close后,for-range会结束循环
for v := range ch {
fmt.Println(v)
}
fmt.Println("for-range end")
}()
ch <- 1
close(ch)
wg.Wait()
}
func forRangeChan2() {
ch := make(chan int, 1)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
// ch被close后,for-range结束循环
for range ch {
}
fmt.Println("for-range end")
}()
close(ch)
wg.Wait()
}
channel实现原理
下面介绍channel底层是怎么实现的,源码在runtime/chan.go文件中。chan分配的是一个hchan的数据结构,定义如下:
const (
maxAlign = 8
// hchanSize为8的倍数,如果不是调整到下一个最小的8的倍数,假如hchan大小9,hchanSize为16
hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1))
debugChan = false
)
type hchan struct {
// 队列中元素的个数
qcount uint
// 环形队列的大小
dataqsiz uint
// 指向含有dataqsiz个元素的数组的地址
buf unsafe.Poin