書籍「Real World HTTP」の学習効率の高さについて

今更ながら、Real World HTTP を読んでみたので、
学習効率という視点で感想を書いてみようと思う。

www.oreilly.co.jp

対象となる読者が広い

この書籍のテーマは HTTP なので、対象となる読者はかなり多いはず。

さらに、書籍でも言及されているが、
HTTP のようなプロトコルは陳腐化しづらい知識なので、
一度身につけるとずっと役に立つ。

サーバサイドエンジニアに限らず、
アプリエンジニア、
WebFE エンジニア、
HTTP に触れるエンジニアであれば、目を通しておいた方がいい。

特に HTTP Header に指定する属性と値の知識は、実際のサービス開発で必須となることが多い。

サーバであっても、
クライアントであっても、
「ヘッダーに、この属性つけなくて良いんですか?」という一言が、
サービスのセキュリティやパフォーマンスを向上させるかもしれない。

対象読者が幅広く、陳腐化しづらい書籍なので、
この書籍を通して学んだ内容が無駄になることは少ない。
効率の良い書籍だと思う。

HTTPについて体系的に学べるので、
新卒などのWeb開発に関わって日が浅いエンジニアには是非読んでもらいたい。


スラスラ読める

そして意外にも(?)普段から HTTP に触れているエンジニアにもオススメできる。

なぜかというと、
この書籍の何割かの内容は、
すでに知っていたり、過去にどっかで調べていることがあるからだ。

「これあったなー」とか「こんなオプションもあったのか」という、
既存知識の部分的な補完や思い出しがメインになるはず。
なので、ページ数のわりにはスラスラ読める。

そして、たまに「これ全然知らなかった」というものが出てくる。

普段から HTTP に触れているエンジニアこそ、
時間を無駄にすることなく、
必要な情報だけ拾うことができると思う。


好きなところから読める

自分は普段「コードの書き方」みたいな書籍を読むことが多い。

こういった書籍は、
第1章でリファクタリング対象システムの仕様を説明し、
第2章から順にリファクタリングを進めるという流れになっていることがある。

第1章を読まなければ、第2章は理解できないし、
第2章のリファクタリング結果が理解できなければ、
第3章のリファクタリング内容も理解できない。
時間が空くと、第1章の仕様を忘れていることだってある。

一方、この書籍は、主に HTTP/1.0 ~ HTTP2 までを時系列にまとめているが、
前後の関係性は薄いので、
気になるところだけ読めばいい。

読む内容を選択でき、
容易に途中から読み進められるので、
こういった点もまた学習効率がいいなと感じる。


一通り目を通すことを推奨

この書籍は特定の章だけ読むことができる。

しかし、HTTP は仕様が多いので、
「実は知らなかったこと」を見逃さないためにも、
全く読まないのではなく、
一通り目を通すことを推奨する。

ページをパラパラめくって、知らない単語周辺を読むだけでいい。
ページをパラパラして、知識が増えるのであれば、パラパラした方がいい。

さらに、この書籍には、ちょっとした面白い内容が載っていたりする。

リファラーのスペルミスだったり
ダウンロードページの広告のことだったり

サラッと書いてある文章や注釈を1つ1つ読んで覚えておくと、
エンジニア飲みの時の話のネタになるかもしれない。


Go言語は関係ない

書籍の目次を見ると、Go言語による実装の章が目に入ると思う。
これを見て「Go言語知らないからな・・・」と敬遠するのはやめて欲しい。

この章は HTTP のリクエスト、レスポンスを実装しているだけなので、
正直、読み飛ばしても問題ない。
ちなみに、Go言語による実装の章を読まない場合、60ページ分削減されるので、
より効率よく HTTP を学ぶことができる。

仮に読もうとした場合でも、
実装対象がシンプルなので、サラッと見ても大体何をやっているのかが分かる。
読んでみて、無理だったら飛ばせばいい。

Go が読み書きできる人は HTTP2 の実装は目を通した方がいいかもしれない。
サーバプッシュ、ServerSendEvent が載っている。
これらを実装したことがあるエンジニアは少ないのではないだろうか。


キャッシュの仕様

自分はサーバサイドのアプリケーションエンジニアだが、
HTTP といえば、キャッシュの設定が複雑という印象が強い。

no-cache, no-store ?
Last-Modified, Expires, Etag?

どれがどれだかよく分からなくなる。

そして、事故るとパフォーマンスが劣化したり、
コンテンツが更新されなかったり、
大きなダメージを受ける

この書籍では、このキャッシュの仕様について、時系列で説明している。

時系列だから何? と思うかもしれないが、
時系列にすることによって、

