Introduction To Golang - Part 2

Sangam Biradar's photo
Sangam Biradar
·Jul 6, 2022·

35 min read

Introduction To Golang - Part 2

Subscribe to our newsletter and never miss any upcoming articles

Table of contents

Concurrency is not parallelism

some quotes to support this statement :)

Concurrency is a property of the code ; parallelism is a property of the running program

Concurrency is about dealing with lot of things at once . Parallelism is about doing lot of things at once

Go is known for its first class support for concurrency code in a program , and channels solve the problem of communicating safely between concurrently running code .

Go also supports more traditional tools for writing concurrency code . Mutexes , pools , locks they are implemented in sync package

Go provides

  • Concurrency execution ( goroutines)
    • synchronisation and messaging ( channels )
    • Muti-way concurrent control ( select )

1. Channels in short

Channel are one of the synchronization primitives in Go . Channel can be imagined as a pipe for stream of data . they are used to communicate information between goroutines .

Create a Channel is simple

channel := make( chan interface {})

there are a few types of channel that you can create

2. Bidirectional Channels

by default you create channel that can be read and written .

package main

import "fmt"

func main() {
    messages := make(chan string)
    go func() { messages <- "ping" }()

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

}

Anyone can read and write to this channel and this can cause problem in concurrent environments .

3. Directional Channels

you can specify if a channel is meant to be only read or written data . This increases the type-safety of the program

the sender cam know when there's no more data to send , and its the receivers responsibility to watch for the channel

package main

import "fmt"

func ping(pings chan<- string, msg string) {
    pings <- msg

}

func pong(pings <-chan string, pongs chan<- string) {
    msg := <-pings
    pongs <- msg
}

func main() {
    pings := make(chan string, 1)
    pongs := make(chan string, 1)
    ping(pings, "passed message")
    pong(pings, pongs)
    fmt.Println(<-pongs)
}

4. Buffered Channels

Default channel are unbuffered , they will only accept sends (chan <-) if there is a receive (<- chan ) ready to read the send value .

Buffered channel accept a limited number of values without a corresponding receiver for those values .

package main

import "fmt"

func main() {

    messages := make(chan string, 2)
    messages <- "buffered"
    messages <- "channel"

    fmt.Println(<-messages)
    fmt.PrintIn(<-messages)

}

Lets understand Concurrency in depth

Don't Communicate by sharing memory , share memory by communicating

5. Creating goroutine

package main

import "fmt"

func main() {
    fmt.Println("Hello,world ")
}
// here func main itself act as goroutine

this is very small program not covering much about groutine

let add one function and try to print something

package main

import "fmt"

func printSomething(s string) {

    fmt.Println(s)

}

func main() {

    printSomething("my 1st line ")
    printSomething("my 2nd line ")

}

output :

my 1st line 
my 2nd line 

Program exited.

so lets add go keyword to function it can start its own goroutine


    go printSomething("my 1st line ")

what happened here go compiler schedule its own goroutine and if your run you see only my 2nd line printed as output . this is so lightweight and fast but there is no error lets put timer after go routine

go printSomething("my 1st line ")

    time.Sleep(1 * time.Second)

it will get you output but this is not best way to do it !

6 . WaitGroups is the right way


package main

import (
    "fmt"
    "time"
)

func printSomething(s string) {

    fmt.Println(s)

}

func main() {

    words := []string{

        "one",
        "two",
        "five",
    }

    for i, x := range words {

        go printSomething(fmt.Sprintf("%d: %s", i, x))

    }

    time.Sleep(1 * time.Second)
    printSomething("my 2nd line ")

}

output

0: one
2: five
1: two
my 2nd line 

Program exited.

if you see here the strings index is not in order that's why time.Sleep(1 * time.Second) is not right way !

       var wg sync.WaitGroup

so we are printing 3 time that is so basically we need to wait each time so we will add wg.add(3) also after running go routine we need to set it to zero . so we will add another parameter to printSomething function wg *sync.WaitGroup and then add defer to run surrounding function

package main

import (
    "fmt"
    "sync"
)

func printSomething(s string, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println(s)

}

func main() {

    var wg sync.WaitGroup

    words := []string{

        "one",
        "two",
        "five",
    }

    wg.Add(len(words))

    for i, x := range words {

        go printSomething(fmt.Sprintf("%d: %s", i, x), &wg)

    }

    wg.Wait()
    wg.Add(1)

    printSomething("my 2nd line ", &wg)

}

7 . Writing tests with WaitGroups


    wg.Add(5)

lets assume you have added value more then the length of string it show error


2: five
1: two
0: one
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc00005c060?)
    /usr/local/go-faketime/src/runtime/sema.go:56 +0x25
sync.(*WaitGroup).Wait(0x496148?)
    /usr/local/go-faketime/src/sync/waitgroup.go:136 +0x52
main.main()
    /tmp/sandbox3776579600/prog.go:33 +0x1c8

Program exited.
package main

import (
    "io"
    "os"
    "strings"
    "sync"
    "testing"
)

func Test_printSomething(t *testing.T) {
    stdOut := os.Stdout

    r, w, _ := os.Pipe()
    os.Stdout = w

    var wg sync.WaitGroup
    wg.Add(1)
    go printSomething("ok print", &wg)
    wg.Wait()

    _ = w.Close()

    result, _ := io.ReadAll(r)
    output := string(result)
    os.Stdout = stdOut
    if !strings.Contains(output, "ok print") {
        t.Errorf("Expected output to contain 'ok print' but got %s", output)
    }
}

output

go test *.go
ok      command-line-arguments  0.099s

let write sync.WaitGroup to print everything in order

package main

import (
    "fmt"
    "sync"
)

var msg string

func updateMessage(s string) {
    defer wg.Done()
    msg = s
}

func printMessage() {
    fmt.Println(msg)
}

var wg sync.WaitGroup

