Intro
AI-generated
A Go executable includes a runtime with a garbage collector, goroutine scheduler, and network poller. The runtime schedules many goroutines over a smaller or equal number of operating-system threads and grows goroutine stacks as needed.
Goroutines are lightweight, but not free. Every goroutine should have a bounded lifetime and an owner that can wait for or cancel it. The main language and library tools are:
- the
gostatement to start a goroutine; - channels and the
<-operator for communication; selectfor coordinating channel operations and cancellation;syncandsync/atomicfor shared-state synchronization;context.Contextfor cancellation and deadlines across API boundaries.
Basic Constructs
Wait Groups
AI-generated
func f1(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("f1 started")
time.Sleep(1 * time.Second)
fmt.Println("f1 stopped")
}
func f2(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("f2 started")
time.Sleep(2 * time.Second)
fmt.Println("f2 stopped")
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go f1(&wg)
go f2(&wg)
wg.Wait()
}
A WaitGroup counts work; it does not detect deadlocks. Call Add before starting
the goroutine and arrange for exactly one Done per unit. A WaitGroup must not
be copied after first use, so pass a pointer when it crosses a function boundary.
Managing State with Concurrency
AI-generated
- "data-races" will happen on mutations happening in go-routines
- "-race" flag/switch in go command :
go run -race {x}shows if there are race conditions and result may not be correct. - we can have same
-raceflag ingo buildcommand as well but DO NOT USE THE BUILD FOR PRODUCTION as there may be some performance degradation.
We can use Mutexes
- we will have to
Lock()andUnlockeverytime!
Or, we can encapsulate our state
type Counter struct {
sync.Mutex
count int
}
func (c *Counter) Inc() {
c.Lock()
defer c.Unlock()
c.count++
}
Or, we can use atomic library
var count int64
atomic.AddInt64(&count, 1)
var atomicCount atomic.Int64
atomicCount.Add(1)
Communication b/w Go-Routines
AI-generated
Avoid communicating by sharing memory; share memory by communicating.
Channels
- Channels don't behave exactly like a "queue".
- It can be visualized as a "door".
- We can use channels for synchronization as well, we do not need to be dependent on Wait-Groups.
- because of their blocking / non-blocking nature in certain scenarios of send and receive.
Declaration and Initialization:
var ch chan int; ch = make(chan int)- Or :
ch := make(chan int)
Operations:
- Send:
ch <- {data} - it is "blocked" until a receive operation is "initiated"
- Receive:
data := <- ch - it is also a blocked until the data is sent through the channel
- Channel constraints:
- receive only channel with
<-chan {type} - send only channel with
chan<- {type} - we can use these syntaxes at parameters of functions or return type of functions
- e.g.,
func Producer(...) <-chan int { ... } - After
close(ch), receives drain buffered values and then return the element type's zero value immediately withok == false. - Detect this with
data, isOpen := <-ch. - Or, we can range over a channel like:
for data := range ch { ... }
The sender that owns the stream should close the channel. Closing is a broadcast that no more values will arrive; it is not needed for garbage collection.
Concurrency - Streaming
Multiple Channels
AI-generated
Use select when a goroutine must wait on more than one channel:
select {
case value := <-results:
return value, nil
case <-ctx.Done():
return Result{}, ctx.Err()
}
If several cases are ready, one is chosen pseudo-randomly. A default case makes
the operation non-blocking and can accidentally create a busy loop; use it only
when dropping or polling is intentional.
Buffered Channels
AI-generated
jobs := make(chan Job, 16)
A send blocks only while the buffer is full; a receive blocks while it is empty. Buffers can absorb short bursts and decouple producer timing, but they do not fix an indefinitely slower consumer. Choose capacity from a measured burst or backpressure requirement, not as a substitute for flow control.
Concurrency - Patterns
Runner
AI-generated
A runner owns the lifetime of several goroutines. It starts them, cancels siblings when one fails or the caller cancels, and waits for all of them before returning. This keeps background work from leaking beyond its request or process owner.
Pool
AI-generated
A worker pool bounds concurrency: start a fixed number of workers, send jobs over a channel, close the jobs channel when production ends, and wait for workers. Propagate cancellation so producers do not block forever after consumers stop.
Worker
AI-generated
func worker(ctx context.Context, jobs <-chan Job, results chan<- Result) {
for {
select {
case <-ctx.Done():
return
case job, ok := <-jobs:
if !ok {
return
}
result := process(job)
select {
case results <- result:
case <-ctx.Done():
return
}
}
}
}
The second select matters: cancellation must also unblock a worker that is
trying to publish a result after the receiver has gone away.