I have developed a web application that makes an HTTP
request to the App Store or to the Play Store or both. Depending on the input parameters. The application parses some HTML
from the response and prints a few information afterward.
The application is developed pretty straightforward. It parses the input queries (which could be android
and/or ios
), put this information into two slices
, and performs the HTTP
requests one after another. First, all android
inputs, followed by all ios
inputs.
Based on the number of input parameters, this could take a lot of time before it prints something.
Assuming that each request to the stores will take 250 milliseconds for one parameter and given 4 parameters; the application will respond in 1 second before printing something. This is not a nice and especially not a scalable solution. Also because we use this application at work and is intended to use with at least 24 input parameters (12 for each platform).
The solution — Goroutines
To make my application response faster I have to execute some code in parallel, not sequential.
The first attempt was to parallelize the android
and ios
requests. This means all android
inputs will be executed in parallel with all ios
inputs. With that solution, I saved 50% of the response time. But that was not enough for me. So I dived deeper and executed each single HTTP
request for each input in parallel as well!
The application behaves now:
- Run
android
andios
requests in parallel - Execute each
HTTP
request for each platform in parallel - Wait until all requests are finished and print it
The output will be printed as fast as the slowest HTTP response. Regardless of the platform or store. If the slowest HTTP
request takes 250 milliseconds, the application prints in 250 milliseconds. Furthermore, the response time of the application does not depend on the number of input parameters anymore. Now I’m able to put “unlimited” input parameters in without affecting the response time.
Now it is finally time to show you some code!
To get familiar with Goroutines I read the “A Tour of Go” concurrency chapter until lesson four. Which helped me quite well because the examples are basically exactly what I need.
In general, to start a new goroutine simply add the go
word in front of your function call. The function will then, well, be executed in a new goroutine and therefore not block the current running code.
func main() {
go printSomething()
}func printSomething() {
println("Print something")
}
If you run the example above, it will print nothing. This is intended because, as said above, the new goroutine does not block the current code. The program terminates before the println
statement can be reached.
But how to wait until a function is finish with goroutines? I need this because in my application I had to wait until each android
and ios
slice
finished their HTTP
requests.
Go has a solution for that with so called channels
. They can be used to send and receive values from another goroutine and therefore blocks until the other side is ready.
func main() {
doneChannel := make(chan bool)
go printSomething(doneChannel)
<-doneChannel
}func printSomething(doneChannel chan bool) {
println("Print something")
doneChannel <- true
}
First, we have to create a channel
with make(chan bool)
. This will make a channel
of type boolean
. But you can of course use any other type. In the printSomething
function we send to the doneChannel
a value. In this case true
to mark the channel “as done”. The <-doneChannel
syntax in the main
function blocks until the channel received a value.
If we run this sample you would see the Print something
output on your screen.
With that, I already parallelize my two slices
. I created two functions, started both in a new goroutine and wait in main
until both functions send the channel
a value. But how to execute each HTTP
request in parallel?
Well, in Go you can create inline functions. You can create a function inside another. With this technique it is possible to loop over an slice
, create a new inline function which will run in another goroutine, and wait in the outer function until all of the started goroutines are finished.
import "fmt"func main() {
channel := make(chan int)
for i := 0; i < 3; i++ {
go func(iterable int) {
fmt.Printf("Called %d\n", iterable)
channel <- iterable
}(i)
}
for i := 0; i < 3; i++ {
<-channel
}
}
This time we make a channel
of the type int
. In the for
loop we create an inline function and move it into a goroutine with the go
keyword. If the “heavy” execution in that function is done, we will send the “terminal signal” to the channel
(channel <- iterable
).
Meanwhile, in the non blocking code, we start another for
loop to wait exactly three times the channel
received a value. Thus, we block the main
function until the channel
received three values.
You may ask what happens if we wait more times than the channel
receives values. In this case, Go will error with a deadlock
.
There are of course other techniques to don’t hardcode the number of sending or receiving signals. Take a look at closing a channel if you are interested in it. But I want to keep this post simple. So I don’t get more into detail yet.
It was pretty easy to start with goroutines. In the past, I always heard that “the Go concurrency api” is nice and easy to use, compared to other languages. Today I can say: “Yep. They are right”. It took me only a few hours to understand and implement goroutines in my application.
Learning sources
A Tour of Go, the first four concurrency chapters.