func main() {

    msg = "Hello, world!"
    wg.Add(1)

    go updateMessage("Hello, One !")
    wg.Wait()
    printMessage()

    wg.Add(1)
    go updateMessage("Hello, two !")
    wg.Wait()
    printMessage()

    wg.Add(1)
    go updateMessage("Hello, three !")
    wg.Wait()
    printMessage()
}

if you see above program we have different go routine that print different values now here we set defer wg.Done() that set value to -1 and we add the wg.Add(1) and then whenever the new go routine start we put wg.Wait() and print value and set back to wg.Add(1) so this way we get the exact same out no matter you run n number of time

output


~/Documents/golabs ❯ go run .
Hello, One !
Hello, two !
Hello, three !

~/Documents/golabs ❯ go run .
Hello, One !
Hello, two !
Hello, three !

8. Race Condition

package main

import (
    "fmt"
    "sync"
)

var msg string
var wg sync.WaitGroup

func updateMessage(s string) {
    defer wg.Done()
    msg = s

}

func main() {

    msg = "Hello, world!"
    wg.Add(2)
    go updateMessage("Hello, one")
    go updateMessage("Hello, two")
    wg.Wait()

    fmt.Println(msg)

}

if you see above program we have two variables one in msg string and another sync.WaitGroup and defer wg.Done() we are decrementing Waitgroup by 1 and then in main function we have two go routine running and if you run this program .


~/Documents/golabs ❯ go run .      
Hello, one

but as some point you may get different output

go run -race .
==================
WARNING: DATA RACE
Write at 0x0000011d09b0 by goroutine 8:
  main.updateMessage()
      /Users/sangam/Documents/golabs/main.go:13 +0x78
  main.main·dwrap·3()
      /Users/sangam/Documents/golabs/main.go:22 +0x47

Previous write at 0x0000011d09b0 by goroutine 7:
  main.updateMessage()
      /Users/sangam/Documents/golabs/main.go:13 +0x78
  main.main·dwrap·2()
      /Users/sangam/Documents/golabs/main.go:21 +0x47

Goroutine 8 (running) created at:
  main.main()
      /Users/sangam/Documents/golabs/main.go:22 +0x164

Goroutine 7 (finished) created at:
  main.main()
      /Users/sangam/Documents/golabs/main.go:21 +0xeb
==================
Hello, two
Found 1 data race(s)
exit status 66

if I run this time with go run -race it will give warning . in this condition its accessing same data but we are not sure which go routine going to finish first so you can run into problem , now how we can fix this problem !

9. Mutex

package main

import (
    "fmt"
    "sync"
)

var msg string
var wg sync.WaitGroup

func updateMessage(s string, m *sync.Mutex) {
    defer wg.Done()
    m.Lock()
    msg = s
    m.Unlock()

}

func main() {

    msg = "Hello, world!"
    var mutex sync.Mutex
    wg.Add(2)
    go updateMessage("Hello, one", &mutex)
    go updateMessage("Hello, two", &mutex)
    wg.Wait()

    fmt.Println(msg)

}

let create var mutex with of mutex is sync.Mutex and it will receive as a parameter func updateMessage(s string, m *sync.Mutex) and then add reference to the mutex go updateMessage("Hello, one", &mutex) now to rescue the race data we will add m.Lock() no one access it until it use and then unlock it m.Unlock()

~/Documents/golabs ❯ go run .
Hello, one

~/Documents/golabs ❯ go run -race .
Hello, two

so here we are accessing data safely and there is no race data warning .

package main

import (
    "testing"
)

func Test_updatemesasge(t *testing.T) {
    msg = "Hello, world!"

    wg.Add(1)
    go updateMessage("Hello, one")
    wg.Wait()

    if msg != "Hello, one" {
        t.Error("Expected ")
    }
}

output :


~/Documents/golabs ❯ go test .
ok      example2        0.262s

let duplicate go routine with same parameter

package main

import (
    "testing"
)

func Test_updatemesasge(t *testing.T) {
    msg = "Hello, world!"

    wg.Add(2)
    go updateMessage("x ")
    go updateMessage("Hello, one")
    wg.Wait()

    if msg != "Hello, one" {
        t.Error("Expected ")
    }
}

output

~/Documents/golabs ❯ go test .
ok      example2        0.439s

let check data race

go test -race .
==================
WARNING: DATA RACE
Write at 0x0000012c0650 by goroutine 9:
  example2.updateMessage()
      /Users/sangam/Documents/golabs/main.go:14 +0x78
  example2.Test_updatemesasge·dwrap·5()
      /Users/sangam/Documents/golabs/main_test.go:12 +0x47

Previous write at 0x0000012c0650 by goroutine 8:
  example2.updateMessage()
      /Users/sangam/Documents/golabs/main.go:14 +0x78
  example2.Test_updatemesasge·dwrap·4()
      /Users/sangam/Documents/golabs/main_test.go:11 +0x47

Goroutine 9 (running) created at:
  example2.Test_updatemesasge()
      /Users/sangam/Documents/golabs/main_test.go:12 +0x164
  testing.tRunner()
      /usr/local/Cellar/go/1.17.3/libexec/src/testing/testing.go:1259 +0x22f
  testing.(*T).Run·dwrap·21()
      /usr/local/Cellar/go/1.17.3/libexec/src/testing/testing.go:1306 +0x47

Goroutine 8 (finished) created at:
  example2.Test_updatemesasge()
      /Users/sangam/Documents/golabs/main_test.go:11 +0xeb
  testing.tRunner()
      /usr/local/Cellar/go/1.17.3/libexec/src/testing/testing.go:1259 +0x22f
  testing.(*T).Run·dwrap·21()
      /usr/local/Cellar/go/1.17.3/libexec/src/testing/testing.go:1306 +0x47
==================
--- FAIL: Test_updatemesasge (0.00s)
    testing.go:1152: race detected during execution of test
FAIL
FAIL    example2        0.270s
FAIL

if you see here I get race data warning !

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

type Income struct {
    Source string
    Amount int
}

