golang channel select 使用

chan管道是多个goroutine进行通信的一种方式,跟linux中管道一样,linux中进程之间的通讯使用管道,管道遵循"先进后出"原则

1. channel的使用场景

  1. 消息传递、消息过滤
  2. 信号广播
  3. 消息传递、消息过滤
  4. 事件订阅与广播
  5. 请求、响应转发
  6. 任务分发
  7. 结果汇总
  8. 并发控制
  9. 同步与异步

2. channel存在3种状态

  1. nil,未初始化的状态,只进行了声明,或者手动赋值为nil
  2. active,正常的channel,可读或者可写
  3. closed,已关闭,千万不要误认为关闭channel后,channel的值是nil

3. channel可进行3种操作

读、写、关闭
把这3种操作和3种channel状态可以组合出9种情况:在这里插入图片描述

4. 无缓冲chan管道-先读后写

package main
import "fmt"
func main() {
	// 定义int channel,无缓冲
	c := make(chan int)
	// 创建一个goroutine
	go func() {
		defer fmt.Println("goroutine 结束")
		fmt.Println("goroutine 正在运行...")
		// 往c管道中写入数据
		c <- 666
	}()
	// 从c中读取数据,并且复制给num。
	num := <-c  
	fmt.Println("num = ", num)
	fmt.Println("main goroutine 结束...")
}

num := <-c 这里会发生阻塞直到有写操作时才会往下执行。也就是管道没有数据时,读操作会阻塞。直到有一方往里写数据了,才会往下执行。总结:读数据时,如果没有数据会发生阻塞

5. 无缓冲chan管道-先写后读

package main
import (
	"fmt"
	"time"
)
func main() {
	// 定义int channel,无缓冲
	c := make(chan int)
	// 创建一个goroutine
	go func() {
		defer fmt.Println("goroutine 结束")
		fmt.Println("goroutine 正在运行...")
		// 往c管道中写入数据
		c <- 666
		fmt.Println("已经往管道写数据")
	}()
	// 这里sleep3秒
	time.Sleep(time.Second * 3)
	// 从c中读取数据,并且复制给num。
	num := <-c // 注意这里会阻塞直到有写操作时才会往下执行,也就是管道没有数据时,读操作会阻塞。直到有一方往里写数据了,

	// 为了主程不要结束那么快
	time.Sleep(time.Second * 1)
	fmt.Println("num = ", num)
	fmt.Println("main goroutine 结束...")
}

c <- 666 当我们再往管道写数据的时候,是阻塞的。因为这时候没有人读数据,直到3秒之后,主go程执行,读取管道里的数据,这时候,c <- 666 才会往下执行,打印出"已经往管道写数据"。结论:写数据时,如果没有人读取数据,会阻塞

5. 有缓冲chan管道

package main
import (
	"fmt"
	"time"
)
func main() {
	// 定义有缓冲的channel,容量为3
	c := make(chan int, 3)
	go func() {
		defer fmt.Println("子go程结束...")
		for i := 0; i < 3; i++ {
			c <- i
			fmt.Println("子go程正在运行,发送的元素=", i, "len(c)=", len(c), "cap(c)=", cap(c))
		}
	}()
	//睡眠一下,让子go写完数据
	time.Sleep(2 * time.Second)

	for i := 0; i < 3; i++ {
		num := <-c
		fmt.Println("num=", num)
	}
	fmt.Println("main 结束")
}

子go先往 c 里写3个数据,因为是有缓冲的,所以写入3个的时候,不会等待。2秒之后,主go程读取管道数据,并打印。

  1. 有缓冲的管道,在容量没有满的时候,往里写数据,是不会阻塞的。容量满的话,就会阻塞,直到有空的位置出来
  2. 如果读取超出容量时会报错,比如容量为3,但是for循环的时候循环了4次,这时候就会报错

5. 关闭chan

package main

import "fmt"

func main() {
	c := make(chan int)
	go func() {
		for i := 0; i < 5; i++ {
			c <- i
		}
		// 关闭管道
		close(c)
	}()
	// 主go程读取数据
	for {
		// ok 如果为true 表示channel没有关闭,如果为false表示channel已经关闭
		if data, ok := <-c; ok {
			fmt.Println(data)
		} else {
			// 关闭了,跳出循环
			break
		}
	}
	fmt.Println("main finished..")
}
  1. channel不像文件一样要经常关闭,只有当你确定没有数据发送了,才去关闭channel
  2. 关闭channel后,无法向chanel再发送数据(会引发panic错误后导致)
  3. 关闭channel后,可以继续从channel读取数据
  4. 对于nil(没有make)的channel,无论读写都发会发生阻塞

6. 遍历chan rang

package main
import (
	"fmt"
	"time"
)
func main() {
	c := make(chan int)
	go func() {
		for i := 0; i < 5; i++ {
			time.Sleep(1 * time.Second)
			c <- i
		}
		// 关闭管道
		close(c)
	}()
	// 主go程读取数据
	//for {
	//	// ok 如果为true 表示channel没有关闭,如果为false表示channel已经关闭
	//	if data, ok := <-c; ok {
	//		fmt.Println(data)
	//	} else {
	//		// 关闭了,跳出循环
	//		break
	//	}
	//}
	// 从c中读取数据,如果没有数据就会阻塞等待数据的到来
	for data := range c {
		fmt.Println(data)
	}
	fmt.Println("main finished..")
}

