Master Go Concurrency: Goroutines, Channels, Locks, Timers and Synchronization
This comprehensive guide explains the fundamentals of concurrent programming in Go, covering the differences between parallelism and concurrency, process and thread concepts, and detailed usage of goroutines, channels, select statements, timers, mutexes, read‑write locks, wait groups, once, sync.Map, and atomic operations with practical code examples and diagrams.
Overview
In short, concurrency programming refers to handling multiple tasks "simultaneously" on a single processor. As hardware evolves, concurrent programs become increasingly important: web servers handle thousands of requests at once, mobile apps render UI while performing background calculations and network requests, and even traditional batch jobs hide I/O latency by using multiple cores.
Parallelism vs Concurrency
parallel: multiple instructions execute on multiple processors at the same time. concurrency: at any moment only one instruction executes, but multiple program instructions are rapidly switched, giving the macro‑level effect of simultaneous execution through CPU time‑slice rotation.
Analogy: two queues using two coffee machines in parallel versus two queues alternating on a single coffee machine for concurrency.
Common Concurrency Techniques
Process Concurrency
A program is a compiled binary on disk that does not occupy system resources. A process is an active program that occupies resources such as CPU, memory, open files, devices, and locks.
Process states include: new, ready, running, blocked, and terminated.
Issues with process concurrency include high system overhead, limited number of processes, and the creation of orphan and zombie processes on Unix/Linux.
Thread Concurrency
Thread (light‑weight process) shares the same address space, while a process has its own address space.
Thread: smallest unit of execution.
Process: smallest unit of resource allocation, can be seen as a process with a single thread.
Thread synchronization ensures that multiple control flows operating on a shared resource execute in a defined order to avoid data races.
Mutex
Linux provides a mutex lock. Before accessing a resource, a thread attempts to lock; only after acquiring the lock can it operate, then it unlocks.
Read‑Write Lock
Allows multiple readers simultaneously but only one writer exclusively. Write mode blocks all readers; read mode allows other readers but blocks writers, with writer priority.
Coroutine Concurrency
Coroutines (light‑weight threads) can create thousands without exhausting system resources. In Go, coroutines are called goroutines.
Go Concurrency
Goroutine
A goroutine is the core of Go's concurrency model. It is lighter than a thread; thousands of goroutines can run on a few OS threads.
package main
import (
"fmt"
"time"
)
func newTask() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d
", i)
time.Sleep(time.Second) // delay 1 second
}
}
func main() {
// create a goroutine to start another task
go newTask()
// loop printing
for i := 0; i < 5; i++ {
fmt.Printf("main goroutine: i = %d
", i)
time.Sleep(time.Second) // delay 1 second
i++
}
}If the main goroutine exits, all other goroutines are terminated.
package main
import (
"fmt"
"runtime"
)
func main() {
go func(s string) {
for i := 0; i < 2; i++ {
fmt.Println(s)
}
}("world")
for i := 0; i < 2; i++ {
runtime.Gosched() // yield CPU
fmt.Println("hello")
}
}GOMAXPROCS
Sets the maximum number of CPU cores that can execute Go code simultaneously.
package main
import (
"fmt"
"runtime"
)
func main() {
n := runtime.GOMAXPROCS(2) // use 2 CPUs
fmt.Println(n)
for {
go fmt.Print(0)
fmt.Print(1)
}
}Channel
A channel is a core Go type that works like a pipe for communication between goroutines, simplifying synchronization without explicit locks.
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
defer fmt.Println("child goroutine finished")
fmt.Println("child goroutine running…")
c <- 666 // send to c
}()
num := <-c // receive from c
fmt.Println("num = ", num)
fmt.Println("main goroutine finished")
}Channels can be unbuffered (synchronous) or buffered (asynchronous). Unbuffered channels block until both sender and receiver are ready; buffered channels block only when the buffer is full.
Unbuffered Channel Example
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 0) // unbuffered channel
go func() {
defer fmt.Println("child goroutine finished")
for i := 0; i < 3; i++ {
c <- i
fmt.Printf("child running[%d]: len(c) = %d, cap(c) = %d
", i, len(c), cap(c))
}
}()
time.Sleep(2 * time.Second)
for i := 0; i < 3; i++ {
num := <-c
fmt.Printf("num = %d
", num)
}
fmt.Println("main goroutine finished")
}Buffered Channel Example
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 3) // buffered channel with capacity 3
fmt.Printf("len(c) = %d, cap(c) = %d
", len(c), cap(c))
go func() {
defer fmt.Println("child goroutine finished")
for i := 0; i < 3; i++ {
c <- i
fmt.Printf("child running[%d]: len(c) = %d, cap(c) = %d
", i, len(c), cap(c))
}
}()
time.Sleep(2 * time.Second)
for i := 0; i < 3; i++ {
num := <-c
fmt.Printf("num = %d
", num)
}
fmt.Println("main goroutine finished")
}Close Channel
When a sender knows no more values will be sent, it can close the channel so receivers can stop waiting.
Select
The select statement monitors multiple channel operations, similar to a switch but each case must be a channel send or receive.
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 6; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}Using select with a default case prevents blocking, and it can also implement timeouts.
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
o := make(chan bool)
go func() {
for {
select {
case v := <-c:
fmt.Println(v)
case <-time.After(5 * time.Second):
fmt.Println("timeout")
o <- true
break
}
}
}()
//c <- 666 // uncomment to avoid timeout
<-o
}Locks
Traditional synchronization tools are provided in the sync package.
Deadlock
A deadlock occurs when two or more goroutines wait indefinitely for each other.
package main
import "fmt"
func main() {
ch := make(chan int)
ch <- 1 // blocked because no receiver yet
fmt.Println("send")
go func() {
<-ch // never executed because main is blocked
fmt.Println("received")
}()
fmt.Println("over")
}Mutex
Only one goroutine can hold the mutex at a time, preventing data races.
package main
import (
"fmt"
"sync"
)
var (
x int64
wg sync.WaitGroup
lock sync.Mutex
)
func add() {
defer wg.Done()
for i := 0; i < 5000; i++ {
lock.Lock()
x = x + 1
lock.Unlock()
}
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}Read‑Write Mutex
Allows multiple concurrent readers but exclusive writers.
package main
import (
"fmt"
"sync"
"time"
)
var (
x int64
wg sync.WaitGroup
rwlock sync.RWMutex
)
func write() {
defer wg.Done()
rwlock.Lock()
x = x + 1
time.Sleep(time.Millisecond * 10)
rwlock.Unlock()
}
func read() {
defer wg.Done()
rwlock.RLock()
time.Sleep(time.Millisecond)
rwlock.RUnlock()
}
func main() {
start := time.Now()
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 1000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
fmt.Println(time.Since(start))
}WaitGroup
Synchronizes a set of goroutines, waiting until the counter drops to zero.
package main
import (
"fmt"
"sync"
)
func sayHello(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("Hello")
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go sayHello(&wg)
}
fmt.Println("main goroutine done!")
wg.Wait()
}Once
Ensures a function is executed only once, even under high concurrency.
package main
import "sync"
type singleton struct{}
var (
instance *singleton
once sync.Once
)
func GetInstance() *singleton {
once.Do(func() { instance = &singleton{} })
return instance
}sync.Map
A concurrent map that does not require explicit locking.
package main
import (
"fmt"
"strconv"
"sync"
)
func main() {
var m sync.Map
var wg sync.WaitGroup
for i := 0; i < 40; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
key := strconv.Itoa(n)
m.Store(key, n)
v, _ := m.Load(key)
fmt.Printf("k: %v, v: %v
", key, v)
}(i)
}
wg.Wait()
}Atomic Operations
Atomic functions from sync/atomic provide lock‑free synchronization for basic types, offering higher performance than mutexes.
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
type Counter interface {
Inc()
Load() int64
}
type NormalCounter struct{ x int64 }
func (c *NormalCounter) Inc() { c.x++ }
func (c *NormalCounter) Load() int64 { return c.x }
type MutexCounter struct{ x int64; lock sync.Mutex }
func (c *MutexCounter) Inc() { c.lock.Lock(); c.x++; c.lock.Unlock() }
func (c *MutexCounter) Load() int64 { c.lock.Lock(); defer c.lock.Unlock(); return c.x }
type AtomicCounter struct{ x int64 }
func (c *AtomicCounter) Inc() { atomic.AddInt64(&c.x, 1) }
func (c *AtomicCounter) Load() int64 { return atomic.LoadInt64(&c.x) }
func test(c Counter) {
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() { defer wg.Done(); c.Inc() }()
}
wg.Wait()
fmt.Printf("Time: %v, Result: %v
", time.Since(start), c.Load())
}
func main() {
var c1 NormalCounter // not safe for concurrency
test(&c1)
var c2 MutexCounter // safe with mutex
test(&c2)
var c3 AtomicCounter // safe and faster
test(&c3)
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
