Newer posts are loading.
You are at the newest post.
Click here to check if anything new just came in.

Gzip 操作について覚え書き - golang

この記事を見て「んん?」となったので,覚え書きとして 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 のフッタデータをフラッシュしているからである。

Close closes the Writer by flushing any unwritten data to the underlying io.Writer and writing the GZIP footer. It does not close the underlying io.Writer.
via gzip - The Go Programming Language

つまり defer で指定した関数は return 後に駆動するため b.Bytes() 関数を呼び出した時点ではまだ不完全なデータということになる1

ここでちょっと考える。

関数の再利用性を考えるのなら,関数内でバッファを生成してバッファ処理の結果を返すのはあまり筋がよろしくない。 また圧縮データを書き込む先はメモリバッファじゃなくてファイルかもしれない。

ゆえに関数をこう書き換える。

func makeGzip(w io.Writer, content []byte) error {
	zw, err := gzip.NewWriterLevel(w, 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(w io.Writer, content []byte) error {
	zw, err := gzip.NewWriterLevel(w, 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 言語の得意なパターンに嵌めることが容易になる。

ところで,特にファイル操作では,生のデータを直接 gzip 圧縮するシチュエーションは少なく,大抵は tar と組み合わせることになる。 そこで tar と組み合わせ,指定フォルダ下の複数ファイルを gzip 圧縮するコードを以下に示しておく。

package main

import (
	"archive/tar"
	"compress/gzip"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
)

func makeTarGzip(w io.Writer) error {
	zw, err := gzip.NewWriterLevel(w, gzip.BestCompression)
	if err != nil {
		return err
	}
	defer zw.Close()

	tw := tar.NewWriter(zw)
	defer tw.Close()

	filepath.Walk("./", 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
	}
}

ブックマーク

参考図書

photo
プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
Alan A.A. Donovan Brian W. Kernighan 柴田 芳樹
丸善出版 2016-06-20
評価

スターティングGo言語 (CodeZine BOOKS) Go言語によるWebアプリケーション開発 Kotlinスタートブック -新しいAndroidプログラミング Docker実戦活用ガイド グッド・マス ギークのための数・論理・計算機科学

著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。

reviewed by Spiegel on 2016-07-13 (powered by G-Tools)


  1. この挙動から分かるとおり, bytes.Buffer.Bytes() 関数は,バッファの内容をそのまま返しているのではなく,内容のコピーを返している。 [return]

Don't be the product, buy the product!

Schweinderl