使用 Go 实现 Async/Await 模式

概述

Golang 是一种并发编程语言。它具有强大的特性,如 GoroutinesChannels,可以很好地处理异步任务。另外,goroutines 不是 OS 线程,这就是为什么您可以在不增加开销的情况下根据需要启动任意数量的 goroutine 的原因,它的堆栈大小初始化时仅 2KB。那么为什么要 async/await 呢? Async/Await 是一种很好的语言特点,它为异步编程提供了更简单的接口。

项目链接:https://github.com/icyxp/AsyncGoDemo

它是如何工作的?

从 F# 开始,然后是 C#,到现在 Python 和 Javascript 中,async/await 是一种非常流行的语言特点。它简化了异步方法的执行结构并且读起来像同步代码。对于开发人员来说更容易理解。让我们看看 c# 中的一个简单示例 async/await 是如何工作的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static async Task Main(string[] args)
{

Console.WriteLine("Let's start ...");
var done = DoneAsync();
Console.WriteLine("Done is running ...");
Console.WriteLine(await done);
}

static async Task<int> DoneAsync()
{
Console.WriteLine("Warming up ...");
await Task.Delay(3000);
Console.WriteLine("Done ...");
return 1;
}

当程序运行时,我们的 Main 函数将被执行。我们有异步函数 DoneAsync。我们使用 Delay 方法停止执行代码 3 秒钟。Delay 本身是一个异步函数,所以我们用 await 来调用它。

await 只阻塞异步函数内的代码执行

Main 函数中,我们不使用 await 来调用 DoneAsync。但 DoneAsync 开始执行后,只有当我们 await 它的时候,我们才会得到结果。执行流程如下所示:

1
2
3
4
5
Let's start ...
Warming up ...
Done is running ...
Done ...
1

对于异步执行,这看起来非常简单。让我们看看如何使用 Golang 的 GoroutinesChannels 来做到这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func DoneAsync() chan int {
r := make(chan int)
fmt.Println("Warming up ...")
go func() {
time.Sleep(3 * time.Second)
r <- 1
fmt.Println("Done ...")
}()
return r
}

func main () {
fmt.Println("Let's start ...")
val := DoneAsync()
fmt.Println("Done is running ...")
fmt.Println(<- val)
}
```
在这里,`DoneAsync` 异步运行并返回一个 `channel`。执行完异步任务后,它会将值写入 `channel`。在 `main` 函数中,我们调用 `DoneAsync` 并继续执行后续操作,然后从返回的 `channel` 读取值。它是一个阻塞调用,等待直到将值写入 `channel`,并在获得值后将其输出到终端。
```go

Let's start ...
Warming up ...
Done is running ...
Done ...
1

我们看到,我们实现了与 C# 程序相同的结果,但它看起来不像 async/await 那样优雅。尽管这确实不错,但是我们可以使用这种方法轻松地完成很多细粒度的事情,我们还可以用一个简单的结构和接口在 Golang 中实现 async/await 关键字。让我们试试。

实现 Async/Await

完整代码可在项目链接中找到(在文章开始的地方)。要在 Golang 中实现 async/await,我们将从一个名为 async 的包目录开始。项目结构看起来是这样的。

1
2
3
4
5
.
├── async
│ └── async.go
├── main.go
└── README.md

async 文件中,我们编写了可以处理异步任务最简单的 Future 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package async

import "context"

// Future interface has the method signature for await
type Future interface {
Await() interface{}
}

type future struct {
await func(ctx context.Context) interface{}
}

func (f future) Await() interface{} {
return f.await(context.Background())
}

// Exec executes the async function
func Exec(f func() interface{}) Future {
var result interface{}
c := make(chan struct{})
go func() {
defer close(c)
result = f()
}()
return future{
await: func(ctx context.Context) interface{} {
select {
case <-ctx.Done():
return ctx.Err()
case <-c:
return result
}
},
}
}

这里发生的事情并不多,我们添加了一个具有 Await 方法标识的 Future 接口。接下来,我们添加一个 future 结构,它包含一个值,即 await 函数的函数标识。现在 futute struct 通过调用自己的 await 函数来实现 Future 接口的 Await 方法。

接下来在 Exec 函数中,我们在 goroutine 中异步执行传递的函数。然后返回 await 函数。它等待 channel 关闭或 context 读取。基于最先发生的情况,它要么返回错误,要么返回作为接口的结果。

现在,有了这个新的 async 包,让我们看看如何更改当前的 go 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func DoneAsync() int {
fmt.Println("Warming up ...")
time.Sleep(3 * time.Second)
fmt.Println("Done ...")
return 1
}

func main() {
fmt.Println("Let's start ...")
future := async.Exec(func() interface{} {
return DoneAsync()
})
fmt.Println("Done is running ...")
val := future.Await()
fmt.Println(val)
}

乍一看,它看起来干净得多,这里我们没有显式地使用 goroutinechannels。我们的 DoneAsync 函数已更改为完全同步的性质。在 main 函数中,我们使用 async 包的Exec 方法来处理 DoneAsync。在开始执行 DoneAsync。控制流返回到可以执行其他代码的 main 函数中。最后,我们对 Await 进行阻塞调用并回读数据。

现在,代码看起来更加简单易读。我们可以修改我们的 async 包从而能在 Golang 中合并许多其他类型的异步任务,但在本教程中,我们现在只坚持简单的实现。

结论

我们经历了 async/await 的过程,并在 Golang 中实现了一个简单的版本。我鼓励您进一步研究 async/await,看看它如何更好的让代码库便于易读。

译自:https://hackernoon.com/asyncawait-in-golang-an-introductory-guide-ol1e34sg

微信订阅号

-------------本文结束感谢您的阅读-------------