func main() {
    // variable for bank balance
    var bankBalance int
    var balance sync.Mutex

    // print out starting values
    fmt.Printf("Initial account balance: $%d.00", bankBalance)
    fmt.Println()

    // define weekly revenue
    incomes := []Income{
        {Source: "google", Amount: 500},
        {Source: "amazon ", Amount: 10},
        {Source: "docker ", Amount: 50},
        {Source: "tenable", Amount: 100},
    }

    wg.Add(len(incomes))

    // loop through 52 weeks and print out how much is made; keep a running total
    for i, income := range incomes {

        go func(i int, income Income) {
            defer wg.Done()

            for week := 1; week <= 52; week++ {
                balance.Lock()
                temp := bankBalance
                temp += income.Amount
                bankBalance = temp
                balance.Unlock()

                fmt.Printf("On week %d, you earned $%d.00 from %s\n", week, income.Amount, income.Source)
            }
        }(i, income)
    }

    wg.Wait()

    // print out final balance
    fmt.Printf("Final bank balance: $%d.00", bankBalance)
    fmt.Println()
}

here in above program we have struct type of Income with two field one in Source and another Amount

type Income struct {
    Source string
    Amount int
}

lets declare length of Waitgroup

    wg.Add(len(incomes))

here we have inner loop which consists of weekly earnings

    for i, income := range incomes {

        go func(i int, income Income) {
            defer wg.Done()

            for week := 1; week <= 52; week++ {
                balance.Lock()
                temp := bankBalance
                temp += income.Amount
                bankBalance = temp
                balance.Unlock()

                fmt.Printf("On week %d, you earned $%d.00 from %s\n", week, income.Amount, income.Source)
            }
        }(i, income)

here we get print final balance remaining same as previous programs

wg.Wait()

    // print out final balance
    fmt.Printf("Final bank balance: $%d.00", bankBalance)
    fmt.Println()
}

output

