コンテキスト認識リーダー
Context-aware Reader
まず、
io.Reader
の簡単な入門書です。この本の他の章を読んだことがあれば、ファイルを開いたり、JSONをエンコードしたり、その他の一般的なタスクを実行したりしたときに、
io.Reader
に出くわすことになります。 something からのデータの読み取りに関する単純な抽象化です。type Reader interface {
Read(p []byte) (n int, err error)
}
io.Reader
を使用すると、標準ライブラリから多くの再利用を得ることができます。 これは、非常に一般的に使用される抽象化で す(対応する io.Writer
とともに)前の章でキャンセルを提供するために「コンテキスト
context
」を使用する方法について説明しました。これは、計算コストがかかる可能性のあるタスクを実行していて、それらを停止できるようにしたい場合に特に便利です。io.Reader
を使用している場合、速度について保証はありません。1ナノ秒または数百時間かかる場合があります。自分のアプリケーションでこの種のタスクをキャンセルできると便利だと思うかもしれませんが、それはMatとDavidが書いたものです。彼らはこの問題を解決するために2つの単純な抽象化(
context.Context
とio.Reader
)を組み合わせました。io.Reader
をラップしてキャンセルできるように、いくつかの機能をTDDで試してみましょう。これをテストすることは興味深い挑戦を引き起こします。通常、
io.Reader
を使用するときは、通常、他の関数にそれを提供しているので、詳細に気を使う必要はありません。 json.NewDecoder
やioutil.ReadAll
など。デモしたいのは、次のようなものです。
"ABCDEF"を持つio.Reader
が与えられたとき、途中でキャンセル信号を送っても、読み続けようとすると何も出ないので、"ABC"しか出ません。
インターフェースをもう一度見てみましょう。
type Reader interface {
Read(p []byte) (n int, err error)
}
Reader
のRead
メソッドは、その内容を、提供する []byte
に読み込みます。したがって、すべてを読むのではなく、次のことができます。
- すべてのコンテンツに適合しない固定サイズのバイト配列を提供します
- キャンセル信号を送信します
- 再試行してもう一度読み取ると、0バイトの読み取りエラーが返されます
とりあえず、キャンセルのない「ハッピーパス」テストを書いてみましょう。これは、まだ本番用のコードを記述しなくても問題に慣れることができるようにするためです。
func TestContextAwareReader(t *testing.T) {
t.Run("lets just see how a normal reader works", func(t *testing.T) {
rdr := strings.NewReader("123456")
got := make([]byte, 3)
_, err := rdr.Read(got)
if err != nil {
t.Fatal(err)
}
assertBufferHas(t, got, "123")
_, err = rdr.Read(got)
if err != nil {
t.Fatal(err)
}
assertBufferHas(t, got, "456")
})
}
func assertBufferHas(t *testing.T, buf []byte, want string) {
t.Helper()
got := string(buf)
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
- いくつかのデータを含む文字列から
io.Reader
を作成します - 読み込む内容がリーダーの内容よりも小さいバイト配列
- 呼び出しを読んで、内容を確認し、繰り返します
これから、2回目の読み取りの前に動作を変更するために何らかのキャンセル信号を送信することを想像できます。
それがどのように機能するかを見てきましたので、残りの機能をTDDします。
io.Reader
をcontext.Context
で作成できるようにしたいと考えています。TDDでは、希望するAPIを想像することから始めて、そのためのテストを作成するのが最善です。
そこから、コンパイラーと失敗したテスト出力で解決策を導きましょう。
t.Run("behaves like a normal reader", func(t *testing.T) {
rdr := NewCancellableReader(strings.NewReader("123456"))
got := make([]byte, 3)
_, err := rdr.Read(got)
if err != nil {
t.Fatal(err)
}
assertBufferHas(t, got, "123")
_, err = rdr.Read(got)
if err != nil {
t.Fatal(err)
}
assertBufferHas(t, got, "456")
})
./cancel_readers_test.go:12:10: undefined: NewCancellableReader
この関数を定義する必要があり、
io.Reader
を返す必要がありますfunc NewCancellableReader(rdr io.Reader) io.Reader {
return nil
}
試しに実行してみると
=== RUN TestCancelReaders
=== RUN TestCancelReaders/behaves_like_a_normal_reader
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10f8fb5]
予想通り
今のところ、渡した
io.Reader
を返すだけですfunc NewCancellableReader(rdr io.Reader) io.Reader {
return rdr
}
これでテストに成功するはずです。
わかっています、わかっています、これは馬鹿げていて衒学的に見えますが、派手な作業に取り掛かる前に、
io.Reader
の「通常の」振る舞いを壊していないかどうかを、ある程度検証することが重要です。次に、キャンセルしてみる必要があります。
t.Run("stops reading when cancelled", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
rdr := NewCancellableReader(ctx, strings.NewReader("123456"))
got := make([]byte, 3)
_, err := rdr.Read(got)
if err != nil {
t.Fatal(err)
}
assertBufferHas(t, got, "123")
cancel()
n, err := rdr.Read(got)
if err == nil {
t.Error("expected an error after cancellation but didnt get one")
}
if n > 0 {
t.Errorf("expected 0 bytes to be read after cancellation but %d were read", n)
}
})
最初のテストは多かれ少なかれコピーできますが、今は次のようになっています。
- 最初の読み取り後に「キャンセル
cancel
」できるように、キャンセル付きのcontext.Context
を作成します - コードを機能させるには、関数に
ctx
を渡す必要があります - そして、
cancel
後に何も読まれなかったことを主張します。
./cancel_readers_test.go:33:30: too many arguments in call to NewCancellableReader
have (context.Context, *strings.Reader)
want (io.Reader)
コンパイラは何をすべきかを指示しています。コンテキストを受け入れるように署名を更新します。
func NewCancellableReader(ctx context.Context, rdr io.Reader) io.Reader {
return rdr
}
(最初のテストが
context.Background
を通過するように更新する必要があります。)これで、非常に明確な失敗したテスト出力を見ることができるはずです。
=== RUN TestCancelReaders
=== RUN TestCancelReaders/stops_reading_when_cancelled
--- FAIL: TestCancelReaders (0.00s)
--- FAIL: TestCancelReaders/stops_reading_when_cancelled (0.00s)
cancel_readers_test.go:48: expected an error but didnt get one
cancel_readers_test.go:52: expected 0 bytes to be read after cancellation but 3 were read
この時点では、MatとDavidによる元の投稿からコピーして貼り付けていますが、ゆっくりと繰り返し実行します。
読み込んだ
io.Reader
とcontext.Context
をカプセル化するタイプが必要であることはわかっ ているので、それを作成して、元のio.Reader
の代わりに関数からそれを返してみましょうfunc NewCancellableReader(ctx context.Context, rdr io.Reader) io.Reader {
return &readerCtx{
ctx: ctx,
delegate: rdr,
}
}
type readerCtx struct {
ctx context.Context
delegate io.Reader
}
このサイトで何度も強調してきたように、ゆっくりと進み、コンパイラに助けてもらいましょう。
./cancel_readers_test.go:60:3: cannot use &readerCtx literal (type *readerCtx) as type io.Reader in return argument:
*readerCtx does not implement io.Reader (missing Read method)
抽象化はいい感じだけど、必要なインターフェースが実装されてないから、メソッドを追加しよう。
func (r *readerCtx) Read(p []byte) (n int, err error) {
panic("implement me")
}
テストを実行すると、それらはコンパイルするはずですが、パニックになります。これはまだ進行中です。
最初のテストを通過させるために、基礎となる
io.Reader
への呼び出しを委任してみましょう。func (r readerCtx) Read(p []byte) (n int, err error) {
return r.delegate.Read(p)
}
この時点で、私たちは再び私たちの幸せなパスのテストが合格し、それは私たちのものがきれいに抽象化されているように感じています。
2回目のテストをパスするためには、
context.Context
がキャンセルされたかどうかを確認する必要があります。func (r readerCtx) Read(p []byte) (n int, err error) {
if err := r.ctx.Err(); err != nil {
return 0, err
}
return r.delegate.Read(p)
}
これですべてのテストが通過するはずです。エラーを
context.Context
から返していることに気づくでしょう。これにより、コードの呼び出し元がキャンセルが発生した様々な理由を調べることができます。- 小さなインターフェイスが良く、構成が簡単
ソフトウェア工学では、デリゲーションパターンはオブジェクト指向の設計パターンであり、オブジェクトを構成して継承と同じコードの再利用を実現することができます。
- この種の作業を始める簡単な方法は、他の部分の動作を変更するために他の部分を合成し始める前に、デリゲートをラップして、デリゲートが通常どのように動作するかを保証するテストを書くことです。これは、目標に向かってコードを書く際に、物事を正しく動作させておくのに役立ちます。
最終更新 1yr ago