Skip to content
On this page

通道(channel)

  • 通道可以理解为Go协程通信的管道

  • 通道是队列式的数据结构,遵循先入先出的原则

  • 每个通道都只能传递一种数据类型的数据,在声明channel的时候,需要指定通道的类型

  • 通道的零值nil

通道的声明和初始化

  • 可以使用var关键字先声明一个channel,再使用make函数初始化

  • 也可以直接使用简短声明make来声明并初始化一个channel

格式如下:

Go
// 第一种方式
var chan_name chan_type // 声明
channel_name = make(chan chan_type, [bufferSize]) // 初始化

// 第二种方式
// 声明并初始化
channel_name := make(chan chan_type, [bufferSize])

代码示例

Go
func createChan() {
	// 声明一个string类型的通道,并未初始化
	var name chan string
	// 声明但并未初始化的chan的值为 <nil>
	fmt.Println("声明但并未初始化的chan的值为", name)
	// 初始化chan name
	name = make(chan string)
	// 初始化后的chan的值为 0x1400006a060
	fmt.Println("初始化后的chan的值为", name)

	// 声明并初始化一个chan
	age := make(chan int)
	// 声明并初始化一个chan,值为 0x1400006a0c0
	fmt.Println("声明并初始化一个chan,值为", age)
}

func main() {
	createChan()
}

使用通道发送和接收数据

  • 往通道发送数据
Go
channel_name <- data
  • 从通道接收数据
Go
// 直接发送出去数据
<- channel_name
// 或者 发送并赋值给某个变量
variable_name := <- channel_name

代码示例

Go
func setChanValue(val chan string) {
	val <- "张环"
}

func sendAndReceiveData() {
	fmt.Println("start")
	str := make(chan string)
	go setChanValue(str)
	receiveValue := <- str
	fmt.Println("已经准备好接收数据了")
	fmt.Println("接收到通道的数据receiveValue为", receiveValue)
}

func main() {
	sendAndReceiveData()
	// 打印如下
	// start
	// 准备好接收数据了
	// 接收到通道的数据receiveValue为 张环
}

关闭通道

  • 通道使用完毕后需要将其关闭,使用close方法来关闭通道

  • 对于一个已经关闭的通道如果再次关闭会导致报错

TIP

  • 从channel中读取数据返回2个值val, ok := <- chan,返回的第二个值ok表示channel是否关闭,并且channel是否为空

  • 如果channel已经关闭,并且channel为空,里面没有元素了,ok会返回false,否则返回true

Go
close(channel_name)

代码示例

Go
func setChanValue(val chan string) {
	defer close(val)
	val <- "张环"
	val <- "李朗"
}

func sendAndReceiveData() {
	fmt.Println("start")
	str := make(chan string)
	go setChanValue(str)
	for {
		val, isClose := <- str
		if(!isClose) {
			fmt.Println("通道关闭")
			break
		}
		fmt.Println("从通道中获取的数据", val)
	}
}

func main() {
	sendAndReceiveData()
	// 打印如下
	// start
	// 从通道中获取的数据 张环
	// 从通道中获取的数据 李朗
	// 通道关闭
}

通道的容量和长度

  • make函数接受两个参数,第一个参数是通道的类型,第二个参数bufferSize通道的缓冲区大小(可选)

  • bufferSize参数表示通道的缓冲区大小,它可以设置为01大于1的整数,分别代表不带缓冲区带有一个缓冲区带有多个缓冲区的通道

  • bufferSize0:表示创建一个无缓冲通道。在无缓冲通道中,发送者和接收者必须同时准备好,否则会造成阻塞

  • bufferSize1:表示创建一个带有一个缓冲区的通道

    • 在带有一个缓冲区的通道中,发送者可以向通道中发送一个数据,而不会被阻塞,除非缓冲区已满。当缓冲区已满时,发送者会被阻塞,直到有接收者接收数据

    • 同样,当接收者从带有一个缓冲区的通道接收数据时,如果缓冲区为空接收者会被阻塞,直到有发送者发送数据

  • bufferSize大于1:表示创建一个带有多个缓冲区的通道

    • 在带有多个缓冲区的通道中,缓冲区可以存储多个数据,发送者可以一次性向通道中发送多个数据,直到缓冲区已满。当缓冲区已满时,发送者会被阻塞,直到有接收者接收数据

    • 同样,当接收者从带有多个缓冲区的通道接收数据时,如果缓冲区为空接收者会被阻塞,直到有发送者发送数据

Go
func chanLenCap() {
	str := make(chan string, 3)
	fmt.Println("初始化后:")
	fmt.Println("str的len", len(str))
	fmt.Println("str的cap", cap(str))
	str <- "张环"
	fmt.Println("放入一个数据后:")
	fmt.Println("str的len", len(str))
	fmt.Println("str的cap", cap(str))
	str <- "李朗"
	fmt.Println("放两个数据后:")
	fmt.Println("str的len", len(str))
	fmt.Println("str的cap", cap(str))
	<- str
	fmt.Println("取出一个数据后:")
	fmt.Println("str的len", len(str))
	fmt.Println("str的cap", cap(str))	
}

func main() {
	chanLenCap()
	// 初始化后:
	// str的len 0
	// str的cap 3
	// 放入一个数据后:
	// str的len 1
	// str的cap 3
	// 放两个数据后:
	// str的len 2
	// str的cap 3
	// 取出一个数据后:
	// str的len 1
	// str的cap 3
}