~/Documents/golabs ❯ go run .
Initial account balance: $0.00
On week 1, you earned $100.00 from tenable
On week 2, you earned $100.00 from tenable
On week 3, you earned $100.00 from tenable
On week 4, you earned $100.00 from tenable
On week 5, you earned $100.00 from tenable
On week 6, you earned $100.00 from tenable
On week 7, you earned $100.00 from tenable
On week 8, you earned $100.00 from tenable
On week 9, you earned $100.00 from tenable
On week 10, you earned $100.00 from tenable
On week 11, you earned $100.00 from tenable
On week 12, you earned $100.00 from tenable
On week 13, you earned $100.00 from tenable
On week 14, you earned $100.00 from tenable
On week 15, you earned $100.00 from tenable
On week 16, you earned $100.00 from tenable
On week 1, you earned $50.00 from docker 
On week 2, you earned $50.00 from docker 
On week 3, you earned $50.00 from docker 
On week 4, you earned $50.00 from docker 
On week 5, you earned $50.00 from docker 
On week 6, you earned $50.00 from docker 
On week 7, you earned $50.00 from docker 
On week 8, you earned $50.00 from docker 
On week 9, you earned $50.00 from docker 
On week 10, you earned $50.00 from docker 
On week 11, you earned $50.00 from docker 
On week 12, you earned $50.00 from docker 
On week 13, you earned $50.00 from docker 
On week 14, you earned $50.00 from docker 
On week 15, you earned $50.00 from docker 
On week 16, you earned $50.00 from docker 
On week 17, you earned $50.00 from docker 
On week 18, you earned $50.00 from docker 
On week 19, you earned $50.00 from docker 
On week 20, you earned $50.00 from docker 
On week 21, you earned $50.00 from docker 
On week 22, you earned $50.00 from docker 
On week 23, you earned $50.00 from docker 
On week 24, you earned $50.00 from docker 
On week 25, you earned $50.00 from docker 
On week 26, you earned $50.00 from docker 
On week 27, you earned $50.00 from docker 
On week 28, you earned $50.00 from docker 
On week 29, you earned $50.00 from docker 
On week 30, you earned $50.00 from docker 
On week 31, you earned $50.00 from docker 
On week 32, you earned $50.00 from docker 
On week 33, you earned $50.00 from docker 
On week 34, you earned $50.00 from docker 
On week 35, you earned $50.00 from docker 
On week 36, you earned $50.00 from docker 
On week 37, you earned $50.00 from docker 
On week 38, you earned $50.00 from docker 
On week 39, you earned $50.00 from docker 
On week 40, you earned $50.00 from docker 
On week 41, you earned $50.00 from docker 
On week 42, you earned $50.00 from docker 
On week 43, you earned $50.00 from docker 
On week 44, you earned $50.00 from docker 
On week 45, you earned $50.00 from docker 
On week 46, you earned $50.00 from docker 
On week 1, you earned $500.00 from google
On week 47, you earned $50.00 from docker 
On week 48, you earned $50.00 from docker 
On week 49, you earned $50.00 from docker 
On week 50, you earned $50.00 from docker 
On week 51, you earned $50.00 from docker 
On week 52, you earned $50.00 from docker 
On week 1, you earned $10.00 from amazon 
On week 2, you earned $10.00 from amazon 
On week 3, you earned $10.00 from amazon 
On week 4, you earned $10.00 from amazon 
On week 5, you earned $10.00 from amazon 
On week 17, you earned $100.00 from tenable
On week 18, you earned $100.00 from tenable
On week 19, you earned $100.00 from tenable
On week 20, you earned $100.00 from tenable
On week 21, you earned $100.00 from tenable
On week 22, you earned $100.00 from tenable
On week 23, you earned $100.00 from tenable
On week 24, you earned $100.00 from tenable
On week 25, you earned $100.00 from tenable
On week 26, you earned $100.00 from tenable
On week 27, you earned $100.00 from tenable
On week 28, you earned $100.00 from tenable
On week 29, you earned $100.00 from tenable
On week 30, you earned $100.00 from tenable
On week 31, you earned $100.00 from tenable
On week 32, you earned $100.00 from tenable
On week 33, you earned $100.00 from tenable
On week 34, you earned $100.00 from tenable
On week 35, you earned $100.00 from tenable
On week 6, you earned $10.00 from amazon 
On week 7, you earned $10.00 from amazon 
On week 8, you earned $10.00 from amazon 
On week 9, you earned $10.00 from amazon 
On week 10, you earned $10.00 from amazon 
On week 11, you earned $10.00 from amazon 
On week 12, you earned $10.00 from amazon 
On week 13, you earned $10.00 from amazon 
On week 14, you earned $10.00 from amazon 
On week 2, you earned $500.00 from google
On week 3, you earned $500.00 from google
On week 4, you earned $500.00 from google
On week 5, you earned $500.00 from google
On week 6, you earned $500.00 from google
On week 7, you earned $500.00 from google
On week 8, you earned $500.00 from google
On week 9, you earned $500.00 from google
On week 10, you earned $500.00 from google
On week 36, you earned $100.00 from tenable
On week 37, you earned $100.00 from tenable
On week 38, you earned $100.00 from tenable
On week 39, you earned $100.00 from tenable
On week 40, you earned $100.00 from tenable
On week 41, you earned $100.00 from tenable
On week 42, you earned $100.00 from tenable
On week 43, you earned $100.00 from tenable
On week 44, you earned $100.00 from tenable
On week 45, you earned $100.00 from tenable
On week 46, you earned $100.00 from tenable
On week 47, you earned $100.00 from tenable
On week 48, you earned $100.00 from tenable
On week 49, you earned $100.00 from tenable
On week 50, you earned $100.00 from tenable
On week 51, you earned $100.00 from tenable
On week 52, you earned $100.00 from tenable
On week 11, you earned $500.00 from google
On week 12, you earned $500.00 from google
On week 13, you earned $500.00 from google
On week 14, you earned $500.00 from google
On week 15, you earned $500.00 from google
On week 16, you earned $500.00 from google
On week 17, you earned $500.00 from google
On week 18, you earned $500.00 from google
On week 19, you earned $500.00 from google
On week 20, you earned $500.00 from google
On week 15, you earned $10.00 from amazon 
On week 16, you earned $10.00 from amazon 
On week 17, you earned $10.00 from amazon 
On week 18, you earned $10.00 from amazon 
On week 19, you earned $10.00 from amazon 
On week 20, you earned $10.00 from amazon 
On week 21, you earned $10.00 from amazon 
On week 22, you earned $10.00 from amazon 
On week 23, you earned $10.00 from amazon 
On week 24, you earned $10.00 from amazon 
On week 25, you earned $10.00 from amazon 
On week 26, you earned $10.00 from amazon 
On week 27, you earned $10.00 from amazon 
On week 28, you earned $10.00 from amazon 
On week 29, you earned $10.00 from amazon 
On week 30, you earned $10.00 from amazon 
On week 31, you earned $10.00 from amazon 
On week 32, you earned $10.00 from amazon 
On week 33, you earned $10.00 from amazon 
On week 34, you earned $10.00 from amazon 
On week 35, you earned $10.00 from amazon 
On week 36, you earned $10.00 from amazon 
On week 37, you earned $10.00 from amazon 
On week 38, you earned $10.00 from amazon 
On week 39, you earned $10.00 from amazon 
On week 40, you earned $10.00 from amazon 
On week 41, you earned $10.00 from amazon 
On week 42, you earned $10.00 from amazon 
On week 43, you earned $10.00 from amazon 
On week 44, you earned $10.00 from amazon 
On week 45, you earned $10.00 from amazon 
On week 46, you earned $10.00 from amazon 
On week 47, you earned $10.00 from amazon 
On week 48, you earned $10.00 from amazon 
On week 49, you earned $10.00 from amazon 
On week 50, you earned $10.00 from amazon 
On week 51, you earned $10.00 from amazon 
On week 52, you earned $10.00 from amazon 
On week 21, you earned $500.00 from google
On week 22, you earned $500.00 from google
On week 23, you earned $500.00 from google
On week 24, you earned $500.00 from google
On week 25, you earned $500.00 from google
On week 26, you earned $500.00 from google
On week 27, you earned $500.00 from google
On week 28, you earned $500.00 from google
On week 29, you earned $500.00 from google
On week 30, you earned $500.00 from google
On week 31, you earned $500.00 from google
On week 32, you earned $500.00 from google
On week 33, you earned $500.00 from google
On week 34, you earned $500.00 from google
On week 35, you earned $500.00 from google
On week 36, you earned $500.00 from google
On week 37, you earned $500.00 from google
On week 38, you earned $500.00 from google
On week 39, you earned $500.00 from google
On week 40, you earned $500.00 from google
On week 41, you earned $500.00 from google
On week 42, you earned $500.00 from google
On week 43, you earned $500.00 from google
On week 44, you earned $500.00 from google
On week 45, you earned $500.00 from google
On week 46, you earned $500.00 from google
On week 47, you earned $500.00 from google
On week 48, you earned $500.00 from google
On week 49, you earned $500.00 from google
On week 50, you earned $500.00 from google
On week 51, you earned $500.00 from google
On week 52, you earned $500.00 from google
Final bank balance: $34320.00

lets write test case for above program

package main

import (
    "io"
    "os"
    "strings"
    "testing"
)

func Test_main(t *testing.T) {

    stdOut := os.Stdout
    r, w, _ := os.Pipe()
    os.Stdout = w
    main()

    _ = w.Close()

    result, _ := io.ReadAll(r)
    output := string(result)
    os.Stdout = stdOut
    if !strings.Contains(output, "$34320.00") {
        t.Errorf("Expected output")
    }

}

output

~/Documents/golabs ❯ go test .
ok      example2        0.464s

test race


~/Documents/golabs ❯ go test -race .
ok      example2        0.291s

10 . Producer-Consumer problem

Producer-Consumer problem

