Every Go service that writes JSON to Kafka, ships logs to a collector, or caches a 4 MB response eventually hits the same wall: compress/gzip burns an entire core on the hot path. The standard library is safe, stable and slow — roughly 3× slower than the best third-party option at the same compression ratio, according to the benchmarks published in klauspost/compress. Swapping it out is usually a one-line change, but the three popular replacements have very different costs hiding behind that line.

This comparison looks at what each library actually is, how its API behaves under load, and which one to import for which workload. Star counts and last-push dates were pulled from GitHub today.

TL;DR

  • Use klauspost/compress (zstd) for almost everything: pure Go, no cgo, four documented encoder tiers, parallel streams, and a drop-in gzip replacement if you only want a speed-up without changing formats.
  • Use pierrec/lz4 v4 when you need the fastest possible round-trip and can accept a lower ratio — block storage, message framing, in-process caches.
  • Skip DataDog/zstd for new code. It is a cgo wrapper whose bundled C sources date back to zstd 0.5.0, which means cross-compilation pain and a codec generation behind upstream.

Side-by-Side

Dimensionklauspost/compresspierrec/lz4 (v4)DataDog/zstd
GitHub stars5,707971806
Last push2026-10-292026-10-272026-07-30
LicenseBSD-3 / Go-style (listed “Other”)BSD-3-Clause“Other” (BSD-3 + bundled C zstd)
ImplementationPure Go (asm fast paths)Pure Gocgo wrapper over C zstd
Cross-compiles without a C toolchainYesYesNo (CGO_ENABLED=1 required)
Algorithmszstd, s2, gzip/deflate, zlib, snappy, lzw, huff0, FSELZ4 frame + blockzstd only
Stream APIio.Writer/io.Reader + EncodeAllio.Writer/io.Readerio.Writer/io.Reader + Compress
Parallel encodingYes (concurrent streams / goroutine-based)Block-level, manualNo
Encoder levelsFastest / Default / Better / BestFast / Level1–Level9int levels mapped to C enum
Maintenance signalFrequent releases (v1.20.1, Sep 2026)Active, stable API since v4Slower; C payload old

Decision Matrix

WorkloadRecommendationReason
HTTP response compressionklauspost/compress/gzip or gzhttpDrop-in stdlib replacement, 2–3× faster, no format change
Kafka / NATS message bodiesklauspost/compress/zstdBest ratio-per-CPU; EncodeAll avoids per-message allocation
Log shipping pipelineklauspost/compress/zstd (Default)Streaming writers, stable framing, good ratio on repetitive text
Append-only block storepierrec/lz4 v4Fastest decode; ratio is secondary when blocks are small
CGO-free static binaries for Alpine/scratchklauspost/compress or pierrec/lz4Both pure Go; DataDog’s cgo breaks CGO_ENABLED=0 builds
Maximizing ratio on cold archivesklauspost/compress/zstd at SpeedBestCompressionDocumented as roughly equivalent to zstd level 11
Existing code that already imports DataDog/zstdMigrate graduallyAPI overlap is small; wrap behind an interface first

klauspost/compress — The Default Answer

This is the library most Go teams end up on, and the popularity is earned: 5,707 stars, a release in September 2026 (v1.20.1), and a scope far beyond zstd. The module ships separately importable packages for zstd, s2, gzip, zip, zlib, snappy, lzw, plus gzhttp for HTTP middleware and the huff0/FSE entropy coders.

Install and use the zstd writer:

1
go get github.com/klauspost/compress@latest
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import (
    "io"
    "github.com/klauspost/compress/zstd"
)

func Compress(in io.Reader, out io.Writer) error {
    enc, err := zstd.NewWriter(out)
    if err != nil {
        return err
    }
    _, err = io.Copy(enc, in)
    if err != nil {
        enc.Close()
        return err
    }
    return enc.Close()
}

The project documents four encoder tiers and their relationship to reference zstd levels: Fastest ≈ level 1, Default ≈ level 3, Better ≈ level 7, Best ≈ level 11. It also states that at its fastest setting it is typically about 2× as fast as stdlib deflate/gzip, and at roughly the same ratio as gzip level 3 it is about 3× faster.

