Skip to content

callmeskyy111/Golang-Concurrency

Repository files navigation

Here’s a clear and practical breakdown of Goroutines in GoLang:


✅ What Are Goroutines?

  • Goroutines are lightweight, concurrent execution units in Go.
  • They are managed by the Go runtime instead of the operating system.
  • Goroutines allow us to run functions or methods concurrently (parallel-like behavior).

✅ Why Use Goroutines?

  • Traditional OS threads are resource-heavy. Goroutines are much lighter.

  • Goroutines scale easily: we can have thousands or even millions running at once.

  • Useful for:

    • Network servers
    • Background tasks
    • Parallel computation

✅ How to Start a Goroutine

We use the go keyword before a function or method call:

package main

import (
    "fmt"
    "time"
)

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

func main() {
    go sayHello() // Starts a new Goroutine

    fmt.Println("Main function")

    time.Sleep(1 * time.Second) // Allow goroutine to complete
}

Important: The main function exits immediately unless we block it somehow (e.g., using time.Sleep, channels, or sync tools).


✅ Goroutine Execution Model

  • Managed by Go’s runtime scheduler (not OS threads directly).

  • Goroutines share memory space (unlike OS threads which have isolated memory).

  • Under the hood:

    • Goroutines are multiplexed onto fewer system threads.
    • Go uses work-stealing and scheduling algorithms.

✅ Cost Comparison: Goroutines vs Threads

Feature Goroutine OS Thread
Memory footprint ~2 KB stack (grows dynamically) ~1 MB stack (fixed size)
Creation cost Very low High
Scheduling Go runtime OS scheduler
Count limit Hundreds of thousands Dozens to thousands

✅ Goroutine Example with Loops

package main

import (
    "fmt"
    "time"
)

func printNumbers() {
    for i := 1; i <= 5; i++ {
        fmt.Println(i)
        time.Sleep(500 * time.Millisecond)
    }
}

func main() {
    go printNumbers() // Runs concurrently
    go printNumbers() // Another one

    time.Sleep(3 * time.Second) // Wait for goroutines
    fmt.Println("Main function done")
}

✅ Notes on Goroutine Safety

Since Goroutines share memory:

  • We must handle data races.

  • We use:

    • Channels: Preferred, Go’s way of synchronizing.
    • sync.Mutex: Traditional locking mechanism.
    • sync.WaitGroup: For waiting on multiple goroutines to finish.

✅ Quick Tip: Goroutines vs Threads Analogy

If threads are like heavyweight trucks carrying cargo between cities, goroutines are like bicycles carrying small packages inside a city — faster, lighter, easier to maneuver.


Here’s a detailed, practical explanation of channels in Go, building on the goroutines concept:


✅ What Are Channels in Go?

Channels are Go’s built-in tool for communication and synchronization between goroutines.

  • Purpose: Share data safely between concurrent goroutines.
  • Channels carry typed values (e.g., int, string, struct).
  • They block by default until both sender and receiver are ready.

✅ Why Use Channels?

When goroutines share memory directly, we risk data races. Channels avoid that by following Go’s concurrency principle:

“Do not communicate by sharing memory; share memory by communicating.”


✅ How to Create and Use a Channel

1️⃣ Declaring a Channel

var ch chan int
ch = make(chan int)

Or shorthand:

ch := make(chan int)

2️⃣ Sending and Receiving Values

ch := make(chan int)

go func() {
    ch <- 42 // Send value to channel
}()

val := <-ch // Receive value from channel
fmt.Println(val) // Output: 42
  • ch <- value sends value into the channel.
  • <- ch receives a value from the channel.
  • Both operations block until the other side is ready.

✅ Example with Two Goroutines

package main

import "fmt"

func main() {
    ch := make(chan string)

    go func() {
        ch <- "Hello from goroutine"
    }()

    msg := <-ch
    fmt.Println(msg)
}

✅ Types of Channels

Type Description
Unbuffered channel Blocks until both sender and receiver are ready
Buffered channel Allows sending a fixed number of values before blocking

