Async/Await in Golang: An Introductory Guide


Author profile picture

@hasantalksMD Ahad Hasan

Software Engineer, https://kickbackapps.com

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, it’s 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.

Project Link: https://github.com/Joker666/AsyncGoDemo

How Does it Work?

Started with F# and then C#, now in Python and Javascript, async/await is an extremely popular feature of a language. It simplifies the asynchronous method execution structure and, it reads like synchronous code. So much easier to follow for developers. Let’s see a simple example in C# 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.

await only blocks the code execution within the async function

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 ...
1

This 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 ...
1

We see, we achieve the same outcome as the C# program but it doesn’t look as elegant as async/await. While this is actually good, we are able to do a lot more granular things with this approach much easily, we can also implement async/await keywords in Golang with 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

.
├── async
│   └── async.go
├── main.go
└── README.md

In the async file, we write the simplest future interface that can handle async tasks.

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
			}
		},
	}
}

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

futute

struct implements

Future

interface’s Await method by invoking its own

await

function.

Next in the

Exec

function, we execute the passed function asynchronously in goroutine. And we return the

await

function. It waits for the channel to close or context to read from. Based on whichever happens first, it either returns the error or the result which is an interface.

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 the first glance, it looks much cleaner, we are not explicitly working with goroutine or channels here. Our

DoneAsync

function has been changed to a completely synchronous nature. In the main function, we use the

async

package’s

Exec

method to handle

DoneAsync

. Which starts the execution of

DoneAsync

. The control flow is returned back to

main

function which can execute other pieces of code. Finally, we make blocking call to

Await

and read back data.

Now the code looks much simpler and easier to read. We can modify our async package to incorporate a lot of other types of asynchronous tasks in Golang, but we would just stick to simple implementation for now in this tutorial.

Conclusion

We have gone through what async/await it and implemented a simple version of that in Golang. I would encourage you to look into async/await a lot more and see how it can ease the readability of the codebase much better.

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.

Don't forget to share

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *