Goroutines and Channels
This is Go’s signature feature, and the main reason many teams choose it for backend services: running many things concurrently is genuinely easy, not an advanced topic bolted on afterward like in many other languages.
Goroutines
func sayHello() {n fmt.Println("Hello from a goroutine")n}nnfunc main() {n go sayHello() // runs concurrently, doesn't blockn time.Sleep(100 * time.Millisecond) // give it time to finish before main() exitsn}
Adding the single word go before a function call runs it as a goroutine — a lightweight, independently-scheduled unit of execution. Goroutines are dramatically cheaper than operating system threads (you can comfortably run hundreds of thousands of them), which is a big part of why concurrent Go code stays simple even at real scale.
Channels — safe communication between goroutines
func worker(results chan<- int) {n results <- 42 // send a value into the channeln}nnfunc main() {n results := make(chan int)n go worker(results)n value := <-results // receive -- blocks until a value is availablen fmt.Println(value)n}
A channel is a typed pipe goroutines use to safely send values to each other, avoiding the raw shared-memory race conditions covered in the Java course’s multithreading lesson. The Go community has a well-known saying for this idea: “Do not communicate by sharing memory; instead, share memory by communicating.”
A word of caution, same as with any concurrency
Goroutines and channels make concurrent code far more approachable than in most languages, but “approachable” does not mean “risk-free” — bugs around goroutine lifetimes and channel deadlocks are still real and still take practice to reliably avoid. Treat this lesson as a solid introduction to the vocabulary and the tools, not a complete education in concurrent systems design.