0
0
GoHow-ToBeginner · 4 min read

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 capacity N.
  • 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.