Day 21: Introduction to Goroutines

Venkat Annangi
Venkat Annangi
30/12/2024 03:57 6 min read 22 views
#Programming # Goroutines # Go Basics # Concurrency #Golang
Day 21: Introduction to Goroutines

What Are Goroutines?

Goroutines are lightweight threads of execution managed by the Go runtime. Unlike traditional threads, goroutines are extremely efficient and scalable, making it easy to run thousands of them simultaneously.

Key Features

  • Lightweight compared to system threads.
  • Managed by the Go runtime, not the OS.
  • Use the go keyword to create a goroutine.

Comparison with Threads: Unlike traditional threads that are OS-managed and consume significant resources, goroutines are designed to be more efficient and provide an abstraction that simplifies concurrency programming.

Syntax

Creating a goroutine is as simple as prefixing a function call with the go keyword:

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    fmt.Println("Hello from goroutine!")
}

func main() {
    go sayHello()
    fmt.Println("Main function execution")
    time.Sleep(1 * time.Second) // Ensure main doesn't exit before goroutine completes
}
        

Key Takeaway: The above example shows how the go keyword enables a function to execute concurrently without blocking the main function.

Examples

Running Multiple Goroutines

package main

import (
    "fmt"
    "time"
)

func printNumber(number int) {
    fmt.Printf("Number: %d\n", number)
}

func main() {
    for i := 0; i < 5; i++ {
        go printNumber(i)
    }
    time.Sleep(1 * time.Second) // Allow goroutines to finish
}

Output: Order may vary due to concurrent execution. This illustrates the asynchronous nature of goroutines.

Using Goroutines with Channels

Channels are used to communicate between goroutines:

package main

import "fmt"

func worker(done chan bool) {
    fmt.Println("Working...")
    done <- true
}

func main() {
    done := make(chan bool)
    go worker(done)
    <-done // Wait for worker to finish
    fmt.Println("Done!")
}

Error Handling in Goroutines

Handling errors effectively in goroutines can be achieved by passing error values through channels:

package main

import (
    "fmt"
    "errors"
)

func riskyTask(errorsChan chan error) {
    // Simulate an error
    err := errors.New("Something went wrong")
    errorsChan <- err
}

func main() {
    errorsChan := make(chan error)
    go riskyTask(errorsChan)
    err := <-errorsChan
    if err != nil {
        fmt.Printf("Error encountered: %s\n", err)
    }
}

Common Pitfalls

1. Goroutine Leaks

Ensure goroutines have a way to exit, such as listening to a channel or using context cancellation. For example:

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Worker exiting...")
            return
        default:
            fmt.Println("Working...")
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    go worker(ctx)
    time.Sleep(3 * time.Second)
}

2. Race Conditions

Use synchronization primitives like sync.Mutex to avoid concurrent writes to shared data. For instance:

package main

import (
    "fmt"
    "sync"
)

var counter int
var mu sync.Mutex

func increment(wg *sync.WaitGroup) {
    defer wg.Done()
    mu.Lock()
    counter++
    mu.Unlock()
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go increment(&wg)
    }
    wg.Wait()
    fmt.Printf("Final Counter: %d\n", counter)
}

Best Practices

  • Use sync.WaitGroup to manage goroutine lifecycles.
  • Avoid shared state; use channels for communication.
  • Always handle errors within goroutines to prevent silent failures.
  • Leverage context for timeout and cancellation management.
  • Test concurrent code thoroughly to ensure correctness under high loads.

Example of combining WaitGroup and context:

package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

func worker(ctx context.Context, wg *sync.WaitGroup, id int) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d exiting...\n", id)
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    var wg sync.WaitGroup
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(ctx, &wg, i)
    }

    wg.Wait()
    fmt.Println("All workers completed.")
}

By mastering goroutines, you unlock the power of concurrency in Go, enabling your programs to handle tasks efficiently and scale effortlessly. Practice writing concurrent programs and experiment with the examples provided to deepen your understanding.

Tags: Golang, concurrency, goroutines, tutorial, Go programming

Comments