# Go Multithreading APIs: Goroutines Explained

> Published 2026-10-12T22:57:37.326Z on https://youblog.app/p/go-multithreading-apis-goroutines-explained/
> Source video: https://www.youtube.com/watch?v=C4lRMBOc-EQ

Go is a compiled language whose runtime schedules goroutines instead of operating-system threads, and its `net/http` server starts a goroutine for every incoming request. That combination lets you handle high request volume and fan-out jobs without sizing a thread pool or writing lock-heavy code.

If you have ever written a thread pool to keep a server responsive, Go multithreading APIs will look strange at first. There is no pool to size, and the standard library server does not need one: each request gets a goroutine, and the runtime schedules those goroutines across CPU cores. Channels then move data between them instead of shared memory guarded by locks.

## How Go multithreading APIs actually work

Go multithreading APIs are built on goroutines, which the Go runtime schedules instead of the operating system. The `go` keyword starts a function on a new goroutine, a goroutine's stack begins around 2 KB and grows or shrinks as needed, and the net/http server starts a goroutine per request.

A goroutine is not a kernel thread. The runtime asks the operating system to create OS threads through system calls, then multiplexes many goroutines onto that smaller set of threads with its own scheduler, which runs cooperatively and preemptively. That scheduler lives in userland, meaning it sits above the kernel and manages goroutines itself, so a program can keep thousands of goroutines alive without a thousand kernel threads. The official documentation for the language is [Go](https://go.dev), and the runtime's own notes live in the [Go runtime package reference](https://pkg.go.dev/runtime).

Goroutines are cheap because they are user-space threads, not OS threads: the kernel handles none of their context switching. Creating a regular OS thread costs on the order of megabytes of stack by default, while a goroutine starts at 2 KB, and every goroutine starts a new context switch in the kernel scheduler, which adds overhead. Scaling goroutines up to thousands is what the runtime was built for.

Channels are the second half of the model. A channel passes values from one goroutine to another, so the receiver never reads a memory location the sender is still writing. Rob Pike's talk [Concurrency Is Not Parallelism](https://go.dev/blog/waza-talk) and the [Go blog](https://go.dev/blog) cover the reasoning behind that design.

## Goroutines vs threads vs Node.js event loop

The comparison that matters is cost per concurrent task and the mechanism that keeps them apart. Go is not the only runtime pursuing cheap concurrency, but the numbers and the mechanics differ from both OS threads and single-threaded event loops.

| Model | Unit of concurrency | Typical starting memory | Coordination |
| --- | --- | --- | --- |
| Go | goroutine, scheduled by the Go runtime | about 2 KB stack, dynamic | channels, or sync primitives |
| Kernel threads | OS thread | around 2 MB stack per thread by default | locks, semaphores, condition variables |
| Node.js | one JS thread plus an event loop | shared heap | callbacks, promises, worker threads for CPU work |

The gap in starting memory is the number worth remembering: 2 KB against roughly 2 MB, a difference of about three orders of magnitude per task. That is why a Go program can hold thousands of concurrent units of work while a thread-per-task model runs out of memory far sooner.

Node.js does support parallelism through worker threads, so the difference is architectural rather than a hard capability gap. The Node.js event loop runs application code on a single thread, and when you need a second thread you fork the whole runtime again, which carries its own overhead. The Go runtime instead schedules goroutines cooperatively and preemptively on that single set of OS threads.

Other runtimes have moved toward lighter concurrency as well. Kotlin coroutines offer a comparable model, and Java added virtual threads to its standard library in recent versions, which is a large step for the Java world even though the semantics are not identical to goroutines.

## Channels instead of shared-memory locks

Channels exist to remove the most common concurrency bug: two goroutines mutating the same variable, also known as a race condition. A sender writes a value into a channel, a receiver reads it, and the value crosses a synchronisation point instead of a lock.

Without channels, the pattern is explicit and fragile. One goroutine locks the value, writes, and unlocks; a second goroutine waits for that lock before it can read. Forget one unlock anywhere in that path and the program becomes intermittently broken, and intermittent race conditions are the hardest failures to reproduce because they depend on timing.

The Go memory model, documented at [The Go Memory Model](https://go.dev/ref/mem), defines the ordering guarantees channels provide. Mutexes still exist in [sync](https://pkg.go.dev/sync) and remain valid when a structure must be protected in place, but a channel is the safer default when the goal is to hand a value from one goroutine to another.

One practical warning: sending into an unbuffered channel blocks until a receiver takes the value. A goroutine that publishes and finds no consumer simply stalls, which is correct behaviour and also a common source of deadlock in first attempts. A channel is emptied when a value is read from it, so the sender cannot place a second value until the receiver has taken the first.

## The net/http server runs one goroutine per request

Go's standard-library HTTP server handles each incoming request on its own goroutine, so requests are processed independently and the runtime spreads them across available cores. No router library is required: `http.ListenAndServe` plus a handler function is a working server, and `net/http` also covers the client side and lower-level transport. The same package speaks TCP, UDP, HTTP, gRPC and WebSockets without an external dependency, which is a large part of why Go services so often ship with a small dependency list.

A minimal handler takes an `http.ResponseWriter` and an `*http.Request`, writes a byte slice to the writer, and is registered with `http.HandleFunc`. Go is strongly typed, so the signatures are not optional: the request parameter is `*http.Request` and the writer is `http.ResponseWriter`, and the compiler enforces both. The package's full surface is documented at [net/http](https://pkg.go.dev/net/http), and the language tour walks through the same example in [Writing Web Applications](https://go.dev/doc/articles/wiki/).

The writer accepts a byte slice rather than a string. Go distinguishes between a string and a slice, which is a dynamic, array-backed sequence of values, and byte slices are the common currency at the boundary of a network call. You can convert a byte slice back to an integer or a string when you need to, which is why many low-level APIs in the standard library traffic in bytes.

Encoding a struct as JSON uses `encoding/json`, and field names are controlled with struct tags such as `json:"product_name"`. Tags are strings the reflection package reads, so the same struct can map to different external representations without a separate DTO class. `json.NewEncoder(w).Encode(product)` writes the encoded value straight to the response writer.

Because each request is an independent goroutine, high request volume does not require a manually sized worker pool at the HTTP layer. The limit that matters is usually downstream: database connections, upstream service latency, and memory held by in-flight requests.

### Structs and methods instead of classes

Go has no classes and no inheritance. The language centres on data structures: you declare a type such as `type Product struct` with fields like `ID`, `Name` and `Price`, and then attach behaviour by declaring methods whose receiver is that type. A method declaration places the struct before the function name, so a `Sell(price float64) error` method with a `Product` receiver is called as `product.Sell(9.99)`. The result is close to an object with attributes and methods, without the class hierarchy.

## A fan-out worker pool from channels

A queue plus several goroutines reading from the same channel is a load balancer written in a dozen lines. Producers write jobs into one channel, and a set of consumers loop over that channel; each message is taken by exactly one consumer.

The steps are short:

1. Create a channel with `make(chan string)` or `make(chan int)`.
2. Start one producer goroutine that writes values into the channel and closes it when finished.
3. Start several consumer goroutines that range over the same channel and process each received value.
4. Wait for the consumers with [sync.WaitGroup](https://pkg.go.dev/sync#WaitGroup) so the process does not exit before the work finishes.

Adding consumers is the scaling knob. In the video's demo the producer waited one second between messages, so a single consumer needed about ten seconds for ten messages; a second consumer took work in parallel with the first. That result is the speaker's own demonstration, not an independent benchmark.

Closing the channel signals that no further values will be sent. Ranging over a closed channel drains the remaining buffered values and then exits, which is why the closing side must be the sender rather than the receiver. If you only run the consumers without a wait group, the program can finish before the workers ever touch the channel, which is exactly what happens when the demo runs long enough to print nothing.

## What Go is a poor fit for

Go is productive and fast on average, which is not the same as being the best choice everywhere. The speaker's own example is a set of simple CRUD endpoints and report generators, where he says he would likely reach for Java and [Spring](https://spring.io) because the framework's tooling produces that shape of application faster.

Machine learning is the other clear case. Python's scientific ecosystem has decades of accumulated libraries behind it, and a language port does not reproduce that. Much of that ecosystem is backed by native code, but the ergonomics and the available implementations still sit with Python. If everything in machine learning were written in Go it would probably run faster, but the maturity of those libraries is the blocker, not the runtime.

Raw performance is not a sufficient reason on its own. Choosing a stack also weighs hiring, learning curve, library maturity and delivery speed. Rust is the clearest example: a full fan-out worker pool with channels that takes a dozen lines in Go is considerably more involved to write in Rust, and that extra work has to be worth the control it buys.

The honest framing is that no language rescues every project, and the choice is normally economic. Go tends to win where it saves hardware and shortens the path to a working service.

## Go vs Rust vs Node.js vs Java: what to pick

Each language here has a different strength, and the right question is which constraint dominates the project. The table below keeps the comparison on dimensions that are actually documented rather than scored on taste.

| Language | Main strength | Concurrency model | Main constraint |
| --- | --- | --- | --- |
| Go | simple syntax, fast builds, goroutines and channels | runtime-scheduled goroutines | fewer generic abstractions, no classes or inheritance |
| Rust | memory safety without a garbage collector, low-level control | threads with compile-time safety checks | steep learning curve, smaller hiring pool |
| Node.js | one language across client and server, large package ecosystem | single JS thread with an event loop, worker threads for CPU work | CPU-bound work needs workers or a second service |
| Java | mature frameworks, large ecosystem | platform threads, plus virtual threads in recent versions | more verbose, heavier runtime |

The Rust comparison is the one most often asked about. Rust offers stronger low-level control and memory guarantees, and Go offers a shorter path to working systems code, so the trade is learning effort against control rather than a single winner. Rust is also used mostly at lower levels of the stack, for drivers and components close to hardware, while Go is comfortable at the server.

Java is the comparison that shows why Go is not automatically the answer for typical business software. A mature framework such as Spring generates that boilerplate faster than writing it by hand in Go, so a lower-performance stack can still ship sooner.

## Where Go runs in production

Container and orchestration tooling is Go's most visible production footprint. [Docker](https://www.docker.com) and [Kubernetes](https://kubernetes.io) are both written in Go, and [Prometheus](https://prometheus.io), the monitoring system, is as well, so the ecosystem around Go services is partly Go itself.

The [Mercado Livre tech blog](https://medium.com/mercadolivre-tech) has described the company's move toward Go microservices serving high request volume, and most of Mercado Livre's microservices are written in Go. Statements about how much of a company's stack runs on Go are company-reported rather than independently audited.

The pattern in these cases is high-concurrency network services: APIs, proxies, schedulers, and agents that run close to the machine without needing manual memory management. Embedded work fits for the same reason: Go compiles to a single small binary that uses little memory, which matters on constrained hardware.

## FAQ

### Is Go multithreading APIs the same as Node.js async?

No. Go goroutines are scheduled by the Go runtime across CPU cores, while Node.js runs application JavaScript on a single thread with an event loop and uses worker threads for CPU-bound work. In Node.js, adding a thread means forking the whole runtime again, which carries its own overhead.

### How much memory does a goroutine use?

A goroutine's stack starts around 2 KB in the Go runtime and grows or shrinks as needed, which is far below the roughly 2 MB default stack of a kernel thread. The exact figure is documented in the [Go runtime package reference](https://pkg.go.dev/runtime).

### Are goroutines faster than operating-system threads?

Starting them is cheaper and thousands can be alive at once, because they are multiplexed onto a smaller number of OS threads. Throughput on real work still depends on CPU cores, memory and the work being done.

### Should I use channels or mutexes?

Channels are the safer default for handing values between goroutines because they remove shared-memory access. Mutexes from [sync](https://pkg.go.dev/sync) remain valid when a structure must be protected in place, but the lock must be released on every path or the bug becomes intermittent.

### Does every HTTP request really get its own goroutine?

Yes, the standard `net/http` server serves each request on a new goroutine. That is why request handling scales without a hand-written worker pool at the HTTP layer.

### Why does my Go program exit before the goroutines finish?

Because `main` does not wait for goroutines it started. A goroutine that writes to a channel will block until someone reads, and if `main` returns first the process ends. Waiting on a [sync.WaitGroup](https://pkg.go.dev/sync#WaitGroup) before the end of `main` keeps the process alive until the workers are done.

### Why does an unbuffered channel send hang?

An unbuffered channel blocks on send until a receiver takes the value. If no goroutine is reading, the sender waits forever, and a program where nothing reads can deadlock instead of erroring.

### Is Go faster than Rust?

Not in general. Rust offers more low-level control and stronger compile-time memory guarantees, while Go trades some of that for simpler code, faster builds and a shorter learning curve.

### Do I need a framework to build a Go API?

No. `net/http` provides server, client and routing primitives in the standard library, which is why simple services often ship with no third-party dependencies.

### Can Go replace Python for machine learning?

Performance is not the blocker; ecosystem maturity is. Python's scientific and numerical libraries represent decades of work, and that collection has no equivalent in Go today.

### Is it worth learning Go in 2026?

It remains a reasonable investment for backend, infrastructure and networking work. [Go](https://go.dev) keeps a small language surface and a low entry barrier, and demand is concentrated in backend and platform roles.

## Turning a walkthrough into written notes

The channel and worker-pool examples in this article exist as a live walkthrough first, then as prose. That order is normal for developers: you learn the mechanism by watching someone type, delete and rerun it, and the explanation hardens later.

If your own explanatory work lives in recorded videos, Skalablog can take a [YouTube](https://www.youtube.com) URL, transcribe it and produce a structured article draft that mirrors how you actually explain the topic. That fits subjects like this one where the value is in ordered reasoning and working code rather than in a single answer.

For a deeper look at Go, concurrency and full-stack engineering content, see [Skala Blog](https://skalablog.com). The recording behind this piece also touched [Crazystack Typescript](https://crazystack.com.br), a Brazilian TypeScript and full-stack program run by Gustavo Dev Doido, also associated with the Bootcamp do Dev Doido, so TypeScript, [Next.js](https://nextjs.org) and Go ended up in the same lesson.

[Source video](https://www.youtube.com/watch?v=C4lRMBOc-EQ)