✅ Buffered Channels Example

ch := make(chan int, 2) // Buffer size = 2

ch <- 1
ch <- 2
// ch <- 3 would block because the buffer is full

fmt.Println(<-ch) // Output: 1
fmt.Println(<-ch) // Output: 2
  • Buffered channels don’t block until the buffer is full.
  • Useful for controlling flow when immediate sync isn’t needed.

✅ Closing Channels

  • Closing signals: "No more values will be sent."
close(ch)
  • Receiving from a closed channel returns zero value.

Example:

ch := make(chan int, 2)
ch <- 10
close(ch)

val, ok := <-ch
fmt.Println(val, ok) // 10 true

val, ok = <-ch
fmt.Println(val, ok) // 0 false (zero value, closed)

✅ Iterating Over a Channel

ch := make(chan int)

go func() {
    for i := 1; i <= 5; i++ {
        ch <- i
    }
    close(ch)
}()

for val := range ch {
    fmt.Println(val)
}
  • The range loop reads until the channel is closed.

✅ When to Use Channels vs Mutex?

Situation Recommended Tool
Passing data between goroutines Channel
Protecting shared variables sync.Mutex
Coordinating completion (multiple goroutines) sync.WaitGroup or channels

✅ Common Mistakes to Avoid

  • Sending on a closed channel → panic
  • Reading from a nil or uninitialized channel → blocks forever
  • Forgetting to close a channel if no more values are coming

✅ Final Thought

Channels + Goroutines = Go’s powerful concurrency model.

Here’s a clear, detailed explanation of both defer and select in Go, focusing on how and why we use them:


✅ What Is defer in Go?

Purpose:

defer schedules a function call to run after the surrounding function finishes.

We typically use it for:

  • Closing files
  • Unlocking mutexes
  • Releasing resources
  • Logging exit points

✅ How It Works:

  • Deferred calls are executed in last-in, first-out (LIFO) order.

✅ Basic Example:

func main() {
    fmt.Println("Start")
    defer fmt.Println("Deferred: Closing resources")
    fmt.Println("End")
}

Output:

Start
End
Deferred: Closing resources

✅ Common Use Case: Closing Files

file, err := os.Open("example.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close() // Ensures file is closed even if there’s an error

If we forget to defer file.Close(), we might cause memory/resource leaks.


✅ Important Notes on defer:

  • Arguments are evaluated immediately:

    x := 5
    defer fmt.Println(x)
    x = 10
    // Prints: 5
  • Useful with recover() and panic() for error handling.


✅ What Is select in Go?

Purpose:

select lets a goroutine wait on multiple channel operations.

It’s like a switch but for channels.


✅ Why Use select?

  • Listen to multiple channels without blocking.
  • Handle timeouts and cancellation.

✅ Basic Example:

ch1 := make(chan string)
ch2 := make(chan string)

go func() {
    ch1 <- "Message from ch1"
}()

go func() {
    ch2 <- "Message from ch2"
}()

select {
case msg1 := <-ch1:
    fmt.Println(msg1)
case msg2 := <-ch2:
    fmt.Println(msg2)
}
  • Whichever channel sends data first is handled.
  • Only one case is executed per select.

✅ Adding default Case

default prevents blocking if no channels are ready:

select {
case msg := <-ch1:
    fmt.Println(msg)
default:
    fmt.Println("No channels ready")
}

✅ Using select for Timeouts

ch := make(chan string)

go func() {
    time.Sleep(2 * time.Second)
    ch <- "Done"
}()

select {
case msg := <-ch:
    fmt.Println(msg)
case <-time.After(1 * time.Second):
    fmt.Println("Timeout!")
}
  • time.After returns a channel that sends a signal after the duration.

✅ Summary Comparison:

Feature defer select
Purpose Schedule clean-up tasks Wait for multiple channels
When it runs After surrounding function returns Immediately when a case is ready
Typical use Closing files, unlocking mutexes Handling multiple channel inputs, timeouts

About

⚡All about concurrency in GoLang 🔵

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages