HTTPリクエストのレスポンスをキャッシュする
HTTPキャッシュ(cache)は、ウェブページなど一度目のリクエストで取得したコンテンツを保存しておき、
次回以降のリクエスト送信時にコンテンツに変更がなければ再利用するというもの。
リクエストを受け取るサーバー側は処理が軽減され負荷が減り、
リクエストを送るクライアント側も通信量が減るので受け取るまでの時間短縮になる。
以下のコードは、 1回目のレスポンスヘッダーの ETag と Last-Modified を それぞれ2回目のリクエストヘッダーの If-None-Match と If-Modified-Since に設定して キャッシュを再利用しても良いかサーバーに確認するもの
この他にも「そもそもキャッシュしてもいいコンテンツなのか」「キャッシュの有効期限はいつまでか」など
考慮すべきことはたくさんあるけどETagだけでもセットしておくと 304 Not Modified(変更がない) が返ってくる。
HTTPキャッシュの仕組みは HTTP キャッシュに、HTTPヘッダーにつては HTTP ヘッダー - HTTP | MDN に載ってる
package main import ( "fmt" "io/ioutil" "log" "net/http" "time" ) type Cache struct { Resp *http.Response Body []byte } var ( // キャッシュを保存しておくためのマップ // データベースを使うのが現実的 cacheMap = make(map[string]*Cache) ) func sendRequest(rawurl string) ([]byte, *http.Response, error) { req, err := http.NewRequest(http.MethodGet, rawurl, nil) if err != nil { return nil, nil, err } cache := cacheMap[rawurl] if cache != nil { // rawurlでキャッシュが存在していたら // 検証するためのヘッダーをリクエストヘッダーにセットする eTag := cache.Resp.Header.Get("etag") req.Header.Set("if-none-match", eTag) lastModified := cache.Resp.Header.Get("last-modified") req.Header.Set("if-modified-since", lastModified) } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, nil, err } body, _ := ioutil.ReadAll(resp.Body) fmt.Println("Status code:", resp.StatusCode) // ステータスコードが 304 (Not modified) だったらキャッシュを返す if resp.StatusCode == http.StatusNotModified { return cache.Body, cache.Resp, nil } else { cacheMap[rawurl] = &Cache{ Resp: resp, Body: body, } } return body, resp, nil } func main() { // 画像はキャッシュに対応してる場合が殆どなので試すのにいい rawurl := "https://cdn-ak.f.st-hatena.com/images/fotolife/g/golang/20181009/20181009042416.png" for i := 0; i < 2; i++ { _, _, err := sendRequest(rawurl) if err != nil { log.Fatal(err) } time.Sleep(1000 * time.Millisecond) } }
LevelDBを使ったキャッシュの保存(いろいろ不十分だけど、こんな感じで保存できる)
package main import ( "bufio" "bytes" "fmt" "io/ioutil" "log" "net/http" "net/http/httputil" "github.com/syndtr/goleveldb/leveldb" ) func main() { rawurl := "https://example.com" db, err := leveldb.OpenFile("cache", nil) if err != nil { log.Fatal(err) } defer db.Close() req, err := http.NewRequest(http.MethodGet, rawurl, nil) if err != nil { log.Fatal(err) } resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } // *http.Response を []byte 化する cache, err := httputil.DumpResponse(resp, true) if err != nil { log.Fatal(err) } // データベースに保存する if err := db.Put([]byte(rawurl), cache, nil); err != nil { log.Fatal(err) } // データベースから取り出す if data, err := db.Get([]byte(rawurl), nil); err != nil { log.Fatal(err) } else { // 保存した []byte を *http.Response に戻す cache, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(data)), req) if err != nil { log.Fatal(err) } body, _ := ioutil.ReadAll(cache.Body) fmt.Printf("%#v\n%s\n", cache, string(body)) } }