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 ↩