Golang Cafe #11 まとめ archiveパッケージ

2014/01/05に開催された「Golang Cafe #11」についてのまとめです。
今回は私が気になったところを質問した後、archiveパッケージtarパッケージzipパッケージをみていきました。
サンプルコード+TakashiYokoyama氏が準備してくれています。

私が質問した内容

先日まで写経+αを行っていた

プログラミング言語Goフレーズブック

プログラミング言語Goフレーズブック

  • 作者: David Chisnall,デイビッド・チズナール,柴田芳樹
  • 出版社/メーカー: ピアソン桐原
  • 発売日: 2012/10/04
  • メディア: 単行本(ソフトカバー)
  • 購入: 1人 クリック: 5回
  • この商品を含むブログを見る
の204ページあたりの下記のコードについてです。(一部私が修正しています)

package example

type Public interface {
    Name() string
}

type Example struct {
    name string
}

//func (e Example) Nme() string {
func (e Example) Name() string {
    return e.name
}

// func NewExample() Example {
//     return Example{"No Name"}
// }

func NewExample() Public {
    return Example{"No Name"}
}

func NewExample2() Public {
    e := Example{"No Name"}
    e.(Public)

    return e
}

Go言語ではinterfaceに定義されるメソッドを自身の型に定義することでinterfaceを実装したことになるのですが、上記のコードでコメントアウトした行の様にメソッド名を間違っていた場合そのバグに気づくのに多大な時間を要する可能性があります。
それを防ぐために上記のような実装はいかがでしょう、ということらしいです。
1つ目の方法はNewExample関数のようにインスタンス生成時に戻り値をinterfaceにする方法です。
2つ目の方法はNewExample2関数のように生成したインスタンスに対してType assertionsを使用して確認しようというものです。
いずれもビルド時にエラーを発見しようというものですが、2つ目の方法はうまく行きません。
Type assertionsのページにも書かれてあるとおり、上記のコードでいうeがinterfaceでなくてはなりません。
正解はどのようなコードになるのか考えてみましが、関数の戻り値としてPublic型のinterfaceを返すのならNewExample関数で十分なのではないかと思います。

tarパッケージ

tarパッケージは所謂tarコマンドと同じようなことをすることができます。

圧縮
func main() {
    var file *os.File
    var err error

    if file, err = os.Create("output/sample.tar"); err != nil {
        panic(err)
    }
    defer file.Close()

    // Closeをしないと、展開できなくなる可能性がある。
    // アーカイブユーティリティ(MacのFinderから)だとエラーで展開されない)    
    tw := tar.NewWriter(file)
    defer tw.Close()

    var filepaths = []string {
        "files/b0044482_1413812.jpg",
        "files/dart_flight_school.png",
        "files/golang.txt",
    }

    for _, filepath := range filepaths {
        body := readFile(filepath)
        if body != nil {
            hdr := &tar.Header {
                Name: path.Base(filepath),
                // ディレクトリ階層を維持したままにする場合はそのまま
                // Name: filepath,
                Size: int64(len(body)),
            }
            if err := tw.WriteHeader(hdr); err != nil {
                println(err)
            }
            if _, err := tw.Write(body); err != nil {
                println(err)
            }
        }
    }

}

手順は非常に簡単で、

  1. tar.NewWriterメソッドでWriterを作成します。引数にはio.WriterをとるのでファイルでなくてもWrite(p []byte) (n int, err error)なメソッドを実装しているものなら何でも構いません。
  2. それぞれのファイルを対象にHeaderWriteHeaderメソッドで書き込みます。
  3. それぞれのファイルを対象にバイトデータをWriteメソッドで書き込みます。

だけです。
ディレクトリ階層を維持したままにするには、HeaderのNameを指定する際にディレクトリから指定するだけでOKです。

解凍
func main() {
    var file *os.File
    var err error

    if file, err = os.Open("output/sample.tar"); err != nil {
        log.Fatalln(err)
    }
    defer file.Close()

    // ReaderはClose()はない。
    reader := tar.NewReader(file)

    var header *tar.Header
    for {
        header, err = reader.Next()
        if err == io.EOF {
            // ファイルの最後
            break
        }
        if err != nil {
            log.Fatalln(err)
        }

        buf := new(bytes.Buffer)
        if _, err = io.Copy(buf, reader); err != nil {
            log.Fatalln(err)
        }

        if err = ioutil.WriteFile("output/" + header.Name, buf.Bytes(), 0755); err != nil {
            log.Fatal(err)
        }
    }
}
  1. NewReaderメソッドでReaderを作成します。引数にはio.ReaderをとるのでファイルでなくてもRead(p []byte) (n int, err error)なメソッドを実装しているものなら何でも構いません。
  2. Nextメソッドでtarファイル内に含まれている各ファイルのヘッダを順次取得でき、またデータを順次処理していきます。
  3. Readメソッドで読み込むか、他のパッケージのメソッドに渡して終了です。

