Go WalkthroughシリーズのBen Johnsonの記事の1つを翻訳して、実世界のタスクのコンテキストでの標準ライブラリのより詳細な研究を行います。
Goは、バイトを扱うのに適したプログラミング言語です。 バイトのリスト、バイトのストリーム、または単一のバイトのいずれを使用する場合でも、Goは簡単に操作できます。 これらは、抽象化とサービスを構築するプリミティブです。
ioパッケージは、標準ライブラリ全体で最も基本的なものの1つです。 バイトストリームを操作するためのインターフェイスとヘルパー関数のセットを提供します。
この投稿は、標準ライブラリのより詳細な分析に関する一連の記事の1つです。 標準ドキュメントは多くの有用な情報を提供するという事実にもかかわらず、実際のタスクのコンテキストでは、何をいつ使用するかを把握するのが難しい場合があります。 この一連の記事は、実際のアプリケーションのコンテキストでの標準ライブラリパッケージの使用を示すことを目的としています。
バイトの読み取り
バイトを使用する場合、読み取りと書き込みの2つの基本的な操作があります。 最初にバイトの読み取りを見てみましょう。
リーダーインターフェース
ストリームからバイトを読み取るための最も簡単な構成は、 Readerインターフェイスです。
type Reader interface { Read(p []byte) (n int, err error) }
このインターフェイスは、 ネットワーク接続からファイル 、メモリ内のスライスのラッパーまで、すべての標準ライブラリに繰り返し実装されます 。
Readerは、バッファ(p)をRead()メソッドの引数として使用するため、メモリを割り当てる必要はありません。 Read()が引数として受け入れる代わりに、新しいスライスを返した場合、リーダーはRead()が呼び出されるたびにメモリを割り当てる必要があります。 ガベージコレクターにとっては災害になります。
Readerインターフェースの問題の1つは、かなり華やかなルールのセットが付属していることです。 まず、通常のビジネスコースでは、データストリームが終了した場合にio.EOFエラーを返します。 これは初心者を混乱させる可能性があります。 第二に、バッファがいっぱいになる保証はありません。 8バイトのスライスを送信した場合、実際には0〜8バイトを読み取ることができます。 部分的な読み取り処理は複雑でエラーが発生しやすくなります。 幸いなことに、これらの問題を解決するための多くの補助機能があります。
読書の保証を改善する
解析する必要があるプロトコルがあり、リーダーから8バイトのuint64値を読み取りたいとします。 この場合、io.ReadFull()を使用することをお勧めします。読みたい量を正確に知っているからです。
func ReadFull(r Reader, buf []byte) (n int, err error)
この関数は、値を返す前にバッファがいっぱいであることを確認します。 受信したデータのサイズがバッファのサイズと異なる場合、io.ErrUnexpectedEOFエラーを受け取ります。 この単純な保証により、コードがかなり簡素化されます。 8バイトを読み取るには、次のようにします。
buf := make([]byte, 8) if _, err := io.ReadFull(r, buf); err != nil { return err }
特定のタイプを解析できるbinary.Read()のような高レベルのパーサーもかなりあります。 他のパッケージに関する次の投稿で、それらについて詳しく知ることにします。
あまり一般的に使用されない別のヘルパー関数は、 ReadAtLeast()です。
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)
この関数は、読み取り可能なデータをバッファーに書き込みますが、指定したバイト数以上を書き込みます。 私は自分でこの関数が必要だとは思いませんでしたが、Read()呼び出しの回数を減らして追加データをバッファリングしたい場合の利点を簡単に想像できます。
スレッドプーリング
多くの場合、複数のリーダーを一緒に組み合わせる必要がある状況に対応できます。 これはMultiReaderを使用すると簡単です 。
func MultiReader(readers ...Reader) Reader
たとえば、ヘッダーがメモリから読み取られ、応答本文の内容がファイルから読み取られるHTTP応答を送信するとします。 多くの人は、送信する前に最初にファイルをメモリ内のバッファに読み込みますが、これは遅く、大量のメモリを必要とする場合があります。
より簡単なアプローチを次に示します。
r := io.MultiReader( bytes.NewReader([]byte("...my header...")), myFile, ) http.Post("http://example.com", "application/octet-stream", r)
MultiReaderを使用すると、 http.Post()で両方のリーダーを1つとして使用できます。
ストリーム複製
リーダーで作業するときに遭遇する可能性のあることの1つは、データが読み取られた場合、再び読み取ることができないことです。 たとえば、アプリケーションはHTTPリクエストの本文を解析できませんでしたが、解析することはできません。パーサーはすでにデータを読み込んでおり、リーダーにないためです。
TeeReaderはここでの優れたソリューションです。読み取りプロセスを妨げることなく、読み取りデータを保存できます。
func TeeReader(r Reader, w Writer) Reader
この関数は、リーダーrの新しいリーダーラッパーを作成します。 新しいリーダーからの読み取り操作も、wにデータを書き込みます。 このライタは、メモリ内のバッファからログファイル、標準STDERRエラーのストリームまで、何でもかまいません。
たとえば、次のようにしてエラーのあるリクエストをキャプチャできます。
var buf bytes.Buffer body := io.TeeReader(req.Body, &buf) // ... process body ... if err != nil { // inspect buf return err }
ただし、メモリを使い果たさないように、差し引かれる応答本文のサイズに注意することが重要です。
フロー長の制限
ストリームのサイズは決して制限されていないため、ストリームから読み取ると、メモリまたはディスク容量に問題が生じる場合があります。 典型的な例は、ファイルアップロードハンドラです。 通常、ダウンロードしたファイルの最大サイズにはディスクをオーバーフローさせないように制限がありますが、手動で実装するのは面倒です。
LimitReaderは、リーダーのラッパーを提供することでこの機能を提供し、校正に使用できるバイト数を制限します。
func LimitReader(r Reader, n int64) Reader
LimitReaderを使用することの1つは、rがnより多く減算されているかどうかを通知しないことです。 nバイトを減算するとすぐにio.EOFを返します。 または、制限をn + 1に設定し、最後にnバイト以上を読み取るかどうかを確認できます。
バイトの書き込み
ストリームからバイトを読み取る方法を学習したので、ストリームにバイトを書き込む方法を見てみましょう。
ライターインターフェイス
Writerインターフェイスは、本質的に反転リーダーです。 ストリームに書き込まれる一連のバイトを指定します。
type Writer interface { Write(p []byte) (n int, err error) }
一般に、バイトの書き込みは読み取りよりも簡単な操作です。 読者にとって、困難なのは部分的または不完全な読み取りで正しく動作することですが、部分的または不完全な書き込みではエラーが発生します。
記録の複製
時には、一度に複数のライターにデータを送信する必要があります。 たとえば、ログファイルやSTDERRにあります。 これはTeeReaderに似ていますが、読み取りではなく、レコードを複製するだけです。
この場合、 MultiWriterが適しています。
func MultiWriter(writers ...Writer) Writer
この名前は、MultiReaderのライターバージョンではないため、少しわかりにくいかもしれません。 MultiReaderが複数のリーダーを1つに結合すると、MultiWriterはライターを返します。ライターはすべてのライターのエントリを複製します。
ユニットテストでMultiWriterを積極的に使用します。ここでは、サービスがログに正しく書き込むことを確認します。
type MyService struct { LogOuput io.Writer } ... var buf bytes.Buffer var s MyService s.LogOutput = io.MultiWriter(&buf, os.Stderr)
MultiWriterを使用すると、bufの内容を確認すると同時に、デバッグのためにターミナルで完全なログ出力を確認できます。
バイトをコピー
バイトの読み取りと書き込みの両方を理解したので、これら2つの操作を組み合わせて、それらの間でデータをコピーする方法を理解することは理にかなっています。
リーダーとライターの組み合わせ
リーダーからライターにコピーする最も簡単な方法は、 Copy()関数を使用することです。
func Copy(dst Writer, src Reader) (written int64, err error)
この関数は、32Kバッファーを使用してsrcから読み取り、dstに書き込みます。 io.EOF以外のエラーが発生した場合、コピーは停止し、エラーが返されます。
Copy()の問題の1つは、コピーされた最大バイト数を保証する方法がないことです。 たとえば、ログファイルを現在のサイズにコピーするとします。 コピー中にログが大きくなり続けると、予想より多くのバイトを受け取ります。 この場合、 CopyN()関数を使用できます。この関数は、指定された数を超えてコピーしません。
func CopyN(dst Writer, src Reader, n int64) (written int64, err error)
Copy()のもう1つの重要な点は、コピーごとに32Kのバッファーが割り当てられることです。 多くのコピー操作を行う必要がある場合は、既に割り当てられているバッファーを再利用してCopyBuffer()を使用できます。
func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error)
Copy()のオーバーヘッドは実際には非常に小さいため、個人的にCopyBuffer()を使用しません。
コピーを最適化する
中間バッファの使用を回避するために、データ型は、直接読み書きするための特別なインターフェースを実装できます。 型に対して実装されている場合、Copy()関数はバッファーを使用しませんが、これらの特別なメソッドを使用します。
型がWriterToインターフェイスを実装する場合、データを直接書き込むことができます。
type WriterTo interface { WriteTo(w Writer) (n int64, err error) }
BoltDB Tx.WriteTo()関数で使用しました。これにより、ユーザーはトランザクションからデータベーススナップショットを作成できます。
一方、ReaderFromインターフェイスを使用すると、型でリーダーから直接データを読み取ることができます。
type ReaderFrom interface { ReadFrom(r Reader) (n int64, err error) }
読者と作家の適応
リーダーを受け入れる機能はあるが、ライターしか持っていない状況に陥ることがあります。 HTTPリクエストにデータを動的に書き込むことができますが、http.NewRequest()はReaderのみを受け入れます。
io.Pipe()を使用してライターを反転できます。
func Pipe() (*PipeReader, *PipeWriter)
ここでは、新しいリーダーとライターを取得します。 PipeWriterのエントリはすべてPipeReaderにリダイレクトされます。
私はこの関数をめったに使用しませんでしたが、 exec.Cmdはこの関数を使用してStdin、Stdout、およびStderrパイプを実装します。これは、実行中のプログラムを操作するときに非常に役立ちます。
スレッドのクローズ
すべての良いことが終わりに近づいており、スレッドでの作業も例外ではありません。 Closerインターフェイスは、スレッドを閉じる一般的な方法を提供します。
type Closer interface { Close() error }
ここで説明することはあまりありません。このインターフェースは非常にシンプルですが、Close()メソッドで常にエラーを返すようにしています。そのため、必要に応じて私のタイプがこのインターフェースを実装します。 Closerは常に直接使用されるわけではなく、多くの場合、 ReadCloser 、 WriteCloser 、 ReadWriteCloserなどの他のインターフェイスと組み合わせて使用されます。
ストリームナビゲーション
ストリームは通常、最初から最後まで常に表示されるデータを表しますが、例外もあります。 たとえば、ファイルはストリームにすることができますが、ファイル内の任意の位置に任意に移動することもできます。
Seekerインターフェースは、ストリーム内をナビゲートする機能を提供します。
type Seeker interface { Seek(offset int64, whence int) (int64, error) }
目的の位置にジャンプするには、現在の位置からの移行、ストリームの最初からの移行、最後からの移行の3つの方法があります。 このメソッドはwhence引数で指定します。 offset引数は、移動するバイト数を示します。
ストリームを下に移動すると、固定サイズのブロックを使用する場合や、ファイルにオフセット付きのインデックスが含まれる場合に役立ちます。 データがヘッダーにあり、ストリームの先頭からトランジションを使用することが論理的である場合がありますが、データが末尾にあり、末尾から移動する方が便利な場合があります。
データ型の最適化
必要なのが1バイトまたはルーンだけである場合、部分の読み取りおよび書き込みは退屈な場合があります。 Goには、これを実現するためのインターフェイスがあります。
個々のバイトを操作する
ByteReaderおよびByteWriterインターフェイスは、単一のバイトを読み書きするための簡単なメソッドを提供します。
type ByteReader interface { ReadByte() (c byte, err error) } type ByteWriter interface { WriteByte(c byte) error }
バイト数のパラメーターはありません。常に0または1になります。バイトが読み書きされていない場合、エラーが返されます。
ByteScannerインターフェイスもあり、 これを使用すると、バッファリングされたリーダーでバイトを簡単に操作できます。
type ByteScanner interface { ByteReader UnreadByte() error }
このインターフェイスを使用すると、バイトをストリームに戻すことができます。 これは、たとえばLL(1)パーサーを書くときに、前方バイトを見ることができるので便利です。
個々のルーンを操作する
Unicodeデータを解析している場合は、個々のバイトではなくルーン文字を使用する必要があります。 この場合、 RuneReaderおよびRuneScannerインターフェースを使用する必要があります 。
type RuneReader interface { ReadRune() (r rune, size int, err error) } type RuneScanner interface { RuneReader UnreadRune() error }
おわりに
バイトストリームは、多くのGoプログラムにとって重要です。 これらは、ネットワーク接続からディスク上のファイル、ユーザーのキーボード入力まで、すべてのインターフェイスです。 ioパッケージは、これらすべてを操作するための基本的なプリミティブを提供します。
バイトの読み取り、書き込み、コピー、および特定のタスクに対するこれらの操作の最適化について検討しました。 これらのプリミティブは単純に見えるかもしれませんが、データを積極的に操作しているアプリケーションの基本的な構成要素です。
ioパッケージを慎重に検討し、プログラムでそのインターフェイスを使用してください。 また、 ioパッケージの興味深い使用方法と、このシリーズの記事を改善するためのヒントを共有していただければ幸いです。