TIGの辻です。Go 1.20 リリース連載の5本目は Core library の HTTP ResponseController のアップデートを紹介します1。
何が変わるのか
net/httpパッケージに新しくResponseController型が追加されます- 従来から存在する
ResponseWriterインタフェースでは実現できなかったHTTPリクエストごとの制御が実現できるようになります ResponseController型にあるSetReadDeadline()やSetWriteDeadline()メソッドを利用して、リクエストごとの読み書きのデッドラインを設定できます
何が嬉しいのか
Go のユーザーとして見たときの ResponseController 型の追加による具体的な嬉しいポイントはHTTPハンドラごとに読み書きのデッドラインが設定できることです。サーバー全体ではデフォルトのデッドラインを設定しつつ、特定のハンドラのみデッドラインを伸ばす、など柔軟な設定が可能になります。
ハンドラでの使用例は以下です。
func RequestHandler(w ResponseWriter, r *Request) { |
ちなみにサーバー単位のデッドラインは http.Server 構造体の ReadTimeout や WriteTimeout フィールドから設定できます。
なぜこのAPIになったのか
ここからは、この変更に至るまでの背景の一部を、私が感じたことも含めて、記載したいと思います。
実は http.Handler がハンドラ単位で読み書きのデッドラインの設定ができない、という Issue は2016年に起票されています (#16100)。なぜこの課題の解決に7年も要したのでしょうか? この一因として後方互換性との戦いがあったと想像しています。
http.Handler は以下のような ServeHTTP() があるインタフェースです。
type Handler interface { |
また http.ResponseWriter も以下のような Header(), Write(), WriteHeader() をもつインタフェースです。
type ResponseWriter interface { |
ハンドラの読み書きに関するデッドラインを設定できるAPIは Handler や ResponseWriter にはありません。それでは ResponseWriter インタフェースに以下のような SetReadDeadline() や SetWriteDeadline() APIを追加すればいいのではないか? と思うかもしれません。
type ResponseWriter interface { |
しかしGo1の互換性ポリシーからこのような破壊的変更はできません。SetReadDeadline() と SetWriteDeadline() を持たない型が ResponseWriter インタフェースを満たさなくなってしまいます。真野さんの contextパッケージのWithCancelCauseとCause の記事でも、このインタフェースの後方互換性問題に触れています。
それではどうするか?
インタフェースを拡張したいときに ResponseWriter インタフェースとは別のインタフェースにAPIを定義して、ResponseWriter インタフェースを実装する型に別インタフェースのメソッドを実装させる、というのが1つの手段としてあります。一例はHTTPハンドラがバッファリングしているデータをクライアントにフラッシュできる http.Flusher インタフェースです。
type Flusher interface { |
Goのデフォルトの HTTP/1.x と HTTP/2 の ResponseWriter の実装は Flusher もサポートしており、これは文章化されています。
これを利用すると、たとえば、アプリケーションのハンドラ内では次のように型アサーションを組み合わせながら Flush() を呼び出せます。
func hello(w http.ResponseWriter, r *http.Request) { |
上記と同様のアプローチで
type ReadDeadliner interface { |
のようなインタフェースを定義することもできたでしょう。しかし ResponseWriter 型をラップしたミドルウェアとの相性など、使い勝手が悪いため見送られています。
議論のすえに生み出された解決策が #54136 で、リリースノートに記載されていたアプローチです。すなわち ResponseWriter 型をラップした http.ResponseController 構造体を定義する、ということです。
func NewResponseController(rw ResponseWriter) *ResponseController { |
インタフェースではなく構造体を追加している利点として、将来的に *ResponseController に新しいメソッドを追加したい場合に後方互換性が壊れない、という点があるでしょう。
注意点はドキュメントを見るとわかるように NewResponseController() の引数は Handler.ServeHTTP() メソッドに渡された元の値であるか、元の ResponseWriter を返す Unwrap() メソッドを持っている必要があることです。
この意味は
- Go のデフォルトの
ResponseWriterの実装はResponseWriterインタフェースのメソッドに加えてFlush(),Hijack(),SetReadDeadline(),SetWriteDeadline()も実装している - サードパーティでラップされた
ResponseWriterはFlush(),Hijack(),SetReadDeadline(),SetWriteDeadline()のすべてを実装する必要はなくUnwrap()でもとのResponseWriterの値を返却すれば良い
と理解しています。ラップする場合は responsecontroller_test.go のテストケースにもあるように基本的には以下のような Unwrap() メソッドを実装することになるでしょう。
type wrapWriter struct { |
Unwrap() メソッドの効果は *ResponseController の実装を見るとわかるでしょう。 Hijack() の実装は以下のようになっています。
func (c *ResponseController) Hijack() (net.Conn, *bufio.ReadWriter, error) { |
仮にサードパーティが実装している ResponseWriter の値が Hijacker インタフェースを実装していなくても Unwrap() でもとの ResponseWriter の値を返却していれば、その Hijack() メソッドが呼び出されます。for ループで再帰しているのがおしゃれですね2。
まとめ
GoのHTTP ResponseControllerのアップデートの概要とその背景を紹介しました。後方互換性との戦いやコミュニティへの配慮が感じられたのではないでしょうか。
次は棚井さんのNew ReverseProxy Rewrite hook を動かしながら理解するです。
- 1.なお本文中のGoのソースバージョンは
go1.20rc3です。 ↩ - 2.ちなみに
contextパッケージでも似たようなfor文で再帰している実装があります。よくある実装パターンの1つでしょう。https://github.com/golang/go/blob/b3160e8bcedb25c5266e047ada01b6f462521401/src/context/context.go#L629-L653 ↩