For per-message work — Kafka payloads, cache entries, RPC bodies — use EncodeAll instead of the stream writer. It lets you reuse one encoder across goroutines and skip the frame overhead of a fresh writer per message:

1
2
3
4
5
6
7
8
var enc, _ = zstd.NewWriter(nil,
    zstd.WithEncoderLevel(zstd.SpeedDefault),
    zstd.WithEncoderConcurrency(1),
)

func encode(payload []byte) []byte {
    return enc.EncodeAll(payload, make([]byte, 0, len(payload)/2))
}

Two build tags matter in constrained environments: nounsafe disables all unsafe usage and noasm disables assembly. Both cost throughput, but they let you ship to platforms where the fast paths are unavailable. Note the documented caveat that the package is heavily optimized for 64-bit CPUs and is significantly slower on 32-bit targets.

If you only want a speed-up with zero format migration, the gzip subpackage is intentionally drop-in:

1
2
3
import "github.com/klauspost/compress/gzip"

w, _ := gzip.NewWriterLevel(respWriter, gzip.BestSpeed)

That single import swap keeps your .gz wire format identical while moving compression off the critical path. pgzip — maintained in a separate repository and linked from the same README — adds parallel block compression for large files, at the cost of a goroutine per block.

pierrec/lz4 — When Decode Speed Is the Product

LZ4 trades ratio for raw speed. pierrec/lz4 (971 stars, pushed 2026-10-27) is a pure-Go implementation of the LZ4 frame and block formats, based on the reference C implementation. If your access pattern is “write once, read constantly” — block devices, snapshots, in-memory caches — decompression latency dominates and LZ4 usually wins by a wide margin.

1
go get github.com/pierrec/lz4/v4
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import (
    "io"
    "github.com/pierrec/lz4/v4"
)

// Streaming compression with the fastest level
w := lz4.NewWriter(dst)
if err := w.Apply(
    lz4.CompressionLevelOption(lz4.Fast),
    lz4.BlockSizeOption(lz4.Block4Mb),
    lz4.ChecksumOption(true),
); err != nil {
    return err
}
if _, err := io.Copy(w, src); err != nil {
    return err
}
return w.Close()

The writer exposes block size (64 KB, 256 KB, 1 MB, 4 MB) and checksum toggles, which is where most of the tuning happens. Bigger blocks compress better but cost memory and latency: a 4 MB block size with N concurrent writers on a 16-core box can quietly reserve gigabytes. For per-message use, the block API (lz4.CompressBlock / lz4.UncompressBlock) is the right primitive — it avoids frame headers entirely, which matters when messages are under 1 KB.

The repository also ships cmd/lz4c, a CLI with the same options:

1
2
go install github.com/pierrec/lz4/v4/cmd/lz4c@latest
lz4c compress -l 0 -size 4M archive.tar

Where LZ4 loses is ratio on text: JSON logs typically land 2–2.5× smaller with zstd at default settings versus LZ4 at comparable speed. If bandwidth, not CPU, is your bill, that gap is the whole argument.

DataDog/zstd — Legacy cgo Wrapper

DataDog/zstd (806 stars, pushed 2026-07-30) exists because, before pure-Go zstd was viable, Go services needed C zstd for Kafka compression. It wraps C code and exposes two APIs: simple Compress/Decompress over byte slices, and a streaming NewWriter/NewReader pair designed as a zlib drop-in.

1
2
3
4
5
6
7
8
9
import "github.com/DataDog/zstd"

func roundTrip(src []byte) ([]byte, error) {
    compressed, err := zstd.CompressLevel(nil, src, 3)
    if err != nil {
        return nil, err
    }
    return zstd.Decompress(nil, compressed)
}

The API is pleasant, and the package has been used in production at scale. The problem is what it carries:

  • The README states the bundled headers and C files are from zstd v0.5.0. That is a 2016-era codec — several format revisions behind current upstream zstd, with none of the later speed work.
  • It requires cgo. CGO_ENABLED=0 builds fail, static binaries for scratch or distroless need extra work, and cross-compiling for linux/arm64 from an x86 laptop means installing a cross toolchain.
  • No parallel encoding, so per-core throughput on multi-gigabyte payloads lags the pure-Go option.

