zstd

package
v1.2.11 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 2, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

README

compress/zstd

compress/zstd is an encoder-only RFC 8878 package for TinyGo and Go web servers. It streams a valid Content-Encoding: zstd representation and calculates its SHA-256 digest during output, so cache entries can retain the encoded bytes and a strong ETag without hashing the bytes in a second pass.

encoded, result, err := zstd.EncodeAll(body)
if err != nil {
	return err
}
header.Set("Content-Encoding", zstd.ContentEncoding)
header.Set("ETag", result.ETag())

ETag calculation is enabled by default. Disable it for responses such as Cache-Control: no-store; this avoids allocating and updating SHA-256:

encoded, result, err := zstd.EncodeAll(body, zstd.WithETag(false))
// result.ETagEnabled is false, result.ETag() is empty, and SHA256 is zero.

NewWriter provides the bounded streaming form. Call Close, then Result; closing the encoder does not close its destination.

Constructing an encoder writes nothing to the destination. As in compress/gzip, the frame header goes out with the first Write, Flush, or Close. A handler can therefore wrap its http.ResponseWriter before rendering and still answer a rendering failure with an uncompressed error response: nothing has reached the wire, so the status is not committed and Content-Encoding can still be dropped.

Flush emits the buffered input as complete blocks so a reader can decode everything written so far, which is what streaming responses and server-sent events need between chunks. It neither ends the frame nor flushes the destination, so flush the destination separately:

if _, err := z.Write(chunk); err != nil {
	return err
}
if err := z.Flush(); err != nil {
	return err
}
w.(http.Flusher).Flush()

Flushing before a block fills reduces the compression ratio, so flush per chunk rather than per Write.

Reset starts a new frame on a new destination, so a server can pool encoders across responses instead of building one per response. It keeps what the encoder is made of — under TinyGo a 128 KiB block buffer and a 16 KiB match table, which is nearly all of its footprint — and keeps the WithETag setting chosen at NewWriter. As with NewWriter, nothing reaches the new destination until the caller writes, flushes, or closes.

z := pool.Get().(*zstd.Writer)
z.Reset(w)
defer func() { z.Close(); pool.Put(z) }()

fasthttp does exactly this: it is the encoder TinyGo builds of that fork compress with, because klauspost's decoder is assembly TinyGo cannot link.

Implementation selection

  • normal host Go builds use github.com/klauspost/compress/zstd
  • TinyGo builds use this package's bounded pure-Go encoder
  • go build -tags force_tinygo_logic forces the TinyGo-compatible encoder on host Go

Both implementations expose the same Writer, Result, Option, and EncodeAll API, Reset included. Encoded bytes and therefore ETags may differ between implementations.

The host backend uses the klauspost default compression level with one encoder, a 128 KiB window, lower-memory mode, and no frame checksum. The TinyGo backend has the following supported subset:

  • standard Zstandard frames with a 128 KiB window
  • raw and RLE blocks of at most 128 KiB, including profitable interior runs
  • compressed blocks carrying many sequences, from a greedy matcher that keeps one candidate per hash slot
  • FSE sequence tables fitted to each block, falling back to the format's predefined tables when a block has too few sequences to pay for a description, and to RLE tables when a stream carries one symbol
  • repeat offsets, for the common case of a match at the previous distance
  • a lazy step, which defers a match by one byte when the next position starts a longer one
  • Huffman-coded literals, in one stream or four, with the direct weight representation; raw and RLE literal blocks are used where either is smaller
  • streaming output with at most one input block retained
  • Flush at block boundaries without ending the frame
  • SHA-256 and encoded size calculated over bytes successfully written
  • strong, quoted ETag formatting for the encoded representation

Every block falls back to raw or RLE when a compressed one would not be smaller, so output never exceeds the input by more than the block headers.

Compression ratio

Measured against compress/flate at its default level, which is the encoding a server would otherwise negotiate:

payload this encoder deflate
14 KiB HTML listing 8.2% 11.6%
11 KiB JSON array 11.0% 13.3%
5 KiB varied text 29.6% 26.6%
one repeated string 1.4% 1.6%
incompressible 100.1% 100.1%

Varied prose is the one case that loses, and the breakdown says why: its cost is 1247 bytes of sequences against 233 of literals, where deflate is finding word-level repeats this matcher does not.

TestRatioAgainstDeflate holds these within a stated multiple of deflate, and every case in the suite decodes through the reference implementation, so no ratio here was bought with bytes a real decoder would reject.

