Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 10300] ファイルの概要

このコミットは、Go言語の標準ライブラリにおけるHTTPチャンク転送エンコーディングのリーダー/ライターに関するコードの構造を再編成するものです。具体的には、net/httpパッケージ内のチャンク処理コードをnet/http/httputilパッケージに直接コピーし、両者のコードを同期しやすくすることを目的としています。これにより、以前の共有またはラップするアプローチで発生していたメンテナンス上の課題を解決しようとしています。

コミット

  • コミットハッシュ: bad305c27bc70b9900739822ca974bff52f4e289
  • Author: Andrew Gerrand adg@golang.org
  • Date: Wed Nov 9 14:55:52 2011 +1100

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/bad305c27bc70b9900739822ca974bff52f4e289

元コミット内容

http: make httputil's chunked reader/writer code a direct copy

Arrange the code so that it's easier to keep edits in sync.

R=golang-dev, mikioh.mikioh, bradfitz, andybalholm, rsc
CC=golang-dev
https://golang.org/cl/5345041

変更の背景

このコミットの主な背景は、net/httpパッケージとnet/http/httputilパッケージの間でHTTPチャンク転送エンコーディングを処理するコードの同期を容易にすることにあります。

Go言語の標準ライブラリにおいて、net/httpパッケージはHTTPクライアントとサーバーの実装を提供し、HTTPプロトコルの中核的な機能を取り扱います。一方、net/http/httputilパッケージは、HTTPプロトコルを扱うためのユーティリティ機能(例えば、リバースプロキシやリクエストのダンプなど)を提供します。

以前のコードベースでは、httputilパッケージがnet/httpパッケージの内部的なチャンク処理ロジックを再利用またはラップする形になっていた可能性があります。しかし、このような構造は、両パッケージでチャンク処理の振る舞いを変更する必要がある場合に、コードの同期を困難にしていたと考えられます。例えば、net/http内部のチャンク処理にバグが見つかったり、パフォーマンス改善が行われたりした場合、その変更がhttputilにも適切に反映されることを保証するのが複雑だったのかもしれません。

コミットメッセージにある「Arrange the code so that it's easier to keep edits in sync.(編集の同期を容易にするためにコードを配置する)」という記述は、このメンテナンス上の課題を明確に示しています。開発チームは、コードを直接コピーすることで、両方の場所で同じ変更を適用するという、より明示的で管理しやすいアプローチを選択しました。これにより、一方が変更されても他方が古いままになるというリスクを減らし、開発者が両方のコードベースを意識して変更を行うことを促します。

前提知識の解説

HTTPチャンク転送エンコーディング (Chunked Transfer Encoding)

HTTP/1.1では、メッセージボディの長さを事前に知ることができない場合に、メッセージボディを送信するためのメカニズムとして「チャンク転送エンコーディング」が導入されました。これは、特に動的に生成されるコンテンツや、大きなファイルをストリーミングで送信する場合に有用です。

通常のHTTPレスポンスでは、Content-Lengthヘッダーによってメッセージボディのバイト数が示されます。しかし、チャンク転送エンコーディングを使用する場合、Transfer-Encoding: chunkedヘッダーが使用され、Content-Lengthヘッダーは存在しません。

チャンク転送エンコーディングのフォーマットは以下のようになります。

  1. チャンクサイズ: 16進数で表現されたチャンクのバイト数。その後にCRLF(キャリッジリターンとラインフィード)が続きます。
  2. チャンクデータ: チャンクサイズで指定されたバイト数のデータ。その後にCRLFが続きます。
  3. 上記1と2が繰り返されます。
  4. 最終チャンク: サイズが0のチャンク(0\r\n)。これはメッセージボディの終わりを示します。
  5. トレーラーヘッダー (オプション): 最終チャンクの後に、追加のヘッダーフィールドを含めることができます。
  6. 最終CRLF: トレーラーヘッダーの後に、またはトレーラーヘッダーがない場合は最終チャンクの後に、CRLFが続きます。

このメカニズムにより、サーバーはメッセージボディ全体のサイズを計算することなく、データをクライアントに送信し始めることができます。クライアントは、各チャンクのサイズを読み取り、データを受信し、最終チャンク(サイズ0)に到達するまでこれを繰り返します。

Go言語のnet/httpパッケージ

