One of Go’s initial design requirements was to make it easy to create multi-threaded applications. While many languages support multi-threading, it’s not always easy to implement. Go wanted to make it easy to do this.
Consider the following code where it runs sequentially.
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("Starting App")
callSequentially()
}
func PrintHello(msg int) {
fmt.Print("Hello! ")
fmt.Print(msg)
fmt.Println("")
}
func callSequentially() {
fmt.Println("Calling Sequentially")
defer timeTrack(time.Now(), "Sequentially")
for i := 0; i < 100; i++ {
PrintHello(i)
}
}
// neat little function to determine how long something takes to process in go
func timeTrack(start time.Time, name string) {
elapsed := time.Since(start)
fmt.Printf("%s took %s\n", name, elapsed)
}
If you run this, every function call to PrintHello() has to complete before it returns to the loop to finish executing. There is a lot of overhead involved in that, and it makes it relatively slow.
However, with a slight modifictation, you dramatically improve performance.
func main() {
fmt.Println("Starting App")
callWithGoroutine()
time.Sleep(2 * time.Second)
}
func callWithGoroutine() {
fmt.Println("Calling With Threads")
defer timeTrack(time.Now(), "Threads")
for i := 0; i < 100; i++ {
go PrintHello(i)
}
}
If you look at them, you may not see what the difference is. However, upon closer inspection, you can see there is a go
command before the function. This tells go, the language, to start a thread for that function call. With each function call being put in a thread, it can execute as it needs, and the loop performs much… much… much faster.
The Minor Issue…
There are a couple of issues that people run into when they use threads. One of which is timing regarding output. If you run the two, you will notice the first one is much slower than the goroutines version. However, you might notice that because PrintHello()
uses multiple print statements, they don’t always print in order with the other print statements of that thread instance.
To avoid these types of issues, go uses something it calls channels. Channels are used to submit data from one thread to another, and can take on the form of a channel and a data type, so it knows what type of data to submit.
// general form for a channel
messages := make(chan <data type>)
Now let’s update the function to send data back and forth.
func PrintHello2(msg int, results chan string) {
results <- "Hello! "
results <- strconv.Itoa(msg) // need a new import for this
results <- "\n"
close(results)
}
Notices how we send data to the channel. The direction of the arrow is used to determine if you are sending it to the channel, or from the channel. However, we need to modify the code to handle the creation of the channels (each instance will have it’s own channel for example), and then reading that data once it is collected.
func callWithGoroutine() {
fmt.Println("Calling With Threads")
defer timeTrack(time.Now(), "Threads")
var list = []chan string{make(chan string)}
for i := 0; i < 100; i++ {
list = append(list, make(chan string))
}
for i := 0; i < 100; i++ {
go PrintHello2(i, list[i])
}
for i := 0; i < 100; i++ {
for msg := range list[i] {
fmt.Println(msg)
}
}
}
With all this extra code, it’s a little slower, because we have bottle necks due to printing to the screen in this instance. However, for heavy compute tasks, this will be faster.
Goroutines and channels was originally found on Access 2 Learn