Go > Concurrency > Goroutines > Goroutine leaks
Goroutine Leak in a <code>for-select</code> Loop Without a <code>default</code> Case
This example demonstrates how a missing default
case in a for-select
loop can lead to a goroutine leak and illustrates how to prevent it using a context and a default
case.
Problem: Blocking select
in a Loop
In this example, a goroutine is started with a for-select
loop. The select
statement only has one case: receiving from the quit
channel. If no value is sent to the quit
channel, the select
statement blocks indefinitely. The main function sends a value to the quit
channel after a delay. However, if the quit channel is unbuffered and no goroutine is ready to receive on the quit channel before the main attempts to send a message to quit, the main goroutine will block, waiting to send message. The worker goroutine will continue running and printing 'Working...' because the `select` statement blocks until it can receive from the `quit` channel. This demonstrates a potential goroutine leak, as the goroutine may never exit if the main goroutine has already exited due to blocking.
package main
import (
"fmt"
"time"
)
func main() {
quit := make(chan bool)
go func() {
for {
select {
case <-quit:
fmt.Println("Exiting goroutine")
return
}
fmt.Println("Working...")
time.Sleep(time.Second)
}
}()
// Simulate some work
time.Sleep(3 * time.Second)
// Try to signal the goroutine to quit, but if quit channel is full,
// this will block the main goroutine.
quit <- true
fmt.Println("Main function exiting")
time.Sleep(2 * time.Second) // Give the goroutine time to exit
}
Solution: Using a default
Case
By adding a default
case to the select
statement, the goroutine will not block if no value is available on the quit
channel. Instead, it will execute the code in the default
case, continuing to work. The main function will eventually send a value to the quit
channel, causing the goroutine to exit. Note that this approach may not always be desirable if the intent is for the program to wait for a specific event on a channel.
package main
import (
"fmt"
"time"
)
func main() {
quit := make(chan bool)
go func() {
for {
select {
case <-quit:
fmt.Println("Exiting goroutine")
return
default:
fmt.Println("Working...")
time.Sleep(time.Second)
}
}
}()
// Simulate some work
time.Sleep(3 * time.Second)
// Signal the goroutine to quit
quit <- true
fmt.Println("Main function exiting")
time.Sleep(2 * time.Second) // Give the goroutine time to exit
}
Solution: Using Context
for Cancellation
Using a context
provides a more robust way to signal cancellation to goroutines. The context.WithCancel
function creates a context and a cancel
function. The goroutine listens for the ctx.Done()
channel to be closed. The main function calls cancel()
after a delay, which closes the ctx.Done()
channel, causing the goroutine to exit. Using context allows for more complex scenarios where cancellation signals need to be propagated across multiple goroutines and functions. In the solution using context
, the default
statement is also present, preventing the leak in scenarios where the context cancellation is not immediate.
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Exiting goroutine")
return
default:
fmt.Println("Working...")
time.Sleep(time.Second)
}
}
}(ctx)
// Simulate some work
time.Sleep(3 * time.Second)
// Signal the goroutine to quit
cancel()
fmt.Println("Main function exiting")
time.Sleep(2 * time.Second) // Give the goroutine time to exit
}
Concepts Behind the Snippet
This snippet showcases the importance of handling blocking operations in goroutines and the dangers of indefinite blocking. The select
statement can block indefinitely if none of its cases are ready to execute. The default
case provides a non-blocking alternative. The context
package offers a powerful mechanism for signaling cancellation to goroutines, enabling graceful shutdown and preventing leaks.
Real-Life Use Case
Consider a system that processes messages from a queue. A goroutine is spawned for each message. If a message requires waiting for an external service, the goroutine might block in a select
statement waiting for a response. If the external service fails, the goroutine could block indefinitely. Using a timeout or a context can prevent the goroutine from leaking if the external service is unavailable.
Best Practices
default
case in select
statements when appropriate: If a select
statement should not block indefinitely, include a default
case.context
for cancellation: When dealing with potentially long-running operations, use the context
package to allow for graceful cancellation.
Interview Tip
Be prepared to explain how the select
statement works and the importance of the default
case. Also, be familiar with the context
package and how it can be used for cancellation and timeouts.
When to Use Them
Use the default
case in select
statements when you want to avoid blocking indefinitely. Use the context
package when you need to signal cancellation across multiple goroutines or enforce deadlines. Use timeouts when you need to prevent operations from taking too long.
Memory Footprint
Each goroutine consumes memory. Leaked goroutines can significantly increase the memory footprint of your application. Using appropriate concurrency management techniques like context cancellation helps to avoid leaks and control resource consumption.
Alternatives
Alternatives to context for cancellation include using channels and manual signaling. However, context provides a more structured and flexible approach, particularly when dealing with complex cancellation scenarios. Manual signaling with channels can become cumbersome to manage in large applications.
Pros
The default
case and context
provide mechanisms for non-blocking operations and graceful cancellation, which help prevent goroutine leaks and improve application reliability. Using these techniques can lead to more robust and maintainable code.
Cons
Incorrect use of the default
case or context
can lead to unexpected behavior. It is crucial to understand the implications of each approach and to choose the appropriate technique for the specific use case. Overusing timeouts can also lead to premature cancellations and unexpected errors.
FAQ
-
How does a
for-select
loop without adefault
case cause a goroutine leak?
If none of the cases in theselect
statement are ready to execute, theselect
statement blocks indefinitely, causing the goroutine to leak if no signal arrives. -
How does adding a
default
case prevent the leak?
Thedefault
case provides a non-blocking alternative, allowing the goroutine to continue executing even if none of the other cases are ready. -
How does
context
help prevent goroutine leaks?
Thecontext
package provides a mechanism for signaling cancellation to goroutines, allowing them to exit gracefully when their work is no longer needed. -
When should I use a
default
case in aselect
statement?
Use adefault
case when you want to avoid blocking indefinitely and allow the goroutine to continue executing even if none of the other cases are ready. -
When should I use
context
for cancellation?
Usecontext
when you need to signal cancellation across multiple goroutines or enforce deadlines.