Matching stays inside the current block, which is what bounds memory to one retained block. A match therefore never reaches back into an earlier block, even though the window would allow it, so a payload whose repeats are further apart than 128 KiB compresses worse than a general-purpose encoder would manage.

Public API exclusions

  • decoding
  • dictionaries and the seekable format
  • compression-level or dictionary options
  • frame content checksums (the cache digest is separate)

The TinyGo backend additionally omits unsafe code, assembly, and CGo, and writes Huffman weights only in the direct representation, never FSE-compressed. That representation encodes its weight count as 127 plus it, so the largest literal byte in a block must be 128 or below; a block whose literals reach higher stores them instead of coding them. Binary payloads therefore compress through their matches alone.

Documentation

Overview

Package zstd writes RFC 8878 Zstandard frames and calculates cache metadata over the encoded representation while it is emitted.

Host Go uses github.com/klauspost/compress/zstd. TinyGo uses this package's bounded encoder, which can also be selected on host Go with the shared force_tinygo_logic build tag. Both implementations expose the same API, calculate Result's SHA-256 digest over bytes successfully written, and write nothing to the destination until the caller writes, flushes, or closes.

Writer is poolable through Reset, which is how the fasthttp fork in this repository compresses responses under TinyGo.

Index

Examples

Constants

View Source
const ContentEncoding = "zstd"

ContentEncoding is the HTTP content-coding token for Zstandard.

Variables

View Source
var (
	ErrClosed            = errors.New("zstd: writer is closed")
	ErrResultUnavailable = errors.New("zstd: result is unavailable before a successful close")
)

Functions

This section is empty.

Types

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option configures both the host-Go and TinyGo encoders.

func WithETag

func WithETag(enabled bool) Option

WithETag controls whether SHA-256 cache metadata is calculated while the encoded representation is written. It is enabled by default. When disabled, Result.SHA256 is zero and Result.ETag returns an empty string.

type Result

type Result struct {
	Size        int64
	SHA256      [sha256.Size]byte
	ETagEnabled bool
}

Result describes an encoded representation. SHA256 covers exactly Size bytes written to the destination, including the Zstandard frame headers.

func EncodeAll

func EncodeAll(src []byte, options ...Option) ([]byte, Result, error)

EncodeAll encodes src, returning the frame and its cache metadata. The digest is produced while the frame is written; the encoded bytes are not traversed a second time.

Example
package main

import (
	"fmt"

	"github.com/shibukawa/tinygodriver/compress/zstd"
)

func main() {
	encoded, result, err := zstd.EncodeAll([]byte("response body"))
	if err != nil {
		panic(err)
	}
	fmt.Println(len(encoded) == int(result.Size))
	fmt.Println(zstd.ContentEncoding)
}
Output:
true
zstd

func (Result) ETag

func (r Result) ETag() string

ETag returns a quoted strong HTTP entity-tag for the encoded representation. It returns an empty string when the encoder used WithETag(false).

type Writer

type Writer struct {
	// contains filtered or unexported fields
}

Writer emits one Zstandard frame through github.com/klauspost/compress/zstd. Writer is not safe for concurrent use. Close must succeed before Result can be read.

func NewWriter

func NewWriter(w io.Writer, options ...Option) (*Writer, error)

NewWriter starts a host-Go Zstandard stream. Constructing a Writer writes nothing to the destination, so an encoder built and then abandoned leaves the destination untouched. TinyGo and builds using the force_tinygo_logic tag select the bounded TinyGo encoder instead.

func (*Writer) Close

func (z *Writer) Close() error

Close finishes the frame. It does not close the destination.

func (*Writer) Flush added in v1.0.4

func (z *Writer) Flush() error

Flush emits the buffered input as complete blocks so that everything written so far can be decoded, and returns once those bytes reach the destination. It does not end the frame and it does not flush the destination itself. Flushing before a block fills reduces the compression ratio.

func (*Writer) Reset added in v1.2.4

func (z *Writer) Reset(w io.Writer)

Reset starts a new frame writing to w, keeping the encoder and its window so that a pooled Writer does not rebuild them. The ETag setting chosen at NewWriter is retained, nothing reaches w until the caller writes, flushes, or closes, and a nil w leaves the Writer in the error state NewWriter would have reported.

func (*Writer) Result

func (z *Writer) Result() (Result, error)

Result returns the encoded size and SHA-256 digest after a successful Close.

func (*Writer) Write

func (z *Writer) Write(p []byte) (int, error)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL