Go进阶-通道channel

TOC

了解什么是通道(channel)?

通道可以被认为是Goroutines通信的管道。数据可以在不同的协程和主线程中通过通道传输和接收。
举例来说就是,要传递某个数据给另一个goroutine(协程),可以把这个数据封装成一个对象,然后把这个对象的指针传入某个channel中,另外一个goroutine从这个channel中读出这个指针,并处理其指向的内存对象。

通道使用方法

通道的声明:

// 初始化声明通道,如果通道为nil(就是不存在),就需要先创建通道
var 通道名 chan 数据类型
通道名 = make(chan 数据类型)
// 另外一种简单的声明方式
通道名 := make(chan 数据类型)

通道的数据类型有: int、string、bool、float等

通道的注意点:
Channel通道在使用的时候,有以下几个注意点:

  • 1.用于goroutine,传递消息的。
  • 2.通道,每个都有相关联的数据类型,nil chan,不能使用,类似于nil map,不能直接存储键值对。
  • 3.使用通道传递数据:<- chan <- data,发送数据到通道。向通道中写数据 data <- chan,从通道中获取数据。从通道中读数据。
  • 4.阻塞: 发送数据:chan <- data,阻塞的,直到另一条goroutine,读取数据来解除阻塞 读取数据:data <- chan,也是阻塞的。直到另一条goroutine,写出数据解除阻塞。
  • 5.本身channel就是同步的,意味着同一时间,只能有一条goroutine来操作。

通道传递数据的使用方法:

// 发送数据
a <- 100
// 接收数据
data := <- a

一个简单的通道示例

下面我们用一个示例来简单演示通道的使用,会发现主线程要等待协程执行完耗时任务才会成功接收到通道的发送数据。
【示例】代码如下:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int) // 创建通道
	go func() {
		// 模拟一个耗时的操作任务
		for i := 1; i <= 3; i++ {
			time.Sleep(time.Second)
			fmt.Printf("时间过去 %d 秒\n", i)
		}
		ch <- 1 // 等待上面耗时操作完成,传入值 1 给通道
	}()
	data := <-ch // 等待接受通道传入的值
	fmt.Println("接受成功,接收值:", data)
	fmt.Println("app is over!!!")
}
/* 运行结果如下
时间过去 1 秒
时间过去 2 秒
时间过去 3 秒
接受成功,接收值: 1
app is over!!!
*/

注意:一个通道发送和接收数据,默认是阻塞的。当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个Goroutine从该通道读取数据。相对地,当从通道读取数据时,读取被阻塞,直到一个Goroutine将数据写入该通道。

通道其他使用详情

出现死锁

如果Goroutine在一个通道上发送数据,那么预计其他的Goroutine应该接收数据。如果这种情况不发生,那么程序将在运行时出现死锁。
【示例】代码如下:

package main

func main() {
	ch := make(chan int)
	ch <- 5
}
/* 运行结果如下
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /Users/devops/projects_for_go/Study_for_Go/死锁示例.go:5 +0x34
*/

关闭通道

发送者可以通过关闭信道,来通知接收方不会有更多的数据被发送到channel上。我们也可以使用额外的变量来检查通道是否关闭。
使用语法:

// 关闭通道
close(ch)
// 检查通道情况
data, ok := <- ch
if !ok {
    fmt.Println("通道已经关闭...")
}

【示例】代码如下:

package main

import "fmt"

func main() {
	ch := make(chan int) // 创建通道
	go func() {
		ch <- 100
		close(ch) // 发送数据完成后关闭通道
	}()
	// 循环接受通道的值
	for {
		data, ok := <-ch // 等待接受通道传入的值
		// 判断通道是否关闭,关闭则跳出循环
		if !ok {
			fmt.Println("全部数据接受完成!!!")
			break
		}
		fmt.Println("接受成功,接收值:", data)
	}
	fmt.Println("app is over!!!")
}
/* 运行结果如下
接受成功,接收值: 100
全部数据接受完成!!!
app is over!!!
*/

通道上的范围循环

我们可以循环从通道上获取数据,直到通道关闭。
【示例】代码如下:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int) // 创建通道
	go func() {
		// 模拟一个耗时的操作任务
		for i := 1; i <= 3; i++ {
			time.Sleep(time.Second)
			fmt.Printf("时间过去 %d 秒\n", i)
			ch <- i // 实时传入i的值到通道
		}
		close(ch) // 通道关闭
	}()
	// 循环接受通道传出的值
	for data := range ch {
		fmt.Println("接受成功,接收值:", data)
	}
	fmt.Println("app is over!!!")
}
/* 运行结果如下
时间过去 1 秒
接受成功,接收值: 1
时间过去 2 秒
接受成功,接收值: 2
时间过去 3 秒
接受成功,接收值: 3
app is over!!!
*/

缓冲通道

缓冲通道就是指一个通道,带有一个缓冲区。发送到一个缓冲通道只有在缓冲区满时才被阻塞。类似地,从缓冲通道接收的信息只有在缓冲区为空时才会被阻塞。
设置缓冲区语法:

通道名 := make(chan 数据类型, 缓冲区大小)

注意:容量应该大于0,以便通道具有缓冲区。默认情况下,无缓冲通道的容量为0,因此在之前创建通道时省略了容量参数。

单双向通道

什么是双向通道,什么是单向通道?

  • 双向通道:既可以发送数据,又可以接收数据的通道。
  • 单向通道: 定向通道,限制通道要么只能发送数据,要么只能接收数据。
    我们之间都是使用的双向通道没有做特别的限制。

