How to Use Channel as Semaphore in Go for Concurrency Control
In Go, you can use a buffered
channel as a semaphore by sending tokens to it before starting a goroutine and receiving tokens back when done. This limits the number of concurrent goroutines to the channel's buffer size, effectively controlling access to resources.Syntax
To use a channel as a semaphore, create a buffered channel with a capacity equal to the maximum number of concurrent operations allowed. Before starting a goroutine, send a token (usually an empty struct) into the channel. When the goroutine finishes, receive from the channel to release the token.
sem := make(chan struct{}, N): Creates a semaphore channel with capacityN.sem <- struct{}{}: Acquires a slot by sending a token.<-sem: Releases a slot by receiving a token.
go
sem := make(chan struct{}, N)
// Acquire semaphore
sem <- struct{}{ }
// Do work in goroutine
go func() {
defer func() { <-sem }() // Release semaphore when done
// work here
}()Example
This example shows how to limit the number of concurrent goroutines to 3 using a channel as a semaphore. Each goroutine simulates work by sleeping for 1 second.
go
package main import ( "fmt" "time" ) func main() { sem := make(chan struct{}, 3) // limit to 3 concurrent goroutines for i := 1; i <= 5; i++ { sem <- struct{}{} // acquire semaphore go func(id int) { defer func() { <-sem }() // release semaphore fmt.Printf("Goroutine %d started\n", id) time.Sleep(1 * time.Second) // simulate work fmt.Printf("Goroutine %d finished\n", id) }(i) } // Wait for all goroutines to finish // by acquiring all semaphore slots for i := 0; i < cap(sem); i++ { sem <- struct{}{} } }
Output
Goroutine 1 started
Goroutine 2 started
Goroutine 3 started
Goroutine 1 finished
Goroutine 4 started
Goroutine 2 finished
Goroutine 5 started
Goroutine 3 finished
Goroutine 4 finished
Goroutine 5 finished
Common Pitfalls
Common mistakes when using channels as semaphores include:
- Using an unbuffered channel, which blocks immediately and does not limit concurrency properly.
- Forgetting to release the semaphore token (
<-sem) after work is done, causing deadlocks. - Not waiting for all goroutines to finish before exiting the main function, which can cause premature program termination.
go
package main import "fmt" func main() { sem := make(chan struct{}) // unbuffered channel - wrong for semaphore // This will block forever because no buffer to hold tokens sem <- struct{}{} fmt.Println("This line will never print") } // Correct way: // sem := make(chan struct{}, N) // buffered channel // sem <- struct{}{} // acquire // defer func() { <-sem }() // release
Quick Reference
- Create semaphore:
sem := make(chan struct{}, maxConcurrent) - Acquire slot:
sem <- struct{}{} - Release slot:
<-sem - Use defer to release:
defer func() { <-sem }() - Wait for all: Acquire all slots at the end to ensure completion
Key Takeaways
Use a buffered channel to limit the number of concurrent goroutines as a semaphore.
Send a token to the channel to acquire a slot and receive from it to release the slot.
Always release the semaphore token to avoid deadlocks.
Wait for all goroutines to finish before exiting the program.
Use an empty struct as the token to save memory.