here its take example of pizza as producer and consumer problem


package main

const NumberofPizzas = 10

var pizzaMade, pizzasFails, total int

type Producer struct {
    data chan Pizzaorder
    quit chan chan error
}

type Pizzaorder struct {
    pizzaNumber int
    message     string
    success     bool
}

func main() {

    // seed the random number generator

    // print out a message

    // create a producer

    // run the producer in the background

    // create and run consumer

    // print out ending messages

}

lets started with functions

package main

import (
    "math/rand"
    "time"

    color "github.com/fatih/color"
)

const NumberofPizzas = 10

var pizzaMade, pizzasFails, total int

type Producer struct {
    data chan Pizzaorder
    quit chan chan error
}

type Pizzaorder struct {
    pizzaNumber int
    message     string
    success     bool
}

func (p *Producer) close() error {
    ch := make(chan error)
    p.quit <- ch
    return <-ch
}

func pizzaria(pizzaMaker *Producer) {
    // keep track of the number of pizzas made
    // run forver or until we get a quit signal
    // try to make a pizza
    // if we can't make a pizza, send a message to the consumer
    for {
        // try to make pizza
        //decision
    }

}

func main() {

    // seed the random number generator
    rand.Seed(time.Now().UnixNano())

    // print out a message
    color.Cyan("Pizza delivery service")
    color.Cyan("======================")

    // create a producer

    pizzaJob := &Producer{
        data: make(chan Pizzaorder),
        quit: make(chan chan error),
    }

    // run the producer in the background
    go pizzaria(pizzaJob)

    // create and run consumer

    // print out ending messages

}

if I run this program

~/Documents/golabs ❯ go run .
Pizza delivery service

package main

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

    "github.com/fatih/color"
)

const NumberOfPizzas = 10

var pizzasMade, pizzasFailed, total int

type Producer struct {
    data chan PizzaOrder
    quit chan chan error
}

type PizzaOrder struct {
    pizzaNumber int
    message     string
    success     bool
}

func (p *Producer) Close() error {
    ch := make(chan error)
    p.quit <- ch
    return <-ch
}

func makePizza(pizzaNumber int) *PizzaOrder {
    pizzaNumber++
    if pizzaNumber <= NumberOfPizzas {
        delay := rand.Intn(5) + 1
        fmt.Printf("Received order #%d!\n", pizzaNumber)

        rnd := rand.Intn(12) + 1
        msg := ""
        success := false

        if rnd < 5 {
            pizzasFailed++
        } else {
            pizzasMade++
        }
        total++

        fmt.Printf("Making pizza #%d. It will take %d seconds....\n", pizzaNumber, delay)
        // delay for a bit
        time.Sleep(time.Duration(delay) * time.Second)

        if rnd <= 2 {
            msg = fmt.Sprintf("*** We ran out of ingredients for pizza #%d!", pizzaNumber)
        } else if rnd <= 4 {
            msg = fmt.Sprintf("*** The cook quit while making pizza #%d!", pizzaNumber)
        } else {
            success = true
            msg = fmt.Sprintf("Pizza order #%d is ready!", pizzaNumber)
        }

        p := PizzaOrder{
            pizzaNumber: pizzaNumber,
            message:     msg,
            success:     success,
        }

        return &p

    }

    return &PizzaOrder{
        pizzaNumber: pizzaNumber,
    }
}

func pizzeria(pizzaMaker *Producer) {
    // keep track of which pizza we are making
    var i = 0

    // run forever or until we receive a quit notification
    // try to make pizzas
    for {
        currentPizza := makePizza(i)
        if currentPizza != nil {
            i = currentPizza.pizzaNumber
            select {
            case pizzaMaker.data <- *currentPizza:
            // we tried to make a pizza (send someting to data channel )
            case pizzaMaker.data <- *currentPizza:
                // we tried to make a pizza (send someting to data channel )
            case quitChan := <-pizzaMaker.quit:
                // we received a quit request
                close(pizzaMaker.data)
                close(quitChan)
                return

            }
        }

    }
}

func main() {
    // seed the random number generator
    rand.Seed(time.Now().UnixNano())

    // print out a message
    color.Cyan("Pizza delivery service!")
    color.Cyan("----------------------------------")

    // create a producer
    pizzaJob := &Producer{
        data: make(chan PizzaOrder),
        quit: make(chan chan error),
    }

    // run the producer in the background
    go pizzeria(pizzaJob)

    for i := range pizzaJob.data {
        if i.pizzaNumber <= NumberOfPizzas {
            if i.success {
                color.Green("%s", i.message)
                color.Green("order #%d is out of deliver !", i.pizzaNumber)
            } else {
                color.Red("%s", i.message)
                color.Red("customer not happy ")
            }
        } else {
            color.Cyan("done making pizzas")
            err := pizzaJob.Close()
            if err != nil {
                color.Red("Error closing channel ", err)
            }
        }
    }

    // create and run consumer

    // print out the ending message
}

output


~/Documents/golabs ❯ go run .                                                                               30s
Pizza delivery service!
----------------------------------
Received order #1!
Making pizza #1. It will take 1 seconds....
Received order #2!
Making pizza #2. It will take 3 seconds....
*** The cook quit while making pizza #1!
customer not happy 
Received order #3!
Making pizza #3. It will take 4 seconds....
Pizza order #2 is ready!
order #2 is out of deliver !
Received order #4!
Making pizza #4. It will take 2 seconds....
*** The cook quit while making pizza #3!
customer not happy 
Received order #5!
Making pizza #5. It will take 3 seconds....
Pizza order #4 is ready!
order #4 is out of deliver !
Received order #6!
Making pizza #6. It will take 3 seconds....
Pizza order #5 is ready!
order #5 is out of deliver !
Received order #7!
Making pizza #7. It will take 4 seconds....
Pizza order #6 is ready!
order #6 is out of deliver !
Received order #8!
Pizza order #7 is ready!
order #7 is out of deliver !
Making pizza #8. It will take 5 seconds....
Received order #9!
Making pizza #9. It will take 1 seconds....
*** The cook quit while making pizza #8!
customer not happy 
Received order #10!
Making pizza #10. It will take 2 seconds....
Pizza order #9 is ready!
order #9 is out of deliver !
*** The cook quit while making pizza #10!
customer not happy 
done making pizzas

