Go Package Recommendations for Production Server Development
I started using Go at my previous job and have continued using it at my current job — about five years of active use in total.
Compared to the early days, the Go ecosystem has grown increasingly rich: communities like GopherCon Korea have emerged, and reference material keeps expanding.
That said, well-organized Korean-language resources are still relatively scarce (not that Go is unique in this regard), and there’s still a fair amount of trial and error involved. To share what I’ve learned alongside colleagues over the years, I previously wrote up our trial and error and best practices in two posts: Golang and gRPC in Production and Banksalad Go Coding Conventions.
In a similar vein, this post is a roundup of the packageslibraries I’ve found most useful and frequently reached for while doing server development in Go.
Go’s standard library certainly has plenty of reliable, well-crafted packages — net/http/httptest and crypto/rand come to mind — but this post focuses on third-party packages.
Every time I write something like this, I second-guess myself: “Anyone could find this by just searching ‘golang xxx package’ — is there even a point?” Still, I hope this saves someone the decision-making overhead and the guesswork, and that the responses from readers help make it even better over time.
Update: In the 2025 Go Developer Survey, 26% of respondents said “finding reliable Go modules and packages” was one of the biggest challenges they face as Go developers — so hopefully this post pulls its weight.
stretchr/testify
func TestSomething(t *testing.T) {
assert.Equal(t, expected, got, "they should be equal")
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.JSONEq(t, `{"name": "hello"}`, msg)
}
A toolkit with common assertions and mocks that plays nicely with the standard library
- Makes Go’s already powerful testing capabilities richer and more enjoyable to write.
- Instead of
if got != expected { t.Errorf("...") }, you can writeassert.Equal(t, expected, got), which is a bit easier to read.
- Instead of
- The packages you’ll reach for most are assert and require.
requireis nearly identical toassert, except that a failure immediately stops the test.- If you’re coming to Go from another language,
require’s behavior will probably feel more familiar. - Using
assertlets subsequent assertions in the same test still run, so you can see multiple failures at once — which can be a real help when debugging. - In a given/when/then structure,
requireis great for the given phase — client initialization and any setup steps that simply must succeed.
- testify comes with a wide variety of assertion functions, so it’s worth exploring and making the most of them.
- The suite package is designed for people coming from object-oriented languages, so if you’re just getting started, the
assertpackage alone is more than enough.- Keep in mind that as of v1, parallel tests are not supported.
- Pairs well with the Antonboom/testifylint linter.
rs/zerolog & sirupsen/logrus
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog/pkgerrors"
)
func main() {
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
// ...
log.Error().Stack().Err(err).Msg("failed to insert row to db")
}
rs/zerolog: Zero Allocation JSON Logger
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
}).Info("A walrus appears")
}
sirupsen/logrus: Structured, pluggable logging for Go.
- Both logging packages support contextual and structured logging.
- For server-side logging, one-line JSON logging is always recommended.
- zerolog is said to have better performance, but both are solid packages for production use.
- logrus is in maintenance mode, but keep in mind that it’s already feature-rich and continues to receive security patches.
- Personally, I found logrus more ergonomic than zerolog thanks to its
WithFieldsmethod.
- Both support hooks, which makes them great for recording metrics alongside logs or injecting stack traces.
log/slog, included in the standard library since Go 1.21, is also a worthwhile alternative to consider.
pkg/errors
resp, err := userSvc.GetUser(ctx, &GetUserRequest{...})
if err != nil {
return errors.Wrap(err, "user.GetUser")
}
Simple error handling primitives
- Adds context and stack traces to errors.
- Born out of the limitations of the
errorsstandard library before Go 1.13, it remains a de-facto standard even afterUnwrap,Is, andAswere introduced.
hashicorp/go-multierror
var errs *multierror.Error
if err := step1(); err != nil {
errs = multierror.Append(errs, err)
}
if err := step2(); err != nil {
errs = multierror.Append(errs, err)
}
return errs.ErrorOrNil()
// Output:
// 2 errors occurred:
// * error 1
// * error 2
A Go (golang) package for representing a list of errors as a single error.
- I often reach for this when validating configs or requests, since it lets you surface richer error information all at once.
multierror.Group(included in the package) also comes in handy when you need to fan out goroutines and collect each of their errors.- A similar option is the semi-standard library package golang.org/x/sync/errgroup.
- Keep in mind that with this package, if one goroutine errors, the error propagates to the others and cancels their work — so choose based on your situation.
- multierror, by contrast, runs all goroutines to completion and then merges the errors.
samber/lo
names := lo.Uniq([]string{"Samuel", "John", "Samuel"})
// []string{"Samuel", "John"}
A Lodash-style Go library based on Go 1.18+ Generics (map, filter, contains, find…)
- Leverages generics (Go 1.18+) to help you write less boilerplate without any performance penalty.
- Personally used
Map,SliceToMap, andKeysthe most. - The same maintainer has two other packages: samber/do for dependency injection and samber/mo for monads — but I didn’t end up using either, as they don’t align with my preferred Go code conventions.
shopspring/decimal
func main() {
price, err := decimal.NewFromString("136.02")
quantity := decimal.NewFromInt(3)
subtotal := price.Mul(quantity)
fmt.Println("Subtotal:", subtotal) // Subtotal: 408.06
}
Arbitrary-precision fixed-point decimal numbers in Go
- Another decimal package, ericlagergren/decimal, also exists, but
shopspring/decimalwas significantly more ergonomic to use. - Maps well with the decimal types in ORM libraries.
- If performance is a concern, I’d recommend running benchmarks against the other packages mentioned in the readme.
dgraph-io/ristretto
func main() {
cache, _ := ristretto.NewCache(&ristretto.Config[string,string]{
NumCounters: 1e7, // number of keys to track frequency of (10M).
MaxCost: 1 << 30, // maximum cost of cache (1GB).
BufferItems: 64, // number of keys per Get buffer.
})
cache.Set("key", "value", 1)
value, found := cache.Get("key")
fmt.Println(value)
}
A high performance memory-bound Go cache
- A solid choice when you need a high-performance in-memory local cache in production.
- For background on why this package was built and what it prioritizes, check out the Introducing Ristretto blog post.
- Optimal configuration varies by situation, so it’s recommended to determine your settings through metrics and benchmarking.
- Generics are supported.
- The patrickmn/go-cache package does not support generics and is no longer maintained, so it’s not recommended.
- If you need further performance tuning under high traffic, creativecreature/sturdyc and maypok86/otter are worth looking into.
- For a deeper dive into what matters most in a local cache for performance, Writing a very fast cache service with millions of entries in Go is worth a read.
coocood/freecache
cacheSize := 100 * 1024 * 1024 // 100MB
cache := freecache.NewCache(cacheSize)
cache.Set([]byte("key"), []byte("value"), 60) // 60 seconds TTL
value, err := cache.Get([]byte("key"))
A cache library for Go with zero GC overhead
- An in-memory local cache that minimizes GC overhead. It uses only 512 pointers regardless of the number of entries.
- It allocates a separate memory block (ring buffer), so the GC has fewer objects to read during mark-and-sweep.
- However, both keys and values must be serialized as
[]byte. If you’re already using protobuf, the migration cost is low.- It’s most useful when heap GC overhead outweighs serialization overhead.
- If ristretto is a general-purpose local cache, freecache is better suited for large-scale caches where GC tuning matters.
volatiletech/sqlboiler
import (
"github.com/volatiletech/sqlboiler/v4"
)
func main() {
users, err := model.Users().All(ctx, db)
token, err := model.Tokens(model.TokenWhere.AccessToken.EQ(accessToken)).One(ctx, db)
}
token.Update(ctx, db, boil.Whitelist(
model.TokenColumns.AccessToken,
model.TokenColumns.AccessTokenExpiredAt,
))
Generate a Go ORM tailored to your database schema.
- Rather than managing DB models in Go code, it generates Go code that follows your already-existing DB schema.
- The experience of building queries was great too — instead of writing raw strings, you can use the already-generated types directly.
- There are other packages out there, but from personal experience, sqlc-dev/sqlc felt cumbersome to use, and go-gorm/gorm is worth trying if it suits your taste.
- That said, sqlboiler has entered maintenance mode. Only minimal upkeep like security patches is being done, so for new projects it’s worth exploring alternatives.
- sqlc-dev/sqlc generates type-safe code from SQL queries, and stephenafamo/bob is a type-safe query builder that’s closer to a spiritual successor to sqlboiler.
- Reference: Golang ORM, Which One Is Good?
DATA-DOG/go-sqlmock
func TestShouldUpdateStats(t *testing.T) {
db, mockDB, err := sqlmock.New()
require.NoError(t, err)
t.Cleanup(db.Close)
mockDB.ExpectQuery(regexp.QuoteMeta(
"SELECT * FROM `token` WHERE (`user_id` = ?);"
)).WithArgs("some-valid-user-id").WillReturnRows(...)
}
Sql mock driver for golang to test database interactions
- Useful for unit testing whether the intended SQL statements are executed when using mysql, postgresql, etc.
- The package path feels a bit odd when you use it, but it doesn’t cause any real problems.
- Not recommended if you’re the type who thinks, “Why do I have to write and verify SQL statements by hand?!“
golangci/golangci-lint & mvdan/gofumpt
# .golangci.yml
linters:
enable:
- govet
- errcheck
- staticcheck
formatters:
enable:
- gofumpt
settings:
gofumpt:
extra-rules: true
golangci-lint: Fast linters runner for Go / gofumpt: A stricter gofmt
- golangci-lint is the de facto linter runner for Go. It integrates 100+ linters and runs them in parallel, making it fast enough to use in CI.
- gofumpt is a stricter formatter than gofmt, useful for enforcing a consistent code style across your team.
- By including gofumpt in your golangci-lint config, you can run linting and formatting in a single pass.
failsafe-go/failsafe-go
retryPolicy := retrypolicy.NewBuilder[*http.Response]().
HandleIf(func(r *http.Response, err error) bool {
return r != nil && r.StatusCode == http.StatusTooManyRequests
}).
WithBackoff(time.Second, 30*time.Second).
WithMaxRetries(3).
Build()
resp, err := failsafe.With(retryPolicy).Get(func() (*http.Response, error) {
return http.Get("https://example.com")
})
Fault tolerance and resilience patterns for Go
- A fault tolerance package that provides useful patterns similar to Java’s resilience4j.
- You can combine various components and policies including retry, circuit breaker, rate limiter, bulkhead, and adaptive throttler.
- If you only need lightweight retry, go-retryablehttp below is sufficient; if you need comprehensive resilience patterns, failsafe-go is the way to go.
hashicorp/go-retryablehttp
client := retryablehttp.NewClient()
client.RetryMax = 3
resp, err := client.Get("https://example.com/api")
Retryable HTTP client in Go
- A thin retry layer on top of
net/http. - It lets you add retry logic while keeping almost the same
http.ClientAPI, so the adoption cost is low — worth using.
twmb/franz-go
// producer
client, _ := kgo.NewClient(kgo.SeedBrokers("localhost:9092"))
defer client.Close()
ctx := context.Background()
client.Produce(ctx, &kgo.Record{Topic: "my-topic", Value: []byte("hello")}, nil)
// consumer
client, _ := kgo.NewClient(
kgo.SeedBrokers("localhost:9092"),
kgo.ConsumeTopics("my-topic"),
kgo.ConsumerGroup("my-group"),
)
for {
fetches := client.PollFetches(ctx)
fetches.EachRecord(func(r *kgo.Record) {
fmt.Println(string(r.Value))
})
}
franz-go is a feature complete, pure Go library for Kafka from 0.8.0 through 4.1+. Producing, consuming, transacting, administrating, etc.
- A pure Go Kafka package. I don’t recommend confluent-kafka-go — it requires a cgo environment and a librdkafka dependency, which makes the build environment a hassle.
- The package API is ergonomic and the plugins it provides are genuinely useful.
- sarama is the long-standing de facto choice with a wide ecosystem, but for new projects I’d recommend franz-go.
go-resty/resty & dghubble/sling
// resty
client := resty.New()
resp, err := client.R().
SetHeader("Accept", "application/json").
SetResult(&ApiResponse{}).
Get("https://api.example.com/users/1")
// sling
type Issue struct {
Title string `json:"title"`
Body string `json:"body"`
}
issue := new(Issue)
resp, err := sling.New().Base("https://api.github.com/").
Path("repos/user/repo/").
Get("issues/1").
ReceiveSuccess(issue)
go-resty: Simple HTTP, REST, and SSE client library for Go / dghubble/sling: A Go HTTP client library for creating and sending API requests
net/httpalone is sufficient in most cases, but these are useful when writing a structured API client.- resty: Offers a rich feature set including a chaining API, automatic JSON marshaling, and built-in retry.
- sling: A lightweight, composable API builder — more minimal in approach.
go-chi/chi
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
})
r.Route("/users", func(r chi.Router) {
r.Get("/", listUsers)
r.Post("/", createUser)
r.Get("/{userID}", getUser)
})
http.ListenAndServe(":3000", r)
lightweight, idiomatic and composable router for building Go HTTP services
- While echo is widely known, chi is more go-ish. It’s fully compatible with
net/httphandlers and is lightweight. - If you have a small number of routes, the
net/httprouting improvements in Go 1.22+ may be enough. See: Routing Enhancements for Go 1.22 - Don’t use gin.
Miscellaneous
- If you need to send something via Slack, use slack-go/slack.
- If you need the GitHub API, use google/go-github.
- It releases updates frequently, so keep it up to date regularly.
- If you need Sentry, refer to the official docs and use getsentry/sentry-go.
- Note that out of the box, error grouping doesn’t work well and requires some tuning — I plan to cover that in a separate post.
- If you need to write to a map concurrently from goroutines, use syncmap.
- Keep in mind that concurrent writes to a map will cause a panic (which is at least easy to catch), but the same is not true for slices — those fail silently, so be careful.
- If you need to work with semantic versioning, use Masterminds/semver.
- If you need UUIDs, use google/uuid.
- It supports UUID v7 as well.
- If you need shorter IDs, consider something like matoous/go-nanoid.
- oklog/ulid: a shorter ID that sorts by timestamp — we use it in production. UUID v7 also supports timestamp-based sorting, but it’s a minor annoyance that you can’t copy it with a double-click in one shot.
- If you’re deploying to Kubernetes, use uber-go/automaxprocs to properly configure your CPU settings.
- Without it, you might end up with 32
GOMAXPROCSwhen the pod is only allocated 1 CPU — that kind of thing.
- Without it, you might end up with 32
- If you’re recording StatsD metrics, use smira/go-statsd.
- I recommend declaring a separate interface on the consumer side and using it alongside a noop client implementation. It makes testing much easier.
- If you’re using Redis, use redis/rueidis.
- The builder pattern may be polarizing, but the performance is solid.
- rueidis shares the same maintainer and codebase as valkey-go, so it supports both Valkey and Redis.
- redis/go-redis is perfectly usable as a baseline, but there are known issues connecting to ElastiCache cluster mode at large scale, so I’d recommend rueidis for high-traffic environments.
- If you want to write unit tests, pair it with alicebob/miniredis.
- For integration testing, testcontainers/testcontainers-go is worth considering.
- There are many approaches to integration testing depending on your team’s setup, but Testcontainers is a solid option to have in the mix.
- If you need internationalization support, check out biter777/countries and nyaruka/phonenumbers.
- countries handles country codes, currencies, languages, and more; phonenumbers is a Go port of Google’s libphonenumber. You won’t need these often, but they’re invaluable when internationalization is a requirement.
- If you’re building container images for a CGO-free Go app, ko-build/ko is worth experimenting with.
- It builds and pushes your Go app as a distroless image — no Dockerfile needed. Consider it when you need a lightweight alternative to GoReleaser.
That wraps up my collection of frequently used and genuinely useful packages. The list skews toward server-side development, and I’ve left out highly niche packages (e.g., one for computing Levenshtein distance). If there are other packages worth recommending, I’ll keep adding to this list over time.