Go的channel实践(一)

98 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第15天,点击查看活动详情

GO语言中数据的通信有两种,一种是共享变量,这种方式一般需要对共享资源进行加锁,一个Go routine抢到锁之后访问资源,此时其它Go routine(Go routine, Go程,协程是一个概念)对该资源的访问是阻塞的,只有当锁被释放后,其它Go routine才能都其进行操作,这样能够保证数据共享安全。另外一种方式就是信道(Channel),这是更加推荐的一种协程间通信的方式。

信道的本质

信道实际上是一个先进先出的队列,可以在创建信道的时候设置有缓冲还是无缓冲信道。信道本身自带锁机制,对于无缓冲信道,如果一个go程往信道中发送了数据,那么该go程会立刻阻塞,直到其它的go程从信道中读取出数据才能继续运行。对于有缓冲信道,假设是大小为2个int的缓冲信道,只有当信道满了,此时go程往信道中发送数据才会阻塞。

下面是Go语言创建信道的示例

ch1 := make(chan int)     //创建无缓冲信道
ch2 := make(chan int, 2)  //创建大小为2的缓冲信道
ch3 := make(chan<- int)   //只写信道
ch4 := make(<-chan int)   //只读信道

请看下面对信道进行读写的示例

package main

import (
	"fmt"
	"log"
)

func main() {
	ch := make(chan int)
	go func() {
		ch <- 10
	}()
	if n, ok := <-ch; ok {
		fmt.Println(n)
	} else {
		log.Fatal("通道读取失败")
	}
}

上述先是创建了一个无缓冲信道,然后在一个go程中往信道传入数据,此时main的协程是执行到if n, ok := <-ch; ok这一行,它会读取信道中的值。 此外,推荐用下面这种优雅的方式读取信道内容

if n, ok := <-ch; ok {
		fmt.Println(n)
	} else {
		log.Fatal("通道读取失败")
	}

问题:假如ch <- 10不在新的go程中执行,而就在mian的go程中执行,会发生什么? 答:死锁,因为main协程在往无缓冲信道发送数据时会立刻被阻塞,所以无法进入到接收数据代码行执行,造成dead blockd。

信道的close问题

有下面代码:

package main

import (
	"fmt"
	"log"
)

func main() {
	ch := make(chan int)
	go func() {
		ch <- 10
	}()
	if n, ok := <-ch; ok {
		fmt.Println(n)
	} else {
		log.Fatal("通道读取失败")
	}
	fmt.Println(<-ch)
}

注意最后一行fmt.Println(<-ch),执行这行代码时信道已经被读空了,当我们继续读取时,会产生死锁,main的go程会一直阻塞在这一行等待其它协程往里面发送数据,所以产生额dead block。 当我们在ch <- 10添加close(ch)时候,此时fmt.Println(<-ch)能输出0,对!就是int的零值。所以结论就是,当信道关闭的时,从里面读取数据时不会阻塞,而是读出零值(如果chan的类型是bool,那么会读出false)。

取出信道中的所有数据

使用range可以读取信道中所有的数据。但是读取的时候信道必须是关闭状态,否则dead block伺候。 看下面一段代码:

package main

import (
	"fmt"
)

func main() {
	ch := make(chan int)
	go func() {
		for i := 0; i < 5; i++ {
			ch <- i
		}
		close(ch)
	}()

	for n := range ch {
		fmt.Println(n)
	}
}

输出结果:

0
1
2
3
4
只读信道、只写信道以及select关键字

见下一章