Skip to content

Concurrency — Overview

JavaScript is single-threaded — Go is genuinely multi-threaded

Section titled “JavaScript is single-threaded — Go is genuinely multi-threaded”

As a TypeScript/Node.js developer you live inside the event loop: all your JS code runs on one thread, and the “concurrency” you see is context-switching between callbacks, Promises, and async/await — not true parallelism.

Go is fundamentally different. Goroutines are scheduled by the Go runtime across real OS threads. Multiple goroutines can genuinely execute at the same time on different CPU cores — no Worker Threads required.

Rob Pike (one of Go’s creators) put it precisely:

  • Concurrency is dealing with many things at once — a structural property.
  • Parallelism is doing many things at once — an execution property.

A chef who chops vegetables while water boils is concurrent (managing multiple things). Two chefs working side by side in the same kitchen are parallel (actually doing multiple things simultaneously).

Node.js excels at concurrency (non-blocking I/O), but cannot be truly parallel without Worker Threads or multiple processes. Go handles both naturally.

TypeScript
// JavaScript: single-threaded event loop
// "concurrency" through callbacks / Promises
async function fetchData(url: string) {
const res = await fetch(url); // yields to event loop
return res.json();
}
// setTimeout / setInterval schedule callbacks on the loop
setTimeout(() => console.log("later"), 0);
console.log("first"); // always prints before "later"
Go
// Go: real concurrent goroutines
// Each 'go' spawns a lightweight thread managed by the Go runtime
package main
import (
"fmt"
"sync"
)
func fetchData(url string, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("Fetching:", url) // runs concurrently
}
func main() {
var wg sync.WaitGroup
urls := []string{"api/users", "api/orders", "api/products"}
for _, url := range urls {
wg.Add(1)
go fetchData(url, &wg)
}
wg.Wait() // block until all goroutines finish
}
  • Goroutinesgo f() launches a goroutine instantly; compare to async functions
  • Channels — typed pipes between goroutines; compare to Promises/callbacks
  • Select — choose from multiple channels; compare to Promise.race
  • SyncWaitGroup, Mutex, RWMutex for shared-state safety
  • Context — cancellation and deadlines; compare to AbortController
  • Worker Pools — a pattern for bounded concurrent work
package main
import (
"fmt"
"sync"
)
func task(name string, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Task %s running\n", name)
}
func main() {
var wg sync.WaitGroup
tasks := []string{"A", "B", "C"}
for _, t := range tasks {
wg.Add(1)
go task(t, &wg)
}
wg.Wait()
fmt.Println("All tasks complete")
}
What is the key difference between concurrency and parallelism?
How many threads does JavaScript run on by default?
What is the initial stack size of a goroutine?