if you see above output its not in sequence

package main

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

    "github.com/fatih/color"
)

const NumberOfPizzas = 10

var pizzasMade, pizzasFailed, total int

type Producer struct {
    data chan PizzaOrder
    quit chan chan error
}

type PizzaOrder struct {
    pizzaNumber int
    message     string
    success     bool
}

func (p *Producer) Close() error {
    ch := make(chan error)
    p.quit <- ch
    return <-ch
}

func makePizza(pizzaNumber int) *PizzaOrder {
    pizzaNumber++
    if pizzaNumber <= NumberOfPizzas {
        delay := rand.Intn(5) + 1
        fmt.Printf("Received order #%d!\n", pizzaNumber)

        rnd := rand.Intn(12) + 1
        msg := ""
        success := false

        if rnd < 5 {
            pizzasFailed++
        } else {
            pizzasMade++
        }
        total++

        fmt.Printf("Making pizza #%d. It will take %d seconds....\n", pizzaNumber, delay)
        // delay for a bit
        time.Sleep(time.Duration(delay) * time.Second)

        if rnd <= 2 {
            msg = fmt.Sprintf("*** We ran out of ingredients for pizza #%d!", pizzaNumber)
        } else if rnd <= 4 {
            msg = fmt.Sprintf("*** The cook quit while making pizza #%d!", pizzaNumber)
        } else {
            success = true
            msg = fmt.Sprintf("Pizza order #%d is ready!", pizzaNumber)
        }

        p := PizzaOrder{
            pizzaNumber: pizzaNumber,
            message:     msg,
            success:     success,
        }

        return &p

    }

    return &PizzaOrder{
        pizzaNumber: pizzaNumber,
    }
}

func pizzeria(pizzaMaker *Producer) {
    // keep track of which pizza we are making
    var i = 0

    // run forever or until we receive a quit notification
    // try to make pizzas
    for {
        currentPizza := makePizza(i)
        if currentPizza != nil {
            i = currentPizza.pizzaNumber
            select {
            case pizzaMaker.data <- *currentPizza:
            // we tried to make a pizza (send someting to data channel )
            case pizzaMaker.data <- *currentPizza:
                // we tried to make a pizza (send someting to data channel )
            case quitChan := <-pizzaMaker.quit:
                // we received a quit request
                close(pizzaMaker.data)
                close(quitChan)
                return

            }
        }

    }
}

func main() {
    // seed the random number generator
    rand.Seed(time.Now().UnixNano())

    // print out a message
    color.Cyan("Pizza delivery service!")
    color.Cyan("----------------------------------")

    // create a producer
    pizzaJob := &Producer{
        data: make(chan PizzaOrder),
        quit: make(chan chan error),
    }

    // run the producer in the background
    go pizzeria(pizzaJob)

    for i := range pizzaJob.data {
        if i.pizzaNumber <= NumberOfPizzas {
            if i.success {
                color.Green("%s", i.message)
                color.Green("order #%d is out of deliver !", i.pizzaNumber)
            } else {
                color.Red("%s", i.message)
                color.Red("customer not happy ")
            }
        } else {
            color.Cyan("done making pizzas")
            err := pizzaJob.Close()
            if err != nil {
                color.Red("Error closing channel ", err)
            }
        }
    }

    // print out the ending message
    color.Cyan("----------------------------------")
    color.Cyan("Done for the day !\n")
    color.Cyan("we made %d pizzas , but failed to make %d , with %d attempts in total ", pizzasMade, pizzasFailed, total)
    switch {
    case pizzasFailed > 9:
        color.Red("We are in trouble !")
    case pizzasFailed >= 6:
        color.Green("We are doing great !")
    case pizzasFailed >= 4:
        color.Yellow("We are doing okay !")
    case pizzasFailed >= 2:
        color.Red("We are pretty well !")
    default:
        color.Green("We are doing great !")

    }
}

final output :


~/Documents/golabs ❯ go run .                                                                               33s
Pizza delivery service!
----------------------------------
Received order #1!
Making pizza #1. It will take 1 seconds....
Received order #2!
Making pizza #2. It will take 4 seconds....
Pizza order #1 is ready!
order #1 is out of deliver !
Received order #3!
Making pizza #3. It will take 1 seconds....
*** The cook quit while making pizza #2!
customer not happy 
Received order #4!
Making pizza #4. It will take 5 seconds....
Pizza order #3 is ready!
order #3 is out of deliver !
Received order #5!
Making pizza #5. It will take 5 seconds....
*** We ran out of ingredients for pizza #4!
customer not happy 
Received order #6!
Making pizza #6. It will take 3 seconds....
*** We ran out of ingredients for pizza #5!
customer not happy 
Received order #7!
Making pizza #7. It will take 4 seconds....
*** The cook quit while making pizza #6!
customer not happy 
Received order #8!
Making pizza #8. It will take 1 seconds....
Pizza order #7 is ready!
order #7 is out of deliver !
Received order #9!
Making pizza #9. It will take 4 seconds....
Pizza order #8 is ready!
order #8 is out of deliver !
Received order #10!
Making pizza #10. It will take 4 seconds....
Pizza order #9 is ready!
order #9 is out of deliver !
Pizza order #10 is ready!
order #10 is out of deliver !
done making pizzas
----------------------------------
Done for the day !
we made 6 pizzas , but failed to make 4 , with 10 attempts in total 
We are doing okay !

11 . Range , Buffered Channel


for value := range ch {

....

}
  • iterate over values received from channel
  • loop automatically breaks when a channel is close
  • range does not return the second boolean value

