Here’s a clear and practical breakdown of Goroutines in GoLang:
- 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).
-
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
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).
-
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.
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 |
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")
}
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.
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:
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.
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.”
var ch chan int
ch = make(chan int)
Or shorthand:
ch := make(chan int)
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.
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
ch <- "Hello from goroutine"
}()
msg := <-ch
fmt.Println(msg)
}
Type | Description |
---|---|
Unbuffered channel | Blocks until both sender and receiver are ready |
Buffered channel | Allows sending a fixed number of values before blocking |
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 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)
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.
Situation | Recommended Tool |
---|---|
Passing data between goroutines | Channel |
Protecting shared variables | sync.Mutex |
Coordinating completion (multiple goroutines) | sync.WaitGroup or channels |
- 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
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:
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
- Deferred calls are executed in last-in, first-out (LIFO) order.
func main() {
fmt.Println("Start")
defer fmt.Println("Deferred: Closing resources")
fmt.Println("End")
}
Output:
Start
End
Deferred: Closing resources
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.
-
Arguments are evaluated immediately:
x := 5 defer fmt.Println(x) x = 10 // Prints: 5
-
Useful with
recover()
andpanic()
for error handling.
select
lets a goroutine wait on multiple channel operations.
It’s like a switch
but for channels.
- Listen to multiple channels without blocking.
- Handle timeouts and cancellation.
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.
default
prevents blocking if no channels are ready:
select {
case msg := <-ch1:
fmt.Println(msg)
default:
fmt.Println("No channels ready")
}
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.
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 |