ただこのサンプルではディレクトリ階層を持つファイルを含んでいる場合は実行時エラーとなるので、

if err = ioutil.WriteFile("output/" + header.Name, buf.Bytes(), 0755); err != nil {
    log.Fatal(err)
}

の部分を

s := "output/" + header.Name
d, _ := filepath.Split(s)

if _, err = os.Stat(d); err != nil {
    os.MkdirAll(d, 0755)
}

if err = ioutil.WriteFile(s, buf.Bytes(), 0755); err != nil {
    log.Fatal(err)
}

のようにディレクトリが存在していない場合は作成してあげると正常に終了します。(エラー処理は適当です)

zipパッケージ

zipパッケージは所謂zipコマンドと同じようなことをすることができます。

圧縮
func main() {
    var file *os.File
    var err error

    if file, err = os.Create("output/sample.zip"); err != nil {
        log.Fatalln(err)
    }
    defer file.Close()

    zw := zip.NewWriter(file)

    var filepaths = []string {
        "files/b0044482_1413812.jpg",
        "files/dart_flight_school.png",
        "files/golang.txt",
    }

    var f io.Writer
    for _, filepath := range filepaths {
        body := readFile(filepath)
        if body != nil {
//            f, err = zw.Create(path.Base(filepath))
            f, err = zw.Create(filepath)
            if err != nil {
                log.Fatal(err)
            }
            _, err = f.Write(body)
            if err != nil {
                log.Fatal(err)
            }
        }
    }

    // zip.WriterはClose時にエラーチェックをすること。
    err = zw.Close()
    if err != nil {
        log.Fatal(err)
    }
}

手順については先程のtarの場合と非常によく似ていますが、ファイルデータの書き出し部分が少し異なります。

  1. NewWriterメソッドでWriterを作成します。引数にはio.WriterをとるのでファイルでなくてもWrite(p []byte) (n int, err error)なメソッドを実装しているものなら何でも構いません。
  2. 圧縮ファイル内でのファイル名(圧縮ファイル内でのディレクトリを含めても良い、ただし相対パスにすること)を引数にCreateメソッドを呼ぶとファイル名のデータを書き出すためのWriterが取得できます。
  3. 取得したWriterに対して実際のデータを書き出します。
解凍
func main() {
    reader, err := zip.OpenReader("output/sample.zip")
    if err != nil {
        log.Fatalln(err)
    }
    defer reader.Close()

    var rc io.ReadCloser
    for _, f := range reader.File {
        rc, err = f.Open()
        if err != nil {
            log.Fatal(err)
        }

        buf := new(bytes.Buffer)
        _, err = io.Copy(buf, rc)
        if err != nil {
            log.Fatal(err)
        }

        s := "output/" + f.Name
        d, _ := filepath.Split(s)

        if _, err = os.Stat(d); err != nil {
            os.MkdirAll(d, 0755)
        }

        if err = ioutil.WriteFile(s, buf.Bytes(), 0755); err != nil {
            log.Fatal(err)
        }
        rc.Close()
    }
}

zipファイルを解凍する場合はtarの場合とは手順が少し異なりますが、簡単に行えます。

  1. 引数に解凍対象のファイルパスを指定してOpenReaderメソッドを実行します。戻り値にはReadCloserというものが返ります。
  2. ReadCloserは内部にReaderを持っており、そのFileフィールドにアクセスすることでzipファイルに含まれるファイルにアクセスすることができます。
  3. 個々のファイルのOpenメソッドを呼ぶとio.ReadCloserが取得できます。
  4. io.ReadCloserio.Readerを実装しているので、適当なWrite系のメソッドに渡すことでファイルのデータを書き出します。

zip圧縮時にはパスワードを設定することができますが、Go言語の現在のバージョンではサポートされていないようです。

まとめ

tarパッケージにしろzipパッケージにしろ、比較的簡単に扱えることが分かりました。
ただかなり慣れてはきましたが、Go言語の特徴の1つであるinterfaceを実装していることを明示的に示さなくてよい(必要なメソッドを定義するだけで実装していることになる)という点や、構造体やinterface内にそれぞれ別の構造体やinterfaceを定義しおくだけで透過的にメソッドにアクセスできる(内部に定義した構造体やinterfaceを継承しているようなイメージ)という点が比較的わかりやすく出ているサンプルになっていると思いました。


次回は引き続きパッケージをみていくようです。