Goroutines
Async functions vs goroutines
Section titled “Async functions vs 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 itIn 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 backgroundThe critical difference: a JavaScript async function is still single-threaded. A goroutine can genuinely run in parallel on a different OS thread.
Launching goroutines
Section titled “Launching goroutines”// TypeScript: async/await (single-threaded event loop)async function greet(name: string): Promise<void> { console.log("Hello from " + name);}
// sequentialawait greet("Alice");await greet("Bob");
// concurrent (still one thread, interleaved via event loop)await Promise.all([greet("Alice"), greet("Bob")]);// 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 — waiting for goroutines
Section titled “sync.WaitGroup — waiting for goroutines”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:
wg.Add(n)— increment the counter before launching (not inside the goroutine)wg.Done()— decrement when the goroutine finishes (almost always viadefer)wg.Wait()— block until the counter reaches zero
Rule: always call
wg.Add(1)beforego f(). If you call it inside the goroutine the scheduler might reachwg.Wait()before the goroutine even starts, andWaitreturns 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:
// TypeScriptawait Promise.all(urls.map(async (url) => { await fetchOne(url);}));// Go 1.25+var wg sync.WaitGroupfor _, 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.
Anonymous goroutines
Section titled “Anonymous goroutines”Just like immediately-invoked arrow functions in JS, you can launch an anonymous goroutine inline:
// TypeScript: immediately invoked async(async () => { const result = await doSomething(); console.log(result);})();// Go: immediately launched anonymous goroutinego func() { result := doSomething() fmt.Println(result)}() // trailing () invokes the function literal immediatelypackage 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")}Compiling on go.dev (Go 1.27)… offline? an in-browser runtime (~8 MB, Go 1.26) is used instead