[インデックス 12935] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージにおける字句解析(lexical analysis)関連のコードをクリーンアップし、効率化することを目的としています。具体的には、HTTPヘッダーやクッキーの解析に使用されるトークン(token)の定義と検証ロジックが変更されています。
変更されたファイルは以下の通りです。
src/pkg/net/http/cookie.go
: クッキー名の検証ロジックが簡素化されました。src/pkg/net/http/lex.go
: 字句解析に関する主要な変更が行われ、isToken
関数の実装が最適化され、不要な関数が削除されました。src/pkg/net/http/lex_test.go
:lex.go
の変更に伴い、テストコードが大幅に修正・削除されました。
コミット
commit b678c197855ef8417cd8ba5df40904c66d60a4c7
Author: Pascal S. de Kloe <pascal@quies.net>
Date: Mon Apr 23 10:26:10 2012 -0700
net/http: lex cleanup
R=rsc, bradfitz
CC=golang-dev
https://golang.org/cl/6099043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b678c197855ef8417cd8ba5df40904c66d60a4c7
元コミット内容
net/http: lex cleanup
R=rsc, bradfitz
CC=golang-dev
https://golang.org/cl/6099043
変更の背景
このコミットの背景には、net/http
パッケージの字句解析部分のコードの可読性、保守性、そしてパフォーマンスの向上が挙げられます。特に、HTTPの仕様(RFC)で定義されている「トークン」の概念をより効率的に扱うための改善が図られています。
以前の実装では、isToken
のような文字の分類関数が switch
文や複数の論理演算子を用いて実装されており、これはコードが冗長になる可能性がありました。また、HTTPヘッダーのフィールド値の解析に関するいくつかの関数(httpUnquotePair
, httpUnquote
, httpSplitFieldValue
)が存在していましたが、これらが実際に必要とされているか、あるいはより効率的な方法で処理できるかどうかの見直しが行われたと考えられます。
この「lex cleanup」というコミットメッセージは、字句解析器(lexer)のコードベースを整理し、よりクリーンで効率的な状態にすることを明確に示しています。これにより、HTTPプロトコルの解析処理がより堅牢かつ高速になることが期待されます。
前提知識の解説
1. HTTPにおける「トークン」とは
HTTP/1.1の仕様(RFC 7230: Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing、現在はRFC 9110: HTTP Semanticsが最新)において、「トークン(token)」は、HTTPヘッダーフィールド名や一部のヘッダーフィールド値(例: Content-Type
のメディアタイプ、Cache-Control
のディレクティブなど)で使用される、特定の文字セットから構成される連続した文字列を指します。
RFC 7230の定義では、トークンは以下の文字セットから構成されるとされています。
- 英数字(
a-z
,A-Z
,0-9
) - 特定の記号:
!
,#
,$
,%
,&
,'
,*
,+
,-
,.
,^
,_
, ````,|
,~
これらの文字は「tchar (token character)」と呼ばれ、トークンは1つ以上のtcharのシーケンスとして定義されます。トークンは、HTTPメッセージの構文解析において、区切り文字(セパレータ)や制御文字と区別するために重要な役割を果たします。
2. 字句解析(Lexical Analysis)
字句解析は、コンパイラやインタプリタの最初の段階であり、入力された文字列(ソースコードやプロトコルデータなど)を、意味を持つ最小単位である「トークン」の並びに分解するプロセスです。このプロセスを担当するプログラムは「字句解析器(lexer)」または「スキャナ(scanner)」と呼ばれます。
HTTPプロトコルの文脈では、受信したHTTPリクエストやレスポンスの生データを、ヘッダー名、ヘッダー値、メソッド名、URIパスなどの意味のあるトークンに分解する役割を字句解析器が担います。この分解されたトークンの並びが、次の段階である構文解析(parsing)に渡され、HTTPメッセージ全体の構造が理解されます。
3. RFC 2616とRFC 7230/9110
- RFC 2616 (HTTP/1.1): 1999年に発行されたHTTP/1.1の主要な仕様書でした。長らくHTTPの標準として広く参照されてきましたが、その内容が非常に大きく、曖昧な点や矛盾も指摘されていました。
- RFC 7230-7235 (HTTP/1.1): 2014年に発行され、RFC 2616を廃止(obsolete)し、HTTP/1.1の仕様をより小さく、明確な複数のRFCに分割しました。RFC 7230はその中でも「Message Syntax and Routing」を扱い、トークンの定義などが含まれています。
- RFC 9110 (HTTP Semantics): 2022年に発行され、RFC 7230を含むいくつかのRFCを統合・更新し、HTTPのセマンティクス(意味論)に関する最新の標準となっています。トークンの定義など、基本的な構文に関する部分はRFC 7230から大きく変更されていませんが、より現代的なHTTPの利用状況に合わせて記述が整理されています。
このコミットが行われた2012年時点では、RFC 2616がまだ主要なHTTP/1.1の仕様でしたが、Go言語の net/http
パッケージは、将来のRFCの変更やより厳密な解釈を見越して、あるいは既存のRFCの解釈をより効率的に実装するために、内部的な改善を継続的に行っています。
4. Go言語における文字・文字列操作
Go言語では、文字列はUTF-8エンコードされたバイトのシーケンスとして扱われます。rune
型はUnicodeコードポイントを表し、for range
ループで文字列をイテレートすると、各rune
が取得できます。
byte
: 8ビットの符号なし整数型で、ASCII文字やUTF-8エンコードされたバイトを扱う際に使用されます。rune
:int32
のエイリアスで、Unicodeコードポイントを表します。Go言語では、文字を扱う際にrune
を使用することが推奨されます。strings.IndexFunc(s string, f func(rune) bool) int
: 文字列s
内で、関数f
がtrue
を返す最初のrune
のインデックスを返します。見つからない場合は-1
を返します。この関数は、特定の条件を満たす文字を文字列から検索するのに非常に便利です。
技術的詳細
このコミットの主要な技術的変更点は、HTTPトークンの文字を判定するロジックの効率化と、関連する不要な関数の削除です。
1. src/pkg/net/http/lex.go
の変更
isSeparator
関数の削除: 以前はHTTPのセパレータ文字を判定するisSeparator
関数が存在しましたが、これは直接的にisToken
の定義から除外される文字を判定するために使われていました。新しい実装では、isToken
の定義が直接的なルックアップテーブルになるため、この関数は不要になりました。isCtl
,isChar
,isAnyText
,isQdText
関数の削除: これらの関数は、HTTPの制御文字(CTL)、任意の文字(CHAR)、任意のテキスト文字(ANYTEXT)、引用符付きテキスト文字(QDTEXT)を判定するために使用されていました。これらは主にhttpUnquote
やhttpSplitFieldValue
といった、今回のコミットで削除される関数群の中で利用されていました。これらの関数が不要になったため、削除されました。httpUnquotePair
,httpUnquote
,httpSplitFieldValue
関数の削除: これらの関数は、HTTPヘッダーのフィールド値の解析、特に引用符で囲まれた文字列のアンクォートや、フィールド値を複数の部分に分割する処理を担当していました。コミットメッセージの「lex cleanup」という言葉から、これらの関数が現在の設計や要件において不要であるか、あるいはより上位のレイヤーで効率的に処理できると判断されたため、削除されたと考えられます。これにより、字句解析器の責務がより明確になり、コードベースがスリム化されました。isToken
関数の大幅な変更:- 旧実装:
isToken
はisChar(c) && !isCtl(c) && !isSeparator(c)
というロジックで、他の複数の関数に依存していました。これは、文字がASCII範囲内であり、かつ制御文字でもセパレータでもない場合にトークン文字と判定するものでした。 - 新実装:
isTokenTable
という[127]bool
型の配列が導入されました。この配列は、ASCII文字(0-126)の各バイト値がHTTPトークン文字であるかどうかを直接true
/false
で保持するルックアップテーブルです。isToken
関数は、引数として受け取ったrune
をint
にキャストし、その値がisTokenTable
の範囲内であれば、テーブルの対応するエントリを参照してトークン文字であるかを判定します。これにより、複雑な条件分岐や複数の関数呼び出しが不要になり、非常に高速な判定が可能になります。
- 旧実装:
isNotToken
関数の追加: 新しいisToken
関数を補完するために、isNotToken
関数が追加されました。これは!isToken(r)
を返すシンプルなヘルパー関数です。
2. src/pkg/net/http/cookie.go
の変更
isCookieNameValid
関数の簡素化:- 旧実装: クッキー名が有効なトークン文字のみで構成されているかを、文字列をループして
isToken
関数を各文字に適用することで判定していました。 - 新実装:
strings.IndexFunc(raw, isNotToken) < 0
という簡潔な表現に変更されました。これは、クッキー名raw
の中にisNotToken
がtrue
を返す文字(つまりトークンではない文字)が一つでも含まれていれば、そのインデックスを返し、含まれていなければ-1
を返すというstrings.IndexFunc
の特性を利用しています。-1
が返された場合、すべての文字がトークン文字であると判断され、クッキー名が有効であると判定されます。この変更により、コードがよりGoらしいイディオムに沿ったものになり、可読性が向上しました。
- 旧実装: クッキー名が有効なトークン文字のみで構成されているかを、文字列をループして
3. src/pkg/net/http/lex_test.go
の変更
lex.go
から多くの関数が削除されたため、それらの関数に依存していたテストコード(lexTest
構造体、lexTests
変数、TestSplitFieldValue
関数など)が削除されました。- 新しい
isToken
関数の正確性を検証するためのTestIsToken
関数が追加されました。このテストは、0から130までのすべてのルーン(文字)に対して、isChar(r) && !isCtl(r) && !isSeparator(r)
という旧来のロジックで期待される結果と、新しいisToken(r)
の結果が一致するかどうかを検証します。これにより、isTokenTable
が正しく初期化され、HTTPトークンの定義に厳密に従っていることが保証されます。また、isChar
,isCtl
,isSeparator
関数はテストのためだけに再定義されています。
コアとなるコードの変更箇所
src/pkg/net/http/lex.go
--- a/src/pkg/net/http/lex.go
+++ b/src/pkg/net/http/lex.go
@@ -6,131 +6,91 @@ package http
// This file deals with lexical matters of HTTP
-func isSeparator(c byte) bool {
- switch c {
- case '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t':
- return true
- }
- return false
+var isTokenTable = [127]bool{
+ '!': true,
+ '#': true,
+ '$': true,
+ '%': true,
+ '&': true,
+ '\'': true,
+ '*': true,
+ '+': true,
+ '-': true,
+ '.': true,
+ '0': true,
+ '1': true,
+ '2': true,
+ '3': true,
+ '4': true,
+ '5': true,
+ '6': true,
+ '7': true,
+ '8': true,
+ '9': true,
+ 'A': true,
+ 'B': true,
+ 'C': true,
+ 'D': true,
+ 'E': true,
+ 'F': true,
+ 'G': true,
+ 'H': true,
+ 'I': true,
+ 'J': true,
+ 'K': true,
+ 'L': true,
+ 'M': true,
+ 'N': true,
+ 'O': true,
+ 'P': true,
+ 'Q': true,
+ 'R': true,
+ 'S': true,
+ 'T': true,
+ 'U': true,
+ 'W': true,
+ 'V': true,
+ 'X': true,
+ 'Y': true,
+ 'Z': true,
+ '^': true,
+ '_': true,
+ '`': true,
+ 'a': true,
+ 'b': true,
+ 'c': true,
+ 'd': true,
+ 'e': true,
+ 'f': true,
+ 'g': true,
+ 'h': true,
+ 'i': true,
+ 'j': true,
+ 'k': true,
+ 'l': true,
+ 'm': true,
+ 'n': true,
+ 'o': true,
+ 'p': true,
+ 'q': true,
+ 'r': true,
+ 's': true,
+ 't': true,
+ 'u': true,
+ 'v': true,
+ 'w': true,
+ 'x': true,
+ 'y': true,
+ 'z': true,
+ '|': true,
+ '~': true,
+}
+
+func isToken(r rune) bool {
+ i := int(r)
+ return i < len(isTokenTable) && isTokenTable[i]
}
-func isCtl(c byte) bool { return (0 <= c && c <= 31) || c == 127 }
-
-func isChar(c byte) bool { return 0 <= c && c <= 127 }
-
-func isAnyText(c byte) bool { return !isCtl(c) }
-
-func isQdText(c byte) bool { return isAnyText(c) && c != '"' }
-
-func isToken(c byte) bool { return isChar(c) && !isCtl(c) && !isSeparator(c) }
-
-// Valid escaped sequences are not specified in RFC 2616, so for now, we assume
-// that they coincide with the common sense ones used by GO. Malformed
-// characters should probably not be treated as errors by a robust (forgiving)
-// parser, so we replace them with the '?' character.
-func httpUnquotePair(b byte) byte {
-// skip the first byte, which should always be '\'
- switch b {
- case 'a':
- return '\a'
- case 'b':
- return '\b'
- case 'f':
- return '\f'
- case 'n':
- return '\n'
- case 'r':
- return '\r'
- case 't':
- return '\t'
- case 'v':
- return '\v'
- case '\\':
- return '\\'
- case '\'':
- return '\''
- case '"':
- return '"'
- }
- return '?'
-}
-
-// raw must begin with a valid quoted string. Only the first quoted string is
-// parsed and is unquoted in result. eaten is the number of bytes parsed, or -1
-// upon failure.
-func httpUnquote(raw []byte) (eaten int, result string) {
- buf := make([]byte, len(raw))
- if raw[0] != '"' {
- return -1, ""
- }
- eaten = 1
- j := 0 // # of bytes written in buf
- for i := 1; i < len(raw); i++ {
- switch b := raw[i]; b {
- case '"':
- eaten++
- buf = buf[0:j]
- return i + 1, string(buf)
- case '\\':
- if len(raw) < i+2 {
- return -1, ""
- }
- buf[j] = httpUnquotePair(raw[i+1])
- eaten += 2
- j++
- i++
- default:
- if isQdText(b) {
- buf[j] = b
- } else {
- buf[j] = '?'
- }
- eaten++
- j++
- }
- }
- return -1, ""
-}
-
-// This is a best effort parse, so errors are not returned, instead not all of
-// the input string might be parsed. result is always non-nil.
-func httpSplitFieldValue(fv string) (eaten int, result []string) {
- result = make([]string, 0, len(fv))
- raw := []byte(fv)
- i := 0
- chunk := ""
- for i < len(raw) {
- b := raw[i]
- switch {
- case b == '"':
- eaten, unq := httpUnquote(raw[i:len(raw)])
- if eaten < 0 {
- return i, result
- } else {
- i += eaten
- chunk += unq
- }
- case isSeparator(b):
- if chunk != "" {
- result = result[0 : len(result)+1]
- result[len(result)-1] = chunk
- chunk = ""
- }
- i++
- case isToken(b):
- chunk += string(b)
- i++
- case b == '\n' || b == '\r':
- i++
- default:
- chunk += "?"
- i++
- }
- }
- if chunk != "" {
- result = result[0 : len(result)+1]
- result[len(result)-1] = chunk
- chunk = ""
- }
- return i, result
+func isNotToken(r rune) bool {
+ return !isToken(r)
}
src/pkg/net/http/cookie.go
--- a/src/pkg/net/http/cookie.go
+++ b/src/pkg/net/http/cookie.go
@@ -258,10 +258,5 @@ func parseCookieValueUsing(raw string, validByte func(byte) bool) (string, bool)
}
func isCookieNameValid(raw string) bool {
- for _, c := range raw {
- if !isToken(byte(c)) {
- return false
- }
- }
- return true
+ return strings.IndexFunc(raw, isNotToken) < 0
}
コアとなるコードの解説
isTokenTable
と isToken
関数の最適化
このコミットの最も重要な変更は、isToken
関数の実装方法です。
isTokenTable
: これは、ASCII文字(0-126)の各コードポイントがHTTPトークン文字であるかどうかを高速に判定するためのブーリアン配列です。配列のインデックスが文字のASCII値に対応し、その値がtrue
であればトークン文字、false
であれば非トークン文字であることを示します。このテーブルはコンパイル時に静的に初期化されるため、実行時のオーバーヘッドが非常に小さいです。- 新しい
isToken
関数:
この関数は、入力されたfunc isToken(r rune) bool { i := int(r) return i < len(isTokenTable) && isTokenTable[i] }
rune
(Unicodeコードポイント) をint
に変換し、それがisTokenTable
の有効なインデックス範囲内にあるかを確認します。範囲内であれば、直接テーブルを参照してtrue
またはfalse
を返します。このルックアップテーブル方式は、以前の複数の条件分岐や関数呼び出しを含む実装と比較して、非常に高速かつ効率的です。特に、HTTPヘッダーの解析のように文字単位の判定が頻繁に行われる場面では、この最適化がパフォーマンスに大きく貢献します。
isCookieNameValid
の strings.IndexFunc
を用いた簡素化
src/pkg/net/http/cookie.go
の isCookieNameValid
関数は、クッキー名が有効なHTTPトークン文字のみで構成されているかを検証します。
- 旧実装:
for
ループを使ってクッキー名の各文字をイテレートし、それぞれがisToken
であるかをチェックしていました。一つでもトークンではない文字が見つかればfalse
を返していました。 - 新実装:
この変更では、Go標準ライブラリのfunc isCookieNameValid(raw string) bool { return strings.IndexFunc(raw, isNotToken) < 0 }
strings.IndexFunc
関数が活用されています。strings.IndexFunc(s, f)
は、文字列s
の中で、関数f
がtrue
を返す最初の文字のインデックスを返します。もしそのような文字が見つからなければ-1
を返します。 ここでisNotToken
は、新しく追加された!isToken(r)
を返すヘルパー関数です。したがって、strings.IndexFunc(raw, isNotToken) < 0
は、「raw
文字列の中にisToken
がfalse
を返す文字(つまりトークンではない文字)が一つも存在しない」ことを意味します。これは、クッキー名がすべて有効なトークン文字で構成されているという条件を簡潔かつ効率的に表現しています。
これらの変更は、Go言語のイディオムに沿ったコードの簡素化と、パフォーマンスの向上を両立させています。
関連リンク
- Go CL 6099043: https://golang.org/cl/6099043
参考にした情報源リンク
- RFC 7230: Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing: https://datatracker.ietf.org/doc/html/rfc7230
- RFC 9110: HTTP Semantics: https://datatracker.ietf.org/doc/html/rfc9110
- Go言語
strings
パッケージ ドキュメント: https://pkg.go.dev/strings - Go言語
rune
型に関する解説 (例: A Tour of Go - Unicode): https://go.dev/tour/moretypes/16 - HTTP token definition RFC (Web Search): https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHDCUVTCmQFKAHeSHMydXnTyKnzAmK_99tbpePdOXHo9bKWiepqgSZJyj_hB1MKmVK13r0QhJU19qDV_WCcpsYlEKdZueXpjKCEDtlpfObMElJspfkrH1dvCT_QX9FslgbZky2ehbw= (and related search results)