net/httpパッケージは、Go言語におけるHTTPクライアントとサーバーの実装を提供します。これは、WebアプリケーションやAPIサービスを構築するための基盤となるパッケージです。このパッケージは、HTTPリクエストの解析、レスポンスの生成、ルーティング、ミドルウェアのサポートなど、HTTPプロトコルに関連する多くの機能を提供します。

Go言語のnet/http/httputilパッケージ

net/http/httputilパッケージは、net/httpパッケージを補完する形で、HTTPプロトコルを扱うための様々なユーティリティ機能を提供します。これには、リバースプロキシの実装、HTTPリクエスト/レスポンスのダンプ(デバッグ目的)、そしてこのコミットで扱われているようなチャンク処理のヘルパーなどが含まれます。このパッケージの機能は、より高度なHTTPアプリケーションやネットワークツールを構築する際に役立ちます。

io.Readerio.Writerインターフェース

Go言語のioパッケージは、I/O操作のための基本的なインターフェースを提供します。

  • io.Readerインターフェースは、データを読み取るためのReadメソッドを定義します。
  • io.Writerインターフェースは、データを書き込むためのWriteメソッドを定義します。 これらのインターフェースは、様々なデータソース(ファイル、ネットワーク接続、メモリバッファなど)からの読み書きを抽象化し、柔軟で再利用可能なコードの記述を可能にします。

bufio.Reader

bufioパッケージは、バッファリングされたI/Oを提供し、I/O操作の効率を向上させます。bufio.Readerは、io.Readerをラップし、内部バッファを使用してより大きなブロックでデータを読み取ることで、基盤となるI/O操作の回数を減らします。これにより、特に小さな読み取りが頻繁に発生する場合にパフォーマンスが向上します。ReadLineReadSliceのような行指向の読み取りメソッドも提供します。

コードの複製と同期