「最初は xxx しかなかったけど、yyy を考慮して zzz が生まれました」

のような説明ができる。

そして、これが意外にもすんなり入ってくる。
この章だけでも読んでおくと良い。


HTTP2が載っている

HTTP2 について、体系的に扱っている書籍ってあまりないのでは? と思っている。
普段使いの HTTP/1.1 と一緒に HTTP2 についても学べるいい機会になるはず。


まとめ

学習効率が良い上に、読んで損しない書籍です。
良書だと思います。
併せて「Web API: The Good Parts」も読みましょう。

www.oreilly.co.jp



その他

レビューをした

自分と渋川さんは DeNA つながりということもあり、
今回、書籍のレビューに参加させていただいた。

ただ、予想以上にレビューは難しかった。

自分はHTTPに強いわけでもないので、
書籍の質を向上させるコメントが全然できない。
他のエンジニアのコメントを見て「ああ、なるほど・・・」と思ってばかりだった。

レビューを通じて、
自分のスキルの低さを痛感し、
謎の向上心が生まれたのを覚えている。

そして、今回書籍を読んでみて、
自分のレビュー内容が書籍に反映されたのは嬉しかった。


pospomeという単語が載っている

レビューをしたということもあり、
謝辞に pospome という単語が載っている。
オライリーに pospome という単語が載るとは思ってもいなかった・・・。


いまさら読んだ理由

レビューしたということもあり、書籍は献本していただいた。
ありがとうございます。

献本していただいたからには、レビュー書かないとなーと思い、
発売してから書こうと思ったが、
発売から数ヶ月間は「読んでみた系」のブログエントリーがちらほら流れてきた。

こういったタイミングで自分が感想を書いても、書かなくても、
書籍を買う人は買うだろう。

であれば、
「そーいえば、買ってなかった」という人に手に取ってもらい、
その人の技術力が少しでも上がればないいなと思ったので、
半年後くらいに書こうと思った。

golang の「埋め込み」を利用した実装アンチパターン

最近、time.Time を独自の struct で埋め込んだ実装をみかけたので書いておこうと思った。

結論から言うと、
標準パッケージやライブラリで定義されている型に対して「埋め込み」を使うと、
コードが正常に動かなくなる可能性があるので、
注意した方がいいというお話。

自分は、普段 GAE/Go で開発をしていて、
ストレージとして Datastore を利用しているが、
time.Time を独自の struct で埋め込んだ実装でハマった経験がある。

問題の説明

time.Time に限らず、
埋め込みによって、既存の struct を拡張するというアプローチはよくあるものだと思う。


以下の実装は既存の time.Time 型の機能に Hello() というメソッドを追加している。

package pospome_time

type Time struct {
	time.Time
}

func (t *Time) Hello() {
	fmt.Println("pospome time hello");
}

こうすることで、
time.Time のメソッドはそのまま利用できるし、
Hello() というアプリケーションコードに必要な振る舞いも定義できる。

ここで気をつけなければならないのは、
埋め込みは既存の struct の型を持たない という点。

上記の例で言うと、pospome_time.Time は time.Time 型ではない

なので、time.Time に依存する処理がある場合、上手く動かないことがある。

最初にサラッと説明した GAE/Go の Datastore に例で言うと、
以下のように time.Time の型を判定する処理がある。
*とりあえず、grep したパッと出てきたやつです。
https://github.com/GoogleCloudPlatform/google-cloud-go/blob/fdbfa8004f3a20b8b375215c71577a2d6ec48ab3/datastore/save.go#L72

pospome_time.Time は time.Time ではないので、
time.Time に対応した処理は実行されない。

これは pospome_time.Time の利用者からすると期待通りの動作ではない可能性がある。
なぜなら、pospome_time は time.Time を埋め込んでいるので、
あたかも time.Time を扱っているように思えてしまうから。

そして、これらのハマりどころは、
引数が interface{} になっている関数やメソッド が原因であることが多い。

以下のように関数の引数が time.Time だった場合、
pospome_time.Time は time.Time 型ではないので、
そもそも引数に指定できない。
このケースで事故ることはない。

// pospome_time.Time は指定できない。
// これは事故らない
func Xxx(t time.Time) {
}



一方、引数が interface{} である場合、
pospome_time.Time も time.Time も指定できてしまう。

// pospome_time.Time が指定できてしまう
// これは事故るかも?
func Xxx(src interface{}) {
	//ここの中で time.Time かどうかを判定している。
}

そして、こーゆー関数に限って関数内で型判定が存在する。
関数を実装する側は、具体的な型を知らなければならないので、当然といえば当然。

