Gzip 操作について覚え書き
この記事を見て「んん?」となったので,覚え書きとして gzip
パッケージについて紹介する。
リンク先の記事で挙げられているコードは以下の通り。
func makeGzip(body string) []byte {
var b bytes.Buffer
gw := gzip.NewWriter(&b)
_, err := gw.Write([]byte(body)); if err != nil {
...
}
gw.Flush()
gw.Close()
return b.Bytes()
}
ここで gw.Close()
関数を defer 指定すると返ってくるバイト列が不完全なデータになってしまう,という話。
これは,リンク先の記事で指摘されている通り, gzip
.Writer.Close()
関数で gzip のフッタデータをフラッシュしているからである。
つまり defer で指定した関数は return 後に駆動するため b.Bytes()
関数を呼び出した時点ではまだ不完全なデータということになる1。
ここでちょっと考える。
関数の再利用性を考えるのなら,関数内でバッファを生成してバッファ処理の結果を返すのはあまり筋がよろしくない。 また圧縮データを書き込む先はメモリバッファじゃなくてファイルかもしれない。
ゆえに関数をこう書き換える。
func makeGzip(dst io.Writer, content []byte) error {
zw, err := gzip.NewWriterLevel(dst, gzip.BestCompression)
if err != nil {
return err
}
defer zw.Close()
if _, err := zw.Write(content); err != nil {
return err
}
return nil
}
つまり圧縮データの書き込み先である Writer
を引数で指定するのである。
これなら生成した gzip
.Writer.Close()
関数を問題なく defer で指定できる。
これを踏まえて完全なコードは以下のようになる。
package main
import (
"compress/gzip"
"fmt"
"io"
"os"
)
func makeGzip(dst io.Writer, content []byte) error {
zw, err := gzip.NewWriterLevel(dst, gzip.BestCompression)
if err != nil {
return err
}
defer zw.Close()
if _, err := zw.Write(content); err != nil {
return err
}
return nil
}
func main() {
content := []byte("Hello world\n")
file, err := os.Create("test.txt.gz")
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
defer file.Close()
if err := makeGzip(file, content); err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
}
このコードでは圧縮データの書き込む先をファイルにしている。
もちろん書き込み先を bytes
.Buffer
に置き換えることもできる。
このようにインスタンスの生存期間を意識することで Go 言語の得意なパターンに嵌めることが容易になる。
ついでに対となる読み込み処理のコードも示しておこう。 ここでは復元したデータを標準出力に直接出力している。
package main
import (
"compress/gzip"
"fmt"
"io"
"os"
)
func readGzip(dst io.Writer, src io.Reader) error {
zr, err := gzip.NewReader(src)
if err != nil {
return err
}
defer zr.Close()
io.Copy(dst, zr)
return nil
}
func main() {
file, err := os.Open("test.txt.gz")
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
defer file.Close()
if err := readGzip(os.Stdout, file); err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
}
ところで,ファイル操作では生のデータを直接 gzip 圧縮するシチュエーションは少なく,大抵は tar と組み合わせることになる。 そこで tar と組み合わせ,指定フォルダ直下の複数ファイルを gzip 圧縮するコードも以下に示しておく。
package main
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
)
func makeTarGzip(dst io.Writer, rt string) error {
zw, err := gzip.NewWriterLevel(dst, gzip.BestCompression)
if err != nil {
return err
}
defer zw.Close()
tw := tar.NewWriter(zw)
defer tw.Close()
filepath.Walk(rt, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
fmt.Println(path)
hd, e := tar.FileInfoHeader(info, "")
if e != nil {
return e
}
content, e := ioutil.ReadFile(path)
if e != nil {
return e
}
if e := tw.WriteHeader(hd); e != nil {
return e
}
if _, e := tw.Write(content); e != nil {
return e
}
return nil
})
if err != nil {
return err
}
return nil
}
func main() {
file, err := os.Create("test.tar.gz")
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
defer file.Close()
if err := makeTarGzip(file, "./"); err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
}
【追記】別解あります
実は最初の makeGzip()
には別解がある。
要するに gzip
処理部分を関数スコープで囲ってしまえばいいのだ。
実際にはこんな感じ。
func makeGzip(body []byte) ([]byte, error) {
var b bytes.Buffer
err := func() error {
gw := gzip.NewWriter(&b)
defer gw.Close()
if _, err := gw.Write(body); err != nil {
return err
}
return nil
}()
return b.Bytes(), err
}
完全なコードはこんな感じになる。
package main
import (
"bytes"
"compress/gzip"
"fmt"
"os"
)
func makeGzip(body []byte) ([]byte, error) {
var b bytes.Buffer
err := func() error {
gw := gzip.NewWriter(&b)
defer gw.Close()
if _, err := gw.Write(body); err != nil {
return err
}
return nil
}()
return b.Bytes(), err
}
func main() {
content := []byte("Hello world\n")
file, err := os.Create("test.txt.gz")
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
defer file.Close()
z, err := makeGzip(content)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
if _, err := file.Write(z); err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
}
このように(defer を含む)一連の処理を関数スコープで囲うやり方は,条件分岐や繰り返し処理の中で役に立つこともあるだろう。
ブックマーク
参考図書
- プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
- 丸善出版 2016-06-20
- 単行本(ソフトカバー)
- 4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN), 9784621300251 (ISBN)
- 評価
著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。