单向通道

  • 上面的通道都是双向通道,既可以发送数据也可以接收数据

  • 单向通道只能发送或者接收数据,具体细分为只读通道只写通道

只读通道

  • <-chan表示只读通道

只写通道

  • chan<-表示只写通道

WARNING

  • 根据Go语言的语法规范,只读通道类型只写通道类型定义时是无法直接使用make()函数来创建通道`的
  • 只读通道只写通道都是基于普通的双向通道类型创建的,并在使用时通过类型转换来限制其操作的权限

示例

Go
func oneWayChannel() {
	// 定义可读通道
	// 这种定义方法也可以
	// type ReaderChan <-chan string
	type ReaderChan = <-chan string
	// 定义可写通道
	// 这种定义方法也可以
	// type WriterChan chan<- string
	type WriterChan = chan<- string

	// 声明并初始化一个双向通道
	bothChan := make(chan string)

	go func(){
		// 只写通道
		// 这种定义方法也可以
		// var writerChan WriterChan = bothChan 
		writerChan := WriterChan(bothChan)
		fmt.Println("准备写入数据")
		writerChan <- "张环、李朗"
	}()
	go func(){
		// 只读通道
		// 这种定义方法也可以
		// var readerChan ReaderChan = bothChan 
		readerChan := ReaderChan(bothChan)
		fmt.Println("准备读取数据")
		msg := <- readerChan
		fmt.Println("读取到的数据为", msg)
	}()
	time.Sleep(time.Second * 1)
	fmt.Println("===================")
}

func main() {
	oneWayChannel()
	// 打印如下
	// 准备写入数据
	// 准备读取数据
	// 读取到的数据为 张环、李朗
	// ===================
}

遍历通道

  • 通道需要在遍历时被关闭,否则将会一直阻塞

  • 可以使用for range来遍历通道

示例

Go
func traverseChannel() {
	ch := make(chan int)
	// 生产者,向通道中发送数据
	go func() {
		for i := 1; i < 7; i++ {
			ch <- i
		}
		close(ch) // 关闭通道
	}()
	// 消费者,从通道中接收数据
	for num := range ch {
		fmt.Println("接收到的数据为=>", num)
	}
}

func main() {
	traverseChannel()
	// 打印如下:
	// 接收到的数据为=> 1
	// 接收到的数据为=> 2
	// 接收到的数据为=> 3
	// 接收到的数据为=> 4
	// 接收到的数据为=> 5
	// 接收到的数据为=> 6
}

用通道做锁

  • 当通道容量为1时,说明通道只能缓存一个数据,若通道中已有一个数据,此时再往里发送数据,会造成程序阻塞

示例

Go
func increment(num chan string, idx *int) {
	num <- "蛇灵"
	*idx += 1
	<- num
}

// 使用容量为 1 的通道可以达到锁的效果
func useChannelLock() {
	num := make(chan string, 1)
	idx := 0
	fmt.Println("------start------")
	for i := 0; i < 100000; i++ {
		go increment(num, &idx)
	}
	time.Sleep(time.Second)
	fmt.Println("idx值为", idx)
	fmt.Println("------end------")
}

func main() {
	useChannelLock()
	// 打印如下
	// ------start------
	// idx值为 100000
	// ------end------
}

死锁

Go
fatal error: all goroutines are asleep - deadlock!

Go中,以下情况可能会导致通道死锁:

  • 发送接收操作阻塞,没有足够的goroutine来处理
Go
func main() {
	ch := make(chan int)
	// 没有足够的 goroutine 来接收数据,发送操作将阻塞
	ch <- 1
}
  • 通道的缓冲区已满,发送操作阻塞,但没有对应的接收操作
Go
func main() {
	ch := make(chan int, 1)
	ch <- 1 
	ch <- 2 // 缓冲区已满,发送操作阻塞
}
  • 通道为,接收操作阻塞,但没有对应的发送操作
Go
func main() {
	ch := make(chan int)
	<-ch // 通道为空,接收操作阻塞
}

WaitGroup

  • WaitGroupGo语言标准库sync包中提供的一种并发同步机制,用于等待一组goroutine完成任务。WaitGroup可以帮助我们在主goroutine中等待其他子goroutine执行完毕,再继续执行后续的代码,从而保证所有的goroutine都完成了任务再退出程序

  • WaitGroup的主要作用是:

    • 统计并等待一组goroutine完成任务。
    • 防止主goroutine提前退出,确保其他子goroutine完成任务后再退出
  • WaitGroup提供了三个方法:

    • Add(delta int):增加计数器的值,delta正数时表示添加delta个等待任务,为负数时表示减少delta个等待任务
    • Done():减少计数器的值,等价于Add(-1),通常使用defer来调用
    • Wait():阻塞调用的goroutine,直到计数器的值变为0

WARNING

如果WaitGroup显式传递到函数中,则应使用指针`

Go
func task(id int, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Printf("执行第%d条任务\n", id)
}

func main() {
	var wg sync.WaitGroup
	wg.Add(6)
	// 启动六个子 goroutine
	go task(1, &wg)
	go task(2, &wg)
	go task(3, &wg)
	go task(4, &wg)
	go task(5, &wg)
	go task(6, &wg)
	// 等待所有子 goroutine 完成
	wg.Wait()
	// 所有子 goroutine 完成后,执行后续操作
	fmt.Println("所有子goroutine已完成任务,可以继续执行后续操作")
}