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.
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!!
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 msecHello 2
comes in 2000 msecHello 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