Range over the channel, the receiver goroutine can use range to receive a sequence of values from the channel. range over the channel will iterate over the values received from a channel. The loop automatically breaks when the channel is closed. So once the sender goroutine has sent all of its values, it will close the channel and the receiver goroutine will break out of the range loop. The range does not return the second boolean value.

12 . Unbuffered channels

synchronous channel


package main

func main() {
    ch := make(chan int)
    go func() {
        for i := 0; i < 6; i++ {
            // send iterator to channel ch
            ch <- i
        }
        close(ch)
    }()
    // range over channel to receive values

    for v := range ch {
        println(v)
    }
}

output


~/Documents/golabs  go run main.go
0
1
2
3
4
5

Normally the receive returns the second boolean value, but range just returns value, as on close, the range will automatically break out of the loop. Unbuffered channels, the channels that we have been creating till now are unbuffered channels. There is no buffer between the sender goroutine and the receiver goroutine. Since there is no buffer, the sender goroutine will block until there is a receiver, to receive the value, and the receiver goroutine will block until there is a sender, sending the value.

13. buffered channels

In buffered channels, there is a buffer between the sender and the receiver goroutine, and we can specify the capacity, that is the buffer size, which indicates the number of elements that can be sent without the receiver being ready to receive the values. The sender can keep sending the values without blocking, till the buffer gets full, when the buffer gets full, the sender will block.

The receiver can keep receiving the values without blocking till the buffer gets empty, when the buffer gets empty, the receiver will block. The buffered channels are in-memory FIFO queues, so the element that is sent first, will be the element


package main

import "fmt"

func main() {
    ch := make(chan int, 6)
    go func() {
        for i := 0; i < 6; i++ {
            // send iterator to channel ch
            fmt.Printf("Sending %d to channel\n", i)
            ch <- i
        }
        close(ch)
    }()
    // range over channel to receive values

    for v := range ch {
        fmt.Printf("Received %d from channel\n", v)
        println(v)
    }
}

output

go run main.go
Sending 0 to channel
Sending 1 to channel
Sending 2 to channel
Sending 3 to channel
Sending 4 to channel
Sending 5 to channel
Received 0 from channel
0
Received 1 from channel
1
Received 2 from channel
2
Received 3 from channel
3
Received 4 from channel
4
Received 5 from channel
5

14 . Channel Direction

when using channels as function parameters , you can specify if a channel is meant to only send or receive values

this specificity increases the type-safety of the program


func pong( in <- chan string , out chan <- string){}

in <- chan string - receive only channel
out chan <- string - send only channel

package main

import "fmt"

func getMsg(ch1 chan<- string) {

    // send message to channel 1

    ch1 <- "Hello CloudNativeFolks"
}
func relayMsg(ch1 <-chan string, ch2 chan<- string) {

    // receive message on channel 1
    m := <-ch1
    ch2 <- m

    // send it on channel 2
}

func main() {
    // create channels ch1 and ch2
    ch1 := make(chan string)
    ch2 := make(chan string)

    // spine goroutine to getMsg() and relayMsg()
    go getMsg(ch1)
    go relayMsg(ch1, ch2)
    // recv message on ch2
    v := <-ch2
    fmt.Println(v)
}

output

 go run main.go
Hello CloudNativeFolks

15 .Channel Ownership

Default value for channel is nil

var ch chan interface{}

reading / writing to a nil channel will block forever


var ch chan interface{}
<-ch
ch <- struct{}{}

closing nil channel will panic


var ch chan interface {}
close(ch)

ensure the channels are initialised first

owner of channel is a goroutine that instantiates writes and closes a channel channel utilisers only have a read-only view into the channel

ownership of channel avoids

  • deadlocking by writing to a nil channel
  • closing a nil channel
  • writing to a closed channel
  • closing a channel more than once
package main

import "fmt"

func main() {
    // return recieve only channel to caller
    // spin a goroutine , which
    // writes data into channel and
    // close the channel when done

    owner := func() <-chan int {
        ch := make(chan int)

        go func() {
            defer close(ch)
            for i := 0; i < 10; i++ {
                ch <- i
            }
        }()
        return ch
    }

    consumer := func(in <-chan int) {
        // read data from channel
        for v := range in {
            fmt.Printf("received: %d\n", v)
        }
        fmt.Println("done")
    }
    ch := owner()
    consumer(ch)
}

output

go run main.go
received: 0
received: 1
received: 2
received: 3
received: 4
received: 5
received: 6
received: 7
received: 8
received: 9
done

16 .Pipeline

Pipline streams or batches of data

Go's concurrency primitives makes it easy to construct streaming pipelines. That enables us to make an efficient use of the I/O and the multiple CPU cores available on the machine, to run our computation faster. Pipelines are often used to process streams or batches of data. Pipeline is a series of stages that are connected by the channels, where each stage is represented by a goroutine.

A goroutine takes the data from an in-bound channel, performs an operation on it and sends the data

on the out-bound channel, that can be used by the next stage. By using pipelines, we can separate the concerns of each stage, and process individual stages concurrently. Stages could consume and return the same type. For example, a square stage can take, receive only channel of type int and return receive only channelof type int as output.