For compatibility with an existing pipeline it still works. For new code in 2026, there is no reason to accept the cgo tax: klauspost’s zstd is pure Go and documents ratios equivalent to reference levels 1/3/7/11.

Migration and Coexistence Strategies

You rarely get to rewrite a compression layer in one commit. A safer path:

1. Wrap the codec behind an interface first. Define Compress([]byte) ([]byte, error) and Decompress([]byte) ([]byte, error) in your own package, then make the current implementation satisfy it. Every call site keeps compiling while you swap backends.

2. Version your payloads. Compression algorithms are not self-describing on the wire unless you make them so. Prepend one byte — 0x01 for gzip, 0x02 for zstd, 0x03 for lz4 — and dispatch on read. This lets old consumers keep working while new producers move on, and it makes rollback a one-line flip instead of a data migration.

3. Measure at your own payload sizes. Published benchmark tables use specific corpora. A 300-byte JSON message and a 40 MB log file have almost nothing in common: for small messages, frame and header overhead can dominate and LZ4 block mode beats zstd outright; for large files the ratio gap reverses the conclusion. Benchmark with your real distribution, not someone else’s Silesia corpus run.

4. Reuse encoders, never allocate per call. All three libraries offer Reset/reuse patterns. A zstd.NewWriter per request is a reliable way to turn a 5% CPU win into a 30% regression through allocation pressure alone.

If you are comparing across languages, our Rust compression crates comparison covers the same trade-off from the Rust side, the C++ compression library roundup benchmarks the reference implementations these Go packages imitate, and the Linux compression tools guide explains where each format belongs in an operations pipeline.

Common Pitfalls

Encoder concurrency defaults are not free. zstd.NewWriter allocates per-concurrency buffers. On a service with 200 goroutines each holding its own encoder, memory balloons. Create one encoder per worker pool, not per request.

Close() is mandatory and its error matters. A truncated frame that “succeeded” because nobody checked Close() is a corrupted payload discovered days later. Always return the close error.

Checksums cost more than you think on small payloads. Frame checksums buy integrity but add per-message CPU. For trusted internal transport, disabling them at the LZ4 frame level is often the difference between hitting and missing a latency target.

Dictionary support is asymmetric. klauspost’s zstd supports dictionaries, which is a large win for many small, similar messages (protobuf events, log lines). LZ4 dictionaries exist but are less ergonomic in Go. If your workload is thousands of 500-byte messages per second, test dictionaries before assuming a plain level bump will help.

Do not mix algorithms inside one stream. Wrapping a zstd writer around an lz4 reader produces garbage that fails only under specific flush patterns. Encode the codec in the frame header or your own envelope — never infer it.

FAQ

Is klauspost/compress a drop-in replacement for compress/gzip? Yes for the gzip, zlib, flate and zip subpackages — the APIs mirror the standard library, so changing the import path is usually the whole migration. Wire-format compatibility is preserved because the output is still valid gzip.

Which Go compression library is fastest for small messages? For messages under roughly 1 KB, LZ4 block mode (lz4.CompressBlock) usually wins because frame overhead dominates. For anything above a few KB, zstd at SpeedFastest or SpeedDefault gives a better ratio at comparable CPU, and dictionaries close the gap on repetitive message shapes.

Does DataDog/zstd still work in 2026? It compiles and runs, but it requires cgo and its bundled C sources are from zstd 0.5.0 per the project README. Use it only to maintain existing pipelines; migrate new code to a pure-Go implementation.

Can I use these libraries in AWS Lambda or other minimal runtimes? klauspost/compress and pierrec/lz4 work with CGO_ENABLED=0, which makes static binaries straightforward. DataDog/zstd needs a C toolchain and glibc-compatible runtime unless you build a custom static image.

How do I pick an encoder level? Start at SpeedDefault. Raise to SpeedBetterCompression only if bytes stored or transferred are the bottleneck, and drop to SpeedFastest if CPU saturation shows up in profiles. Measure p99 latency, not averages — compression cost is bursty.

Do these libraries support parallel compression of a single stream? klauspost’s zstd supports concurrent encoding across frames and ships pgzip for parallel gzip blocks; LZ4 parallelizes at the block level if you shard input yourself. DataDog/zstd does not offer parallel encoding.


💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com