What is Go routine(Thread)

A goroutine is a lightweight thread managed by the Go runtime.
Memory allocation on stack, heap is very less wrt OS Threads. Goroutine vs OS Threads

Code

Hello World


package main
import (
    "fmt"
    "time"
)
func sayHello() {
    fmt.Println("Hello from goroutine!")
}
func main() {
    go sayHello()       // Start a goroutine
    fmt.Println("Hello from main!") // Print from the main goroutine
    // Sleep for a while to allow the goroutine to execute
    time.Sleep(time.Second)
}
$ go run main.go
Hello from main!
Hello from goroutine!
      

10 Threads executing 1 function

Code Description

package main
import (
    "fmt"
    "sync"
    "time"
)
func worker(id int, wg *sync.WaitGroup) {
    // ie wg.Done() would be called when surrounding function finishes
    defer wg.Done()
    fmt.Printf("Worker %d started\n", id)
    time.Sleep(time.Second) // Simulate work
    fmt.Printf("Worker %d finished\n", id)
}

func main() {
    var wg sync.WaitGroup               // Create WaitGroup object
    numWorkers := 10                    // Number of goroutines to create
    wg.Add(numWorkers)                  // Create these goroutines

    for i := 1; i <= numWorkers; i++ {
        go worker(i, &wg)
    }
    wg.Wait()                               // Wait for all goroutines to finish
    fmt.Println("All workers finished")
}

$ go run test.go
Worker 10 started
Worker 8 started
Worker 9 started
Worker 2 started
Worker 1 started
Worker 6 started
Worker 3 started
Worker 4 started
Worker 5 started
Worker 7 started
Worker 10 finished
Worker 8 finished
Worker 1 finished
Worker 9 finished
Worker 2 finished
Worker 3 finished
Worker 6 finished
Worker 4 finished
Worker 7 finished
Worker 5 finished
All workers finished
      
sync.waitgroup:
- Synchronization primitive provided by the Go to synchronize the execution of a group of goroutines.
- How it works?
  A WaitGroup maintains shared counter internally(which is initially set to 0). counter is thread-safe.
  At start of goroutine wg.Add(numberOfWorkers) is called. This makes counter=10
  goroutine finishes (defer make wg.Done() to called after function finishes), and counter is decremented
  wg.Wait() method in main waits until counter=0

defer
- keyword in Go schedules a function call to be executed just before the surrounding function returns.
- ie wg.Done() would be called when surrounding function finishes
- defer wg.Done() is called reliably, even in case of errors or unexpected goroutine termination.

Memory Leak in goroutine

Memory allocated to goroutine: Around 2k bytes of Stack is allocated to goroutines by Go runtime at time of creation and this memory grows/shrinks dynamically.
When goroutine dies then this memory is taken back by go runtime(ie no leak).
How leak happens in goroutine? Memory leak in goroutine means go runtime fails to cleanup the goroutine and hence fails to claim back this allocated memory(2k bytes or more).
Leak Condition:
- When no reciever is present on unbuffered channel, then that goroutine which is sending data on channel and channel are not Garbage collected by Go runtime.
- Memory allocated to goroutine + memory allocated to go channel is leaked.

              ---Unbufferd channel---
                                    /\
                                     |
                              goroutine sends data
        
How to avoid Memory leak? Use buffered channel

Bounded go routines

By default, Go makes it easy to spawn thousands of goroutines.
And each goroutine consumes memory or network handles, spawning them without a limit (unbounded) can lead to Out of Memory (OOM) errors or CPU exhaustion.
Bounded goroutines ensure only a fixed number of goroutines run at once
We can implement bounded goroutines using 2 patterns:
1. Semaphore OR
2. Worker Pool

1. Semaphore based bounded goroutines

Use a buffered channel with limit on number of values it can hold.