Go语言学习笔记(十二)

一、通道简介

通道帮助管理Goroutine之间的通信。通道和Goroutine一道提供了一个受控的环境,能够帮助我们开发并发软件

1 使用通道

  1. 如果说Goroutine是一种支持并发编程的方式,那么通道就是一种与Goroutine通信的方式。通道让数据能够进入和离开Goroutine,可方便Goroutine之间进行通信
  2. 《Effective Go》有一句话很好的说明了Go语言的并发实现理念:不要通过共享内存来实现通信,而通过通信来共享内存
  3. 在其他的编程语言中,并发编程通常是通过在多个进程或线程之间共享内存实现的。共享内存能够让程序同步,确保程序以合乎逻辑的方式执行。在程序执行过程中,进程或线程可能对共享内存加锁,以禁止其他进程或线程修改它。这合乎情理,因为如果在操作期间共享内存被其他进程修改,可能会造成灾难性后果——引发Bug或导致程序崩溃。通过这种方式给内存加锁,可确保它是独占的——只有一个进程或线程能够访问它。
  4. 虽然共享内存看似很方便,但是实现共享内存和锁的管理工作并非那么容易。Go语言使用通道在Goroutine之间收发信息,避免了使用共享内存。严格的说,Goroutine并不是线程,但是我们可以将它视为线程,因为它们能够以非阻塞的方式执行代码。

通道的创建语法如下:

c := make (chan string)

对这种语法解读如下:

  • 使用简短变量赋值,将变量c初始化为:=右边的值
  • 使用内置函数make创建一个通道,这是使用关键词chan指定的
  • 关键词chan后面的string指出这个通道将用于储存字符串数据,这意味着这个通道只能用于收发字符串值

向通道发送消息的语法如下:

	c <- "hello world"
	//这里要注意的是:当通道被设定为字符串类型,那么通道只能接收字符串消息

从通道那里接收消息的语法如下:

	msg := <-c 

在这里插入图片描述
一个较为详细的例子如下:

package main

import (
	"fmt"
	"time"
)

func slowFunc(c chan string) {
	time.Sleep(time.Second * 2)
	c <- "slowFunc() finished"
}

func main() {
	c := make(chan string)
	go slowFunc(c)

	msg := <-c//只有当msg收到来自通道c的消息,程序才会执行下去
	fmt.Println(msg)
}

2 使用缓冲通道

通常,通道收到消息后就可将其发送给接收者,但有时候可能没有接收者。在这种情况下,可使用缓冲通道。缓冲意味着可将数据存储在通道中,等接收者准备就绪再交给它。要创建缓冲通道,可向内置函数make中传递另一个表示缓冲区长度的参数。

messages := make(chan string, 2)
  1. 这条代码创建了一个可储存两条消息的缓冲通道。
  2. 缓冲通道最多只能储存指定数量的消息,如果向他发送更多的消息将导致错误
  3. 消息将存储在通道中,并在接收者准备就绪后接收了这些消息
package main

import (
	"fmt"
	"time"
)

func receiver(c chan string) {
	for msg := range c {
		fmt.Println(msg)
	}
}

func main() {

	messages := make(chan string, 2)
	messages <- "hello"
	messages <- "world"//此时没有可用的接收者,消息被缓冲
	close(messages)
	fmt.Println("Pushed two messages onto channel with no receivers")
	time.Sleep(time.Second * 1)
	receiver(messages)
}

在这里close函数用来关闭通道,禁止再向通道发送消息

3 阻塞和流程控制

  1. Goroutine是Go语言提供的一种并发编程方式。速度缓慢d额网络调用或函数会阻塞程序的执行,而Goroutine能够帮助我们管理这些。
  2. 在并发编程中,通常应该避免阻塞式操作,但有时需要让代码处于阻塞状态。例如,需要在后台运行的程序必须阻塞,这样才不会退出。
  3. Goroutine会立即返回(非阻塞),因此要让进程处于阻塞状态,必须采用一些流程控制技巧。例如,从通道接收并打印消息的程序需要阻塞,以免终止
  4. 给通道指定接收者是一个阻塞操作,因为它将组织函数返回,直到收到一条消息为止
package main

import (
	"fmt"
	"time"
)

func pinger(c chan string) {
	t := time.NewTicker(1 * time.Second)
	for {
		c <- "ping"
		<-t.C
	}
}
func main() {
	messages := make(chan string)
	go pinger(messages)
	msg := <-messages
	fmt.Println(msg)//输出ping
}

time.NewTicker一般作为时间周期执行命令,在这里是每隔一秒将"ping"送入通道中。
在主程序中添加for循环可以控制打印的次数