そして、以下のような type による独自型定義も time.Time としては扱われない。
埋め込みだけではなく、こちらも注意。

type PospomeTime time.Time



どう解決すればいいのか?

自分は、埋め込みが難しそうなときは、
以下のようにそれ用の関数を作っている。

func Hello(t time.Time) {
	//なんかする
}



以下のように、
埋め込みをした struct から、
元の struct へ変換するようなものも考えたが、
変換を忘れて、そのまま interface{} の引数に突っ込んでしまいそうなので避けている

package pospome_time

type Time struct {
	time.Time
}

func (t *Time) NewTime() time.Time {
	//レシーバの t を元に time.Time を作る。
}



interface{} の引数を持つ関数を利用する箇所が限られているのであれば、
上記の実装でも問題ないかもしれない。
ここは既存のアーキテクチャとチームの方針次第になる。


標準パッケージやライブラリに対する埋め込みはNGなのか

標準パッケージやライブラリに対する埋め込みはNGなのか?
というと、そんなことはない。

アプリケーションコードでしか利用しないような struct の拡張であれば問題はない。

time.Time は、
Datastore や RDB のようなストレージでよく利用される特性を持ち、
それらが提供している関数の引数が interface{} であることが多いので、
ハマったが、
それ以外のケースで利用する場合、
埋め込みでもいけると思う。

例えば、複雑な日時計算をするような場合、
time.Time を埋め込んだ struct を作成することで、
いい感じのコードになることは十分に考えられる。

ちなみに、型判定は reflect パッケージを利用する。
int, string のようなプリミティブ型であれば、
独自の型を定義しても reflect で判定可能なので、問題にならなかったりする。
https://github.com/GoogleCloudPlatform/google-cloud-go/blob/fdbfa8004f3a20b8b375215c71577a2d6ec48ab3/datastore/save.go#L76

//こーゆーのは問題ない
type MyInt int



しかし、struct に関しては、
reflect.Struct というザックリした型しか存在しない。

なので、
以下のように、それっぽい struct を想定したコードを書くのが限界。
https://github.com/GoogleCloudPlatform/google-cloud-go/blob/fdbfa8004f3a20b8b375215c71577a2d6ec48ab3/datastore/save.go#L71-L72

当然、Datastore の作者は pospome_time.Time を知るはずもないので、
ここに pospome_time.Time が追加されることはない。

今回の件は
interface{} の引数に渡されやすく、
複雑な計算が要求されやすい time.Time ならではのハマりどころなのかもしれない。


まとめ

特に無いです。

time.Time + 埋め込みの件については、
自分を含めてハマった人を複数人見たので書き残しておこうと思いました。
これから GAE/Go やる人増えてくると、そこそこハマるんじゃないかなーと思います・・・。

アサーション、reflect 周りの知識が弱いので、
間違いや補足があれば、教えてください。
修正させていただきます。

GAE の TaskQueue(PushQueue) で、delay パッケージと HTTP の受け口(handler)を定義するのは何が違うのか?

TQ を利用するとき、
いつもはタスクを受ける HTTP の受け口(handler)を定義して、
taskqueue パッケージで実装を完結させていただけど、
「今回は delay パッケージ使ってみようかなー」
と思ったので違いを調べてみた。

それぞれの使い方とかはネットに転がってるので、
ググってください。

挙動に大きな違いはない気がする

自分が軽く触った結果、挙動に大きな違いはなかった。

handler でも delay でも、
どっちでもできることは同じに思える。

細かい実装の違い

handler でも delay でも、
どっちでもできることは同じだが、
handler が HTTP を受けるのに対して、
delay は以下のような関数なので、
これらの違いによる 実装方法の違い は存在する。

var laterFunc = delay.Func("key", func(c context.Context, x string) {
    // ...
})



例えば、リトライの実装方法が違う。

handler は HTTP Status によってリトライするかどうかを制御する。
https://cloud.google.com/appengine/docs/standard/go/taskqueue/push/retrying-tasks

一方、delay は HTTP を受け取らないので、
関数の戻り値で error を返すかどうかによってリトライするかどうかを制御する。

If the function has any return arguments, and the last one is of type error, the function may return a non-nil error to signal that the function should be retried.

https://cloud.google.com/appengine/docs/standard/go/taskqueue/delay


HTTP Status を返すよりも、
error を返すほうが簡単かなーと思わなくもないが、
実際大して変わらないと思う。

パッと思いつくのは、
リトライ実装くらいだが、
handler or 関数 の性質の違いが実装方法に違いを与えることがある。

delay パッケージには Function.Call() がある

