Streamの扱い方

株式会社サイバーエージェント

AWA株式会社

辻 純平

サイトの画像をダウンロードして、ちょっと画質とかサイズとか

いじりたいなぁ

こんなコードをよく見る

func getImage(url string) (image.Image, error) {
    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    // 一度[]bytesに変換
    data, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    // jpeg.Decode()で扱える形に変換
    buf := bytes.NewBuffer(data)

    img, err := jpeg.Decode(buf)
    if err != nil {
        return nil, err
    }
    return img, nil
}

何が問題?

一度ioutil.ReadAll()する癖がついてしまってる

func getImage(url string) (image.Image, error) {
    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    img, err := jpeg.Decode(resp.Body)
    if err != nil {
        return nil, err
    }
    return img, nil
}

これで十分

func getImage(url string) (image.Image, error) {
    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    img, err := jpeg.Decode(resp.Body)
    if err != nil {
        return nil, err
    }
    return img, nil
}

jpeg.Decodeの引数はio.Reader

resp.Bodyはio.ReadCloser

なぜStreamを使うべきか

1. メモリの効率化

  • ioutil.ReadAllで全て[]byteに変換すると、その分メモリを消費する&アロケーションやGCに依る速度低下が起きる
  • io.Readerやio.Writerは各chunkの処理に同じバイトを使いまわすので、メモリの効率が良い。

2. 標準パッケージの多くがサポート

  • io.Reader、io.Writerがメソッドの少ない非常に良いインターフェース
  • 多くの標準パッケージがインタフェースを実装していたり、引数として扱える形でサポートしてる
    • ex) json, bytes.Buffer, os.File, image, base64, cipher

推測するな、計測せよ

[]byte変換

Stream

func buf1(body io.Reader, w http.ResponseWriter) {
    b, err := ioutil.ReadAll(body)
    if err != nil {
        panic(err)
    }
    w.Write(b)
}
func buf2(body io.Reader, w http.ResponseWriter) {
    _, err := io.Copy(w, body)
    if err != nil {
        panic(err)
    }
}

ProxyサーバがS3から画像データを

取得し、レスポンスを返すケース

var image []byte

func init() {
    body, _ := os.Open("./image.jpg")
    image, _ = ioutil.ReadAll(body)
    body.Close()
}

func BenchmarkImage1(b *testing.B) {
    for i := 0; i < b.N; i++ {
        w := httptest.NewRecorder()
        r := bytes.NewReader(image)
        buf1(r, w)
    }
}

func BenchmarkImage2(b *testing.B) {
    for i := 0; i < b.N; i++ {
        w := httptest.NewRecorder()
        r := bytes.NewReader(image)
        buf2(r, w)
    }
}

ベンチマーク

BenchmarkImage1-8         100000         18950 ns/op       92960 B/op          16 allocs/op
BenchmarkImage2-8         200000          6397 ns/op       29472 B/op          10 allocs/op

結果

BenchmarkImage1-8           3000        549892 ns/op     3062820 B/op          21 allocs/op
BenchmarkImage2-8          10000        172470 ns/op      967713 B/op          10 allocs/op
BenchmarkImage1-8           1000       1906131 ns/op    11844643 B/op          23 allocs/op
BenchmarkImage2-8           3000        568504 ns/op     3458080 B/op          10 allocs/op

940KB画像

3MB画像

26KB画像

具体的な実装

json.Unmarshalでなくjson.NewDecoderを使おう

NG

OK

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    var u User
    data, _ := ioutil.ReadAll(req.Body)
    err = json.Unmarshal(data, &u)
    ...
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    var u User
    err = json.NewDecoder(req.Body).Decode(&u)
    ...
})

リクエストをデコードするケース

io.Copyを使おう

NG

OK

func saveImage1(url string) {
    response, _ := http.Get(url)
    defer response.Body.Close()

    file, _ := os.Create("save.jpg")
    defer file.Close()

    buf, _ := ioutil.ReadAll(response.Body)

    file.Write(buf)
}
func saveImage2(url string) {
    response, _ := http.Get(url)
    defer response.Body.Close()

    file, _ := os.Create("save.jpg")
    defer file.Close()

    io.Copy(file, response.Body)
}

画像を保存するケース

bytes.Bufferより

io.Pipeを使おう

NG

OK

func pipe1(v User) {
    var buf bytes.Buffer

    err := json.NewEncoder(&buf).Encode(&v)

    resp, err := http.Post("example.com", "application/json", &buf)
}
func pipe2(v User) {
    pr, pw := io.Pipe()

    go func() {
        err := json.NewEncoder(pw).Encode(&v)
        pw.Close()
    }()

    resp, err := http.Post("example.com", "application/json", pr)
}

大きめのデータを転送したいケース

io.Readerのまま使おう

NG

OK

func LoadGzippedJSON(r io.Reader, v interface{}) error {
    data, err := ioutil.ReadAll(r)
    if err != nil {
        return err
    }
    // oh wait, we need a Reader again.. 
    raw := bytes.NewBuffer(data)
    unz, err := gzip.NewReader(raw)
    if err != nil {
        return err
    }
    buf, err := ioutil.ReadAll(unz)
    if err != nil {
        return err
    }
    return json.Unmarshal(buf, &v)
}
func LoadGzippedJSON(r io.Reader, v interface{}) error {
    raw, err := gzip.NewReader(r)
    if err != nil {
        return err
    }
    return json.NewDecoder(raw).Decode(&v)
}

まとめ

  • io.Reader, io.Writerを引数としているものはStreamのままで扱う
  • ioutil.ReadAllなどを使ってわざわざ[]byteには変換しない

Streamの扱い方

By jun06t

Streamの扱い方

  • 11,006