Skip to content

Goroutines Preview

JavaScript’s concurrency model: async/await

Section titled “JavaScript’s concurrency model: async/await”

JavaScript is single-threaded. Concurrency comes from the event loop: when an async operation (network, I/O) completes, a callback is scheduled. async/await is syntactic sugar over Promises — it makes asynchronous code read like synchronous code, but there is still only one thread executing user JavaScript at a time.

Go takes a completely different approach: goroutines are genuinely concurrent lightweight threads, and the Go runtime schedules them across multiple OS threads. You can run millions of goroutines on a typical machine.

The go keyword launches a function in a new goroutine — a lightweight, independently scheduled execution context. It returns immediately; the goroutine runs concurrently.

TypeScript
// TypeScript — async/await (event loop, single thread)
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/users/${id}`);
return res.json();
}
async function main() {
// These run sequentially by default:
const u1 = await fetchUser(1);
const u2 = await fetchUser(2);
// To run concurrently, use Promise.all:
const [u1, u2] = await Promise.all([fetchUser(1), fetchUser(2)]);
}
Go
// Go — goroutines (OS-thread-backed, truly parallel)
func fetchUser(id int) User {
// ... actual network call
return User{ID: id}
}
func main() {
// go keyword — launches fetchUser concurrently
go fetchUser(1) // fire and forget (result lost)
go fetchUser(2)
// To collect results, use channels or sync.WaitGroup
}

sync.WaitGroup — wait for goroutines to finish

Section titled “sync.WaitGroup — wait for goroutines to finish”

sync.WaitGroup is the simplest way to wait for a known number of goroutines to complete before the main goroutine exits. Think of it as a counter that blocks until it reaches zero.

TypeScript
// TypeScript — Promise.all to wait for all
const results = await Promise.all(
[1, 2, 3].map(id => fetchUser(id))
);
console.log(results);
Go
func processItem(id int, wg *sync.WaitGroup) {
defer wg.Done() // decrement when done
fmt.Printf("processing %d\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // increment counter
go processItem(i, &wg) // launch goroutine
}
wg.Wait() // block until counter == 0
fmt.Println("all done")
}

Channels — communicate between goroutines

Section titled “Channels — communicate between goroutines”

A channel is a typed conduit for sending values between goroutines. You send a value with <- and receive a value with <-. Channels synchronize the sender and receiver — sending blocks until someone receives, and vice versa (for unbuffered channels).

TypeScript
// TypeScript — no native channel primitive
// Closest approximation: a queue + event emitter, or a stream
import { Readable } from "stream";
// ... complex plumbing required
Go
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i // send i into channel
}
close(ch) // signal no more values
}
func main() {
ch := make(chan int) // unbuffered channel
go producer(ch) // runs concurrently
for v := range ch { // receive until channel closed
fmt.Println("received:", v)
}
}
package main
import (
"fmt"
"sync"
)
func fetchData(id int, wg *sync.WaitGroup, results chan<- string) {
defer wg.Done()
// Simulate work
result := fmt.Sprintf("result from worker %d", id)
results <- result
}
func main() {
const numWorkers = 3
var wg sync.WaitGroup
results := make(chan string, numWorkers) // buffered channel
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go fetchData(i, &wg, results)
}
// Close channel when all goroutines finish
go func() {
wg.Wait()
close(results)
}()
// Collect results
for r := range results {
fmt.Println(r)
}
fmt.Println("all workers done")
}
How do you launch a goroutine in Go?
How much stack memory does a new goroutine start with (approximately)?
What does sync.WaitGroup.Wait() do?
In Go, sending to an unbuffered channel blocks until: