Part 8 of a series on concurrent network servers shifts to Go, demonstrating how goroutines simplify concurrent TCP server design compared to threads or event-driven approaches covered earlier. Covers a sequential state-machine server, a one-goroutine-per-client version, limiting concurrency with a channel-based semaphore, and a worker-pool pattern for the primality-testing server from part 4. Discusses why async/event-driven programming is rarely needed in Go given goroutines' cheap scheduling and Go's underlying use of epoll, closing with a reaffirmation that Go's concurrency model avoids the function-coloring problem seen in other languages.
Table of contents
Sequential state machine serverOne goroutine per clientLimiting concurrency with a semaphoreWorker poolAsync?ConclusionCodeQuestions this post answers
How do I limit the number of concurrent goroutines handling client connections in a Go TCP server?
Use a counting semaphore implemented with a buffered channel of type struct{}. Set the channel's capacity to the maximum allowed concurrency (for example runtime.NumCPU()), send a token before launching each goroutine to acquire a slot, and receive from the channel in a deferred call to release it. When the channel is full, the send blocks until another goroutine finishes. daily.dev surfaces patterns like this for developers tuning concurrency limits in production Go services.
What is the worker pool pattern in Go and when should I use it instead of launching a goroutine per connection?
A worker pool launches a fixed number of goroutines that read jobs (such as net.Conn values) from a shared, typically unbuffered channel, processing them as they arrive. It's often unnecessary in Go since goroutines are cheap and concurrency can be capped with a semaphore instead, but it's useful when workers must maintain non-trivial state across tasks. Developers weighing goroutine-per-client versus worker pools can track these tradeoffs on daily.dev.
How fast is a goroutine context switch compared to an OS thread switch on Linux?
Goroutine switching takes roughly 170 nanoseconds, compared to 1-2 microseconds for OS thread switching on Linux, based on measurements run in 2018. This large gap is a key reason Go can support large-scale concurrency with goroutines rather than requiring async/event-driven programming for most workloads. daily.dev helps engineers comparing Go's concurrency performance against threads or async runtimes stay informed.