package main

import (
	"fmt"
	"time"
)

func pinger(c chan string) {
	t := time.NewTicker(1 * time.Second)
	for {
		c <- "ping"
		<-t.C
	}
}
func main() {
	messages := make(chan string)
	go pinger(messages)
	for {
		msg := <-messages
		fmt.Println(msg)//不断输出ping
	}
	//或者for循环添加终止条件
	for i:=0;i<5;i++{
		msg := <-messages
		fmt.Println(msg)//输出5个ping		
	}
}

4 将通道用作函数参数

  1. 我们可将通道作为参数传递给函数,并在函数中向通道发送消息。
  2. 要进一步指定在函数中如何使用传入的通道,可在传递通道时将其指定为只读、只写或读写的。指定通道是只读、只写、读写的语法差别不大。
    func channelReader(messages <-chan string){
    	msg := <=messages
    	fmt.Println(msg)
    }
    
    func channelWriter(messages chan<-string){
    	messages <- "hello world"
    }
    
    func channelReaderAndWriter(messages chan string){
    	msg := <-messages
    	fmt.Println(msg)
    	messages <- "hello world"
    }
    
  3. <-位于关键词chan左边时,表示通道在函数内是只读的;<-位于关键词chan右边时,表示通道在函数内是只写的;没有指定<-时,表示通道是可读写的。
  4. 通过指定通道访问权限,有助于确保通道中数据的完整性,还可以指定程序的哪部分可向通道发送数据或接收来自通道的数据。

5 使用select语句

  1. 假设有多个Goroutine,而程序将根据最先返回的Goroutine执行相应的操作,此时可使用select语句。
  2. select语句类似switch语句,为通道创建一系列接收者,并执行最先收到消息的接收者
package main

import (
	"fmt"
	"time"
)

func ping1(c chan string) {
	time.Sleep(time.Second * 1)
	c <- "ping on channel1"
}
func ping2(c chan string) {
	time.Sleep(time.Second * 2)
	c <- "ping on channel2"
}

func main() {
	channel1 := make(chan string)
	channel2 := make(chan string)

	go ping1(channel1)
	go ping2(channel2)

	select {
	case msg1 := <-channel1:
		fmt.Println("received", msg1)
	case msg2 := <-channel2:
		fmt.Println("received", msg2)
	}
}
  1. select语句执行哪条case语句取决于消息到达的时间,哪条消息先到达决定了执行哪条case语句。通常,接下来收到的其他消息将被丢弃。收到一条消息后,select语句将不再阻塞。

如果select语句没有收到消息呢?

我们可以增加一个超时case语句,指定在0.5s内没有收到消息时将采取的措施。

select {
case msg1 := <-channel1:
	fmt.Println("received", msg1)
case msg2 := <-channel2:
	fmt.Println("received", msg2)
case <-time.After(500*time.Millisecond):
	fmt.Println("no messages received. giving up")
}

6 退出通道

  1. 在已知需要停止执行的时间的情况下,使用超时时间是不错的选择,但在有些情况下,不确定select语句该在何时返回,因此不能使用定时器。在这种情况下,可使用退出通道。
  2. 这种技术并非语言规范的组成部分,但可通过向通道发送消息来理解退出阻塞的select语句。

来看这样的一种情形:程序需要使用select语句实现无限制地阻塞,但同时要求能够随时返回。通过在select语句中添加一个退出通道,可向退出通道发送消息来结束该语句,从而停止阻塞。可将退出通道视为阻塞式select语句的开关。对于退出通道,可随便命名,但通常情况下将其命名为stop或者quit。在下面的实例中,在for循环中使用了一条select语句,这意味着它将无限制地阻塞,并不断地接收消息。通过向通道stop发送消息,可让select语句停止阻塞:从for循环中返回,并继续往下执行
在这里插入图片描述

在应用程序的某部分向通道发送消息,并要在未来的某个位置时点终止时,这种技术是很有效的

package main

import (
	"fmt"
	"time"
)

func sender(c chan string) {
	t := time.NewTicker(1 * time.Second)
	for {
		c <- "I'm sending a message"
		<-t.C
	}
}

func main() {
	messages := make(chan string)
	stop := make(chan bool)
	go sender(messages)
	go func() {
		time.Sleep(time.Second * 2)
		fmt.Println("Time's up!")
		stop <- true
	}()

	for {
		select {
		case <-stop:
			return
		case msg := <-messages:
			fmt.Println(msg)
		}
	}
}
参考书籍
 [1]: 【Go语言入门经典】[英] 乔治·奥尔波 著 张海燕 译
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是兔不是秃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值