Documentation
¶
Overview ¶
Package helper provides streaming compression and decompression helpers that wrap compression algorithms with unified io.ReadWriteCloser interfaces.
Overview ¶
This package simplifies working with compression algorithms by providing a consistent interface for both compression and decompression operations on streaming data. It acts as an adapter layer between standard io interfaces and compression algorithms from github.com/nabbar/golib/archive/compress.
Key capabilities:
- Transparent compression/decompression during read or write operations
- Support for any compression algorithm that implements arccmp.Algorithm
- Automatic handling of io.Reader/io.Writer to io.ReadCloser/io.WriteCloser conversions
- Streaming operations with minimal memory overhead
- Thread-safe for single-instance usage
Design Philosophy ¶
1. Interface Simplicity: Provide a single Helper interface (io.ReadWriteCloser) for all operations 2. Algorithm Agnostic: Work with any compression algorithm without algorithm-specific code 3. Streaming First: Process data in chunks without loading entire streams into memory 4. Resource Safety: Proper cleanup through Close() and automatic wrapper conversions 5. Type Safety: Compile-time guarantees through interface-based design
Architecture ¶
The package provides four internal implementation types, selected automatically based on operation type and I/O direction:
┌──────────────────────────────────────────────┐
│ Helper Interface │
│ (io.ReadWriteCloser) │
└──────────────────────┬───────────────────────┘
│
┌────────────┴────────────┐
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ Compress │ │ Decompress │
└──────┬──────┘ └──────┬──────┘
┌────┴────┐ ┌────┴────┐
▼ ▼ ▼ ▼
┌────────┬────────┐ ┌────────┬────────┐
│ │ │ │ │ │
│ Reader │ Writer │ │ Reader │ Writer │
│ │ │ │ │ │
└────────┴────────┘ └────────┴────────┘
compressReader deCompressReader
compressWriter deCompressWriter
Component Responsibilities:
- compressReader: Reads from source, compresses data, provides compressed output
- compressWriter: Accepts data, compresses it, writes compressed data to destination
- deCompressReader: Reads compressed data from source, provides decompressed output
- deCompressWriter: Accepts compressed data, decompresses it, writes to destination
Data Flow ¶
Compression Read Flow (compressReader):
- Client calls Read(p) on Helper
- Helper reads raw data from source reader
- Data is compressed using algorithm's Writer
- Compressed data is buffered internally
- Compressed chunks are returned to client
Compression Write Flow (compressWriter):
- Client calls Write(p) on Helper
- Data is passed to algorithm's Writer
- Compressed data is written to destination
- Close() finalizes compression stream
Decompression Read Flow (deCompressReader):
- Client calls Read(p) on Helper
- Compressed data is read from source
- Algorithm's Reader decompresses data
- Decompressed data is returned to client
Decompression Write Flow (deCompressWriter):
- Client calls Write(p) on Helper
- Compressed data is buffered
- Background goroutine reads from buffer
- Algorithm's Reader decompresses data
- Decompressed data is written to destination
Basic Usage ¶
Compress data while reading from a source:
import (
"io"
"os"
"github.com/nabbar/golib/archive/compress"
"github.com/nabbar/golib/archive/helper"
)
func compressFile(inputPath, outputPath string) error {
input, err := os.Open(inputPath)
if err != nil {
return err
}
defer input.Close()
output, err := os.Create(outputPath)
if err != nil {
return err
}
defer output.Close()
// Create compression reader
h, err := helper.NewReader(compress.GZIP, helper.Compress, input)
if err != nil {
return err
}
defer h.Close()
// Copy compressed data to output
_, err = io.Copy(output, h)
return err
}
Compress data while writing to a destination:
func writeCompressed(data []byte, outputPath string) error {
output, err := os.Create(outputPath)
if err != nil {
return err
}
defer output.Close()
// Create compression writer
h, err := helper.NewWriter(compress.GZIP, helper.Compress, output)
if err != nil {
return err
}
defer h.Close()
// Write data - it will be compressed automatically
_, err = h.Write(data)
return err
}
Decompress data while reading:
func decompressFile(inputPath string) ([]byte, error) {
input, err := os.Open(inputPath)
if err != nil {
return nil, err
}
defer input.Close()
// Create decompression reader
h, err := helper.NewReader(compress.GZIP, helper.Decompress, input)
if err != nil {
return nil, err
}
defer h.Close()
// Read decompressed data
return io.ReadAll(h)
}
Decompress data while writing:
func writeDecompressed(compressedData []byte, outputPath string) error {
output, err := os.Create(outputPath)
if err != nil {
return err
}
defer output.Close()
// Create decompression writer
h, err := helper.NewWriter(compress.GZIP, helper.Decompress, output)
if err != nil {
return err
}
defer h.Close()
// Write compressed data - it will be decompressed automatically
_, err = h.Write(compressedData)
return err
}
Automatic Source Type Detection ¶
The New() function automatically determines the operation type based on source type:
// Source is io.Reader - creates a reader helper h1, _ := helper.New(compress.GZIP, helper.Compress, input) data, _ := io.ReadAll(h1) // Source is io.Writer - creates a writer helper h2, _ := helper.New(compress.GZIP, helper.Compress, output) h2.Write(data)
Supported Compression Algorithms ¶
The package works with any algorithm implementing arccmp.Algorithm from github.com/nabbar/golib/archive/compress. Common algorithms include:
- compress.GZIP: Standard gzip compression (RFC 1952)
- compress.ZLIB: Zlib compression (RFC 1950)
- compress.DEFLATE: Raw deflate compression (RFC 1951)
- compress.BZIP2: Bzip2 compression
- compress.LZ4: LZ4 fast compression
- compress.ZSTD: Zstandard compression
- compress.SNAPPY: Snappy compression
See the compress package documentation for the complete list and algorithm details.
Implementation Details ¶
Thread Safety:
- Each Helper instance is safe for single-goroutine use
- Do not share Helper instances across goroutines
- Create separate instances for concurrent operations
Memory Management:
- compressReader uses a 512-byte internal buffer (chunkSize)
- deCompressWriter spawns a single background goroutine
- bufNoEOF implements backpressure through wait loops
- Close() must be called to release resources properly
Atomicity:
- compressReader uses atomic.Bool for close state
- deCompressWriter uses atomic.Bool for close and run states
- Prevents race conditions in concurrent read/write scenarios
Error Handling ¶
The package defines three error types:
- ErrInvalidSource: Returned when source is neither io.Reader nor io.Writer
- ErrClosedResource: Returned when writing to a closed deCompressWriter
- ErrInvalidOperation: Returned for unsupported Operation values
Errors from underlying readers/writers and compression algorithms are propagated unchanged to the caller.
Read() and Write() methods of incorrect types (e.g., Read() on a writer) return ErrInvalidSource with zero bytes to clearly indicate the misuse.
Performance Considerations ¶
Buffer Sizing:
- Default chunkSize is 512 bytes
- compressReader allocates buffers based on caller's buffer size
- Larger read buffers reduce overhead for large data transfers
Compression Trade-offs:
- Compression adds CPU overhead but reduces I/O size
- Choose algorithms based on speed vs compression ratio needs
- GZIP: Good balance, widely compatible
- LZ4/Snappy: Fast compression, lower ratios
- ZSTD/BZIP2: Slower, higher compression ratios
Decompression Writer Overhead:
- deCompressWriter uses a background goroutine
- Adds minimal latency but enables streaming
- Wait group ensures proper cleanup on Close()
Use Cases ¶
1. Compressed File I/O
Read and write compressed files transparently:
h, _ := helper.NewWriter(compress.GZIP, helper.Compress, file) defer h.Close() h.Write(data) // Automatically compressed
2. Network Data Compression
Compress data before sending over network:
h, _ := helper.NewWriter(compress.LZ4, helper.Compress, conn) defer h.Close() h.Write(payload) // Compressed before transmission
3. Transparent Decompression
Read compressed data as if uncompressed:
h, _ := helper.NewReader(compress.GZIP, helper.Decompress, compressedStream) defer h.Close() scanner := bufio.NewScanner(h) // Works with any io.Reader consumer
4. Format Conversion
Convert between compression formats:
// Read GZIP, write LZ4 src, _ := helper.NewReader(compress.GZIP, helper.Decompress, gzipFile) dst, _ := helper.NewWriter(compress.LZ4, helper.Compress, lz4File) defer src.Close() defer dst.Close() io.Copy(dst, src)
5. Streaming Processing
Process compressed data in streams without full decompression:
h, _ := helper.NewReader(compress.ZSTD, helper.Decompress, compressedData) defer h.Close() processInChunks(h) // Read and process incrementally
Limitations ¶
Read/Write Type Restrictions:
- Compression/decompression readers do not support Write()
- Compression/decompression writers do not support Read()
- Attempting wrong operation returns ErrInvalidSource
Concurrency:
- Helper instances are not safe for concurrent Read/Write
- Use separate instances per goroutine
- Close() should be called from a single goroutine
Algorithm Requirements:
- Algorithm must implement arccmp.Algorithm interface
- Algorithm must provide working Reader() and Writer() methods
- Invalid algorithms cause creation errors
Backpressure:
- deCompressWriter uses time.Sleep for backpressure (100µs)
- Not suitable for hard real-time systems
- May cause latency spikes under memory pressure
Best Practices ¶
Always Close Resources:
h, err := helper.NewWriter(algo, helper.Compress, dst)
if err != nil {
return err
}
defer h.Close() // Ensures compression finalization
Check Error Returns:
n, err := h.Write(data)
if err != nil {
return fmt.Errorf("compression failed: %w", err)
}
if n != len(data) {
return fmt.Errorf("incomplete write: %d of %d bytes", n, len(data))
}
Choose Appropriate Algorithms:
// Fast compression for temporary data helper.NewWriter(compress.LZ4, helper.Compress, dst) // Maximum compression for archival helper.NewWriter(compress.ZSTD, helper.Compress, dst) // Compatibility with external tools helper.NewWriter(compress.GZIP, helper.Compress, dst)
Handle Large Data Efficiently:
// Stream large files instead of loading into memory h, _ := helper.NewReader(algo, helper.Compress, largeFile) defer h.Close() io.Copy(destination, h) // Streaming, constant memory
Related Packages ¶
This package integrates with:
- github.com/nabbar/golib/archive/compress: Compression algorithm implementations
- github.com/nabbar/golib/ioutils/nopwritecloser: io.WriteCloser wrapper utilities
Standard library compatibility:
- io.Reader, io.Writer, io.Closer: Fully compatible
- io.Copy: Works transparently with Helper instances
- bufio: Can wrap Helper for buffered operations
Testing ¶
The package includes comprehensive tests covering:
- Constructor functions (New, NewReader, NewWriter)
- Compression read/write operations
- Decompression read/write operations
- Error handling and edge cases
- Resource cleanup and close semantics
- Concurrent operation safety
- Integration with various compression algorithms
See TESTING.md for detailed test documentation and coverage reports.
Example (ChainedCompression) ¶
Example_chainedCompression demonstrates chaining compression operations. Shows converting between different compression algorithms.
package main
import (
"bytes"
"fmt"
arccmp "github.com/nabbar/golib/archive/compress"
"github.com/nabbar/golib/archive/helper"
)
func main() {
original := "Hello, World!"
var buf1 bytes.Buffer
h1, _ := helper.NewWriter(arccmp.Gzip, helper.Compress, &buf1)
h1.Write([]byte(original))
h1.Close()
fmt.Printf("Original: %d bytes\n", len(original))
fmt.Printf("GZIP compressed: %d bytes\n", buf1.Len())
}
Output: Original: 13 bytes GZIP compressed: 37 bytes
Example (CompressStream) ¶
Example_compressStream demonstrates compressing a data stream. This is useful for processing large amounts of data efficiently.
package main
import (
"bytes"
"fmt"
"io"
"log"
"strings"
arccmp "github.com/nabbar/golib/archive/compress"
"github.com/nabbar/golib/archive/helper"
)
func main() {
input := strings.NewReader("Line 1\nLine 2\nLine 3\n")
var output bytes.Buffer
h, err := helper.NewReader(arccmp.Gzip, helper.Compress, input)
if err != nil {
log.Fatal(err)
}
defer h.Close()
n, err := io.Copy(&output, h)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Compressed %d bytes\n", n)
}
Output: Compressed 37 bytes
Example (DecompressStream) ¶
Example_decompressStream demonstrates decompressing a data stream. Shows how to transparently decompress while reading.
package main
import (
"bytes"
"fmt"
"io"
"log"
arccmp "github.com/nabbar/golib/archive/compress"
"github.com/nabbar/golib/archive/helper"
)
func main() {
original := "Hello"
var buf bytes.Buffer
cw, _ := helper.NewWriter(arccmp.Gzip, helper.Compress, &buf)
cw.Write([]byte(original))
cw.Close()
h, err := helper.NewReader(arccmp.Gzip, helper.Decompress, &buf)
if err != nil {
log.Fatal(err)
}
defer h.Close()
data, err := io.ReadAll(h)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Decompressed: %s\n", data)
}
Output: Decompressed: Hello
Example (ErrorHandling) ¶
Example_errorHandling demonstrates proper error handling with helpers.
package main
import (
"fmt"
"io"
"strings"
arccmp "github.com/nabbar/golib/archive/compress"
"github.com/nabbar/golib/archive/helper"
)
func main() {
h, err := helper.NewReader(arccmp.Gzip, helper.Compress, strings.NewReader("test"))
if err != nil {
fmt.Printf("Creation error: %v\n", err)
return
}
defer h.Close()
buf := make([]byte, 10)
n, err := h.Read(buf)
if err != nil && err != io.EOF {
fmt.Printf("Read error: %v\n", err)
return
}
fmt.Printf("Read %d bytes successfully\n", n)
}
Output: Read 10 bytes successfully
Example (InvalidOperation) ¶
Example_invalidOperation demonstrates error handling for invalid operations.
package main
import (
"fmt"
"strings"
arccmp "github.com/nabbar/golib/archive/compress"
"github.com/nabbar/golib/archive/helper"
)
func main() {
_, err := helper.NewReader(arccmp.Gzip, 99, strings.NewReader("test"))
if err != nil {
fmt.Printf("Error: %v\n", err)
}
}
Output: Error: invalid operation
Example (InvalidSource) ¶
Example_invalidSource demonstrates error handling for invalid sources.
package main
import (
"fmt"
arccmp "github.com/nabbar/golib/archive/compress"
"github.com/nabbar/golib/archive/helper"
)
func main() {
_, err := helper.New(arccmp.Gzip, helper.Compress, "invalid source")
if err != nil {
fmt.Printf("Error: %v\n", err)
}
}
Output: Error: invalid source
Example (LargeData) ¶
Example_largeData demonstrates handling larger data efficiently.
package main
import (
"bytes"
"fmt"
"strings"
arccmp "github.com/nabbar/golib/archive/compress"
"github.com/nabbar/golib/archive/helper"
)
func main() {
data := strings.Repeat("test data ", 100)
var buf bytes.Buffer
h, _ := helper.NewWriter(arccmp.Gzip, helper.Compress, &buf)
h.Write([]byte(data))
h.Close()
ratio := float64(len(data)) / float64(buf.Len())
fmt.Printf("Compression ratio: %.2fx\n", ratio)
}
Output: Compression ratio: 23.81x
Example (MultipleWrites) ¶
Example_multipleWrites demonstrates writing data in multiple chunks.
package main
import (
"bytes"
"fmt"
"io"
"log"
arccmp "github.com/nabbar/golib/archive/compress"
"github.com/nabbar/golib/archive/helper"
)
func main() {
var buf bytes.Buffer
h, err := helper.NewWriter(arccmp.Gzip, helper.Compress, &buf)
if err != nil {
log.Fatal(err)
}
chunks := []string{"Hello", " ", "World", "!"}
for _, chunk := range chunks {
h.Write([]byte(chunk))
}
h.Close()
r, _ := helper.NewReader(arccmp.Gzip, helper.Decompress, &buf)
defer r.Close()
data, _ := io.ReadAll(r)
fmt.Printf("%s\n", data)
}
Output: Hello World!
Example (WriterToReader) ¶
Example_writerToReader demonstrates reading compressed data from a writer operation.
package main
import (
"bytes"
"fmt"
"io"
"log"
arccmp "github.com/nabbar/golib/archive/compress"
"github.com/nabbar/golib/archive/helper"
)
func main() {
var buf bytes.Buffer
w, err := helper.NewWriter(arccmp.Gzip, helper.Compress, &buf)
if err != nil {
log.Fatal(err)
}
w.Write([]byte("Data to compress"))
w.Close()
r, err := helper.NewReader(arccmp.Gzip, helper.Decompress, &buf)
if err != nil {
log.Fatal(err)
}
defer r.Close()
data, _ := io.ReadAll(r)
fmt.Printf("%s\n", data)
}
Output: Data to compress
Index ¶
Examples ¶
- Package (ChainedCompression)
- Package (CompressStream)
- Package (DecompressStream)
- Package (ErrorHandling)
- Package (InvalidOperation)
- Package (InvalidSource)
- Package (LargeData)
- Package (MultipleWrites)
- Package (WriterToReader)
- New
- NewReader (Compress)
- NewReader (Decompress)
- NewWriter (Compress)
- NewWriter (Decompress)
Constants ¶
This section is empty.
Variables ¶
var ( // ErrInvalidSource is returned when the provided source is not an io.Reader or io.Writer. ErrInvalidSource = errors.New("invalid source") // ErrClosedResource is returned when attempting to write to a closed resource. ErrClosedResource = errors.New("closed resource") // ErrInvalidOperation is returned when an unsupported operation is requested. ErrInvalidOperation = errors.New("invalid operation") )
Functions ¶
This section is empty.
Types ¶
type Helper ¶
type Helper interface {
io.ReadWriteCloser
}
Helper provides a unified interface for compression and decompression operations. It implements io.ReadWriteCloser to enable transparent compression/decompression in streaming scenarios.
func New ¶
New creates a new Helper instance based on the provided algorithm, operation, and source.
Parameters:
- algo: The compression algorithm to use (from github.com/nabbar/golib/archive/compress)
- ope: The operation type (Compress or Decompress)
- src: The data source, must be either io.Reader or io.Writer
Returns:
- Helper: A new Helper instance for compression/decompression operations
- error: ErrInvalidSource if src is neither io.Reader nor io.Writer
The function automatically determines whether to create a reader or writer based on the type of src. For io.Reader, it creates a Helper that can be read from. For io.Writer, it creates a Helper that can be written to.
Example ¶
ExampleNew demonstrates creating a Helper using automatic type detection. This is the simplest way to create a Helper when the source type determines usage.
package main
import (
"bytes"
"fmt"
"log"
arccmp "github.com/nabbar/golib/archive/compress"
"github.com/nabbar/golib/archive/helper"
)
func main() {
data := []byte("test data")
src := bytes.NewReader(data)
h, err := helper.New(arccmp.Gzip, helper.Compress, src)
if err != nil {
log.Fatal(err)
}
defer h.Close()
fmt.Printf("Helper created: %T\n", h)
}
Output: Helper created: *helper.compressReader
func NewReader ¶
NewReader creates a new Helper instance for reading data from the provided io.Reader.
Parameters:
- algo: The compression algorithm to use
- ope: The operation type (Compress to compress while reading, Decompress to decompress while reading)
- src: The source reader to read data from
Returns:
- Helper: A new Helper instance that wraps the source reader
- error: ErrInvalidOperation if the operation is not Compress or Decompress
When operation is Compress, reading from the returned Helper will compress data from src. When operation is Decompress, reading from the returned Helper will decompress data from src.
Example (Compress) ¶
ExampleNewReader_compress demonstrates compressing data while reading. Data is read from source, compressed, and returned to the caller.
package main
import (
"fmt"
"io"
"log"
"strings"
arccmp "github.com/nabbar/golib/archive/compress"
"github.com/nabbar/golib/archive/helper"
)
func main() {
input := strings.NewReader("Hello, World!")
h, err := helper.NewReader(arccmp.Gzip, helper.Compress, input)
if err != nil {
log.Fatal(err)
}
defer h.Close()
compressed, err := io.ReadAll(h)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Original: 13 bytes, Compressed: %d bytes\n", len(compressed))
}
Output: Original: 13 bytes, Compressed: 37 bytes
Example (Decompress) ¶
ExampleNewReader_decompress demonstrates decompressing data while reading. Compressed data is read from source and returned decompressed.
package main
import (
"bytes"
"fmt"
"io"
"log"
arccmp "github.com/nabbar/golib/archive/compress"
"github.com/nabbar/golib/archive/helper"
)
func main() {
original := "Hello"
var buf bytes.Buffer
cw, _ := helper.NewWriter(arccmp.Gzip, helper.Compress, &buf)
cw.Write([]byte(original))
cw.Close()
h, err := helper.NewReader(arccmp.Gzip, helper.Decompress, &buf)
if err != nil {
log.Fatal(err)
}
defer h.Close()
data, err := io.ReadAll(h)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", data)
}
Output: Hello
func NewWriter ¶
NewWriter creates a new Helper instance for writing data to the provided io.Writer.
Parameters:
- algo: The compression algorithm to use
- ope: The operation type (Compress to compress while writing, Decompress to decompress while writing)
- dst: The destination writer to write data to
Returns:
- Helper: A new Helper instance that wraps the destination writer
- error: ErrInvalidOperation if the operation is not Compress or Decompress
When operation is Compress, writing to the returned Helper will compress data to dst. When operation is Decompress, writing to the returned Helper will decompress data to dst.
Example (Compress) ¶
ExampleNewWriter_compress demonstrates compressing data while writing. Data written to the Helper is compressed and written to the destination.
package main
import (
"bytes"
"fmt"
"log"
arccmp "github.com/nabbar/golib/archive/compress"
"github.com/nabbar/golib/archive/helper"
)
func main() {
var buf bytes.Buffer
h, err := helper.NewWriter(arccmp.Gzip, helper.Compress, &buf)
if err != nil {
log.Fatal(err)
}
n, err := h.Write([]byte("Hello, World!"))
if err != nil {
log.Fatal(err)
}
if err := h.Close(); err != nil {
log.Fatal(err)
}
fmt.Printf("Wrote %d bytes, compressed size: %d bytes\n", n, buf.Len())
}
Output: Wrote 13 bytes, compressed size: 37 bytes
Example (Decompress) ¶
ExampleNewWriter_decompress demonstrates decompressing data while writing. Compressed data written to the Helper is decompressed and written to destination.
package main
import (
"bytes"
"fmt"
"log"
arccmp "github.com/nabbar/golib/archive/compress"
"github.com/nabbar/golib/archive/helper"
)
func main() {
original := "Hello"
var compressedBuf bytes.Buffer
temp, _ := helper.NewWriter(arccmp.Gzip, helper.Compress, &compressedBuf)
temp.Write([]byte(original))
temp.Close()
compressed := compressedBuf.Bytes()
var buf bytes.Buffer
h, err := helper.NewWriter(arccmp.Gzip, helper.Decompress, &buf)
if err != nil {
log.Fatal(err)
}
n, err := h.Write(compressed)
if err != nil {
log.Fatal(err)
}
if err := h.Close(); err != nil {
log.Fatal(err)
}
fmt.Printf("Wrote %d compressed bytes, decompressed: %s\n", n, buf.String())
}
Output: Wrote 29 compressed bytes, decompressed: Hello