Async/Await in Golang
How to implement async/await in Golang.
Project Link: Github
Golang is a concurrent programming language. It has powerful features like Goroutines and Channels that can handle asynchronous tasks very well. Also, goroutines are not OS threads, and that's why you can spin up as many goroutines as you want without much overhead, their stack size starts at 2KB only. So why async/await? Async/Await is a nice language feature that provides a simpler interface to asynchronous programming.
How Does it Work?
Originating in F# and C#, and now present in Python and JavaScript, async/await has become an extremely popular language feature. It simplifies asynchronous execution and reads like synchronous code, making it much easier for developers to follow. Let's see a simple example in C# of how async/await works:
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;
}We have the Main function that would be executed when the program is run. We have DoneAsync which is an async function. We stop the execution of the code with Delay function for 3 seconds. Delay is an async function itself, so we call it with await.
awaitonly blocks the code execution within theasyncfunction
In the main function, we do not call DoneAsync with await. But the execution starts for DoneAsync. Only when we await it, we get the result back. The execution flow looks like this:
Let's start ...
Warming up ...
Done is running ...
Done ...
1This looks incredibly simple for asynchronous execution. Let's see how we can do it with Golang using Goroutines and Channels
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)
}Here, DoneAsync runs asynchronously and returns a channel. It writes a value to the channel once it's done executing the async task. In main function, we invoke DoneAsync and keep doing our operations and then we read the value from the returned channel. It is a blocking call that waits till the value is written to the channel and after it gets the value it writes to the console.
Let's start ...
Warming up ...
Done is running ...
Done ...
1We see we achieve the same outcome as the C# program, but it doesn't look quite as elegant as async/await. While this approach is good and allows for granular control over concurrency, we can easily implement an async/await-like pattern in Golang using a simple struct and interface. Let's try that.
Implementing Async/Await
The full code is available in the project link. To implement async/await in Golang, we will start with a package directory named async. The project structure looks like this:
.
|-- async
| -- async.go
|-- main.go
|-- README.mdIn the async file, we write the simplest future interface that can handle async tasks.
package async
// Future interface has the method signature for await
type Future interface {
Await() interface{}
}
type future struct {
await func() interface{}
}
func (f future) Await() interface{} {
return f.await()
}
// Exec executes the async function
func Exec(f func() interface{}) Future {
c := make(chan interface{}, 1)
go func() {
defer close(c)
c <- f()
}()
return future{
await: func() interface{} {
return <-c
},
}
}Not a lot is happening here. We add a Future interface that has the Await method signature. Next, we add a future struct that holds one value: a function signature of the await function. Now the future struct implements the Future interface's Await method by invoking its own await function.
The Exec function executes the passed function asynchronously in a goroutine. We create a buffered channel of size 1 so the goroutine doesn't block if Await is never called. We return the future struct with an await function that simply reads and returns the value from the channel.
Now armed with this new async package, let's see how we can change our current go code
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)
}At first glance, it looks much cleaner; we are not explicitly working with goroutines or channels here. Our DoneAsync function has been changed back to a completely synchronous nature. In the main function, we use the async package's Exec method to handle DoneAsync, which starts its execution. The control flow is returned back to the main function, which can execute other pieces of code. Finally, we make a blocking call to Await and read back the data.
Now the code looks much simpler and easier to read. We can modify our async package to incorporate other types of asynchronous tasks in Golang, but we will stick to this simple implementation for now in this tutorial.
Conclusion
We have gone through what async/await is and implemented a simple version of it in Golang. I would encourage you to look into async/await a lot more and see how it can improve the readability of your codebase.