🤖

🤖

:gijutsu_burogu:

問題を解いて学ぶio.Writerとio.Reader(Go)

はじめに

Go ならわかるシステムプログラミングを読みました。

Goならわかるシステムプログラミング

Goならわかるシステムプログラミング

  • 作者:渋川 よしき
  • 発売日: 2017/10/23
  • メディア: 単行本(ソフトカバー)

僕は本で読みましたが、元々は Web で連載していたみたいで無料で読むことができます。(本の方がコンテンツは充実しています)

ascii.jp

この本で学んだことを小出しにアウトプットしていきたいと思います。 まずは、I/O 関係の基本の io.Writerio.Reader を紹介します。

io.Writer とは

全ての Write 処理の基本となる インターフェース です。 Go のインターフェースは、構造体などの具象型が満たすべき仕様(持つべきメソッド)を表現するための言語機能です。 OS がカーネルのレイヤーでさまざまな入出力をファイルと同様に扱えるファイルディスクリプタという仕組みがあるのですが、このインターフェースではそのような共通化の仕組みを言語レベルで行っており、OS による差異も吸収しています。

type Writer interface {
    Write(p []byte) (n int, err error)
}

例えば、io.Writerインターフェースで言えば、引数[]byte、返り値 int, errorを持つメソッドWrite を実装する構造体であれば、io.Writerインターフェースを満たしていると言えます。

os.Stdoutos.Fileio.Writer インターフェースを満たしています。

io.Reader とは

全ての Read 処理の基本となる インターフェース です。

type Reader interface {
    Read(p []byte) (n int, err error)
}
p := make([]byte, 1024) // メモリ確保
n, err := file.Read(p) // メモリに格納, 読み込みサイズを返す

// こちらが使われることが多い
buffer, err := ioutil.ReadAll(reader) // メモリを気にせず全て読む便利メソッド

問題

ASCII.jp:Goならわかるシステムプログラミング より引用します。 連載を読みながらでないと隠れた文脈を理解できないので、実際に解くのはハードルが高いです。 解答を見て、雰囲気を理解するだけでも良いと思います。

ファイルのコピー

古いファイル(old.txt)を新しいファイル(new.txt)にコピーしてみましょう。

シェルでのcpコマンドのようなものです。

答え

package main

import (
    "io"
    "os"
)

func main() {
    // エラー処理は省略

    // のちにio.Readerとして使われるos.File
    oldFile, _ := os.Open("old.txt")

    // 終了したらファイルディスクリプタを閉じる
    defer oldFile.Close()

    // のちにio.Writerとして使われるos.File
    newFile, _ := os.Create("new.txt")

    // 終了したらファイルディスクリプタを閉じる
    defer newFile.Close()

    // oldFileからnewFileへコピー
    io.Copy(newFile, oldFile)
}

io.Copyio.Readerからio.Writerへそのままデータを渡したい時に使います。 サイズを指定指定したいときは、io.CopyNを使います。

zip ファイルをウェブサーバからダウンロード

zip ファイルの出力先は単なる io.Writer です。 ウェブサーバにブラウザでアクセスしたらファイルがダウンロードさ1れるようにしてみましょう。 この場合は、Content-Type ヘッダーを使ってファイルの種類が zip ファイルであることをブラウザに教えてあげる必要があります。

答え

package main

import (
    "archive/zip"
    "io"
    "net/http"
    "strings"
)

func handler(w http.ResponseWriter, r *http.Request) {
    // エラー処理は省略

    // ブラウザにzipファイルであることを伝える
    w.Header().Set("Content-Type", "application/zip")
    // ファイル名を指定
    w.Header().Set("Content-Disposition", "attachment; filename-ascii_sample.zip")

    // 出力先のWriterをzip.NewWriterに渡すとzipファイル書き込み用の構造体ができる
    zipWriter := zip.NewWriter(w)
    // 終了したらファイルディスクリプタを閉じる
    defer zipWriter.Close()

    // zipファイルに含めるファイルを作成
    a, _ := zipWriter.Create("a.txt")
    io.Copy(a, strings.NewReader("1つめのファイルのテキストです"))

    // zipファイルに含めるファイルを作成
    b, _ := zipWriter.Create("b.txt")
    io.Copy(b, strings.NewReader("2つめのファイルのテキストです"))
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

まとめ

私は、io.Writerio.Readerはファイルディスクリプタを言語レベルで実現し、Go実行環境による差異を吸収し抽象化したものと捉えています。 ファイルディスクリプタとはOSがカーネルレイヤーで入出力を抽象化する仕組みであり、これによってファイルや標準入出力など外部入出力が異なっていても差異を吸収して同じように扱うことができます。 io.Writerio.Readerは、[]byteに変換せずストリームのように扱うことができるので今後また記事にまとめます。