func square(in <-chan int) <-chan int {

This enables the composability of the pipeline.

square(square (generator(2,3)))

For example, a generator stage can return a receiver only channel of type int, which a square stage can take as input, and we can compose the output of the square stage as input to another square stage.


package main

func generator(nums ...int) <-chan int {

    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

// square - receive on inbound channel
// square the number
//output on outbound channel
func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

func main() {

    //    ch := generator(2, 3)
    //    out := square(ch)
    //    for n := range out {
    //        println(n)
    //    }

    for n := range square(square(generator(2, 3))) {
        println(n)
    }
    // setup pipeline
    // run the last stage of pipeline
    // receive the value from square stage
    // print each value , until channel is closed
}

output

go run main.go
16
81

17 . fan out & fan in

what is fan-out?

  • multiple goroutine are started read data from the single channel

  • Distribute work amongst a group of workers coroutines to parallelise the CPU usage and the I/O usages

  • Helps computational intensive stage to run faster

What is fan In ?

  • Process ion combining multiple results into one channel

  • we create merge go routine to read data from multiple input channel and send the data to a single output channel .

package main

import (
    "sync"
)

func generator(nums ...int) <-chan int {

    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

// square - receive on inbound channel
// square the number
//output on outbound channel
func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

func merge(cs ...<-chan int) <-chan int {
    // implement fan in
    // merge a list of channels into a single channel
    out := make(chan int)
    var wg sync.WaitGroup
    output := func(c <-chan int) {
        for n := range c {
            out <- n
        }
        wg.Done()
    }
    wg.Add(len(cs))
    for _, c := range cs {
        go output(c)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

func main() {

    //    ch := generator(2, 3)
    //    out := square(ch)
    //    for n := range out {
    //        println(n)
    //    }

    in := generator(2, 3)
    ch1 := square(in)
    ch2 := square(in)

    for n := range merge(ch1, ch2) {
        println(n)
    }
    // setup pipeline
    // run the last stage of pipeline
    // receive the value from square stage
    // print each value , until channel is closed
}

output

go run main.go
4
9

18 .cancelling go routine

downstream stages keep receiving values from inbound channel until the channel is closed .


func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

All go routines exit once all values have been successfully sent downstream

func merge(cs ...<-chan int) <-chan int {
    // implement fan in
    // merge a list of channels into a single channel
    out := make(chan int)
    var wg sync.WaitGroup
    output := func(c <-chan int) {
        for n := range c {
            out <- n
        }
        wg.Done()
    }
    wg.Add(len(cs))
    for _, c := range cs {
        go output(c)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
  • Real Pipelines

  • Real Pipelines - Receiver stages may only need a subset of values to make progress

  • A stage can exit early because an inbound value represents an error in an earlier stage
  • Receiver should not have to wait for the remaining values to arrives
  • We want earlier stages to stop producing values that later stages don't need

Cancellation of goroutine

  • Pass a read-only 'done' channel to goroutine
  • Close the channel, to send broadcasts signal to all goroutine
  • on receiving the signal on done channel , Goroutines needs to abandon their work and terminate
  • we use 'select` to make send/receive operation on channel pre-emptible
select {
case out <- n:
case <-done 
return 
}
package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

func generator(done <-chan struct{}, nums ...int) <-chan int {

    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            select {
            case out <- n:
            case <-done:
                return
            }
        }
    }()
    return out
}

func square(done <-chan struct{}, in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            select {
            case out <- n * n:
            case <-done:
                return
            }
        }

    }()
    return out
}

func merge(done <-chan struct{}, cs ...<-chan int) <-chan int {
    // implement fan in
    // merge a list of channels into a single channel
    out := make(chan int)
    var wg sync.WaitGroup
    output := func(c <-chan int) {
        defer wg.Done()
        for n := range c {
            select {
            case out <- n:
            case <-done:
                return
            }
        }
    }
    wg.Add(len(cs))
    for _, c := range cs {
        go output(c)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

func main() {

    done := make(chan struct{})
    in := generator(done, 2, 3)

    ch1 := square(done, in)
    ch2 := square(done, in)

    out := merge(done, ch1, ch2)
    fmt.Println(<-out)
    close(done)
    time.Sleep(10 * time.Millisecond)
    g := runtime.NumGoroutine()
    fmt.Printf("number of active go routines = %d\n", g)

}

output :-

~/Documents/golabs ❯ go run main.go
4
number of active go routines = 1

19 . Context Package

we often need to manage goroutines because of timeouts , cancellations or failures in related goroutine

also we needs to pass request-specific values across API boundaries and between processes

the content package serves two primary purpose

  • to provide an API for Canceling branches of your call-graph
    • to provide a data-bag for transporting request scoped data through your call-graph

Cancelation

func WithCancel(parent Context)(ctx Context, cancelFunc)

func WithDeadline(parent Context,deadline time.Time)(Context , cancelFunc)

func WithTimeOut(parent Context, timeout time.Duration)(Conrxr , cancelFunc)
  • WithCancel returns a Context that closed its done channel when the cancel function is called

  • WithDeadline return a Context the closes it done channel when the machine's clock passed the given deadline

  • WithTimeout returns a Context that closes its done channel after the given time duration

Value

func WithValue(parent Context, Key , val interface{} ) Context 

value(key interface{}) interface
  • WithValue returns a copy of parent context in which the value associated with key

  • value returns the associated with this context for key or nil if no value associated with key

  • use context Values only for request-specific data , not for passing optionals parameters to functions

20 . What are the limits of channels

Message Size

the maximum message size ( orhannel type ) is 2^16 bytes 0r 64 kilobytes

// https://github.com/golang/go/blob/release-branch.go1.18/src/runtime/chan.go#L72
func makechan(t *chantype, size int) *hchan {
    elem := t.elem

    // compiler checks this but be safe.
    if elem.size >= 1<<16 {
        throw("makechan: invalid channel element type")
    }
....
...
}

Maximum allocation

The maximum allocation that is allowed by the compile 2^47 byte(140 Terabyte) in 64 bit unix like system

// https://github.com/golang/go/blob/release-branch.go1.18/src/runtime/malloc.go#L225 
    // maxAlloc is the maximum size of an allocation. On 64-bit,
    // it's theoretically possible to allocate 1<<heapAddrBits bytes. On
    // 32-bit, however, this is one less than 1<<32 because the
    // number of bytes in the address space doesn't actually fit
    // in a uintptr.
    maxAlloc = (1 << heapAddrBits) - (1-_64bit)*1

but you'll meet memory issues if you will try to allocate maximum size

Speed

the time for the speed send and receive operations os dominated by the price of goroutine context switching ( which should be consistently <+ 200ms

go-ch go test -bench=.

unbuffered channels are slightly faster then buffered