Concurrency in Go (Part 1)

Let's explore the most important feature in Go, Concurrency. We are going to be introduced by Goroutines and Channels here. They are built-in features in Go, not libraries. In part 1, let's get familiar with the concept and syntaxes.

Concurrency is the ability to be executed out-of-order without affecting the outcome. It is about executing multiple stuff at the same time and not in a particular order. Let's make some code to understand this concept.

  1. Goroutines
  2. Channels
  3. Buffered Channels
  4. Range and Close
  5. Select
  6. Default Selection
  7. Closing
  8. References

Goroutines

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
    "fmt"
    "time"
)

func count() {
    for i := 1; true; i++ {
        fmt.Println(i)
        time.Sleep(time.Second)
    }
}

func main() {
    go count()
    fmt.Println("Let's count!")
    time.Sleep(time.Second * 3)
    fmt.Println("Enough counting!")
}

Let's execute the code above. A goroutine is a lightweight thread managed by the Go runtime. As we can see from the code above, we put the go syntax before the count function. That means we put the count function in a goroutine. We slept for 3 seconds means counting to 3. Let's try to remove the go syntax. It would be a different result.

Channels

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
    "fmt"
    "time"
)

func count(c chan int) {
    for i := 1; true; i++ {
        c <- i
        time.Sleep(time.Second)
    }
}

func main() {
    c := make(chan int)
    go count(c)
    fmt.Println("Let's count!")
    for i := 0; i < 3; i++ {
        fmt.Println(<-c)
    }
    fmt.Println("Enough counting!")
}

The similarity of our code above with the previous one is we have the same result.

Let's count!
1
2
3
Enough counting!

The difference is that our code above uses a channel inside a variable called c with int as the data type. The chan syntax before the int means we defined a channel for the integer type. It has the same result as the previous one. We counted the i variable 3 times is equal to 3 seconds because a second happened in the count function every time we put the i into the channel, c, with the <- syntax.

Remember, they are out of order. But we just made it in order using a trick such as channels and time.Sleep. You can play around with the code to make the result out of order.

Buffered Channels

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int, 3)
    initCount := 3
    printCount := 3

    for i := 1; i <= initCount; i++ {
        c <- i
        time.Sleep(time.Second)
    }

    fmt.Println("Let's count!")
    for i := 0; i < printCount; i++ {
        fmt.Println(<-c)
    }
    fmt.Println("Enough counting!")
}

We got a 3 seconds delay while defined the counts. Let's set the initCount to be more than 3. We will get an error because the provided buffer length is 3. If we set the initCount to be lower than 3, we will get the same error because that is lower than how we printed it, printCount.

fatal error: all goroutines are asleep - deadlock!

Channels can be buffered. Provide the buffer length as the second argument to make to initialize a buffered channel. Sends to a buffered channel block only when the buffer is full. Receives block when the buffer is empty.

Range and Close

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
    "fmt"
    "time"
)

func count(c chan int) {
    for i := 1; i <= cap(c); i++ {
        c <- i
        time.Sleep(time.Second)
    }

    close(c)
}

func main() {
    c := make(chan int, 3)
    go count(c)
    fmt.Println("Let's count!")
    for i := range c {
        fmt.Println(i)
    }
    fmt.Println("Enough counting!")
}

We counted using the range syntax for the channel. We put the i to the channel under the provided buffer length using the cap syntax. We closed the channel to indicate that no more values will be sent using the close syntax.

value, receive := <-c
fmt.Println(value)
fmt.Println(receive)

You can put the code above before or after printing the channel. The boolean receive is to check whether the channel receives the value or not and the value is the current value of the channel.

Select

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
    "fmt"
)

func count(c, quit chan int) {
    i := 1

    for {
        select {
        case c <- i:
            i++
        case <-quit:
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)

    go func() {
        for i := 0; i < 3; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()

    fmt.Println("Let's count!")
    count(c, quit)
    fmt.Println("Enough counting!")
}

We have two channels here. The c is to count. The quit is to quit. The select statement lets a goroutine wait on multiple communication operations. It looks similar to the switch syntax.

Default Selection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
    "fmt"
    "time"
)

func main() {
    c := time.Tick(time.Second)
    quit := time.After(time.Second * 3)
    i := 1

    fmt.Println("Let's count!")
    for {
        select {
        case <-c:
            fmt.Println(i)
            i++
        case <-quit:
            fmt.Println("Enough counting!")
            return
        default:
            fmt.Print(".")
            time.Sleep(time.Second / 2)
        }
    }
}

The result of the code above is below.

Let's count!
..1
..2
..3
Enough counting!

The default case in a select is run if no other case is ready. Use a default case to try a send or receive without blocking.

Closing

Maybe in the next part, we will try to implement this in the app or explore the advanced feature. You can get the source code on https://github.com/aristorinjuang/concurrency_01.

References