DEV Community

Cover image for Use Go Channels as Promises and Async/Await
Minh-Phuc Tran
Minh-Phuc Tran

Posted on • Edited on • Originally published at phuctm97.com

3 2

Use Go Channels as Promises and Async/Await

If you’ve ever programmed with JavaScript, you definitely know about Promise and async/await. C#, Java, Python, and some other programming languages apply the same pattern using different names such as Task or Future.

On the contrary, Go doesn’t follow the pattern at all. Instead, it introduces goroutines and channels. However, it isn’t difficult to replicate the pattern with goroutines and channels.


Single async/await

First, let’s experiment with a simple use case: await a result from an async function.

// JavaScript
// ---

const longRunningTask = async () => {
    // simulate a workload
    sleep(3000);
    return Math.floor(Math.random() * Math.floor(100));
};

const r = await longRunningTask();
console.log(r);
Enter fullscreen mode Exit fullscreen mode
// Go
// ---

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func longRunningTask() <-chan int32 {
    r := make(chan int32)

    go func() {
        defer close(r)

        // simulate a workload
        time.Sleep(time.Second * 3)
        r <- rand.Int31n(100)
    }()

    return r
}

func main() {
    r := <-longRunningTask()
    fmt.Println(r)
}
Enter fullscreen mode Exit fullscreen mode

To declare an “async” function in Go:

  • The return type is <-chan ReturnType.

  • Within the function, create a channel by make(chan ReturnType) and return the created channel at the end of the function.

  • Start an anonymous goroutine by go func() {...} and implement the function’s logic inside that anonymous function.

  • Return the result by sending the value to the channel.

  • At the beginning of the anonymous function, add defer close(r) to close the channel once done.

To “await” the result, simply read the value from the channel by v := <- fn().


Promise.all()

It’s very common that we start multiple async tasks then wait for all of them to finish and gather their results. Doing that is quite simple in both JavaScript and Go.

// JavaScript
// ---

const longRunningTask = async () => {
    // simulate a workload
    sleep(3000);
    return Math.floor(Math.random() * Math.floor(100));
};

const [a, b, c] = await Promise.all(longRunningTask(), 
                                    longRunningTask(), 
                                    longRunningTask());
console.log(a, b, c);
Enter fullscreen mode Exit fullscreen mode
// Go
// ---

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func longRunningTask() <-chan int32 {
    r := make(chan int32)

    go func() {
        defer close(r)

        // simulate a workload
        time.Sleep(time.Second * 3)
        r <- rand.Int31n(100)
    }()

    return r
}

func main() {
    aCh, bCh, cCh := longRunningTask(), longRunningTask(), longRunningTask()
    a, b, c := <-aCh, <-bCh, <-cCh

    fmt.Println(a, b, c)
}
Enter fullscreen mode Exit fullscreen mode

We have to do it in 2 lines of code and introduce 3 more variables, but it’s clean and simple enough.

We can not do <-longRun(), <-longRun(), <-longRun(), which will longRun() one by one instead all in once.


Promise.race()

Sometimes, a piece of data can be received from several sources to avoid high latencies, or there’re cases that multiple results are generated but they’re equivalent and the only first response is consumed. This first-response-win pattern, therefore, is quite popular. Achieving that in both JavaScript and Go is very simple.

// JavaScript
// ---

const one = async () => {
    // simulate a workload
    sleep(Math.floor(Math.random() * Math.floor(2000)));
    return 1;
};

const two = async () => {
    // simulate a workload
    sleep(Math.floor(Math.random() * Math.floor(1000)));
    sleep(Math.floor(Math.random() * Math.floor(1000)));
    return 2;
};

const r = await Promise.race(one(), two());
console.log(r);
Enter fullscreen mode Exit fullscreen mode
// Go
// ---

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func one() <-chan int32 {
    r := make(chan int32)

    go func() {
        defer close(r)

        // simulate a workload
        time.Sleep(time.Millisecond * time.Duration(rand.Int63n(2000)))
        r <- 1
    }()

    return r
}

func two() <-chan int32 {
    r := make(chan int32)

    go func() {
        defer close(r)

        // simulate a workload
        time.Sleep(time.Millisecond * time.Duration(rand.Int63n(1000)))
        time.Sleep(time.Millisecond * time.Duration(rand.Int63n(1000)))
        r <- 2
    }()

    return r
}

func main() {
    var r int32
    select {
    case r = <-one():
    case r = <-two():
    }

    fmt.Println(r)
}
Enter fullscreen mode Exit fullscreen mode

select-case is the pattern that Go designed specifically for racing channel operations. We can even do more stuff within each case, but we’re focusing only on the result so we just leave them all empty.


Promise.then() and Promise.catch()

Because Go’s error propagation model is very different from JavaScript, there aren't clean ways to replicate Promise.then() and Promise.catch(). In Go, errors are returned as return values, there aren't exceptions. Therefore, if your function can fail, you can consider changing your return <-chan ReturnType into <-chan ReturnAndErrorType, which is a struct holding both the result and error.

SurveyJS custom survey software

Build Your Own Forms without Manual Coding

SurveyJS UI libraries let you build a JSON-based form management system that integrates with any backend, giving you full control over your data with no user limits. Includes support for custom question types, skip logic, an integrated CSS editor, PDF export, real-time analytics, and more.

Learn more

Top comments (1)

Collapse
 
joelolsson profile image
joel1st

Promise.all example isn't quite equivalent behaviour. Promise.all will settle as soon as any promises are rejected (whereas the Go code will wait). Promise.allSettled will behave the same way as the Go code.

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay