golang并发编程-04-通道-01-基本使用/缓冲通道/非缓冲通道
@
1. 通道的基本使用
- 同一时间,仅允许一个协程对其写入/读出
- 严格排序,先进先出
- 通道元素有原子性
比如:放进一个 []string,也不能单次取出一个 string
- 已被接收的元素值会立刻被从通道中删除
1.1 声明通道
- 语法
type 通道名 chan 通道中数据类型
- 示例
type ch1 chan int
- 值表示法
通道类型是引用类型,所以被初始化之前它的值一定是nil。
1.2 初始化通道
1)初始化方法
- 语法
make(chan 通道中数据类型[, N])
N表示同一时刻最多可以容纳N个元素值
- 示例
make (chan int,10)
2)缓冲/非缓冲通道
- 缓冲通道:始化时给定了第二个参数的通道
- 非缓冲通道:初始化时未给定第二个参数
一个通道类型的值的缓冲容量是固定不变的,发送给非缓冲通道的的元素值必须被立刻取走。
1.3 发送元素值
- 语法
通道名 <- 变量名|值
- 示例
c <- "aaa"
- 示例
input函数将数组names传入通道
func input( c chan string) { names := []string{"关羽", "张飞", "赵云", "马超","黄忠"} for _,x := range names { c <- x } close(c) } func main() { c := make(chan string, 10) input(c) for i := range c { fmt.Printf("hello %s \n",i) } time.Sleep(3*time.Second) }
-
通道写满
如果通道写满,向通道中写入的协程将被阻塞,直到通道中数据被接收时该协程被唤醒。 -
传入通道的值仅是副本
变量将值传给通道之后,变量值变化与通道中数据无关。
func main() { name := "关羽" c := make(chan string,3) defer close(c) c <- name name = "张飞" if i,ok := <- c;ok { fmt.Println("channel :",i) fmt.Println("name :",name) } }
输出
channel : 关羽 name : 张飞
可见 name 改为张飞之后,channel里的值依然是关羽。
1.4 接收元素值
- 语法
变量名[,布尔值] := <-通道名
示例
name := <- c
或
name,ok := <- c
ok值:如果读出数据为true ,未读出为false(比如通道中没有数据了)
ok值的作用:作为一个布尔值,可用作之后的if判断,如下:
name,ok := <- c if ok{ go fmt.Println(name) }else { break }
- 示例
func input( c chan string) { names := []string{"关羽", "张飞", "赵云", "马超","黄忠"} for _,x := range names { c <- x } close(c) } func main() { c := make(chan string, 10) input(c) for { name,ok := <- c if ok{ go fmt.Println(name) }else { break } } time.Sleep(3*time.Second) }
打印结果
没有顺序,五个协程谁先打印出结果看命。
黄忠 关羽 张飞 赵云 马超
- 从未初始化的通道接收数据
此时通道会被永远阻塞
1.5 关闭通道
- 语法
close(通道名)
示例
close(c1)
-
关闭时机
建议在输入端关闭
通道关闭之后仍可以读出 -
示例
一个通道
两个函数 input()写,output()读,
两个协程分别执行读写函数
关闭通道在写函数中
func input( c chan string) { names := []string{"关羽", "张飞", "赵云", "马超","黄忠"} for _,x := range names { c <- x } close(c) } func output (c chan string ){ for i := range c { fmt.Println("hello ",i) } } func main() { c := make(chan string, 1) go input(c) go output(c) time.Sleep(3*time.Second) }
1.6 通道的长度与容量
1)概念
- len() :表示通道中目前有多少个元素
- cap():表示通道可以容纳多少个元素
2)演示
我们将上例修改一下,初始化的时候,设定通道容量为8,打印长度和容量,然后将读取调用注释掉。
func input( c chan string) { names := []string{"关羽", "张飞", "赵云", "马超","黄忠"} for _,x := range names { c <- x } close(c) } func output (c chan string ){ for i := range c { fmt.Println("hello ",i) } } func main() { c := make(chan string,8) input(c) //output(c) fmt.Println("通道长度是: ",len(c)) fmt.Println("通道容量是: ",cap(c)) time.Sleep(3*time.Second) }
输出
通道长度是: 5 通道容量是: 8
输入的5个元素都停留在通道中,所以长度是5。
而通道容量是我们初始化时候的容量。
- 将读取打开
func main() { c := make(chan string,8) input(c) fmt.Println("通道长度是: ",len(c)) fmt.Println("通道容量是: ",cap(c)) output(c) fmt.Println("读取后通道长度是: ",len(c)) fmt.Println("读取后通道容量是: ",cap(c)) time.Sleep(3*time.Second) }
输出
通道长度是: 5 通道容量是: 8 hello 关羽 hello 张飞 hello 赵云 hello 马超 hello 黄忠 读取后通道长度是: 0 读取后通道容量是: 8
可见,读取后通道长度变为0,而通道容量不变。(有兴趣的话你可以打印每次写入/读取时的通道长度)
2. 单项通道
2.1 发送通道和接收通道
- 概念:单向通道可分为接收通道和发送通道
- 作用:一个仅发送,一个仅接收(否则报错)。
- 语法
发送通道:
通道名 chan<- 传入数据类型
传出通道:
通道名 <-chan 传出数据类型
- 示例
var in chan<- string var out <-chan string
- 意义:
在函数中将通道定义为单项,避免函数对通道造成污染。
2.2 使用单项通道
示例1
说明:
- 定义输入函数,形参为发送通道
- 定义传出函数,形参为接收通道
- 主函数中定义一个双向通道
- 主函数调用传入函数,该通道为实参(该函数中为发送通道)。
- 主函数调用传出函数,该通道位实参(该函数中为接收通道)。
- 完整代码如下
func main() { chan1 := make(chan string,10) go input(chan1) //调用发送通道函数 go output(chan1) //调用接收通道函数 time.Sleep(1*time.Second) } func input(in chan<- string) { names := []string{"关羽", "张飞", "赵云", "马超","黄忠"} for _,i := range names { in <- i } close(in) } func output(out <-chan string) { for i := range out { fmt.Println("hello ", i) } }
- 输出
hello 关羽 hello 张飞 hello 赵云 hello 马超 hello 黄忠
- 不使用协程
作为缓冲通道,只要通道容量给的足够,我们可以不使用协程,而非缓冲通道则必须使用协程了。
func main() { chan1 := make(chan string,10) go input(chan1) //调用发送通道函数 go output(chan1) //调用接收通道函数 time.Sleep(1*time.Second) }
示例2
说明:
如果我们要求传入传出两个通道分开(chan1、chan2),那么我们还需要用一个函数将chan1的数据写入chan2。
- 定义输入函数,形参为发送通道
- 定义传出函数,形参为接收通道
- 定义转换函数,形参为发送函数和接收函数
- 主函数中定义两个双向通道chan1、chan2
- 主函数调用传入函数,以chan1位实参(chan1为发送通道)。
- 主函数调用传出函数,以chan2位实参。(chan2为接收通道)
- 主函数调用转换函数,实参和调用传出、传入函数相反(即:chan2为发送通道,chan1为接收通道),从而将chan1的数据写入chan2.
- 完整示例
func main() { chan1 := make(chan string) chan2 := make(chan string) go input(chan1) //调用发送通道函数 go in2out(chan2, chan1) //将发送通道的数据写入接收通道 go output(chan2) //调用接收通道 time.Sleep(1*time.Second)函数 } func input(in chan<- string) { names := []string{"关羽", "张飞", "赵云", "马超","黄忠"} for _,i := range names { in <- i } close(in) } func output(out <-chan string) { for i := range out { fmt.Println("hello ", i) } } func in2out(out chan<- string, in <-chan string) { for i:= range in { out <- i } close(out) }
3. 循环和语句中的通道
3.1 for循环
-
前边示例一直在用,注意一下通道未初始化的问题,其他不多说了。
-
另外,下边这个示例读写我没用协程,注意通道容量要给够,否则报错。(当然使用协程不存在这个问题)
package main import ( "errors" "fmt" "time" ) func input( c chan string) error{ names := []string{"关羽", "张飞", "赵云", "马超","黄忠"} for _,x := range names { if c == nil { return errors.New("通道为nil") } c <- x } close(c) return nil } func output (c chan string ) error{ if c == nil { return errors.New("通道为nil") } for i := range c { fmt.Println("hello ",i) } return nil } func main() { c := make(chan string, 8) //var c chan string //注释掉初始化,打开本行看报错 err := input(c) if err != nil { fmt.Println(err) } err = output(c) if err != nil { fmt.Println(err) } time.Sleep(3*time.Second) }
3.2 select语句
作用:通道的选择
示例
-
要求:
1)两个协程分别向两个通道中写入蜀将和魏将的名字
2)将结果分别打印出来。 -
思路:
创建两个函数,分别向两个通道c1、c2中写。
主函数启动两个协程,分别调用两个写函数。
主函数启动一个协程(避免for的死循环)分别打印两个通道中的数据 -
代码:
func inputShu( c chan string) { names := []string{"关羽", "张飞", "赵云", "马超","黄忠"} for _,x := range names { c <- x } close(c) } func inputWei( c chan string) { names := []string{"张辽", "乐进", "于禁", "张合","徐晃"} for _,x := range names { c <- x } close(c) } func main() { c1 := make(chan string,8) c2 := make(chan string,8) go inputShu(c1) go inputWei(c2) go func() { for { time.Sleep(500*time.Millisecond) select { case msg1,ok := <-c1: //if msg1 == ""{continue} if ok { fmt.Println("蜀将: ", msg1) } case msg2,ok := <-c2: if ok { fmt.Println("魏将: ", msg2) } default: fmt.Println("hello world") } } }() time.Sleep(10*time.Second) }
输出:
蜀将: 关羽 魏将: 张辽 蜀将: 张飞 魏将: 乐进 蜀将: 赵云 蜀将: 马超 魏将: 于禁 蜀将: 黄忠 魏将: 张合 魏将: 徐晃
4 非缓冲通道
4.1 概述
概念:
容量设置成0,或者直接忽略对容量的设置的通道
特点:
只能同步的传递发送给它的元素值
非缓冲通道的“happens before”原则:
- 向此类通道发送元素值的操作会被阻塞,直到至少有一个针对该通道的接收操作开始进行为止。
- 从此类通道接收元素值的操作会被阻塞,直到至少有一个针对该通道的发送操作开始进行为止。
- 针对非缓冲通道的接收操作会在与之相对应的发送操作完成之前完成。
发送操作会等待接收操作完成后结束
func main() { unbufChan := make(chan string) go func() { fmt.Println("( • - •) 孔明在睡觉") time.Sleep(5*time.Second) fmt.Println("很久以后~~~~~~~孔明睡醒了") massage := <-unbufChan fmt.Printf("( • - •) 孔明收到了 《%s》\n", massage) }() massage:= "先生请出山" fmt.Printf("[ x - x] 主公来了 %s\n", massage) unbufChan <- massage fmt.Println("[ x - x] 主公走了") }
输出:
[ x - x] 主公来了 先生请出山 ( • - •) 孔明在睡觉 很久以后~~~~~~~孔明睡醒了 ( • - •) 孔明收到了 《先生请出山》 [ x - x] 主公走了
执行可见,主函数的输入等待到协程读取通道之后才结束。
4.2 单向的非缓冲通道
和缓冲通道中的使用几乎相同
值得关注的是:
- 非缓冲通道必须使用协程
- 缓冲通道在容量够大的情况下,一个协程就可以完成读写。
func main() { chan1 := make(chan string) input(chan1) //调用发送通道函数 output(chan1) //调用接收通道函数 time.Sleep(1*time.Second) } func input(in chan<- string) { names := []string{"关羽", "张飞", "赵云", "马超","黄忠"} for _,i := range names { in <- i } close(in) } func output(out <-chan string) { for i := range out { fmt.Println("hello ", i) } }
4.3 for循环和select的非缓冲通道
若同是 for循环+协程的应用,通道是否有缓冲,在使用上并没有太大区别
func inputShu( c chan string) { names := []string{"关羽", "张飞", "赵云", "马超","黄忠"} for _,x := range names { c <- x } close(c) } func inputWei( c chan string) { names := []string{"张辽", "乐进", "于禁", "张合","徐晃"} for _,x := range names { c <- x } close(c) } func main() { c1 := make(chan string) c2 := make(chan string) go inputShu(c1) go inputWei(c2) func() { for { time.Sleep(500*time.Millisecond) select { case msg1,ok := <-c1: //if msg1 == ""{continue} if ok { fmt.Println("蜀将: ", msg1) } case msg2,ok := <-c2: if ok { fmt.Println("魏将: ", msg2) } default: fmt.Println("hello world") } } }() time.Sleep(10*time.Second) }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了