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