Go语言 | goroutine不只有基础的用法,还有这些你不知道的操作

时间:2022-07-23
本文章向大家介绍Go语言 | goroutine不只有基础的用法,还有这些你不知道的操作,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

今天是golang专题第15篇文章,我们来继续聊聊channel的使用。

在我们的上篇文章当中我们简单介绍了golang当中channel的使用方法,channel是golang当中一个非常重要的设计,可以理解为生产消费者模式当中的队列。但channel和队列不一样的是,golang当中集成了一些其他的用法,使得我们的使用更加灵活,开发并发相关的功能更加简单。

select机制

我们来思考一个问题,假设我们的数据源有多个,也就是说我们可能会从多个入口获取数据,但是我们并不知道这些数据源当中哪个先把数据准备好。我们希望实现轮询这些channel,哪个数据准备好了就读取哪个,否则就阻塞等待,这个功能应该怎么办呢?

我们当然可以自己用循环来实现,但是这显然是不合理的,golang当中针对这个问题提供了专门的解决方法,它就是select关键字。

select机制并不是golang这门语言独创的,早在Unix时代就有了select机制。通过select函数监控一系列文件的句柄,一旦其中一个发生了改动,select函数就会返回。而golang当中的select则用来在channel当中进行选择,有点像是switch,写出来的代码大概是这个样子:

select {
    case <- chan1:
    case chan2 <- 1:
    default:
}

select后面跟多个case以及default,其中default并不是必须的case后面必须要接一个chan的操作,可以是从一个chan当中读入数据,也可以是向一个chan当中写入数据。如果所有的case都不成功,则会进入default语句当中,如果没有default语句,那么select会陷入阻塞。

一般情况下我们使用select是为了从多个数据源中获取数据,当多个chan同时有数据的时候,使用select可以让我们避免判断哪个数据源数据ready的问题。

range机制

我们之前在介绍slice遍历的时候曾经介绍过range机制,我们可以通过range来遍历一个数组或者是map。就像是这样:

arr := make([]int, 0)
for i := range arr {
    // do something
}

mp := make(map[string]int)

for k, v := range mp {
    // do something
}

很多时候我们会把这个用法当做是迭代器的迭代,就像是Java和Python中的那样。但实际上range机制的底层原理是chan,当我们使用range的时候,它表示会不断地从chan当中接受值,直到它关闭。

所以我们也可以这样遍历一个chan当中的数据:

ch := make(chan int)

for c := range ch {
    // do something
}

超时机制

有没有想过一个问题,channel的写入和写出都是阻塞的,也就是说如果是从chan当中读取数据,必须要上游已经传输了才可以读取到。同样,如果往没有缓冲区的chan写入数据也需要下游消费了才能写入成功。阻塞往往是有很大隐患的,如果处理不好很容易导致整个程序锁死。

我们需要设计机制来解决这个问题,比较好的方案就是设置定时器,如果超过一定的时间chan还没有响应成功的话,那么就人工停止程序。这一点说起来还有点麻烦的,比如我们要启动一个定时器,要手动终止goroutine,但是结合select机制其实并不难实现,我们来看代码。

timeout := make(chan bool)
go func() {
    time.Sleep(1e9)
    timeout <- true
}()


select {
 case <- ch:
     // do something
 case <- timeout: 
     // break
}

说白了很简单,也就是我们额外启动一个goroutine做休眠操作,当休眠结束之后也通过chan发送消息,这样如果我们select先接受到了timeout的信号就说明了程序已经超时了。当然这只是一个很简单的demo,实际使用的话需要考虑的情况可能还会更多。

channel传递

有没有想过一个问题,既然chan可以传输任何类型的数据,那么我们能不能用一个chan传输一个chan呢?

这样的操作是可以的,因为在有些场景当中相比于直接把数据传输给下游,我们传输读取数据的chan可能更加方便。有点授人以渔的意思,更加厉害的是我们可以结合函数式编程,把处理数据的函数一并传输给下游。这样下游读取到数据,并且用读取到的处理函数来处理,这样可以更加定制化,如果以后数据和处理方式都发生改动,也只需要在上游修改,可以更加解耦。

我们同样来看一个demo:

type MetaData struct {
    value interface{}
    handler func(interface{}) int
    downstream chan interface{}
}


func handle(queue chan *MetaData) {
    for data := range queue {
        data.downstream <- data.handler(data.value)
    }
}

这只是一个简单的案例,想要在实际应用当中真的使用上还需要定义大量的接口以及做很多设计。我们只需要知道有这么一种设计模式和用法就可以了。

单向channel

最后,我们来说说单向channel,也就是说我们指定channel是只读的或者是只写的。但其实这是一个伪命题,原因也很简单,如果只写数据没人读,或者是只读但是不能写,那么这个channel有什么用呢?只有有人读有人写才可以完成数据流通不是吗?

的确如此,所以这里所说的单向channel其实并不是真正意义上的单向,只是说我们为了规范,对使用方进行限制。比如说我们限定在消费函数当中不能写入,在生产函数当中不能消费。我们在通过函数传递chan的时候,可以通过加上限定让chan在函数当中变成单向的。

var ch chan <- float32 // 只写chan
var ch <- chan float32 // 只读chan

除此以外我们还可以把一个正常的chan转化成单向的chan:

var ch chan int
ch1 := <- chan int(ch)
ch2 := chan <- int(ch)

我们一般不在程序当中做这样的转化,而是用在函数参数当中,这也主要是为了起到规范的作用。

func Test(ch <- chan int) {
    for val := range ch {
        // do something
    }
}

- END -