How Far Can You Go with Go's Standard Library
How much mileage Go's rich standard library actually gives you, and a principled way to decide when to reach for an external dependency.
One of the first things people notice when they start learning Go is that the standard library is unusually rich. HTTP server, JSON processing, cryptography, file system, templating, testing infrastructure — all of it available without a single third-party package.
Coming from the PHP world, this feels like a culture shock. In PHP, you can barely do anything the “modern” way without installing third-party packages first. Working without Composer is theoretically possible, but practically nobody does it. In Go, the standard library is genuinely a production-grade choice in its own right.
What the Standard Library Covers
Let’s take a quick look at what you can do with Go imports that never include github.com:
HTTP service: The net/http package covers both client and server. For a simple service, you don’t need a router library at all:
package main
import (
"encoding/json"
"net/http"
)
type Response struct {
Message string `json:"message"`
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Response{Message: "ok"})
}
func main() {
http.HandleFunc("/health", healthHandler)
http.ListenAndServe(":8080", nil)
}
This code is sufficient for the health-check endpoint of a production service. No extra dependencies.
JSON processing: encoding/json handles both serialization and deserialization. For large datasets or custom transformation scenarios I sometimes look elsewhere, but for standard use cases encoding/json is more than enough.
Database access: database/sql defines the interface; drivers implement it. You pull the driver package from outside, but an ORM is never required. Running raw SQL through the standard library feels completely natural.
Templating: The html/template package has XSS (Cross-Site Scripting) protection built in. For server-side HTML generation, you simply don’t need anything else.
Testing infrastructure: The testing package ships with table-driven tests, benchmarks, and fuzz testing. Go’s testing culture is largely built on top of this one package.
Where You Hit the Limits
The standard library does not cover everything, and that is a deliberate design decision. The places where I most often hit the ceiling:
Routing. The default router in net/http does not support path parameters (like :id). Go 1.22 partially addressed this by introducing a new pattern syntax:
// Go 1.22+
http.HandleFunc("GET /users/{id}", getUserHandler)
But for complex routing scenarios — especially when you need middleware chains and route grouping — I reach for packages like chi or gorilla/mux.
ORM. Writing raw SQL with database/sql gets repetitive after a while. A lightweight library like sqlx preserves the standard interface while making struct scanning much easier. The number of situations where I can get by without a heavy ORM is far greater than I expected.
Validation. There is no validation library in the standard library. go-playground/validator has effectively become the de facto standard for this need.
Logging. The log/slog package arrived in Go 1.21, and the standard library is now sufficient for structured logging. In earlier versions, zap or zerolog were practically mandatory.
The Value of Fewer Dependencies
The Go community’s “vendor everything” philosophy and its minimal-dependency culture are two sides of the same coin. Fewer dependencies means shorter build times, a smaller security-vulnerability surface, and less maintenance overhead from keeping dependencies up to date.
Before I add a package, I ask myself: can I cover this with the standard library or a few helper functions? The answer is yes more often than not. When the answer is no, I add the package — but that is a deliberate decision, not a default.
In PHP I never think about writing without Composer; the ecosystem is built that way. In Go, the standard library is not just a starting point — it is a genuine, long-lasting option. That difference has reshaped how I think about dependencies in general.
The research behind this post
I measured PHP and Go on the same OAuth2 API: no gap at 10,000 writes, a real one at 50,000 reads
The same API verifies an OAuth2 token on every request and then writes to or reads from PostgreSQL. On four cores, how much CPU do PHP-FPM, FrankenPHP worker mode and Go need for 10,000 writes and 50,000 reads a second?
Finding
At 10,000 writes a second all three candidates hit the target in five runs out of five, and none had a p99 above 2.5 ms: at this load the language is not a capacity line item. At 50,000 reads a second only Go held the target on four cores (p99 6.45 ms); PHP-FPM stopped at 23,528 and FrankenPHP at 22,859. CPU per read request is 64 microseconds for Go, 117 for FrankenPHP and 168 for PHP-FPM. Sized by instance, 50,000 reads take 3.4 cores in Go and 8.2–8.8 cores for the two PHP candidates. FrankenPHP's CPU saving does not turn into capacity: at saturation it leaves about one of its four cores idle.
measured today
Experiments on this topic
Two languages sharing one queue without the serialization lock-in
A frozen JSON envelope in place of language-specific serialization; it became BabelQueue, a polyglot queue standard.
What it does today
The frozen JSON envelope has four languages — PHP, Python, Go, Node.js — reading the same bytes, with no sidecar and no broker plugin. Teams running a polyglot queue can pick up the spec and the SDKs today.
Local code review without letting the diff leave the machine
A provider-agnostic Go CLI that has a model review my diff without the diff leaving the machine; it became CommitBrief.
What it does today
Reviews code locally, with the diff never leaving the machine; the `Provider` interface lets Anthropic, OpenAI, Gemini and Ollama plug into one contract. Signed binaries are published — anyone who does not want to hand their diff to a cloud provider can install it.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.