Skip to content

Goroutines

In TypeScript you mark a function async and await its result. The function pauses at every await and yields back to the event loop, but everything still runs on one thread:

async function processUser(id: number) {
const user = await fetchUser(id);
return user;
}
await processUser(1); // you must await or .then() to consume it

In Go you prepend go to any function call. That call is dispatched as a goroutine — a lightweight, independently executing unit managed by the Go runtime. No async keyword on the function, no await at the call site:

go processUser(1) // fires immediately in the background

The critical difference: a JavaScript async function is still single-threaded. A goroutine can genuinely run in parallel on a different OS thread.

TypeScript
// TypeScript: async/await (single-threaded event loop)
async function greet(name: string): Promise<void> {
console.log("Hello from " + name);
}
// sequential
await greet("Alice");
await greet("Bob");
// concurrent (still one thread, interleaved via event loop)
await Promise.all([greet("Alice"), greet("Bob")]);
Go
// Go: goroutines (truly concurrent)
package main
import (
"fmt"
"sync"
)
func greet(name string, wg *sync.WaitGroup) {
defer wg.Done() // signal WaitGroup when done
fmt.Printf("Hello from %s\n", name)
}
func main() {
var wg sync.WaitGroup
names := []string{"Alice", "Bob", "Carol"}
for _, name := range names {
wg.Add(1) // register before launching
go greet(name, &wg) // launch goroutine
}
wg.Wait() // block until all goroutines finish
fmt.Println("All done!")
}

sync.WaitGroup is Go’s primary tool for waiting on a group of goroutines. It works like Promise.all but uses a counter instead of an array:

  1. wg.Add(n) — increment the counter before launching (not inside the goroutine)
  2. wg.Done() — decrement when the goroutine finishes (almost always via defer)
  3. wg.Wait() — block until the counter reaches zero

Rule: always call wg.Add(1) before go f(). If you call it inside the goroutine the scheduler might reach wg.Wait() before the goroutine even starts, and Wait returns immediately with work still pending.

wg.Go — Add, go, and Done in one call (Go 1.25+)

Section titled “wg.Go — Add, go, and Done in one call (Go 1.25+)”

The Add-before-go rule is easy to get wrong, so Go 1.25 added WaitGroup.Go. It does the Add(1), launches the goroutine, and calls Done when the function returns — the shape now reads almost exactly like Promise.all over a list of async calls:

TypeScript
// TypeScript
await Promise.all(urls.map(async (url) => {
await fetchOne(url);
}));
Go
// Go 1.25+
var wg sync.WaitGroup
for _, url := range urls {
wg.Go(func() { fetchOne(url) }) // Add(1) + go + Done, handled for you
}
wg.Wait()

Use wg.Go in new code; you will still see wg.Add(1) / defer wg.Done() in most existing codebases (and in some examples below), and go vet now flags an Add placed inside the goroutine.

Just like immediately-invoked arrow functions in JS, you can launch an anonymous goroutine inline:

TypeScript
// TypeScript: immediately invoked async
(async () => {
const result = await doSomething();
console.log(result);
})();
Go
// Go: immediately launched anonymous goroutine
go func() {
result := doSomething()
fmt.Println(result)
}() // trailing () invokes the function literal immediately
package main
import (
"fmt"
"sync"
)
func printSquare(n int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("%d² = %d\n", n, n*n)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go printSquare(i, &wg)
}
wg.Wait()
fmt.Println("All squares printed")
}
Which keyword launches a goroutine in Go?
When should wg.Add(1) be called?
What is the approximate starting stack size of a goroutine?
Which WaitGroup method signals that a goroutine has finished?