delay には Function.Call() というメソッドが生えている。
https://cloud.google.com/appengine/docs/standard/go/taskqueue/delay#Function

これは delay における以下のコードを、

t, _ := f.Task(...)
_, err := taskqueue.Add(c, t, "")



以下のように Call() のみで書くことができる。

err := f.Call(c, ...)



Call() では queue.yaml の name を指定することができないので、
queue name は default になる。

まあ、それだけなんだけど・・・。

delay パッケージはタスクに struct を突っ込める

delay では、
タスクを処理する関数側で user struct を以下のように受け取ることができる。

var laterFunc = delay.Func("key", func(c context.Context, user User) {
    // ...
})



なので、タスクを追加する際のコードでは、
以下のように user struct を突っ込むことができる。

laterFunc.Call(c, User{"pospome"})



一方、handler を定義する場合、
タスクに突っ込める値は url.Values になっているので、
string しか扱えない。

func NewPOSTTask(path string, params url.Values) *Task

https://cloud.google.com/appengine/docs/standard/go/taskqueue/reference#Task

ただ、
handler のタスクである taskqueue.Task の url.Values は、
以下の Task.Payload に []byte として突っ込まれるので、

type Task struct {
    // Path is the worker URL for the task.
    // If unset, it will default to /_ah/queue/<queue_name>.
    Path string

    // Payload is the data for the task.
    // This will be delivered as the HTTP request body.
    // It is only used when Method is POST, PUT or PULL.
    // url.Values' Encode method may be used to generate this for POST requests.
    Payload []byte

    //省略
}

delay のように struct を gob とかでシリアライズしてあげれば、
いけるのかもしれないが、
やはり delay の方が便利かなと思う。
*動作確認はしていません。

delay パッケージには罠がある

以下に載っている。
https://qiita.com/hogedigo/items/fae5b6fe7071becd4051

・delay関数の生成タイミング間違えてハマった(´・ω・`)
・delay関数使ったコードのファイル名変えてハマった(´・ω・`)

ファイル名の変更が NG というのは結構なハマりポイントな気がする・・・。
忘れそう・・・。

外部から叩けるかどうか

handler として定義すると、
以下のように handler をセキュアな設定にすると思うが、
https://cloud.google.com/appengine/docs/standard/go/taskqueue/push/creating-handlers#securing_task_handler_urls

この設定を取っ払ってしまえば、 handler を外部から叩くことができる。

オンプレの外部システムから handler を叩いてタスクを実行したい
というケースでは役に立つかもしれない。

ただ、delay を利用した場合でも、
それ用の handler を用意すればいいだけだし、
handler の処理は同期処理になると思うので、
結局 handler で定義することに明確なアドバンテージはない気がする。


どっちを使えばいいのか?

delay パッケージを触ってみて、
TQ(Push)は全部 delay でいーかなと思いました。

handler 定義した方がいいよ っていうケースがあれば、
ブログのコメント欄 or twitter で教えてください。

GAE/Go の urlfetch のタイムアウトを設定する

ググると、旧 appgneine パッケージに対する設定方法しか引っかからないので、
google.golang.org/appengine に対する設定方法をメモとして残しておく。

結論から言うと、
context.WithTimeout() or context.WithDeadline() を利用して設定する。

以下は context.WithTimeout() の例。

c, _ := context.WithTimeout(ctx, 30*time.Second)
fetch := urlfetch.Client(c)

*今回は WithTimeout() の2つ目の戻り値の CancelFunc は使わないので、 _ にしています。


ちなみに、旧 appgneine パッケージは以下のように指定する。

client := &http.Client{
  Transport: &urlfetch.Transport{
    Context:  ctx,
    Deadline: 30 * time.Second,
  },
}


google.golang.org/appengine の設定方法はググってもなかなか見つからなかったが、
実は google.golang.org/appengine の readme に context を利用するように記載があったり、

urlfetch.Transport no longer has a Deadline field; set a deadline on the context.Context instead.

https://github.com/golang/appengine#2-update-code-using-deprecated-removed-or-modified-apis


GCPのドキュメントでも言及されていたりする。

Any deadline of the provided context will be used for requests through this client; if the client does not have a deadline then a 5 second default is used.

https://cloud.google.com/appengine/docs/standard/go/urlfetch/reference#Client

GAE の SocketAPI とは何か?

存在は知っていたが、
普段 GAE を利用する上で特に利用する機会がなかったので、
SocketAPI について調べてみた。

SocketAPI

ドキュメントは以下。
https://cloud.google.com/appengine/docs/standard/go/sockets/

なぜか java は日本語。
https://cloud.google.com/appengine/docs/standard/java/sockets/

