Go was built with the purpose of being a really powerful concurrent language, and it is. To achieve that, it leverages two primitives, goroutines and channels.

Goroutines

The most basic definition of goroutine is a lightweight thread managed by the Go runtime. They behave like OS threads, but they are much cheaper regarding memory consumption, setup, teardown and switching time. For example the creation of goroutines, requires only 2kB of stack space. Creating and destroying a goroutine is much faster because the runtime doesn't have to request resources from the OS to every goroutine.

The Go runtime keeps one or more real OS threads and decides when and in what OS thread the goroutines are going to be executed.

To spawn a goroutine you must use the keyword go followed by a function call. Let's say you have the function exec(s string), to execute that function concurrently we use go exec(s).

package main

import (
    "fmt"
    "time"
)

func exec(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go exec("world")
    exec("hello")
}

We can also spawn a goroutine for an anonymous function call.

package main

import (
    "fmt"
    "time"
)

func main() {
    go func(s string) {
        for i := 0; i < 5; i++ {
            time.Sleep(100 * time.Millisecond)
            fmt.Println(s, i)
        }
    }("executing...")

    time.Sleep(1 * time.Second)
}

Notice that I deliberately added a time.Sleep(1 * time.Second) line at the end of the main function. That's necessary because once you spawn a goroutine, the next line will execute immediately and will not wait for the goroutine to complete.

Let's try to decrease the sleeping time, and check if the main function terminates before the goroutine finishes.

package main

import (
    "fmt"
    "time"
)
func main() {
    go func(s string) {
        for i := 0; i < 5; i++ {
            time.Sleep(100 * time.Millisecond)
            fmt.Println(s, i)
        }
    }("executing...")

    time.Sleep(300 * time.Millisecond)
}

As you can see, the main function slept for 300 milliseconds, making the loop inside the goroutine run only 3 times before the main function exited, terminating the goroutine as well.

Notice that when the main function exists, the whole program is terminated, including all running, goroutines. The same is not true if one goroutine spawns another one, the child goroutine will not end if it's parent finishes. Let's take a look.

package main

import (
    "fmt"
    "time"
)

func first() {
    go second()
    time.Sleep(100 * time.Millisecond)
    fmt.Println("exiting first after 100 ms")
}

func second() {
    time.Sleep(200 * time.Millisecond)
    fmt.Println("exiting second after 200 ms")
}

func main() {
    go first()
    time.Sleep(300 * time.Millisecond)
    fmt.Println("exiting main after 300 ms")
}

Channels

In Go, channels provides communication between two goroutines. They are the pipes that connect concurrent goroutines. You can use them to send or receive data without worrying about concurrent access to shared state. Channels like array or slices, are typed, which means, you can only send values that share the same channel's type.

Here is a simple example. To initialize a new channel we use the function make. The keyword chan followed by a type, indicates which type that channel accepts. To send or receive messages we use the <- operator. In other to send a message on the channel, messages <- "ping" and to receive it's the inverse, msg := <-messages.

package main

import "fmt"

func main() {
    messages := make(chan string)

    go func() { messages <- "ping" }()

    msg := <-messages
    fmt.Println(msg)
}

Channels come in two flavors, buffered and unbuffered channels. By default, channels are unbuffered. In practice, this means that they will block the execution of your goroutine until the receiver and the sender are ready.

Let's take the previous example, and change it a little bit to make it more clear; the main goroutine will block in the msg := <-messages until it gets the message in the channel.

package main

import (
    "fmt"
    "time"
)

func main() {
    messages := make(chan string)

    go func() {
        fmt.Println("Sleeping 1 second to prove that the main function will wait")
        time.Sleep(1 * time.Second)

        messages <- "ping"
    }()

    fmt.Println("Waiting some message in the messages channel to appear before proceed...")
    msg := <-messages
    fmt.Println("Finaly got the message.")
    fmt.Println(msg)
}

Here we have a more complex and useful usage of channels plus goroutines. Our main function needs to execute 5 expensive operations, instead of doing in serial, within a for loop, let's run one goroutine for each operation. To collect the results of each operation we are going to use a channel, so the goroutines can send the operation result to the main function without worrying about sharing state. By using channels, you get a complete concurrent code, but feels and looks as simple as the execution os sequential.

Notice that expensive operations could be anything. Imagine you are building an API that needs to request n other external APIs, instead of doing one request at the time, you can do it all at once, and collect the results of them all afterward.

package main

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

func expensiveOperation(ch chan int) {
    time.Sleep(100 * time.Millisecond)
    ch <- rand.Int()
}

func main() {
    numOp := 5
    resultCh := make(chan int)

    for i := 0; i < numOp; i++ {
        go expensiveOperation(resultCh)
    }

    randNumbers := []int{}
    for i := 0; i < numOp; i++ {
        num := <-resultCh
        randNumbers = append(randNumbers, num)
    }

    fmt.Printf("Expensive operation result: %v", randNumbers)
}

Luiz Filho

Luiz is a Software Engineer with 8+ years of experience. Currently working for Globo.com, the internet branch of Globo, the 4th largest media conglomerate in the world. His current activities include: developing and maintaining Globo.com Live Stream CDN, which successfully broadcast more than 50 live channels during Rio Olympics and a bunch of microservices to support that.

  1. Comments for Concurrency

You must login to comment

You May Also Like