JavaRush /Courses /Go SELF /Go ecosystem: popular third-party libraries

Go ecosystem: popular third-party libraries

Go SELF
Level 72, Lesson 3
Available

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
flag
Simple; enough for most utilities
HTTP server
net/http
The base many frameworks build on
URL and query params
net/url
Safer than “string concatenation”
JSON
encoding/json
Not the fastest, but the de facto standard
Logs
log, log/slog
slog—solid structure and levels
Tests
testing, httptest
A strong base, especially with a table-driven style
Working with time
time
Timeouts, deadlines, formatting
Files/FS
os, io, bufio, io/fs, filepath
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
flag, flag.FlagSet
cobra, urfave/cli, kong
When there are many subcommands, help autogeneration, complex UX
HTTP routing
http.ServeMux
chi, gin, echo, (sometimes older gorilla/mux) When you want convenient routing, middleware groups, “nice” parameters
Config
os.LookupEnv, flag
viper, envconfig-like packages
When there are many settings and you want a unified “flags/env/file” layer
Logging
log, log/slog
zap, zerolog, logrus
When speed/format/integrations matter, though stdlib is often enough
Struct validation by hand (ifs)
go-playground/validator
When there are many DTOs and validation rules, especially at boundaries (HTTP/JSON)
Working with SQL
database/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)
golang-migrate, goose
When the project has a DB and you need schema discipline
Test asserts and mocks
testing
testify, gomock
When you want shorter tests, but do not want them to become “magical”
Metrics/tracing (no “complete set”)
Prometheus client, OpenTelemetry
When a service runs in production and you need an observable contract
UUID (no “standard everywhere” generator)
google/uuid, gofrs/uuid
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.

1
Task
Go SELF, level 72, lesson 3
Locked
Package cheat sheet
Package cheat sheet
1
Task
Go SELF, level 72, lesson 3
Locked
Import analysis
Import analysis
1
Task
Go SELF, level 72, lesson 3
Locked
Dependency map
Dependency map
1
Task
Go SELF, level 72, lesson 3
Locked
Logger adapter
Logger adapter
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION