一、channel(管道) 的基本介绍。
1、channle 本质就是一个数据结构-队列,数据是先进先出【FIFO : first in first out】。
2、channel是线程安全的,多个 goroutine 访问时,不需要加锁。
3、channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。
4、channle 的数据放满后,就不能再放入了,如果从 channel 取出数据后,可以继续放入。
5、 主线程和其他协程读取管道数据不一样的地方:主线程在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock。如果其他协程读取数据会阻塞等待数据输入。所以主线程读取管道数据,如果 channel 数据取完了,再取,这时候管道在其他协程有关闭行为才能保证不死锁。
6、channel(管道)dead lock(死锁)的错误发生的原因有2点,1、一直往管道写数据,但是没有读数据的地方。2、一直读数据,但是没写数据的地方。这个和是主线程还是协程没有关系。
二、定义/声明 channel 的语法:
var 变量名 chan 数据类型
示例:
var intDatas chan int //intDatas 用于存放 int 数据
var mapDatas chan map[int]string //mapDatas 用于存放 map[int]string 类型
var stus chan Student //用于存放Student
var pstus chan *Student //用于存放Student的指针
说明:
1、 channel 是引用类型, channel 必须初始化才能写入数据, 即 make 后才能使用。
2、管道是有类型的。
var intDatas chan int
管道变量intDatas只能写入int类型数据。管道类型是空接口var testChan chan interface{}
,可以存放任意类型。
1.1.1. 三、向管道写数据,读数据。
1、管道是引用类型。
// 创建一个可以存放 3 个 int 类型的管道
var intChan chan int = make(chan int,3)
//intChan 的值=0xc000092080 , intChan的地址=0xc000088018
fmt.Printf("intChan 的值=%v , intChan的地址=%p \n", intChan, &intChan)
可以看到管道的值是一个地址,管道是引用类型。
2、向管道写数据。
var intChan chan int = make(chan int,3)
//向管道写入3个数据。当我们给管写入数据时,不能超过其容量,否则报错死锁deadlock。
intChan<- 10
num := 211
intChan<- num
intChan<- 50
//看看管道的长度和 cap(容量)
fmt.Printf("管道 len= %v cap=%v \n", len(intChan), cap(intChan))// 管道 len= 3 cap=3
说明:
1、向管道写数据,不能超过管道的容量。例如:上面管道的容量是3,写入4个数据,运行就会报错死锁deadlock。除非有一个协程在读数据。
2、len(intChan) 获取管道的长度,也就是管道中现在存了多少个数据。
3、cap(intChan) 获取管道的容量。make给管道分配空间时,分配的容量。
3、读取管道的数据。
var intChan chan int = make(chan int,3)
//向管道写入3个数据。
intChan<- 10
intChan<- 211
intChan<- 50
//从管道中读取数据
num2 := <-intChan
fmt.Println("num2=", num2) //num2= 10
//channel len= 2 cap=3
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan))
说明:
1、在没有使用协程的情况下,如果管道数据已经全部取出,再取就会报错deadlock。
例如:管道中有3个数据,读取4次就会报错死锁deadlock。
2、可以把管道的数据取出,但是不用变量接收。
<-intChan
,把这个数据丢掉。
4、空接口的管道。存放任意类型数据。
type Student struct {
Name string
Age int
}
func main() {
allChan := make(chan interface{},3)
allChan<- Student{Name:"张三",Age:28}
allChan<- 10
allChan<- "你好"
//从管道中读取数据
stuInterface := <-allChan
//类型断言,返回的ok是bool类型,表示是否成功。
stu,ok := stuInterface.(Student)
if ok {
fmt.Println(stu.Name)
}else {
fmt.Println("不是Student类型...")
}
}
使用类型断言
stu,ok := stuInterface.(Student)
,判断从管道取出的数据类型。
1.1.2. 四、channel 的遍历和关闭。
1、关闭channel。
使用内置函数 close 可以关闭 channel。当 channel 关闭后,就不能再向 channel 写数据了,但是仍然可以从该 channel 读取数据。
intChan := make(chan int,3)
intChan<- 10
intChan<- 20
// 关闭管道
close(intChan)
//intChan<- 30 //关闭管道之后再写数据,运行报错 panic: send on closed channel
// 读数据
num := <-intChan
fmt.Println("num =",num) //num = 10
关闭管道之后再写数据,运行会报错 panic: send on closed channel,但是可以读数据。
2、channel 的遍历。
channel 支持 for--range 的方式进行遍历,请注意两个细节:
- 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误。
- 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
nameChan := make(chan string,3)
nameChan<- "张三"
nameChan<- "李四"
nameChan<- "王二"
// 关闭管道后,才能遍历
close(nameChan)
// 遍历输出
for v := range nameChan{
fmt.Println(v)
}
必须关闭管道之后,才能遍历读数据。否则报错。
1.1.3. 五、管道数据的读取要点(防止死锁)。
1、主线程中读取。
nameChan := make(chan string,3)
close(nameChan) // 关闭管道后,可以任意读取。
for v := range nameChan{
fmt.Println(v)
}
// 读取数据,使用标识ok,判断是不是读到了数据。
num,ok:= <-nameChan
if ok {
fmt.Println("读取数据成功,num=",num)
}else {
fmt.Println("读取数据失败") //读取数据失败
}
上面代码中,读取管道数据:
1、close(nameChan) 关闭管道后,可以任意读取管道的数据。即使没有数据,也可以再读。不会死锁。
2、如果管道不关闭,不能使用for--range 的方式进行遍历(因为会一直循环读取数据,如果其他地方没有关闭管道,会发生死锁);可以使用普通方式读取,但是管道没有数据,再继续读,就会报错死锁。
3、管道关闭之后,使用
num,ok := <-nameChan
读取数据;然后使用标识ok,判断是不是读到了数据,没读到数据表示到管道末尾了。
2、协程(goroutine)中读取管道(channel)数据。
在协程中读取channel数据,无论channel是否关闭,都不会出错。
- 如果channel是开启的:使用for--range 的方式式进行遍历会一直阻塞,直到管道关闭,读取完所有的数据。使用或普通方式读取,如果有数据就读取到数据,如果没数据也会一直阻塞,直到进入新的数据完成这一次读取或者管道关闭。
- 如果channel关闭:无论使用for--range 的方式还是普通方式,直接读取。不会出错。
//管道
nameChan := make(chan string,3)
//协程
go func() {
nameChan<- "张三"
for v := range nameChan{
fmt.Println("读取数据成功,name=",v)
}
fmt.Println("协程 end......")
}()
for {
time.Sleep(time.Second * 3)
fmt.Println("first sleep ......")
break
}
nameChan<- "222"
nameChan<- "3333"
nameChan<- "4444"
close(nameChan) // 关闭管道
for {
time.Sleep(time.Second * 3)
break
}
fmt.Println("主线程end。。。。")
输出如下:
读取数据成功,name= 张三
first sleep ......
读取数据成功,name= 222
读取数据成功,name= 3333
读取数据成功,name= 4444
协程 end......
主线程end。。。。
3、具体有没有死锁出现。还得看具体问题。
channel(管道)死锁发生的原因有2点,1、一直往管道写数据,但是没有读数据的地方。2、一直读数据,但是没写数据的地方。
这个和是主线程还是协程没有关系。
func main() {
//管道
nameChan := make(chan string,3)
// 协程
go func() {
time.Sleep(time.Second * 3)
nameChan<- "aaa"
close(nameChan)
}()
// 循环
for {
fmt.Println("in for loop....")
name,ok := <-nameChan
if ok {
fmt.Println("name==",name)
break
}else {
fmt.Println("没有读到。。。。")
}
}
}
运行结果如下:
in for loop....
name== aaa
总结:主线程的读取操作
name,ok := <-nameChan
阻塞等待协程对nameChan管道的操作。如果协程中没有对nameChan管道的操作,就会报死锁错误。因为主线程一直读取,没有其他地方写入或者关闭管道。