[インデックス 10106] ファイルの概要
このコミットは、Go言語の標準ライブラリ内の複数のパッケージ(csv
, gob
, json
, mail
, mime
, xml
)において、文字を扱う際の型をint
からrune
に変更するものです。これにより、Unicode文字の正確な処理が保証され、コードの堅牢性が向上します。
コミット
commit b50a847c3cf4ffa9064f03652126ef603efa3cf5
Author: Russ Cox <rsc@golang.org>
Date: Tue Oct 25 22:23:54 2011 -0700
csv, gob, json, mail, mime, xml: use rune
Nothing terribly interesting here.
R=golang-dev, r, borman
CC=golang-dev
https://golang.org/cl/5315043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b50a847c3cf4ffa9064f03652126ef603efa3cf5
元コミット内容
このコミットの元のメッセージは以下の通りです。
csv, gob, json, mail, mime, xml: use rune
Nothing terribly interesting here.
これは、csv
, gob
, json
, mail
, mime
, xml
の各パッケージでrune
型を使用するように変更したことを示しています。コミットメッセージ自体は簡潔で、特筆すべき点はないとされていますが、その技術的な意味合いは重要です。
変更の背景
Go言語において、文字列はUTF-8でエンコードされたバイトのシーケンスとして扱われます。しかし、文字(Unicodeコードポイント)を直接扱う必要がある場合、byte
型(8ビット)では不十分であり、int
型(通常32ビットまたは64ビット)を使用すると、その意図が不明瞭になる可能性がありました。
この変更の背景には、Go言語がUnicodeとUTF-8を第一級の市民として扱う設計思想があります。int
型を文字の表現に使うことは、そのint
がバイト値を表すのか、それともUnicodeコードポイントを表すのかが曖昧になる原因となります。特に、多言語対応や絵文字など、ASCII範囲外の文字を正確に処理するためには、バイトではなくUnicodeコードポイントを意識したプログラミングが不可欠です。
rune
型は、Go言語においてUnicodeコードポイントを明示的に表現するためのエイリアス型(int32
のエイリアス)です。このコミットは、文字を扱う変数や関数の引数、戻り値の型をint
からrune
に統一することで、コードの可読性と正確性を向上させ、将来的なUnicode関連のバグを防ぐことを目的としています。
前提知識の解説
Go言語における文字列と文字
Go言語では、文字列は不変のバイトスライスとして内部的に表現されます。これはUTF-8エンコーディングを前提としています。
string
: UTF-8でエンコードされたバイトのシーケンス。直接インデックスアクセスするとバイト値が返される。byte
: 8ビットの符号なし整数型。ASCII文字や単一バイトのデータを扱うのに適している。uint8
のエイリアス。rune
: Unicodeコードポイントを表す型。int32
のエイリアス。UTF-8でエンコードされた文字列から個々のUnicode文字を取り出す際に使用される。1つのrune
は1バイトから4バイトのUTF-8シーケンスに対応する。
UnicodeとUTF-8
- Unicode: 世界中の文字を統一的に扱うための文字コード標準。各文字に一意の番号(コードポイント)を割り当てる。
- UTF-8: Unicodeコードポイントをバイトシーケンスにエンコードするための可変長エンコーディング方式。ASCII文字は1バイトで表現され、それ以外の文字は2バイト以上で表現される。Go言語の文字列のデフォルトエンコーディング。
int
とrune
の使い分け
Go言語では、int
はプラットフォーム依存の整数型であり、通常32ビットまたは64ビットです。文字を扱う際にint
を使用すると、それが単なる整数値なのか、それとも文字コードポイントなのかが不明瞭になります。
rune
型を導入することで、開発者はその変数がUnicodeコードポイントを意図していることを明確に理解できます。これにより、文字の比較、変換、操作がより安全かつ意図通りに行われるようになります。例えば、unicode
パッケージの関数はrune
型を引数にとることが多く、rune
を使用することでこれらの関数との連携がスムーズになります。
技術的詳細
このコミットでは、主に以下の変更が行われています。
-
型定義の変更: 構造体のフィールドや関数の引数、戻り値の型が
int
からrune
に変更されています。src/pkg/csv/reader.go
:Reader
構造体のComma
とComment
フィールドがint
からrune
に変更。readRune()
,skip()
,parseField()
などのメソッドの引数や戻り値の型もint
からrune
に変更。src/pkg/csv/reader_test.go
: テスト構造体のComma
とComment
フィールドがint
からrune
に変更。src/pkg/csv/writer.go
:Writer
構造体のComma
フィールドがint
からrune
に変更。Write()
メソッド内の文字処理もrune
を使用するように変更。src/pkg/gob/encoder_test.go
: テストコード内で[]int
が[]rune
に変更。これは、gob
エンコーダがスライスを再利用するテストにおいて、文字のスライスをより適切に表現するためと考えられます。src/pkg/json/decode.go
:getu4()
関数の戻り値がint
からrune
に変更。unquoteBytes()
関数内の文字処理もrune
を使用するように変更。src/pkg/json/decode_test.go
:noSpace()
関数の引数と戻り値がint
からrune
に変更。src/pkg/json/scanner.go
:isSpace()
関数の引数がint
からrune
に変更。src/pkg/json/scanner_test.go
:genString()
関数内で[]int
が[]rune
に変更。src/pkg/json/stream.go
:nonSpace()
関数内でisSpace()
の呼び出し時にint(c)
からrune(c)
にキャスト。src/pkg/mail/message.go
:decodeRFC2047Word()
関数内でb.WriteRune(int(c))
がb.WriteRune(rune(c))
に変更。src/pkg/mime/grammar.go
:isTSpecial()
,IsTokenChar()
,IsQText()
関数の引数がint
からrune
に変更。src/pkg/mime/mediatype.go
:isNotTokenChar()
関数の引数がint
からrune
に変更。consumeValue()
関数内でleadQuote
がint
からrune
に変更され、ループ内の文字処理もrune
を使用するように変更。src/pkg/strconv/quote.go
:quoteWith()
,unhex()
,UnquoteChar()
関数の引数や戻り値、内部変数でint
がrune
に変更。src/pkg/xml/read.go
:fieldName()
関数内でstrings.Map
に渡す匿名関数の引数がint
からrune
に変更。src/pkg/xml/xml.go
:isInCharacterRange()
関数の引数がint
からrune
に変更。procInstEncoding()
関数内でstrings.IndexRune
の呼び出し時にint(v[0])
からrune(v[0])
にキャスト。
-
変数名の変更:
rune
型を扱う変数名が、元のrune
からr1
やrr
など、より具体的な名前に変更されている箇所もあります。これは、単に型を変更するだけでなく、コードの意図をより明確にするためのリファクタリングの一環と考えられます。
これらの変更は、Go言語の標準ライブラリが文字処理においてUnicodeのセマンティクスをより厳密に遵守するための重要なステップです。
コアとなるコードの変更箇所
このコミットは広範囲にわたるため、特にcsv
パッケージのreader.go
における変更をコアな例として挙げます。
--- a/src/pkg/csv/reader.go
+++ b/src/pkg/csv/reader.go
@@ -101,8 +101,8 @@ var (
//
// If TrimLeadingSpace is true, leading white space in a field is ignored.
type Reader struct {
- Comma int // Field delimiter (set to ',' by NewReader)
- Comment int // Comment character for start of line
+ Comma rune // Field delimiter (set to ',' by NewReader)
+ Comment rune // Comment character for start of line
FieldsPerRecord int // Number of expected fields per record
LazyQuotes bool // Allow lazy quotes
TrailingComma bool // Allow trailing comma
@@ -173,23 +173,23 @@ func (r *Reader) ReadAll() (records [][]string, err os.Error) {
// readRune reads one rune from r, folding \r\n to \n and keeping track
// of how far into the line we have read. r.column will point to the start
// of this rune, not the end of this rune.
-func (r *Reader) readRune() (int, os.Error) {
- rune, _, err := r.r.ReadRune()
+func (r *Reader) readRune() (rune, os.Error) {
+ r1, _, err := r.r.ReadRune()
// Handle \r\n here. We make the simplifying assumption that
// anytime \r is followed by \n that it can be folded to \n.
// We will not detect files which contain both \r\n and bare \n.
- if rune == '\r' {
- rune, _, err = r.r.ReadRune()
+ if r1 == '\r' {
+ r1, _, err = r.r.ReadRune()
if err == nil {
- if rune != '\n' {
+ if r1 != '\n' {
r.r.UnreadRune()
- rune = '\r'
+ r1 = '\r'
}
}
}
r.column++
- return rune, err
+ return r1, err
}
// unreadRune puts the last rune read from r back.
@@ -199,13 +199,13 @@ func (r *Reader) unreadRune() {
}
// skip reads runes up to and including the rune delim or until error.
-func (r *Reader) skip(delim int) os.Error {
+func (r *Reader) skip(delim rune) os.Error {
for {
- rune, err := r.readRune()
+ r1, err := r.readRune()
if err != nil {
return err
}
- if rune == delim {
+ if r1 == delim {
return nil
}
}
@@ -224,12 +224,12 @@ func (r *Reader) parseRecord() (fields []string, err os.Error) {
// If we are support comments and it is the comment character
// then skip to the end of line.
- rune, _, err := r.r.ReadRune()
+ r1, _, err := r.r.ReadRune()
if err != nil {
return nil, err
}
- if r.Comment != 0 && rune == r.Comment {
+ if r.Comment != 0 && r1 == r.Comment {
return nil, r.skip('\n')
}
r.r.UnreadRune()
@@ -252,10 +252,10 @@ func (r *Reader) parseField() (haveField bool, delim int, err os.Error) {
// parseField parses the next field in the record. The read field is
// located in r.field. Delim is the first character not part of the field
// (r.Comma or '\n').
-func (r *Reader) parseField() (haveField bool, delim int, err os.Error) {
+func (r *Reader) parseField() (haveField bool, delim rune, err os.Error) {
r.field.Reset()
- rune, err := r.readRune()
+ r1, err := r.readRune()
if err != nil {
// If we have EOF and are not at the start of a line
// then we return the empty field. We have already
@@ -267,30 +267,30 @@ func (r *Reader) parseField() (haveField bool, delim int, err os.Error) {
}
if r.TrimLeadingSpace {
- for rune != '\n' && unicode.IsSpace(rune) {
- rune, err = r.readRune()
+ for r1 != '\n' && unicode.IsSpace(r1) {
+ r1, err = r.readRune()
if err != nil {
return false, 0, err
}
}
}
- switch rune {
+ switch r1 {
case r.Comma:
// will check below
case '\n':
// We are a trailing empty field or a blank line
if r.column == 0 {
- return false, rune, nil
+ return false, r1, nil
}
- return true, rune, nil
+ return true, r1, nil
case '"':
// quoted field
Quoted:
for {
- rune, err = r.readRune()
+ r1, err = r.readRune()
if err != nil {
if err == os.EOF {
if r.LazyQuotes {
@@ -300,16 +300,16 @@ func (r *Reader) parseField() (haveField bool, delim int, err os.Error) {
}
return false, 0, err
}
- switch rune {
+ switch r1 {
case '"':
- rune, err = r.readRune()
- if err != nil || rune == r.Comma {
+ r1, err = r.readRune()
+ if err != nil || r1 == r.Comma {
break Quoted
}
- if rune == '\n' {
- return true, rune, nil
+ if r1 == '\n' {
+ return true, r1, nil
}
- if rune != '"' {
+ if r1 != '"' {
if !r.LazyQuotes {
r.column--
return false, 0, r.error(ErrQuote)
@@ -321,21 +321,21 @@ func (r *Reader) parseField() (haveField bool, delim int, err os.Error) {
r.line++
r.column = -1
}
- r.field.WriteRune(rune)
+ r.field.WriteRune(r1)
}
default:
// unquoted field
for {
- r.field.WriteRune(rune)
- rune, err = r.readRune()
- if err != nil || rune == r.Comma {
+ r.field.WriteRune(r1)
+ r1, err = r.readRune()
+ if err != nil || r1 == r.Comma {
break
}
- if rune == '\n' {
- return true, rune, nil
+ if r1 == '\n' {
+ return true, r1, nil
}
- if !r.LazyQuotes && rune == '"' {
+ if !r.LazyQuotes && r1 == '"' {
return false, 0, r.error(ErrBareQuote)
}
}
@@ -353,20 +353,20 @@ func (r *Reader) parseField() (haveField bool, delim int, err os.Error) {
// are at the end of the line (being mindful
// of trimming spaces).
c := r.column
- rune, err = r.readRune()
+ r1, err = r.readRune()
if r.TrimLeadingSpace {
- for rune != '\n' && unicode.IsSpace(rune) {
- rune, err = r.readRune()
+ for r1 != '\n' && unicode.IsSpace(r1) {
+ r1, err = r.readRune()
if err != nil {
break
}
}
}
- if err == os.EOF || rune == '\n' {
+ if err == os.EOF || r1 == '\n' {
r.column = c // report the comma
return false, 0, r.error(ErrTrailingComma)
}
r.unreadRune()
}
- return true, rune, nil
+ return true, r1, nil
}
コアとなるコードの解説
上記のcsv/reader.go
の変更は、csv.Reader
がCSVデータを解析する際に文字をどのように扱うかを根本的に改善しています。
-
Reader
構造体のフィールド:Comma int
からComma rune
へ: フィールド区切り文字をint
で保持していたのをrune
に変更。これにより、区切り文字がASCII範囲外のUnicode文字であっても正確に扱えるようになります。Comment int
からComment rune
へ: コメント文字も同様にrune
に変更。
-
readRune()
メソッド:- 戻り値の型が
int, os.Error
からrune, os.Error
へ変更。このメソッドは、内部のbufio.Reader
から1つのUnicodeコードポイントを読み取る役割を担っています。int
ではなくrune
を返すことで、読み取られた値が文字であることを明確に示します。 - 内部で
rune
という変数名が使われていた箇所がr1
に変更されています。これは、Goの慣習として、シャドーイング(外側のスコープの変数を内側のスコープで同じ名前で宣言すること)を避けるため、または単に変数名をより明確にするためのリファクタリングです。
- 戻り値の型が
-
skip()
メソッド:- 引数の型が
delim int
からdelim rune
へ変更。スキップする区切り文字がrune
として渡されるようになります。
- 引数の型が
-
parseField()
メソッド:- 戻り値の型が
delim int
からdelim rune
へ変更。フィールドの区切り文字をrune
として返します。 - 内部で文字を読み取る際に
rune, err := r.readRune()
としていた箇所がr1, err := r.readRune()
に変更され、その後のswitch
文やif
文でr1
が使用されています。これにより、文字処理がrune
型に統一され、Unicode文字の正確な比較や判定が可能になります。例えば、unicode.IsSpace(r1)
のように、unicode
パッケージの関数に直接rune
を渡せるようになります。
- 戻り値の型が
これらの変更により、csv
パーサーは、UTF-8でエンコードされたCSVファイル内の多バイト文字(例:日本語、絵文字など)を、区切り文字やコメント文字として、あるいはフィールドの内容として、より正確に処理できるようになります。同様の変更が他のパッケージにも適用されており、Go言語の標準ライブラリ全体で文字処理の堅牢性とUnicode対応が強化されています。
関連リンク
- Go言語の
rune
に関する公式ドキュメントやブログ記事: - UnicodeとUTF-8に関する一般的な情報源。
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のブログ記事
- GitHubのコミット履歴
- UnicodeおよびUTF-8に関する一般的な技術資料
- RFC 1521, RFC 2045, RFC 822 (MIMEおよびメール関連の標準)
- XML Character Range (XML関連の標準)