Golang Async/Await implementation with Goroutine and Channel

eye-catchGolang
Sponsored links

Asynchronous work is important to use PC resources efficiently. Golang supports Goroutine and Channel to implement it very easily while it’s not easy in other languages.

These features should be used if possible if you implement your application in Golang.

Let’s learn how to use them.

Sponsored links

How to make the code async with Goroutine

If a process needs to read/write for I/O or communicate with another program/server, it takes a while to complete the work.

Let’s consider this case where the main thread needs to receive 3 messages from someone else while another work is also running.

Synchronous work

The first implementation is Synchronous.

func sendWithCallback(cb func(data string)) {
    for i := 0; i < 3; i++ {
        data := fmt.Sprintf("Hello: %d", i+1)
        cb(data)
        time.Sleep(time.Second)
    }
}

func receiver(data string) {
    fmt.Printf("Received: [%s]\n", data)
}

func runWithoutGoroutine() {
    sendWithCallback(receiver)
    fmt.Println("Do something...")
}

We want to do something parallelly but this one doesn’t work as expected.

$ go run main.go 
Received: [Hello: 1]
Received: [Hello: 2]
Received: [Hello: 3]
Do something...

Do something is processed after send/receive is completed because for loop in sendWithCallback doesn’t go out until it ends.

Make it asynchronous (Concurrency)

Let’s update the code to make it asynchronous. It is very easy to do it. Just add go keyword where you want to make it asynchronous.

import (
    "fmt"
    "sync"
    "time"
)

func sendWithCallback(cb func(data string), wg *sync.WaitGroup) {
    for i := 0; i < 3; i++ {
        data := fmt.Sprintf("Hello: %d", i+1)
        cb(data)
        time.Sleep(time.Second)
    }

    if wg != nil {
        wg.Done()
    }
}

func receiver(data string) {
    fmt.Printf("Received: [%s]\n", data)
}

func runWithGoroutine() {
    var wg sync.WaitGroup
    wg.Add(1)
    go sendWithCallback(receiver, wg)   // Async here
    fmt.Println("Do something...")
    wg.Wait()
}

I also added sync.WaitGroup so that the program can do all the work. The program ends soon without it.

The result is the following.

$ go run main.go 
Do something...
Received: [Hello: 1]
Received: [Hello: 2]
Received: [Hello: 3]

Do something comes first this time and send/receive work runs background.

We could make it async very easily!!

Sponsored links

Channel

Replace a callback with Channel

The current code is something like event-driven way like JavaScript with Node.js. I guess the basic way to do the same thing is to use Channel in Golang.

Let’s use Channel instead of callback.

func send(channel chan string) {
    for i := 0; i < numberOfMsg; i++ {
        channel <- fmt.Sprintf("Hello: %d", (i + 1))
        time.Sleep(time.Second)
    }
}

func receive(channel chan string) {
    for {
        data := <-channel
        fmt.Printf("Received: [%s]\n", data)
    }
}

func runChannelTest2() {
    channel := make(chan string)

    go send(channel)
    go receive(channel)

    fmt.Println("Do something...")
    time.Sleep((numberOfMsg + 1) * time.Second)
}

There are two go routines which means that there are two threads. The message is passed via channel. data := <-channel waits until it receives the data. So it’s not necessary to write the code explicitly.

The result is the same as the previous version.

$ go run main.go 
Do something...
Received: [Hello: 1]
Received: [Hello: 2]
Received: [Hello: 3]

Use only some messages that comes within timeout

There are some cases where one of the resources is slow to process and the main application doesn’t want to wait for all the responses.

In the following case, it uses only the two responses and ignores the 3rd one because the response time is too long.

Let’s try to implement it with Channel.

func runChannelTest2() {
    channel data := <-channel:= make(chan string)

    send := func(channel chan string, data string, delay time.Duration) {
        time.Sleep(delay)
        channel <- data
    }
    go send(channel, "Hello 1", 100*time.Millisecond)
    go send(channel, "Hello 2", 2000*time.Millisecond)
    go send(channel, "Hello 3", 500*time.Millisecond)

    timeout := time.After(1000 * time.Millisecond)
    var results []string
    for i := 0; i < 3; i++ {
        select {
        case result := <-channel:
            results = append(results, result)
        case <-timeout:
            fmt.Println("timed out")
        }
    }

    fmt.Println(results)
}

The timeout is 1 second. If the response time is more than that, it is ignored.

Hello 1 comes in 100 msec
Hello 2 comes in 2000 msec
Hello 3 comes in 500 msec

In this case, Hello 2 is ignored and other responses are added to the results.

$ go run main.go 
timed out
[Hello 1 Hello 3]

Use only the fastest response

You can call the channel only once if you want to use only one response that comes in the shortest time.

func runChannelTest3() {
    channel := make(chan string)

    send := func(channel chan string, data string, delay time.Duration) {
        time.Sleep(delay)
        channel <- data
    }
    go send(channel, "Hello 1", 100*time.Millisecond)
    go send(channel, "Hello 2", 2000*time.Millisecond)
    go send(channel, "Hello 3", 50*time.Millisecond)

    result := <-channel // receives the fastest response

    fmt.Println(result)
}

It receives the fastest response only in this way. So the result is the following.

$ go run main.go 
Hello 3

If you want to add timeout too, use select clause there.

func runChannelTest4() {
    channel := make(chan string)

    send := func(channel chan string, data string, delay time.Duration) {
        time.Sleep(delay)
        channel <- data
    }
    go send(channel, "Hello 1", 100*time.Millisecond)
    go send(channel, "Hello 2", 2000*time.Millisecond)
    go send(channel, "Hello 3", 50*time.Millisecond)

    timeout := time.After(10 * time.Millisecond)
    select {
    case result := <-channel:
        fmt.Println(result)
    case <-timeout:
        fmt.Println("timed out")
    }
}

I have set 10 to the timeout. So it doesn’t receive any response.

$ go run main.go 
timed out

Comments

Copied title and URL