Sau mười phần hiểu nén từ trong ra và trên dòng lệnh, giờ ta viết code. Go là ngôn ngữ tốt để nói về nén trong code vì nó có nén tích hợp sẵn trong thư viện chuẩn — compress/gzip, compress/flate, compress/zlib — và thiết kế của chúng minh họa đẹp một triết lý Go: nén là một io.Writer/io.Reader, ghép vào bất kỳ đường ống nào. Nhờ vậy nén theo luồng (streaming) trở nên tự nhiên: bạn nén một file 10GB mà chỉ tốn vài chục KB bộ nhớ. Bài này (phần 11 loạt Nén) đo thật cách dùng, các mức nén, và những cái bẫy thực tế (quên Close(), tái dùng Writer).
gzip là io.Writer, giải nén là io.Reader
Ý tưởng cốt lõi: gzip.Writer bọc một io.Writer đích — bạn ghi dữ liệu chưa nén vào nó, nó nén rồi đẩy dữ liệu đã nén xuống đích. Ngược lại gzip.Reader bọc một io.Reader nguồn, đọc ra dữ liệu đã giải nén. Vì cả hai đều tuân giao diện io chuẩn, chúng cắm được vào mọi thứ: file, kết nối mạng, http.ResponseWriter, buffer.
w, _ := gzip.NewWriterLevel(&buf, gzip.BestCompression) // chọn mức nén
w.Write(data)
w.Close() // BẮT BUỘC: flush nốt còn lại + ghi trailer CRC. Quên = dữ liệu HỎNG.
Ba mức đặt tên sẵn: gzip.BestSpeed (=1), gzip.DefaultCompression (=-1, tương đương ~6), gzip.BestCompression (=9).

Hình 1: Trong Go, gzip.Writer là io.Writer và gzip.Reader là io.Reader — ghép vào mọi pipeline; NewWriterLevel chọn mức nén, phải Close() để flush + ghi trailer; streaming qua io.Copy giữ RAM cố định; flate/gzip/zlib cùng ruột DEFLATE khác lớp vỏ; tái dùng Writer bằng Reset + sync.Pool.
Đo thật: level, streaming, flate vs gzip
Mình nén 10MB log bằng Go, đo cả bốn khía cạnh:

