Goroutines and channels, the basics
This is the lesson everyone comes to Go for. Concurrency that does not feel like a chore. Goroutines are functions that run in parallel, and channels are how they talk. You will write your first worker in fifteen lines. The deeper patterns get their own lesson later in this track.
Launching a goroutine
Put go in front of a function call. That is it.
func sendSMS(phone, msg string) {
// pretend this hits an API
time.Sleep(200 * time.Millisecond)
fmt.Printf("sent to %s\n", phone)
}
func main() {
go sendSMS("+91-9000000001", "OTP: 4729")
go sendSMS("+91-9000000002", "OTP: 8821")
time.Sleep(time.Second) // wait so we see the output
}Each go call schedules a goroutine on Go's runtime. Goroutines start at ~2 KB of stack and grow as needed. You can launch tens of thousands without sweating. Compare to OS threads at 1 MB each and you see why "spawn a goroutine per request" is normal Go.
Senior rule: never launch a goroutine you do not know how to stop. The
time.Sleepabove is a teaching shortcut. In real code, you use channels orsync.WaitGrouporcontext.Contextto coordinate.
Channels, the typed pipe
A channel is a typed, thread-safe pipe between goroutines. Make one with make(chan T).
ch := make(chan string)
go func() {
ch <- "hello" // send
}()
msg := <-ch // receive, blocks until something arrives
fmt.Println(msg) // helloThe arrow points where the data flows. ch <- v sends, v := <-ch receives. Both block until the other side shows up.
Unbuffered vs buffered, the rendezvous model
Unbuffered channels are a rendezvous. The sender waits for a receiver, the receiver waits for a sender, hand the value off, both continue. Buffered channels are a small queue: the sender does not block until the buffer fills.
a := make(chan int) // unbuffered, capacity 0, rendezvous
b := make(chan int, 3) // buffered, capacity 3
b <- 1; b <- 2; b <- 3 // all non-blocking
// b <- 4 would block here, buffer is fullSenior rule: start unbuffered. Add a buffer only when you have measured that the sender genuinely produces faster than the receiver consumes in bursts.
A buffer is not a free performance win. It hides backpressure. Sometimes hiding backpressure is what you want (rate-limited writes to a logger). Often it is how you ship a bug that only shows up in production.
close and range over a channel
The sender closes a channel to say "no more values". The receiver can detect this with comma-ok or by ranging.
ch := make(chan int)
go func() {
for i := 1; i <= 3; i++ {
ch <- i
}
close(ch)
}()
for v := range ch { // exits when ch is closed and drained
fmt.Println(v)
}Closing is the producer's job, never the consumer's. Sending on a closed channel panics. Closing a closed channel panics. Closing is a one-time signal from the one party that owns the channel.
Function signatures can also restrict a channel to send-only (chan<- T) or receive-only (<-chan T). Always type your channel parameters this way: it tells the reader and the compiler which side of the conversation this function is on, and it prevents an entire class of "who closes this?" arguments in code review. The common shape is a producer taking chan<- T, a consumer taking <-chan T, and the orchestrator in main holding the bidirectional chan T it created.
A tiny worker example
Putting it together. One channel of jobs, three workers pulling from it.
func worker(id int, jobs <-chan int) {
for j := range jobs {
fmt.Printf("worker %d processed job %d\n", id, j)
}
}
func main() {
jobs := make(chan int, 10)
for w := 1; w <= 3; w++ { go worker(w, jobs) }
for j := 1; j <= 5; j++ { jobs <- j }
close(jobs)
time.Sleep(time.Second) // teaching shortcut
}Three goroutines, one queue, work distributed automatically. This pattern, with sync.WaitGroup to wait for workers, scales to real OTP senders, payment retry processors, image resizers in your tool backend.
Quick reference
- Goroutine
- A function running concurrently on Go's runtime scheduler. Cheap, starts at ~2 KB.
- Channel
- A typed, thread-safe pipe used to send values between goroutines.
- Unbuffered channel
- Capacity 0. Sender and receiver rendezvous: both block until the handoff happens.
- Buffered channel
- Capacity N. Sender blocks only when the buffer is full.
- close
- Signal from the producer that no more values will be sent. Receivers can range until drained.
- Goroutine leak
- A goroutine that never exits. Holds memory and any resources it captured. The most common Go bug.
That wraps the beginner track's introduction to concurrency. The deeper lesson covers select, fan-in/fan-out, worker pools, and the patterns that hold up under real load. You will know when to read it.
Watching quietly. Tap me if you want a tip.
Go cannot run natively in a browser. Run copies your code and opens go.dev/play ; paste and click Run there.
Try this (0 of 1 done)
- 1
Predict the output.
show answer
// editor has it