kdocs
GitHub
Lang - Runtime
Lang - Runtime
  • Go
    • Modules & Packages
    • Variables
    • Structs
    • Interfaces
    • Functions
    • Control Structures
    • Erro Handling
    • Concurrency
    • Testing
    • Fuzzing
  • Web
    • V8 Engine
    • Node.js
      • New Project
    • Deno
      • New Project
Powered by GitBook
On this page
  • Goroutines
  • Best practices
  • Starting a Goroutine
  • Channels
  • Defer
  1. Go

Concurrency

Goroutines

A goroutine is a lightweight thread managed by the Go runtime.

It allows a function in Go to run concurrently or in parallel with other functions.

They don't support return values, any sort of communication to the outside must be done through channels.

Concurrency and Parallelism

Concurrency: The ability to manage multiple tasks at once.

Most of the goroutines will run concurrently.

Parallelism: The ability to execute multiple tasks simultaneously on multiple physical cores.

Parallelism in Go:

It is not garanteed that goroutines will run in true parallelism (achieved only by executing in different physical cores).

Goroutine vs OS thread

Go doesn't create OS threads for each goroutine.

Instead, Go runs thousands of goroutines on a small number of OS threads (Pool of OS threads).

  • Go Runtime Scheduler dynamically assigns goroutines to available OS threads.

  • It does it by multiplexing the goroutines, so if a goroutine blocks (e.g wating for I/O), Go moves another goroutine onto the OS thread, ensuring efficient CPU utilization. (Just like an OS does with OS threads in logical CPU cores)

Too many goroutines can lead to memory pressure and context switching overhead.

Feature
Goroutine
Thread (OS level)

Memory Usage

~2 Kb per Goroutine

~1 Mb per thread

Scheduling

Cooperative, managed by Go

Preemptive, managed by OS

Speed

Fast to start, low overhead

Slow to start, heavy overhead

Communication

Use channels (safe)

Use locks/mutexes (error-prone)

Monitoring and Profiling

Use pprof, Go's profiling tool to analyse CPU usage and goroutines blocking times.

Best practices

Goroutines inside loops

func main() {
    for i := 1; i <= 5; i++ {
        go func() {
            fmt.Println(i) // ⚠️ BUG: Uses the same `i`, may print unexpected results
        }()
    }
    time.Sleep(time.Second)
}
func main() {
    for i := 1; i <= 5; i++ {
        go func(n int) {
            fmt.Println(n) // ✅ Correct: Each goroutine gets its own copy of `i`
        }(i)
    }
    time.Sleep(time.Second)
}

Goroutine leaks

A goroutine leak happens when it never exits, causing memory issues.

Always have a receiver for every channel send operation.

func main() {
    ch := make(chan int) // Channel with no receiver
    go func() {
        ch <- 42 // 🚨 Deadlock: No one is reading from this channel
    }()
}

Use sync.WaitGroup to wait for multiple Goroutines

Don't use time.Sleep(), use sync.WaitGroup to wait for multiple goroutines.

Limit Goroutines with Worker Pools

Worker Pools prevent flooding your app with goroutines.

Too many goroutines can overload memory. Use worker pools to limit the number of concurrent goroutines.

Use GOMAXPROCS with container CPU limits

If limiting the Docker container's or Kubernetes Pod's CPU amount, make sure to also provide the same number to Go Scheduler.

env:
  - name: GOMAXPROCS
  value: "2"

Failing to do so can increase to likelihood of CPU throttling.

Starting a Goroutine

func greet() {} // Takes 3 sec to execute

greet()
fmt.Print("Finish") // Prints after 3 sec

go greet()
fmt.Print("Finish") // Prints immediatly, while greet takes 3 sec to execute

Channels

Channels can accept any kind of data, including pointers.

If sharing memory address in channels between Goroutines, you must implement Mutex/Locks to avoid race conditions and deadlocks.

It is simply a value that can be used as a communication device, to transmit some kind of data.

func greet(doneChan chan bool) {
    ...
    doneChan <- true
}

done := make(chan bool)
go greet(done)

// This will hold until the channel receives some data
<- done

In this example, the channel holded the main program until the goroutine ended, because when it finished it emitted a value to the channel.

Waiting for multiple goroutines

In the example bellow, to wait for both goroutines, you would have to listen two times on the channel.

But this is not very scalable.

func greet(doneChan chan bool) {
    doneChan <- true
}
func greet2(doneChan chan bool) {
    doneChan <- true
}

done := make(chan bool)
go greet(done)
go greet2(done)

<- done
<- done

A solution is would be to have a Slice of channels, and then listen to them in a loop.

func greet(doneChan chan bool) {
    doneChan <- true
}
func greet2(doneChan chan bool) {
    doneChan <- true
}

dones := make([]chan bool, 2)
done := make(chan bool)
go greet(done[0])
go greet2(done[1])

for _, done := range dones {
    <- done
}

Handle errors in Channels

To handle this use the select control structure to either read from errorChan if there were any errors, or from doneChan if all wen't good.

The select control structure, will run code from the channel case that emmited the value first, and will not care about handling the other cases after.

func greet(doneChan chan bool, errorChan chan error) {
    _, err := ...
    
    if err != nil {
        errorChan <- err
        return
    }
    
    doneChan <- true
}

doneChan := make(chan bool)
errorChan := make(chan error)
go greet(doneChan, errorChan)

select {
    // Get the channel value
    case err := <- errorChan:
        fmt.Println(err)
    // Does not care about the channel value
    case <- doneChan:
}

To handle multiple channel Slices.

func greet(doneChan chan bool, errorChan chan error) {
    ...
}

doneChan := make([]chan bool)
errorChan := make([]chan error)
go greet(doneChan[0], errorChan[0])
go greet(doneChan[1], errorChan[1])

for i := 0; i < 2; i++ {
    select {
        // Get the channel value
        case err := <- errorChan[i]:
            fmt.Println(err)
        // Does not care about the channel value
        case <- doneChan[i]:
    }
}

Defer

A keyword defer that indicates to Go that a called function should execute only when the surronding function or method finishes.

func handleFile() {
    file, err := os.Open(...)
    
    if err != nil {}
    
    defer file.Close()
    
    // ... Work with the file without having to worry about closing it
}

In this example, after the file was opened you may state to close it with defer. So when the handleFile function exits, the file.Close() will be called.

PreviousErro HandlingNextTesting

Last updated 1 month ago

When using Goroutines inside loops, be careful with . Each routine will get the value as reference and trigger race conditions on it.

Closures