Daksh Pareek

Welcome to my personal portfolio website, showcasing my projects and blogs.

Go Learning Journal 2: Concurrency

2025-02-05


These are raw learning notes from my journey with Go, documenting key concepts and examples for future reference. I reason with LLMs to understand some concepts better and sometimes copy paste the responses of that in my notes. These are not original thoughts.

Concurrency in Go

Concurrency is the computer science term for breaking up a single process into independent components and specifying how these components safely share data.

process: A process is an instance of a program that’s being run by a computer’s operating system. The operating system associates some resources, such as memory, with the process and makes sure that other processes can’t access them. A process is composed of one or more threads.

thread: A thread is a unit of execution that is given some time to run by the operating system. Threads within a process share access to resources.

Understanding This Visually

KITCHEN (Operating System)
┌──────────────────────────────────────┐
│                                      │
│   COOKING PASTA (Process 1)          │
│   ├── Boiling water (Thread 1)       │
│   ├── Preparing sauce (Thread 2)     │
│   └── Grating cheese (Thread 3)      │
│                                      │
│   MAKING SALAD (Process 2)           │
│   ├── Chopping veggies (Thread 1)    │
│   └── Making dressing (Thread 2)      │
│                                      │
└──────────────────────────────────────┘

Think of it this way:

  1. Kitchen (Operating System)

    • The kitchen is like your operating system
    • It has resources (stove, counter space, utensils)
    • It manages who can use what and when
  2. Recipes (Processes)

    • Each recipe (like cooking pasta or making salad) is a separate process
    • Each has its own ingredients and tools (resources)
    • They don’t interfere with each other’s ingredients
  3. Tasks (Threads)

    • Within each recipe, you have multiple tasks that can happen simultaneously
    • These tasks share the recipe’s resources
    • Like while boiling pasta, you can prepare sauce at the same time

In computer terms:

COMPUTER (Operating System)
┌──────────────────────────────────────┐
│                                      │
│   CHROME BROWSER (Process 1)         │
│   ├── UI Thread                      │
│   ├── Networking Thread              │
│   └── Rendering Thread               │
│                                      │
│   WORD PROCESSOR (Process 2)         │
│   ├── Editing Thread                 │
│   └── Auto-save Thread               │
│                                      │
└──────────────────────────────────────┘

Key Points:


Goroutine is a lightweight thread, managed by the Go runtime.

OS Scheduler:
- Manages OS threads
- Decides which thread runs on which CPU core
- Works at operating system level

Go Scheduler:
- Manages goroutines
- Decides which goroutine runs on which OS thread
- Works within Go program

Go’s scheduler is an extra layer that makes goroutine management more efficient than if we tried to create one OS thread per concurrent operation.


Basic Goroutine Launch

// Regular function call
doSomething()

// Launch as goroutine
go doSomething()    // adds 'go' keyword

Channels

ch := make(chan int)

Reading From Channel

val := <-ch // reads a value from ch and assigns it to val

Writing To Channel

ch <- b // write the value in b to ch
func readOnly(ch <-chan int) { val := <-ch }
func writeOnly(ch chan<- int) { ch <- 42 }

By default, channels are unbuffered.

What is an Unbuffered Channel?

Key Behavior

Example

func main() {
    ch := make(chan int) // unbuffered channel

    // Sender goroutine
    go func() {
        fmt.Println("Trying to send...")
        ch <- 42                    // Blocks here until someone reads
        fmt.Println("Sent!")
    }()

    // Receiver (main goroutine)
    fmt.Println("Trying to receive...")
    value := <-ch                   // Blocks here until someone sends
    fmt.Println("Received:", value)
}

Output:

Trying to send...
Trying to receive...
Received: 42
Sent!

Buffered Channels in Go

What is a Buffered Channel?

Key Behaviors

  1. Writing:

    • Can write without blocking until buffer is full
    • Blocks when buffer is full until someone reads
  2. Reading:

    • Can read until buffer is empty
    • Blocks when buffer is empty until someone writes

Example

func main() {
    ch := make(chan int, 3) // buffered channel with capacity 3

    // Can write 3 values without blocking
    ch <- 1
    ch <- 2
    ch <- 3
    // ch <- 4  // This would block as buffer is full

    fmt.Println(len(ch)) // Current buffer size (3)
    fmt.Println(cap(ch)) // Maximum buffer size (3)
}

Useful Functions


Using for-range with Channels in Go

Basic Syntax

for value := range channel {
    // Process value
}

Key Points

  1. Reading Values:

    • Gets one value at a time from channel
    • Only one variable needed (unlike slice/map for-range)
    • Automatically handles the receive operation (<-)
  2. Behavior:

    • If channel has value: continues execution
    • If channel empty: pauses until value available
    • If channel closed: loop ends
    • Can exit early with break or return

Example

func main() {
    ch := make(chan int, 3)

    // Sender
    go func() {
        ch <- 1
        ch <- 2
        ch <- 3
        close(ch)  // Important: close channel when done
    }()

    // Receiver using for-range
    for num := range ch {
        fmt.Println("Received:", num)
    }
}

Output:

Received: 1
Received: 2
Received: 3

Note

Closing Channels in Go

Key Behaviors

  1. Writing to Closed Channel:

    • Will cause panic
    • Cannot close already closed channel
  2. Reading from Closed Channel:

    • Always succeeds
    • Returns remaining buffered values if any
    • Returns zero value when empty

Detecting Closed Channel

// Comma-ok idiom
value, ok := <-ch
if ok {
    fmt.Println("Channel open, received:", value)
} else {
    fmt.Println("Channel closed, received zero value")
}

Select in Go

In Go, the select statement is a control structure that allows a goroutine to wait on multiple communication operations (channel sends or receives).

Basic Syntax

select {
case <-ch1:
    // Code to execute when ch1 receives a value
case val := <-ch2:
    // Code to execute when ch2 sends a value into val
case ch3 <- val:
    // Code to execute when val is sent to ch3
default:
    // Code to execute if none of the above cases are ready
}

How select Works

The For-Select Loop

A common pattern is to use select inside a for loop, continuously waiting for channel operations.

Example For-Select Loop

for {
    select {
    case <-done:
        return
    case v := <-ch:
        fmt.Println(v)
    }
}

Best Practices