使用range跟for一样的效果,range更加简洁。如果在其他协程中调用了close(ch),那么就会跳出for range循环。这也就是for range的特别之处

6.chan select

select功能:在多个通道上进行读或写操作,让函数可以处理多个事情,但1次只处理1个。以下特性也都必须熟记于心

  1. 每次执行select,都会只执行其中1个case或者执行default语句。
  2. 当没有case或者default可以执行时,select则阻塞,等待直到有1个case可以执行。
  3. 当有多个case可以执行时,则随机选择1个case执行。
  4. case后面跟的必须是读或者写通道的操作,否则编译出错。

go 的select 类似linux的IO多路复用中的select事件

package main
import "fmt"
func main() {
	readCh := make(chan int, 1)
	writeCh := make(chan int, 1)
	y := 1
	select {
	case x := <-readCh:
		fmt.Printf("Read %d\n", x)
	case writeCh <- y:
		fmt.Printf("Write %d\n", y)
	default:
		fmt.Println("Do what you want")
	}
}

我们创建了readCh和writeCh2个通道:

  1. readCh中没有数据,所以case x := <-readCh读不到数据,所以这个case不能执行。
  2. writeCh是带缓冲区的通道,它里面是空的,可以写入1个数据,所以case writeCh <- y可以执行。
  3. 有case可以执行,所以default不会执行。

7.chan select 配合使用:

同时监听不同的channel,做同一件工作,可以最快的返回结果。

package main

import (
	"fmt"
	"net/http"
)
var q = make(chan int)
var reqNum = 0
func main() {
	// 无缓冲chan
	ch1 := make(chan int)
	ch2 := make(chan int)
	ch3 := make(chan int)

	go getUrlData("https://www.baidu.com/", ch3)
	go getUrlData("https://www.baidu.com/", ch2)
	go getUrlData("https://www.baidu.com/", ch1)

	for {
		// 注意:如果3个ch都同时可读的话,select只会随机取一个。那剩下2个需要依靠for再次去读取
		select {
		case v := <-ch1:
			fmt.Println(v)
		case v := <-ch2:
			fmt.Println(v)
		case v := <-ch3:
			fmt.Println(v)
		case n := <-q:
			// 3次请求之后,退出for循环
			if n >= 3 {
				return
			}
		default:
			continue
		}
	}
	fmt.Println("循环结束")
}
func getUrlData(url string, ch chan int) {
	req, err := http.Get(url)
	if err == nil {
		ch <- req.StatusCode
		reqNum += 1
		q <- reqNum
	} else {
		fmt.Println(err)
	}
}

select 时 chan关闭

package main

import (
	"fmt"
	"time"
)
func main() {
	c := make(chan interface{})
	go func() {
		time.Sleep(2 * time.Second)
		close(c)
	}()
	select {
	case _, ok := <-c:
		fmt.Printf("chan关闭:%v", ok)
	}
}

chan关闭时,第二个参数会返回false,可以根据返回值做响应的处理

在for 和select 配合使用中 使用 break 退出循环是无效的,正确的做法如下:

package main
import (
	"fmt"
	"time"
)
func main() {
	chExit := make(chan bool)
	go func() {
		time.Sleep(2 * time.Second)
		close(chExit)
	}()
	// 定义标签
loop:
	for {
		select {
		case v, ok := <-chExit:
			if !ok {
				fmt.Println("close channel 1", v)
				// 跳出标签
				break loop
			}
			fmt.Println("ch1 val =", v)
		}
	}
	fmt.Println("exit testSelectFor")
}

select 超时控制

package main
import (
	"fmt"
	"time"
)
func main() {
	ch := make(chan int)
	quit := make(chan bool)
	//新开一个协程
	go func() {
		for {
			select {
			// case 1
			case num := <-ch:
				fmt.Println("num = ", num)
			// case 2
			case <-time.After(3 * time.Second):
				// 第一次for 会执行这里进入3秒倒计时,1秒时 num有数据可读,执行case 1,select结束,执行下一次for循环
				// 每次执行 time.After 把前面执行的倒计时又覆盖,又倒计时3秒
				// 等到第五次循环时,num已经没有数据写入了,倒计时3秒结束后,执行 case 2 代码,打印"超时"
				fmt.Println("超时")
				quit <- true

			}
			fmt.Println(111)
		}
	}()
	for i := 0; i < 5; i++ {
		ch <- i
		time.Sleep(time.Second)
	}
	// 前面for循环5秒就结束了,<-quit 读取数据,由于没有数据,所以这里会阻塞,直到3秒之后,quit <- true 才会继续往下走
	<-quit
	fmt.Println("程序结束")
}

case <-time.After(3 * time.Second)

3秒之后会执行这个 case 。for循环每执行一次,又会重新计时3秒,所以如果num一直有数据的话,case 2是一直不会执行的。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值