1. A map of the Go ecosystem
When people say “the Go ecosystem”, it is easy to picture a spice market: routers here, loggers there, and over there an ORM for every taste and color. But it is more useful to keep a more boring (yet practical) picture in mind: we have the standard library, we have “semi-official” packages (for example, golang.org/x/...), and we have third-party libraries from the community. And that is normal: Go does not try to ship everything that could possibly exist in the standard distribution, but it still provides a very powerful core set.
An important idea for a beginner developer is this: you do not have to know every library. It is enough to understand what kinds of tasks exist at all (CLI, HTTP, config, logs, DB, tests…), what the standard library covers, and how to assess the cost of an external dependency. It is like tools in a kitchen: a basic knife and a cutting board solve 80% of tasks, and an “ultrasonic tomato knife” is worth buying only if you are actually slicing tomatoes at scale.
Standard library: your first backpack
Go’s standard library is not a “minimal set to make it compile”, but a very practical foundation you can actually build applications on. And this foundation evolves: sometimes new packages appear that cover things you previously had to implement manually or through third-party solutions. For example, in Go 1.21 they added log/slog for structured logging and the packages slices, maps, cmp, which simplify typical operations.
It helps to think of the standard library as “a set of time-tested bricks”. They are not always the trendiest, not always the most convenient in a single call, but they are usually stable, predictable, documented, and maintained by the Go team. And they almost always give you the right abstractions, which are then easy to test and maintain: io.Reader/io.Writer, context.Context, net/http, encoding/json, database/sql (as an interface to SQL drivers), and so on.
To avoid turning the lecture into a reference manual, let’s capture this as a table of “what an application typically needs” and “what stdlib answers with”.
| Application task | Standard library (first candidate) | Comment: “how to think about it” |
|---|---|---|
| CLI arguments and flags | |
Simple; enough for most utilities |
| HTTP server | |
The base many frameworks build on |
| URL and query params | |
Safer than “string concatenation” |
| JSON | |
Not the fastest, but the de facto standard |
| Logs | |
slog—solid structure and levels |
| Tests | |
A strong base, especially with a table-driven style |
| Working with time | |
Timeouts, deadlines, formatting |
| Files/FS | |
They cover most practical needs |
A mini example in the spirit of our tasker: a structured logger from the standard library is no longer “one line for everything”, but proper fields.
package main
import (
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
logger.Info("task created", "id", 42, "title", "buy milk")
// task created id=42 title="buy milk"
}
Notice a nice thing: we did not “install” anything, did not argue about the “best logger of 2026”, and still got a useful format. That is often how Go works: start with stdlib, and very often you can stop there.
2. When stdlib is the best choice
Beginners sometimes treat choosing the standard library as “I just do not know the good libraries”. In practice, for experienced Go developers it is often the opposite: stdlib is a deliberate choice, because it reduces the number of moving parts. Fewer moving parts means fewer places where something can break on Friday evening (and Friday, as we all know, was specifically created for production outages).
There is a simple mental test: if you can solve the task with the standard library in a reasonable amount of time and with clear code, then stdlib almost always wins. You save time on reading docs, on updates, on transitive dependencies, and on surprises like “the new version changed the default behavior”. And most importantly: you reduce the project’s “surface area”—less third-party code you do not control.
For our tasker this is very clear at the HTTP layer. A basic server on net/http looks simple and transparent:
package main
import (
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
_ = http.ListenAndServe(":8080", mux)
}
And yes: this already looks like a “real application”. You can add middleware (in Go that is often just wrappers around handlers), plug in logging, produce JSON responses—and it will remain readable.
A separate bonus of stdlib is its evolution. Things you used to do manually or via third-party packages gradually show up “officially” when there is broad demand. For example, the family of slice functions in slices evolved to reduce common mistakes, including working with “tails” and releasing references via clear. This is a good example of how Go tries to make life easier without turning the language into a “zoo of magic”.
4. Why third-party libraries exist
If the standard library is so good, why do we need external packages at all? Because the world is always more complex than the baseline set. Sometimes you need more convenience (developer experience), sometimes higher performance on a hot path, sometimes ready-made integrations (for example, a specific database, a specific metrics system), and sometimes simply a better API for your team.
The most important framing: a third-party library is not only functionality, but also obligations. You bring someone else’s code into your project, which means you agree to live with its updates, potential vulnerabilities, API changes, and even the possibility that the author may one day get tired and stop maintaining it. So the right logic sounds like: “we take this dependency because it provides measurable benefit, and we understand its cost”.
There is a second aspect: Go projects often build the architecture so that third-party libraries live at the boundaries (in adapters), while the core (domain and use cases) stays as simple as possible. Then you can replace a library without rewriting the entire application. We already did this conceptually when we separated CLI/HTTP from business logic and when we “mapped” domain errors to exit codes or HTTP statuses.
5. Landmarks among popular libraries
A key caveat: library popularity changes, and the goal of this lecture is not to hand you “the one true list of packages”, but to give you a map of the terrain. That is: what categories exist and what names you will regularly encounter in real repositories. Even if you do not choose these libraries, you at least will not look at them as if they were aliens.
Below is a practical table: “task”, “stdlib alternative”, “common third-party options” (as landmarks).
| Category | What stdlib has | What people often use externally (landmarks) | When it can be justified |
|---|---|---|---|
| CLI commands/subcommands | |
|
When there are many subcommands, help autogeneration, complex UX |
| HTTP routing | |
chi, gin, echo, (sometimes older gorilla/mux) | When you want convenient routing, middleware groups, “nice” parameters |
| Config | |
|
When there are many settings and you want a unified “flags/env/file” layer |
| Logging | |
|
When speed/format/integrations matter, though stdlib is often enough |
| Struct validation | by hand (ifs) | |
When there are many DTOs and validation rules, especially at boundaries (HTTP/JSON) |
| Working with SQL | |
pgx (as a driver/client), sqlx, ORMs like gorm | When you want more convenient scanning/mapping, but do not want to lose control |
| DB migrations | (none) | |
When the project has a DB and you need schema discipline |
| Test asserts and mocks | |
|
When you want shorter tests, but do not want them to become “magical” |
| Metrics/tracing | (no “complete set”) | |
When a service runs in production and you need an observable contract |
| UUID | (no “standard everywhere” generator) | |
When the ID format must be standard |
How do you use this in practice? Very simply: when you see chi in someone else’s project, you understand “this is a router on top of net/http”; when you see cobra, you understand “this is a CLI combine harvester”; when you see pgx, you understand “this is about PostgreSQL”. And you can read the code without panic (and panic, I remind you, is not for everything in this course).
6. Dependency cost: graph, updates, and maintenance
When you add a dependency, you add not only a line in go.mod, but a whole tail. Because dependencies can be direct (what you import) and transitive (what your dependencies import). As a result, a small “handy little package” can pull in dozens of other modules, and along with them—updates, CVEs, incompatibilities, and surprises.
Go Modules are designed specifically so you can manage dependencies in a disciplined way. Commands like go get and go mod tidy modify go.mod and go.sum, and that is expected behavior: the tool keeps the project in a consistent state. Also remember installing tools at a fixed version via go install module@version, so “it worked yesterday, it works today”, not “magic on my laptop”.
It is useful to sometimes draw a simple dependency picture. For example, our tasker (simplified) looks like this:
flowchart TD
cmd[cmd/tasker/main.go] --> cli[adapters/cli]
cmd --> http[adapters/http]
cli --> app[app/usecases]
http --> app
app --> domain[domain/model]
app --> storage[adapters/storage]
cli -.-> std[stdlib: flag, fmt, os]
http -.-> std2[stdlib: net/http, encoding/json]
If you add, say, an external router, it should live inside adapters/http, and not leak into domain and app. Then the dependency cost is controllable: you can swap it without ripping out half the house along with the foundation.
7. How to choose a library: a thinking checklist
The problem with choosing libraries for beginners is usually not that they “choose badly”, but that they choose based on the wrong signals. The most common wrong signal is “the tutorial did it this way”. The second most common is “this library has lots of stars”. Stars can be useful, but they do not guarantee the library is right for you and your context.
Instead, use a more engineering-minded approach. First, formulate what exactly you need: routing with params? middleware chaining? CLI help generation? structured JSON logs? Then check whether stdlib solves it well enough. If yes—you save months of your future life. If not—choose a third-party library, but in a way that it does not spread throughout the project.
Next, evaluate dependency “viability”. Is the project maintained, are there releases, is the documentation clear, are there tests, how heavy is the dependency graph, how aggressive are breaking changes. And—very important—how well the library aligns with the Go approach: “simple API, explicit errors, minimal magic”. Sometimes a “too smart” framework makes code shorter, but makes team thinking more expensive. And Go is a language people often choose specifically to keep thinking cheap.
8. Plug in external dependencies carefully: via adapters
In this section we will tie this back to our tasker and show the main practical technique: we do not “use the library everywhere”, we build an adapter. Then the third-party library becomes a replaceable part, not part of the application’s DNA.
Imagine we have a domain layer that does not know anything about HTTP and CLI:
package domain
type Task struct {
ID int
Title string
Done bool
}
And in the app layer we depend on interfaces, not on specific libraries:
package app
import "context"
type Task struct {
ID int
Title string
Done bool
}
type Storage interface {
Create(ctx context.Context, title string) (Task, error)
}
Now the HTTP adapter can be built on pure net/http, or it can use an external router—but app will not know about it.
You can do the same with logging: inside the application you can depend on a very small interface, and outside you can plug in log/slog or any other logger.
package app
type Logger interface {
Info(msg string, args ...any)
Error(msg string, args ...any)
}
And an adapter for slog can look as boring as possible (and boring is a compliment):
package adapters
import "log/slog"
type SlogLogger struct {
L *slog.Logger
}
func (l SlogLogger) Info(msg string, args ...any) { l.L.Info(msg, args...) }
func (l SlogLogger) Error(msg string, args ...any) { l.L.Error(msg, args...) }
Why is this great? Because if in a year you decide “we need a different logger”, you rewrite one adapter file, not the entire project. And that is exactly the “packaging of results” we talked about at the end of the course: quality is not only “it works”, but also “it changes without pain”.
9. Typical mistakes when choosing stdlib vs third-party libraries
Mistake #1: taking a third-party library “just in case”.
It looks harmless: “let’s include cobra, maybe it will come in handy”. The problem is that the dependency starts living its own life: updates, incompatibilities, transitive modules. If you are not actually using the library’s capabilities, you pay for it simply by having it.
Mistake #2: dragging the library into the domain and use cases.
The most expensive scenario is when domain starts importing something external (router, logger, ORM models with tags, HTTP types). Then the library stops being an implementation detail and becomes an architectural decision “forever”. It is much cheaper to keep everything external at the boundary: CLI/HTTP/storage/adapters.
Mistake #3: confusing “easier to write” with “cheaper to maintain”.
Some libraries really do let you write less code today. But maintenance is about reading, testing, updating, and debugging a year later. In Go, it is beneficial to write a bit more verbosely, but predictably. It is like texting: you can shorten words down to emojis, but later you will not even understand what you meant.
Mistake #4: choosing based on hype instead of requirements.
“Everyone uses X” is a weak argument until you understand what problem X solves. The correct argument sounds like: “we need A, B, C; stdlib covers A and B, but not C; library X covers C and does not break our architecture”.
Mistake #5: forgetting module discipline and tool version pinning.
If everyone on the team has “their own tool versions”, you get oddities like “tests pass for me, not for you”. In Go it is customary to pin dependencies and keep things tidy via go mod tidy, and to install tools with versions (via go install ...@version). This reduces randomness and makes the project reproducible.
GO TO FULL VERSION