Zstandard compression for Go, transpiled, CGo-free.
Can be built with CGO_ENABLED=0 and do not need system libzstd,
pkg-config, a C compiler, or an external zstd binary at runtime.
The module exposes two packages:
go.dw1.io/zstd: the Go API for application code.go.dw1.io/zstd/abi: the low-level generated libzstd ABI for generated or systems packages that need raw zstd symbols.
Generated ABI output is checked in. Users do not need to run the generator to import the module.
This repository currently generates ABI files for:
linux/amd64linux/arm64
Other targets fail clearly from go.dw1.io/zstd/abi
instead of silently using a different implementation.
The checked-in source baseline is upstream zstd v1.5.7 at commit
f8745da6ff1ad1e7bab384bd1f9d742439278e99.
go get go.dw1.io/zstdpackage main
import (
"fmt"
"go.dw1.io/zstd"
)
func main() {
src := []byte("hello zstd")
compressed, err := zstd.Compress(nil, src, zstd.CompressOptions{
Level: zstd.DefaultLevel(),
Checksum: true,
})
if err != nil {
panic(err)
}
decompressed, err := zstd.Decompress(nil, compressed, zstd.DecompressOptions{})
if err != nil {
panic(err)
}
fmt.Println(string(decompressed))
}For frames without a declared content size, one-shot decompression requires an explicit limit:
decompressed, err := zstd.Decompress(nil, compressed, zstd.DecompressOptions{
MaxDecodedSize: 8 << 20,
})Use NewReader for large or
unknown-size streams where allocating the full decoded payload up front is not
appropriate.
var compressed bytes.Buffer
w, err := zstd.NewWriter(&compressed, zstd.WriterOptions{
CompressOptions: zstd.CompressOptions{Checksum: true},
})
if err != nil {
panic(err)
}
if _, err := w.Write([]byte("streamed zstd")); err != nil {
panic(err)
}
if err := w.Close(); err != nil {
panic(err)
}
r, err := zstd.NewReader(bytes.NewReader(compressed.Bytes()), zstd.ReaderOptions{})
if err != nil {
panic(err)
}
out, err := io.ReadAll(r)
if err != nil {
panic(err)
}
if err := r.Close(); err != nil {
panic(err)
}Writer supports
Write,
Flush,
Close, and
Reset.
Reader supports
incremental reads, multiple frames, skippable frames, dictionaries, window-size
limits, and Reset.
Raw dictionaries can be passed directly:
dict := []byte("customer status invoice total account")
src := []byte("customer account status: invoice total paid")
compressed, err := zstd.Compress(nil, src, zstd.CompressOptions{Dict: dict})
if err != nil {
panic(err)
}
decompressed, err := zstd.Decompress(nil, compressed, zstd.DecompressOptions{Dict: dict})
if err != nil {
panic(err)
}For repeated workloads, compile dictionaries once with
NewCDict and
NewDDict.
The package also exposes
TrainDictionary,
FinalizeDictionary,
DictID, and
DictHeaderSize.
Warning
CDict and
DDict values are immutable after
construction and may be shared for read-only use. Mutable compressors,
decompressors, readers, and writers are NOT safe for concurrent mutation.
Use the metadata APIs to inspect frames before deciding how to decode them:
header, err := zstd.FrameHeaderOf(compressed)
if err != nil {
panic(err)
}
fmt.Println(header.ContentSize, header.WindowSize, header.DictID, header.Checksum)Available helpers include
ContentSize,
FrameCompressedSize,
FrameHeaderSize,
FrameDictID,
IsFrame, and
IsSkippableFrame.
Unknown content size and malformed content-size errors are distinct. A forged or very large content-size field does not cause implicit allocation without a user provided limit.
zstd result-code failures are wrapped in typed Go errors:
Use errors.As to inspect categories and the
underlying *zstd.Error details:
_, err := zstd.Decompress(nil, []byte("not a zstd frame"), zstd.DecompressOptions{
MaxDecodedSize: 1024,
})
var zerr *zstd.DecompressionError
if errors.As(err, &zerr) {
fmt.Println(zerr.Err.Operation, zerr.Err.Code, zerr.Err.Name)
}Stream errors wrap the underlying io.Reader or
io.Writer error.
go.dw1.io/zstd/abi exposes the
generated libzstd-like ABI for low-level consumers. It is intentionally
version-coupled to the pinned upstream source and uses ccgo/modernc runtime
conventions.
Application code should prefer the root package. Generated packages that need
raw zstd symbols can depend on
go.dw1.io/zstd/abi instead of
carrying a private libzstd copy.
The generator lives in internal/cmd/genzstd and is wired through go generate:
go generate ./...The generated package includes:
abi/zstd_linux_amd64.goabi/zstd_linux_arm64.goabi/SOURCE-MANIFEST.txtabi/SYMBOLS.txt- ABI symbol compile tests
- unsupported-target build failure stubs
SOURCE-MANIFEST.txt records the zstd source commit, header version, selected
source files, macros, include directories, local patch hashes, target list, and
ccgo version. SYMBOLS.txt records the public header symbols expected in the
ABI package.
Check that committed generated output is current with:
go run ./internal/cmd/genzstd -checkUseful local checks:
CGO_ENABLED=0 go test ./...
go test ./...
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go test ./...
go run ./internal/cmd/genzstd -checkThe test suite covers one-shot APIs, streaming, reusable contexts, dictionaries, dictionary training/finalization, frame metadata, typed errors, upstream golden fixtures, same-source official libzstd oracle comparisons where a C compiler is available, generated-consumer ABI usage, unsupported targets, and fuzz targets.
Short fuzz smoke:
go test -run '^$' -fuzz=FuzzRoundTrip -fuzztime=1s .This module preserves the upstream zstd BSD-style license path. See LICENSE.