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).

Ảnh chụp đoạn mã nền tối minh hoạ nén trong Go compress gzip level và streaming io Writer io Reader nhớ Close nén luồng không nạp hết vào RAM, gzip NewWriterLevel chọn mức nén w gzip NewWriterLevel buf gzip BestSpeed bằng 1 nhanh gzip DefaultCompression bằng -1 khoảng 6 gzip BestCompression bằng 9 chậm w Write data w Close bắt buộc flush nốt còn lại cộng ghi trailer CRC quên bằng dữ liệu hỏng, streaming nén luồng qua io Copy RAM cố định gw gzip NewWriter dst dst là io Writer file net http ResponseWriter io Copy gw src src là io Reader đẩy từng chunk qua không nạp cả file vào RAM nén file 10GB với bộ nhớ cố định vài chục KB gzip Writer là io Writer gzip Reader là io Reader ghép vào mọi pipeline, flate DEFLATE raw vs gzip vs zlib compress flate DEFLATE raw không header nhỏ nhất compress gzip cộng header 10B cộng CRC32 ISIZE 8B dùng cho file gz HTTP compress zlib cộng header 2B cộng adler32 4B dùng trong nhiều giao thức cùng ruột DEFLATE bài 05 chọn gói theo định dạng cần, giải nén và tái dùng Writer gr gzip NewReader src dec io ReadAll gr gr Close server nén nhiều response dùng w Reset dst cộng sync Pool tái dùng Writer thay vì tạo mới mỗi lần giảm cấp phát GC klauspost compress nhanh hơn nữa

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:

Ảnh chụp bảng kết quả chạy thật nén trong Go output thật go-lab Go compress gzip cộng flate 10063570 byte log, a gzip NewWriterLevel size và time từng level gzip level size B ratio time ms BestSpeed 1 1571345 6.4x 19.3 nhanh nhất Default -1 1454228 6.9x 100.7 mặc định khoảng level 6 BestCompression 9 1447430 7.0x 436.5 Default tới 9 nhỏ hơn dưới 0.5 phần trăm chậm 4x, b streaming io Copy qua gzip Writer io Copy gzip NewWriter dst src 1454228 byte RAM không phụ thuộc kích thước dữ liệu nén file 10GB với vài chục KB bộ nhớ, c flate raw vs gzip chênh đúng phần header CRC flate raw 1454210 byte gzip 1454228 byte cộng 18 byte bằng 10 header cộng 8 trailer đúng bài 05, d round-trip gzip NewReader bytes Equal giải nén gốc bằng true nén giải ra đúng dữ liệu gốc lưu ý quên w Close là mất nốt cuối cộng trailer dữ liệu hỏng lỗi hay gặp

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/gzip cho 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: flate khi cần nhỏ nhất và tự lo khung, gzip cho file .gz/HTTP, zlib cho các giao thức dùng adler32.
  • (d) Round-trip và cái bẫy Close(): gzip.NewReader + io.ReadAll giả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ên w.Close(). gzip.Writer đệm dữ liệu, và chỉ khi Close() nó mới flush phần cuối và ghi trailer CRC. Quên Close() là file nén thiếu đuôi — giải nén sẽ lỗi hoặc thiếu dữ liệu. Luôn defer 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ề

  1. 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).
  2. Streaming giữ RAM cố định: đo thật io.Copy qua gzip.Writer cho 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.
  3. 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.Pool cho đường nóng, và cân nhắc klauspost/compress khi cần nhanh hơn.

Nguồn

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.