Hình 2: Chạy thật trên 10.063.570 byte — (a) gzip BestSpeed(1) 1571KB/6.4x/19.3ms, Default(-1) 1454KB/6.9x/100.7ms, BestCompression(9) 1447KB/7.0x/436.5ms; (b) streaming io.Copy cho 1454228 byte, RAM cố định; (c) flate raw 1454210B vs gzip 1454228B (+18 byte header/trailer); (d) round-trip true.
- (a) Level: cùng luật lợi ích giảm dần (phần 6): BestSpeed nén 10MB trong 19ms cho 6.4x. Default (~level 6) mất 101ms cho 6.9x. BestCompression(9) mất 437ms — chậm hơn 4 lần — mà chỉ nhỏ hơn Default chưa tới 0.5%. Đúng như phần 6 đã đo với gzip CLI: cuối thang level, bạn trả rất nhiều thời gian cho rất ít dung lượng. Trong code, chọn level theo ngữ cảnh (throughput → BestSpeed, asset tĩnh nén sẵn → BestCompression).
- (b) Streaming — điểm mạnh lớn nhất:
io.Copy(gzip.NewWriter(dst), src)đẩy dữ liệu từng chunk từ nguồn qua bộ nén xuống đích, không nạp cả dữ liệu vào RAM. Kết quả nén giống hệt (1.454.228 byte), nhưng bộ nhớ dùng cố định (buffer nội bộ vài chục KB) bất kể dữ liệu lớn cỡ nào. Đây là cách bạn nén một file 10GB, hay nén response HTTP đang stream, mà không làm hết RAM. So với nén cả buffer (gzip.compress(toàn_bộ)), streaming là lựa chọn đúng cho dữ liệu lớn hoặc luồng. - (c) flate vs gzip — đúng phần header như phần 5:
compress/flate(DEFLATE raw) cho 1.454.210 byte,compress/gzipcho 1.454.228 — chênh đúng 18 byte (10 header + 8 trailer CRC/ISIZE), khớp chính xác những gì ta mổ ở phần 5. Chọn gói theo định dạng:flatekhi cần nhỏ nhất và tự lo khung,gzipcho file.gz/HTTP,zlibcho các giao thức dùng adler32. - (d) Round-trip và cái bẫy Close():
gzip.NewReader+io.ReadAllgiải nén ra đúng dữ liệu gốc (bytes.Equal = true). Nhưng cái bẫy phổ biến nhất: quênw.Close().gzip.Writerđệm dữ liệu, và chỉ khiClose()nó mới flush phần cuối và ghi trailer CRC. QuênClose()là file nén thiếu đuôi — giải nén sẽ lỗi hoặc thiếu dữ liệu. Luôndefer w.Close()(và với file,Close()writer trước khi đọc lại).
Đánh đổi cần cân nhắc
defer w.Close() chưa đủ khi bạn cần kiểm tra lỗi hoặc đọc lại ngay. Close() có thể trả lỗi (ví dụ đích ghi thất bại lúc flush), mà defer w.Close() bỏ qua giá trị trả về. Với dữ liệu quan trọng, gọi Close() tường minh và kiểm lỗi. Và nếu bạn nén vào một buffer rồi đọc lại trong cùng hàm, phải Close() trước khi đọc — nếu không phần cuối chưa được flush và bạn đọc thiếu. Đây là nguồn bug "dữ liệu nén thiếu vài byte cuối" rất khó lần.
Tái dùng Writer bằng Reset + sync.Pool để giảm cấp phát. Một server nén hàng nghìn response mỗi giây mà tạo gzip.NewWriter mới mỗi lần sẽ tạo áp lực GC lớn (mỗi writer cấp phát buffer nội bộ). Dùng w.Reset(dst) để tái dùng một writer cho đích mới, kết hợp sync.Pool để gom writer nhàn rỗi. Đây là tối ưu chuẩn trong middleware nén HTTP của Go. Đo trước khi tối ưu (loạt Debug), nhưng với đường nóng thì đây là mẹo đáng.
Thư viện chuẩn ổn, nhưng klauspost/compress nhanh hơn đáng kể. compress/gzip của Go chính xác và đủ dùng, nhưng thư viện bên thứ ba github.com/klauspost/compress (thay thế tương thích API) thường nhanh hơn nhiều (SIMD, thuật toán tối ưu), và còn cung cấp zstd/s2 thuần Go. Với dịch vụ nén khối lượng lớn, cân nhắc nó — API gần như y hệt nên đổi rất dễ. Với nhu cầu thường, thư viện chuẩn là đủ và không thêm phụ thuộc.
Ba ý mang về
- Nén trong Go là io.Writer/io.Reader: đo thật gzip BestSpeed nén 10MB trong 19ms (6.4x), BestCompression 437ms (7.0x) — cùng luật lợi ích giảm dần (phần 6); chọn level theo ngữ cảnh và luôn
Close()để flush + ghi trailer (quên = dữ liệu hỏng). - Streaming giữ RAM cố định: đo thật
io.Copyquagzip.Writercho cùng kết quả nhưng bộ nhớ không phụ thuộc kích thước — nén file 10GB với vài chục KB; đây là cách nén luồng/dữ liệu lớn không làm hết RAM. - flate/gzip/zlib cùng ruột, khác vỏ: đo thật flate raw vs gzip chênh đúng 18 byte header/trailer (khớp phần 5); chọn gói theo định dạng, tái dùng Writer bằng
Reset+sync.Poolcho đường nóng, và cân nhắcklauspost/compresskhi cần nhanh hơn.
Nguồn
- Go docs — compress/gzip: https://pkg.go.dev/compress/gzip
- Go docs — compress/flate: https://pkg.go.dev/compress/flate
- klauspost/compress — thư viện nén Go hiệu năng cao: https://github.com/klauspost/compress
Phần sau — bài cuối loạt — ta gom mọi thứ thành một khung quyết định: chọn thuật toán và mức nén theo tỉ lệ, tốc độ, bộ nhớ và tính tương thích; kèm tổng kết cả 12 phần.