双向通道示例

这里演示双向通道既可以发送数据,又可以接收数据。
【示例】代码如下:

package main

import "fmt"

func demo(ch chan string, done chan bool) {
	mainData := <-ch // 接受来自main传来的值
	ch <- "我是demo"   // 传入值
	fmt.Println("main传来的值:", mainData)
	done <- true
}

func main() {
	// 创建两个通道分别用于: ch用于主线程和协程接收和传送值,done用于demo任务完成传入值
	ch := make(chan string)
	done := make(chan bool)
	go demo(ch, done)
	ch <- "我是main"   // 传入值
	demoData := <-ch // 接受demo传来的值
	fmt.Println("demo传来的值:", demoData)
	<-done
	fmt.Println("app is over!!!")
}
/* 运行结果如下
demo传来的值: 我是demo
main传来的值: 我是main
app is over!!!
*/

单向通道

使用语法:

// 需要在方法传入的通道对象上定义
// 只能接收数据
func receiveData(ch <-chan int)
// 只能发送数据
func sendData(ch chan<- int)

下面我们用一个示例来写一个只能发送数据或只能接收数据的方法。
【示例】代码如下:

package main

import (
	"fmt"
	"time"
)

// 只发送数据的函数
func sendData(ch chan<- int) {
	for i := 1; i <= 5; i++ {
		fmt.Println("🔺我是发送数据服务,正在发送数据:", i)
		ch <- i // 只能写入
		time.Sleep(500 * time.Millisecond)
	}
	close(ch) // 发送完成后关闭通道
}

// 只接收数据的函数
func receiveData(ch <-chan int) {
	for data := range ch { // 只能读取
		fmt.Println("⭕我是接受数据服务,正在接受数据:", data)
	}
}

func main() {
	ch := make(chan int)
	// 启动发送和接收协程
	go sendData(ch)
	receiveData(ch)
}
/* 运行结果如下
🔺我是发送数据服务,正在发送数据: 1
⭕我是接受数据服务,正在接受数据: 1
🔺我是发送数据服务,正在发送数据: 2
⭕我是接受数据服务,正在接受数据: 2
🔺我是发送数据服务,正在发送数据: 3
⭕我是接受数据服务,正在接受数据: 3
🔺我是发送数据服务,正在发送数据: 4
⭕我是接受数据服务,正在接受数据: 4
🔺我是发送数据服务,正在发送数据: 5
⭕我是接受数据服务,正在接受数据: 5
*/

协程和通道配合使用示例

有了通道之后,我们就可以配合使用来实现读写能够有序地进行。
【示例】代码如下:

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup  // 初始化WaitGroup对象

func writeData(ch chan int) { // 定义写方法,循环写入数据到ch管道
	defer wg.Done()
	for i := 1; i <= 5; i++ {
		ch <- i
		fmt.Printf("我正在写入第 %d 个数据: %d\n", i, i)
		time.Sleep(time.Millisecond * 500)
	}
	close(ch)  // 等待任务完成关闭通道
}

func readData(ch chan int) { // 定义读方法,循环读取ch管道中的数据
	defer wg.Done()
	i := 1
	for data := range ch {
		fmt.Printf("我正在读取第 %d 个数据: %d\n", i, data)
		i++
	}
}

func main() {
	// 定义一个管道,读和写的协程共同操作该管道
	ch := make(chan int, 10)
	// 开启读和写两个协程
	wg.Add(2)
	go readData(ch)
	go writeData(ch)
	wg.Wait() // 等待所有协程执行完成,就停止
	fmt.Println("app is over!!!")
}
/* 运行结果如下
我正在写入第 1 个数据: 1
我正在读取第 1 个数据: 1
我正在写入第 2 个数据: 2
我正在读取第 2 个数据: 2
我正在写入第 3 个数据: 3
我正在读取第 3 个数据: 3
我正在写入第 4 个数据: 4
我正在读取第 4 个数据: 4
我正在写入第 5 个数据: 5
我正在读取第 5 个数据: 5
app is over!!!
*/

select语句

select 是 Go 中的一个控制结构。select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。
select语句说明:

  • 每个case都必须是一个通信
  • 所有channel表达式都会被求值
  • 所有被发送的表达式都会被求值。
  • 如果有多个case都可以运行,select会随机公平地选出一个执行。其他不会执行
    • 否则:
    • 1.如果有default子句,则执行该语句。
    • 2.如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

【示例】代码如下:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	ch2 := make(chan string)
	data := []string{"apple", "banana", "watermelon", "cherry"}

	// 循环向ch1通道传入值
	go func() {
		for i := 1; i <= 5; i++ {
			time.Sleep(time.Millisecond * 500)
			ch1 <- i
		}
		close(ch1)
	}()
	// 循环向ch2通道传入值
	go func() {
		for _, fruit := range data {
			time.Sleep(time.Millisecond * 500)
			ch2 <- fruit
		}
		close(ch2)
	}()
	// 取三次随机数据对比
	for i := 1; i <= 3; i++ {
		// 定义select语句
		select {
		case numData, ok := <-ch1:
			if ok {
				fmt.Println("ch1中取数据 --", numData)
			} else {
				fmt.Println("ch1通道已经关闭。。")
			}
		case strData, ok := <-ch2:
			if ok {
				fmt.Println("ch2中取数据 --", strData)
			} else {
				fmt.Println("ch2通道已经关闭。。")
			}
		}
	}
}
/* 运行结果如下
ch1中取数据 -- 1
ch2中取数据 -- apple
ch2中取数据 -- banana
*/