ドキュメントに書いてあるので説明不要だと思うが、
SocketAPI は、その名の通りソケット通信をする際に利用する API のこと。

java ドキュメントの一部を抜粋する。

・リッスンソケットは作成できません。作成できるのはアウトバウンドソケットだけです。
FTP はサポートされません。
・使用できるのは TCPUDP だけで、任意のプロトコルを使用できるわけではありません。

で、これは何に使うのか?
自分は普段 GAE を使っているものの使ったことがない。

利用用途としてパッと思いつたのが、
GAE から 外部環境の DB に接続する用途。
*あまりないケースだと思うけど・・・

ググってみると、それっぽいのが引っかかる。
https://groups.google.com/forum/#!topic/google-appengine/hmXkSr_EVvQ

以下は上記のスレッドで紹介されている mysql driver で、
GAE から外部の DB に接続するためのものらしい。
https://github.com/pjebs/GAE-Toolkit-Go/tree/master/sql

接続時に SocketAPI が利用されている。
https://github.com/pjebs/GAE-Toolkit-Go/blob/master/sql/sql.go#L44

少し古い記事だけど、
redis に接続する記事もあった。
http://www.xhroot.com/blog/2013/04/12/accessing-redis-from-google-app-engine/

GAE で外部通信といえば、
urlfetch パッケージを利用するが、
これは HTTP しか扱えないので、
その他の通信は SocketAPI を利用することになるのかな。

とはいえ、自分は GCP 上のサービスしか利用しないので、
あまり利用する機会はないかなーと思っている。


クオータ

クオータは以下になる。
https://cloud.google.com/appengine/quotas?hl=en#Sockets

SocketAPI を明示的に利用していないせいもあって、
クオータを意識したことはない。


GCPサービスとSocketAPI

最近知ったのだが、
GAE が GCP サービスと通信する場合、
内部で SocketAPI が利用されるケースがあるらしい。

パッと調べた限り、
以下の2つのパッケージを GAE で利用したところ、
SocketAPI のクオータが消費されていた。
*なぜこのパッケージを選択したのかは謎

1.cloud.google.com/go/datastore
2.cloud.google.com/go/logging

以前、
GAE Datastore と Cloud Datastore の golang パッケージって、
両方 GAE で使えそうなのに何が違うのだろう?
と思ったことがあった。

Cloud Datastore パッケージには、
Transaction.Commit(), Transaction.Rollback() のような、
GAE Datastore パッケージには存在しない API が実装されているので、
Cloud Datastore の方が高機能なパッケージだと思っていたが、
SocketAPI 経由でアクセスするかどうかの違いがあるみたい。

GAE では GAE Datastore パッケージを使った方がよさそう。

ところが、
全てのGCPサービスが SocketAPI 経由でコールされるわけではない。

試しに GAE から BigQuery にアクセスしても、
SocketAPI のクオータに変化はなかった。

BigQuery のソースをザックリと grep したところ、
BigQuery は urlfetch 経由でアクセスしているっぽい。
https://github.com/google/google-api-go-client/blob/3d8027ad5aac1c3f32ad0eeae4458a5d69682263/bigquery/v2/bigquery-gen.go#L6225-L6246

BigQuery アクセス後に urlfetch のクオータを確認してみると、変化があった。

一方、Cloud Datastore は grpc 経由でアクセスしていた。
https://github.com/google/go-genproto/blob/891aceb7c239e72692819142dfca057bdcbfcb96/googleapis/datastore/v1/datastore.pb.go#L1376-L1383

grpc を利用したアクセスだと、
SocketAPI が利用される or Socket利用 とみなされるみたい。

ザックリ grep しただけなので、
見当違いのコードを提示しているかもしれません。

この grep が正しいとした場合、
他のサービスは確認していないが、
以下のリポジトリ内にライブラリが存在するサービスは grpc 経由でアクセスしていると考えていいのだろうか?
https://github.com/google/go-genproto/tree/master/googleapis

リポジトリ内には pubsub パッケージがある。
pubsub は性質上、高トラフィックになりやすいと思うので、
注意した方がいいのかもしれない。

GAE から Cloud pubsub をガンガン利用する場合、
クオータを意識したことない とも言ってられない。

課金してないとエラー

ドキュメントにも明記してあるが、SocketAPI は課金を有効にしないと利用できない。

詳しくは覚えていないが、
課金無効状態で利用しようとすると、
TransientFailure 的なエラーが発生した気がする。

ついに自分も課金ユーザーになった。


まとめ

特にないです。
間違っていたら、twitter or この記事にコメントいただければと思います。