ソフトウェア開発において、コードの複製(コピー&ペースト)は一般的に避けられるべきプラクティスとされています。これは、コードの重複がメンテナンスの負担を増やし、バグの温床となる可能性があるためです(DRY: Don't Repeat Yourself原則)。しかし、特定の状況下では、コードの複製が意図的に行われることがあります。このコミットのケースでは、「編集の同期を容易にする」という目的のために、チャンク処理のロジックをnet/httpnet/http/httputilの両方に直接コピーするという選択がなされました。これは、両パッケージが独立して進化する可能性があり、かつチャンク処理の内部実装が密接に関連しているため、共有ライブラリや抽象化レイヤーを介するよりも、明示的な複製と手動同期の方が管理しやすいと判断されたためと考えられます。このようなアプローチは、コメントなどで明確に意図を伝え、変更時に両方の場所を更新する規約を設けることで、重複による問題を緩和しようとします。

技術的詳細

このコミットの技術的な核心は、HTTPチャンク転送エンコーディングを処理するコード(リーダーとライター)を、net/httpパッケージからnet/http/httputilパッケージへ「直接コピー」することにあります。これにより、両パッケージがチャンク処理の独立した実装を持つことになりますが、コメントによって両者の同期が求められる構造になります。

具体的な変更点は以下の通りです。

  1. src/pkg/net/http/chunked.goの新規作成とコードの移動/追加:

    • 以前はsrc/pkg/net/http/request.goに存在していたchunkedReader構造体とそのメソッド(beginChunk, Read)、およびヘルパー関数であるreadLineBytesreadLineが、この新しいファイルに移動されました。
    • newChunkedReader関数もこのファイルに移動・追加されました。
    • newChunkedWriter関数とchunkedWriter構造体もこのファイルに存在し、チャンクエンコーディングの書き込みを担当します。
    • ファイル冒頭に「This code is duplicated in httputil/chunked.go. Please make any changes in both files.(このコードはhttputil/chunked.goに複製されています。両方のファイルで変更を行ってください。)」という重要なコメントが追加されました。これは、将来の変更が両方の実装に反映されるべきであることを開発者に指示しています。
  2. src/pkg/net/http/chunked_test.goの新規作成とテストコードの移動/追加:

    • chunked.goのコードに対応するテストケースが、この新しいファイルに移動・追加されました。
    • ここにも「This code is duplicated in httputil/chunked_test.go. Please make any changes in both files.」というコメントがあり、テストコードも同期の対象であることを示しています。
  3. src/pkg/net/http/httputil/chunked.goの変更:

    • このファイルは、net/http/chunked.goからチャンクリーダー/ライターのコードを直接コピーして受け入れました。
    • コピーされたコードは、net/httpパッケージの内部関数名(例: newChunkedReader)がhttputilパッケージでエクスポートされる関数名(例: NewChunkedReader)に変更されています(s/newChunked/NewChunked/g)。
    • ファイル冒頭には「This code is a duplicate of ../chunked.go with these edits: ... Please make any changes in both files.」というコメントが追加され、複製元と変更点が明記されています。
    • 以前のNewChunkedReaderの実装は、http.ReadRequestを内部的に使用するという「ハック」的なアプローチを取っていましたが、これが削除され、net/http/chunked.goと同じ直接的な実装に置き換えられました。
  4. src/pkg/net/http/httputil/chunked_test.goの変更:

    • net/http/chunked_test.goからテストコードがコピーされ、関数名がNewChunkedReader/NewChunkedWriterに対応するように変更されました。
    • ここにも同期を促すコメントが追加されています。
  5. src/pkg/net/http/request.goからのコード削除:

    • 以前request.goに存在していたchunkedReader構造体、そのメソッド、およびreadLineBytes, readLineヘルパー関数が完全に削除されました。これらの機能はnet/http/chunked.goに移動されたため、request.goからは不要になりました。
    • maxLineLength定数とErrLineTooLongエラーもrequest.goから削除され、chunked.gohttputil/chunked.goに移動されました。
  6. src/pkg/net/http/response_test.goの微修正:

    • newChunkedWriterの呼び出しが、以前の&chunkedWriter{wr}からnewChunkedWriter(wr)に変更されました。これは、chunkedWriterがエクスポートされていない内部型であるため、ヘルパー関数を介してインスタンス化する必要があることを反映しています。

この変更により、net/httpnet/http/httputilはチャンク処理に関して独立したコードパスを持つことになりますが、開発者は両方の実装を同期させる責任を負うことになります。これは、コードの重複を許容しつつも、メンテナンスの容易さを優先した設計判断と言えます。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更箇所は、主に以下のファイルに集中しています。

  1. src/pkg/net/http/chunked.go:

    • このファイルが新規作成され、HTTPチャンク転送エンコーディングの読み書きに関する主要なロジックがここに集約されました。
    • chunkedReader構造体とそのbeginChunk()Read()メソッド。
    • readLineBytes()readLine()ヘルパー関数。
    • newChunkedReader()newChunkedWriter()関数。
    • chunkedWriter構造体とそのWrite()Close()メソッド。
    // The wire protocol for HTTP's "chunked" Transfer-Encoding.
    
    // This code is duplicated in httputil/chunked.go.
    // Please make any changes in both files.
    
    package http
    
    import (
    	"bufio"
    	"bytes"
    	"errors"
    	"io"
    	"strconv"
    )
    
    const maxLineLength = 4096 // assumed <= bufio.defaultBufSize
    
    var ErrLineTooLong = errors.New("header line too long")
    
    // newChunkedReader returns a new chunkedReader that translates the data read from r
    // out of HTTP "chunked" format before returning it.
    // The chunkedReader returns io.EOF when the final 0-length chunk is read.
    //
    // newChunkedReader is not needed by normal applications. The http package
    // automatically decodes chunking when reading response bodies.
    func newChunkedReader(r io.Reader) io.Reader {
    	br, ok := r.(*bufio.Reader)
    	if !ok {
    		br = bufio.NewReader(r)
    	}
    	return &chunkedReader{r: br}
    }
    
    type chunkedReader struct {
    	r   *bufio.Reader
    	n   uint64 // unread bytes in chunk
    	err error
    }
    
    func (cr *chunkedReader) beginChunk() {
    	// chunk-size CRLF
    	var line string
    	line, cr.err = readLine(cr.r)
    	if cr.err != nil {
    		return
    	}
    	cr.n, cr.err = strconv.Btoui64(line, 16)
    	if cr.err != nil {
    		return
    	}
    	if cr.n == 0 {
    		cr.err = io.EOF
    	}
    }
    
    func (cr *chunkedReader) Read(b []uint8) (n int, err error) {
    	if cr.err != nil {
    		return 0, cr.err
    	}
    	if cr.n == 0 {
    		cr.beginChunk()
    		if cr.err != nil {
    			return 0, cr.err
    		}
    	}
    	if uint64(len(b)) > cr.n {
    		b = b[0:cr.n]
    	}
    	n, cr.err = cr.r.Read(b)
    	cr.n -= uint64(n)
    	if cr.n == 0 && cr.err == nil {
    		// end of chunk (CRLF)
    		b := make([]byte, 2)
    		if _, cr.err = io.ReadFull(cr.r, b); cr.err == nil {
    			if b[0] != '\r' || b[1] != '\n' {
    				cr.err = errors.New("malformed chunked encoding")
    			}
    		}
    	}
    	return n, cr.err
    }
    
    // Read a line of bytes (up to \n) from b.
    // Give up if the line exceeds maxLineLength.
    // The returned bytes are a pointer into storage in
    // the bufio, so they are only valid until the next bufio read.
    func readLineBytes(b *bufio.Reader) (p []byte, err error) {
    	if p, err = b.ReadSlice('\n'); err != nil {
    		// We always know when EOF is coming.
    		// If the caller asked for a line, there should be a line.
    		if err == io.EOF {
    			err = io.ErrUnexpectedEOF
    		} else if err == bufio.ErrBufferFull {
    			err = ErrLineTooLong
    		}
    		return nil, err
    	}
    	if len(p) >= maxLineLength {
    		return nil, ErrLineTooLong
    	}
    
    	// Chop off trailing white space.
    	p = bytes.TrimRight(p, " \r\t\n")
    
    	return p, nil
    }
    
    // readLineBytes, but convert the bytes into a string.
    func readLine(b *bufio.Reader) (s string, err error) {
    	p, e := readLineBytes(b)
    	if e != nil {
    		return "", e
    	}
    	return string(p), nil
    }
    
    // newChunkedWriter returns a new chunkedWriter that translates writes into HTTP
    // "chunked" format before writing them to w. Closing the returned chunkedWriter
    // sends the final 0-length chunk that marks the end of the stream.
    //
    // newChunkedWriter is not needed by normal applications. The http
    // package adds chunking automatically if handlers don't set a
    // Content-Length header. Using newChunkedWriter inside a handler
    // would result in double chunking or chunking with a Content-Length
    // length, both of which are wrong.
    func newChunkedWriter(w io.Writer) io.WriteCloser {
    	return &chunkedWriter{w}
    }
    
    // Writing to chunkedWriter translates to writing in HTTP chunked Transfer
    // Encoding wire format to the underlying Wire chunkedWriter.
    type chunkedWriter struct {
    	Wire io.Writer
    }
    
    func (cw *chunkedWriter) Write(p []byte) (n int, err error) {
    	if len(p) == 0 {
    		return 0, nil
    	}
    	if _, err = io.WriteString(cw.Wire, strconv.Itob64(int64(len(p)), 16)+"\r\n"); err != nil {
    		return 0, err
    	}
    	if n, err = cw.Wire.Write(p); err != nil {
    		return n, err
    	}
    	if _, err = io.WriteString(cw.Wire, "\r\n"); err != nil {
    		return n, err
    	}
    	return n, nil
    }
    
    func (cw *chunkedWriter) Close() error {
    	_, err := io.WriteString(cw.Wire, "0\r\n")
    	return err
    }
    
  2. src/pkg/net/http/httputil/chunked.go:

    • このファイルは、src/pkg/net/http/chunked.goからコードを直接コピーし、エクスポートされた関数名(NewChunkedReader, NewChunkedWriter)に修正されています。
    • 以前のNewChunkedReaderの「ハック」的な実装が削除され、新しい直接的な実装に置き換えられました。
    // The wire protocol for HTTP's "chunked" Transfer-Encoding.
    
    // This code is a duplicate of ../chunked.go with these edits:
    //	s/newChunked/NewChunked/g
    //	s/package http/package httputil/
    // Please make any changes in both files.
    
    package httputil
    
    import (
    	"bufio"
    	"bytes"
    	"errors"
    	"io"
    	"strconv"
    )
    
    const maxLineLength = 4096 // assumed <= bufio.defaultBufSize
    
    var ErrLineTooLong = errors.New("header line too long")
    
    // NewChunkedReader returns a new chunkedReader that translates the data read from r
    // out of HTTP "chunked" format before returning it.
    // The chunkedReader returns io.EOF when the final 0-length chunk is read.
    //
    // NewChunkedReader is not needed by normal applications. The http package
    // automatically decodes chunking when reading response bodies.
    func NewChunkedReader(r io.Reader) io.Reader {
    	br, ok := r.(*bufio.Reader)
    	if !ok {
    		br = bufio.NewReader(r)
    	}
    	return &chunkedReader{r: br}
    }
    
    type chunkedReader struct {
    	r   *bufio.Reader
    	n   uint64 // unread bytes in chunk
    	err error
    }
    
    func (cr *chunkedReader) beginChunk() {
    	// chunk-size CRLF
    	var line string
    	line, cr.err = readLine(cr.r)
    	if cr.err != nil {
    		return
    	}
    	cr.n, cr.err = strconv.Btoui64(line, 16)
    	if cr.err != nil {
    		return
    	}
    	if cr.n == 0 {
    		cr.err = io.EOF
    	}
    }
    
    func (cr *chunkedReader) Read(b []uint8) (n int, err error) {
    	if cr.err != nil {
    		return 0, cr.err
    	}
    	if cr.n == 0 {
    		cr.beginChunk()
    		if cr.err != nil {
    			return 0, cr.err
    		}
    	}
    	if uint64(len(b)) > cr.n {
    		b = b[0:cr.n]
    	}
    	n, cr.err = cr.r.Read(b)
    	cr.n -= uint64(n)
    	if cr.n == 0 && cr.err == nil {
    		// end of chunk (CRLF)
    		b := make([]byte, 2)
    		if _, cr.err = io.ReadFull(cr.r, b); cr.err == nil {
    			if b[0] != '\r' || b[1] != '\n' {
    				cr.err = errors.New("malformed chunked encoding")
    			}
    		}
    	}
    	return n, cr.err
    }
    
    // Read a line of bytes (up to \n) from b.
    // Give up if the line exceeds maxLineLength.
    // The returned bytes are a pointer into storage in
    // the bufio, so they are only valid until the next bufio read.
    func readLineBytes(b *bufio.Reader) (p []byte, err error) {
    	if p, err = b.ReadSlice('\n'); err != nil {
    		// We always know when EOF is coming.
    		// If the caller asked for a line, there should be a line.
    		if err == io.EOF {
    			err = io.ErrUnexpectedEOF
    		} else if err == bufio.ErrBufferFull {
    			err = ErrLineTooLong
    		}
    		return nil, err
    	}
    	if len(p) >= maxLineLength {
    		return nil, ErrLineTooLong
    	}
    
    	// Chop off trailing white space.
    	p = bytes.TrimRight(p, " \r\t\n")
    
    	return p, nil
    }
    
    // readLineBytes, but convert the bytes into a string.
    func readLine(b *bufio.Reader) (s string, err error) {
    	p, e := readLineBytes(b)
    	if e != nil {
    		return "", e
    	}
    	return string(p), nil
    }
    
    // NewChunkedWriter returns a new chunkedWriter that translates writes into HTTP
    // "chunked" format before writing them to w. Closing the returned chunkedWriter
    // sends the final 0-length chunk that marks the end of the stream.
    //
    // NewChunkedWriter is not needed by normal applications. The http
    // package adds chunking automatically if handlers don't set a
    // Content-Length header. Using NewChunkedWriter inside a handler
    // would result in double chunking or chunking with a Content-Length
    // length, both of which are wrong.
    func NewChunkedWriter(w io.Writer) io.WriteCloser {
    	return &chunkedWriter{w}
    }
    
    // Writing to chunkedWriter translates to writing in HTTP chunked Transfer
    // Encoding wire format to the underlying Wire chunkedWriter.
    type chunkedWriter struct {
    	Wire io.Writer
    }
    
    func (cw *chunkedWriter) Write(p []byte) (n int, err error) {
    	if len(p) == 0 {
    		return 0, nil
    	}
    	if _, err = io.WriteString(cw.Wire, strconv.Itob64(int64(len(p)), 16)+"\r\n"); err != nil {
    		return 0, err
    	}
    	if n, err = cw.Wire.Write(p); err != nil {
    		return n, err
    	}
    	if _, err = io.WriteString(cw.Wire, "\r\n"); err != nil {
    		return n, err
    	}
    	return n, nil
    }
    
    func (cw *chunkedWriter) Close() error {
    	_, err := io.WriteString(cw.Wire, "0\r\n")
    	return err
    }
    
  3. src/pkg/net/http/request.go:

    • 以前このファイルに存在していたchunkedReader関連のコード(構造体、メソッド、ヘルパー関数)が削除されました。
    --- a/src/pkg/net/http/request.go
    +++ b/src/pkg/net/http/request.go
    @@ -19,12 +19,10 @@ import (
     	"mime/multipart"
     	"net/textproto"
     	"net/url"
    -	"strconv"
     	"strings"
     )
    
     const (
    -	maxLineLength    = 4096 // assumed <= bufio.defaultBufSize
     	maxValueLength   = 4096
     	maxHeaderLines   = 1024
     	chunkSize        = 4 << 10  // 4 KB chunks
    @@ -43,7 +41,6 @@ type ProtocolError struct {
     func (err *ProtocolError) Error() string { return err.ErrorString }
    
     var (
    -	ErrLineTooLong          = &ProtocolError{"header line too long"}
     	ErrHeaderTooLong        = &ProtocolError{"header too long"}
     	ErrShortBody            = &ProtocolError{"entity body too short"}
     	ErrNotSupported         = &ProtocolError{"feature not supported"}
    @@ -375,44 +372,6 @@ func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header) err
     	return nil
     }
    
    -// Read a line of bytes (up to \n) from b.
    -// Give up if the line exceeds maxLineLength.
    -// The returned bytes are a pointer into storage in
    -// the bufio, so they are only valid until the next bufio read.
    -func readLineBytes(b *bufio.Reader) (p []byte, err error) {
    -	if p, err = b.ReadSlice('\n'); err != nil {
    -		// We always know when EOF is coming.
    -		// If the caller asked for a line, there should be a line.
    -		if err == io.EOF {
    -			err = io.ErrUnexpectedEOF
    -		} else if err == bufio.ErrBufferFull {
    -			err = ErrLineTooLong
    -		}
    -		return nil, err
    -	}
    -	if len(p) >= maxLineLength {
    -		return nil, ErrLineTooLong
    -	}
    -
    -	// Chop off trailing white space.
    -	var i int
    -	for i = len(p); i > 0; i-- {
    -		if c := p[i-1]; c != ' ' && c != '\r' && c != '\t' && c != '\n' {
    -			break
    -		}
    -	}
    -	return p[0:i], nil
    -}
    -
    -// readLineBytes, but convert the bytes into a string.
    -func readLine(b *bufio.Reader) (s string, err error) {
    -	p, e := readLineBytes(b)
    -	if e != nil {
    -		return "", e
    -	}
    -	return string(p), nil
    -}
    -
     // Convert decimal at s[i:len(s)] to integer,
     // returning value, string position where the digits stopped,
     // and whether there was a valid number (digits, not too big).
    @@ -448,55 +407,6 @@ func ParseHTTPVersion(vers string) (major, minor int, ok bool) {
     	return major, minor, true
     }
    
    -type chunkedReader struct {
    -	r   *bufio.Reader
    -	n   uint64 // unread bytes in chunk
    -	err error
    -}
    -
    -func (cr *chunkedReader) beginChunk() {
    -	// chunk-size CRLF
    -	var line string
    -	line, cr.err = readLine(cr.r)
    -	if cr.err != nil {
    -		return
    -	}
    -	cr.n, cr.err = strconv.Btoui64(line, 16)
    -	if cr.err != nil {
    -		return
    -	}
    -	if cr.n == 0 {
    -		cr.err = io.EOF
    -	}
    -}
    -
    -func (cr *chunkedReader) Read(b []uint8) (n int, err error) {
    -	if cr.err != nil {
    -		return 0, cr.err
    -	}
    -	if cr.n == 0 {
    -		cr.beginChunk()
    -		if cr.err != nil {
    -			return 0, cr.err
    -		}
    -	}
    -	if uint64(len(b)) > cr.n {
    -		b = b[0:cr.n]
    -	}
    -	n, cr.err = cr.r.Read(b)
    -	cr.n -= uint64(n)
    -	if cr.n == 0 && cr.err == nil {
    -		// end of chunk (CRLF)
    -		b := make([]byte, 2)
    -		if _, cr.err = io.ReadFull(cr.r, b); cr.err == nil {
    -			if b[0] != '\r' || b[1] != '\n' {
    -				cr.err = errors.New("malformed chunked encoding")
    -			}
    -		}
    -	}
    -	return n, cr.err
    -}
    -
     // NewRequest returns a new Request given a method, URL, and optional body.
     func NewRequest(method, urlStr string, body io.Reader) (*Request, error) {
     	u, err := url.Parse(urlStr)
    

コアとなるコードの解説

chunkedReadernewChunkedReader

chunkedReaderは、HTTPチャンク転送エンコーディングされたデータストリームを読み取るためのio.Readerインターフェースを実装する構造体です。

  • r *bufio.Reader: 基盤となるバッファリングされたリーダー。
  • n uint64: 現在のチャンクに残っている未読バイト数。
  • err error: 読み取り中に発生したエラー。

newChunkedReader(r io.Reader)関数は、与えられたio.ReaderをラップしてchunkedReaderの新しいインスタンスを返します。これにより、基盤となるリーダーから読み取られるデータがチャンクデコードされます。

chunkedReaderRead(b []uint8)メソッドは、チャンクデコードの主要なロジックを含んでいます。

  1. まず、cr.errが設定されている場合は、そのエラーを返します。
  2. cr.n(現在のチャンクの残りバイト数)が0の場合、新しいチャンクの読み取りを開始するためにcr.beginChunk()を呼び出します。
  3. cr.beginChunk()は、チャンクサイズを示す行を読み取り、それを16進数として解析してcr.nに設定します。チャンクサイズが0の場合、それはストリームの終わり(io.EOF)を示します。
  4. 読み取りバッファbの長さが現在のチャンクの残りバイト数cr.nよりも大きい場合、bcr.nの長さに切り詰めます。これにより、現在のチャンクの境界を越えて読み取ることがなくなります。
  5. 基盤となるリーダーcr.rからデータを読み取り、nバイト読み取られたことを記録し、cr.nからそのバイト数を減算します。
  6. もしcr.nが0になり、かつエラーがない場合、現在のチャンクの終わりを示すCRLF(\r\n)を読み取ります。これが存在しない場合、不正なチャンクエンコーディングとしてエラーを返します。

readLineBytesreadLine

これらのヘルパー関数は、bufio.Readerから1行(改行文字まで)を読み取るために使用されます。

  • readLineBytes(b *bufio.Reader): bufio.Readerからバイトスライスとして1行を読み取ります。maxLineLengthを超える行や、読み取りエラー(EOF、バッファフル)を適切に処理します。読み取った行の末尾の空白文字(スペース、CR、タブ、LF)をトリムします。
  • readLine(b *bufio.Reader): readLineBytesを呼び出し、結果のバイトスライスを文字列に変換して返します。

これらの関数は、チャンクサイズを読み取る際に使用され、HTTPプロトコルの行ベースの性質に対応しています。

chunkedWriternewChunkedWriter

chunkedWriterは、io.WriteCloserインターフェースを実装し、書き込まれたデータをHTTPチャンク転送エンコーディング形式に変換して基盤となるio.Writerに書き込みます。

  • Wire io.Writer: チャンクエンコードされたデータが書き込まれる基盤となるライター。

newChunkedWriter(w io.Writer)関数は、与えられたio.WriterをラップしてchunkedWriterの新しいインスタンスを返します。

chunkedWriterWrite(p []byte)メソッドは、チャンクエンコードの主要なロジックを含んでいます。

  1. 書き込むデータpの長さが0の場合、何もせずに0を返します。
  2. まず、データの長さを16進数文字列に変換し、その後にCRLFを付けてcw.Wireに書き込みます(例: 7\r\n)。
  3. 次に、実際のデータpcw.Wireに書き込みます。
  4. 最後に、データチャンクの終わりを示すCRLFをcw.Wireに書き込みます。

chunkedWriterClose()メソッドは、ストリームの終わりを示す最終チャンク(サイズ0のチャンク、0\r\n)を書き込みます。

コードの複製と同期の指示

両方のchunked.goファイル(net/httpnet/http/httputil)の冒頭には、以下のコメントが追加されています。

// This code is duplicated in httputil/chunked.go.
// Please make any changes in both files.

または

// This code is a duplicate of ../chunked.go with these edits:
//	s/newChunked/NewChunked/g
//	s/package http/package httputil/
// Please make any changes in both files.

これらのコメントは、このコードが意図的に複製されたものであり、将来的にチャンク処理のロジックに変更を加える際には、両方のファイルで同じ変更を行う必要があることを開発者に明確に指示しています。これは、コードの重複によるメンテナンス上のリスクを、明示的な規約によって管理しようとするアプローチです。

関連リンク

参考にした情報源リンク