I was learning Golang recently. What interests me in Golang is the goroutine + channel concurrency model. In Golang, we use goroutines to execute asynchronous tasks while these goroutines communicate with each other via the data structure called Channel
.
As a frontend developer, I found that the goroutine + channel model in Golang is much like the event loop in Javascript.
Channel
.
So I am wondering if there is a way that we could control asynchronous tasks in Golang via Async/Await pattern just like what we do in Javascript ES7. The answer is yes!
For more details → https://github.com/sun0day/async
Before we continue, let’s give a flashback about the common asynchronous model. There are two kinds of common asynchronous models: the blocking asynchronous models & non-blocking asynchronous models. The difference between a blocking asynchronous model and a non-blocking asynchronous model is whether the synchronous code execution should wait for the asynchronous task response to continue.
In Javascript ES7, we use async
reserved word to tag a function
as an asynchronous task. Once you call a async function
, it will be sent to another sub-thread to execute. Then the rest synchronous code will be executed immediately no matter whether the async function
is done. The async funciton
calling flow above is non-blocking asynchronous. We can also transform the async function
calling flow to be blocking asynchronous using await
reserved word.
async function nonBlock() {...}
async function block() {...}
async function main() {
nonBlock() // will not block main
await block() // will block main
}
Golang provides a more granular and flexible way to control asynchronous task execution. But flexibility also means we have to write more code to control asynchronous task execution. And what’s worse is that we have to deal with exceptions such as goroutine leak, deadlock, panic, etc. Introducing Async/Await pattern in golang can help Goer to write more neat, readable, and robust code.
In Golang we can convert a synchronous func
to be asynchronous using a func
named Async
. The Async
type definition is:
func Async[V any](f func () V) func() *AsyncTask[V] {...}
Async
accepts a func
as a parameter and returns a new func
which will create a struct
named AsyncTask
. You should tell Async
what the return
type of f
is via the generic parameter V
. AsyncTask
represents the asynchronous task created by Async
. It has two properties: data
& err
.
type AsyncTask[V any] struct {
data chan V
err chan any
}
We can observe f()
result through AsyncTask.data
and the possible error through AsyncTask.err
. Then let's take a look at the internals of Async
.
func Async[V any](f func () V) func() *AsyncTask[V] {
exec := func() *AsyncTask[V] {
data, err := handle[V](f)
return &AsyncTask[V]{data: data, err: err}
}
return exec
}
A func
named exec
will create a AsyncTask
pointer, inside exec
, handle
handles the synchronous f
result and possible error asynchronously. Both data
Channel
and err
Channel
capacity are set to 1 to prevent goroutine leaks. Once f()
is done, the sub goroutine created by handle
can exit immediately regardless of whether there are other goroutines receiving data
and err
.
func handle[V any](f func () V) (chan V, chan any) {
data := make(chan V, 1)
err := make(chan any, 1)
go func() {
var result V
defer func() {
if e:= recover(); e == nil {
data <- result
} else {
err <- e
}
close(data)
close(err)
}()
result = f()
}()
return data, err
}
The example below shows how Async
encapsulates f
and how f
works asynchronously.
func main() {
a := 1
b := 2
af := Async[int](func() int {
c := a + b
fmt.Println("f() result is", c)
return c
})
fmt.Println("sync start, goroutine=", runtime.NumGoroutine())
af()
fmt.Println("sync end, goroutine=", runtime.NumGoroutine())
time.Sleep(1 * time.Second)
fmt.Println("async end, goroutine=", runtime.NumGoroutine())
}
/* stdout:
sync start, goroutine=1
sync end, goroutine=2
f() result: 3
async end, goroutine=1
*/
Await
implementation in Golang is simple. What Await
does is waiting for AsyncTask.data
or AsyncTask.err
sending data.
func Await[V any](t *AsyncTask[V]) (V, any) {
var data V
var err any
select {
case err := <-t.err:
return data, err
case data := <-t.data:
return data, err
}
}
The example below shows how Await
waits for Async
response.
func main() {
a := 1
b := 2
af1 := Async[int](func() int {
c := a + b
return c
})
af2 := Async[int](func() int {
panic("f() error")
})
fmt.Printf("sync start, goroutine=%d\n", runtime.NumGoroutine())
data, _ := Await[int](af1())
_, err := Await[int](af2())
fmt.Printf("sync end, goroutine=%d, af1() result=%d, af2() result='%s'\n", runtime.NumGoroutine(), data, err)
}
/* stdout
sync start, goroutine=1
sync end, goroutine=1, af1() result=3, af2() result='f() error'
*/
This article shows how to implement ES7-style Async/Await API in Golang via goroutine and Channel
. In addition to the above AsyncTask
definition, we still need to consider its execution result and state. Going a step further, we can also implement other asynchronous streaming APIs based on Async
and Await
, such as all
, race
, etc.
The full source code can be found here.