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

[インデックス 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言語の文字列のデフォルトエンコーディング。

intruneの使い分け

Go言語では、intはプラットフォーム依存の整数型であり、通常32ビットまたは64ビットです。文字を扱う際にintを使用すると、それが単なる整数値なのか、それとも文字コードポイントなのかが不明瞭になります。

rune型を導入することで、開発者はその変数がUnicodeコードポイントを意図していることを明確に理解できます。これにより、文字の比較、変換、操作がより安全かつ意図通りに行われるようになります。例えば、unicodeパッケージの関数はrune型を引数にとることが多く、runeを使用することでこれらの関数との連携がスムーズになります。

技術的詳細

このコミットでは、主に以下の変更が行われています。

  1. 型定義の変更: 構造体のフィールドや関数の引数、戻り値の型がintからruneに変更されています。

    • src/pkg/csv/reader.go: Reader構造体のCommaCommentフィールドがintからruneに変更。readRune(), skip(), parseField()などのメソッドの引数や戻り値の型もintからruneに変更。
    • src/pkg/csv/reader_test.go: テスト構造体のCommaCommentフィールドが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()関数内でleadQuoteintからruneに変更され、ループ内の文字処理もruneを使用するように変更。
    • src/pkg/strconv/quote.go: quoteWith(), unhex(), UnquoteChar()関数の引数や戻り値、内部変数でintruneに変更。
    • 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])にキャスト。
  2. 変数名の変更: rune型を扱う変数名が、元のruneからr1rrなど、より具体的な名前に変更されている箇所もあります。これは、単に型を変更するだけでなく、コードの意図をより明確にするためのリファクタリングの一環と考えられます。

これらの変更は、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データを解析する際に文字をどのように扱うかを根本的に改善しています。

  1. Reader構造体のフィールド:

    • Comma intからComma runeへ: フィールド区切り文字をintで保持していたのをruneに変更。これにより、区切り文字がASCII範囲外のUnicode文字であっても正確に扱えるようになります。
    • Comment intからComment runeへ: コメント文字も同様にruneに変更。
  2. readRune()メソッド:

    • 戻り値の型がint, os.Errorからrune, os.Errorへ変更。このメソッドは、内部のbufio.Readerから1つのUnicodeコードポイントを読み取る役割を担っています。intではなくruneを返すことで、読み取られた値が文字であることを明確に示します。
    • 内部でruneという変数名が使われていた箇所がr1に変更されています。これは、Goの慣習として、シャドーイング(外側のスコープの変数を内側のスコープで同じ名前で宣言すること)を避けるため、または単に変数名をより明確にするためのリファクタリングです。
  3. skip()メソッド:

    • 引数の型がdelim intからdelim runeへ変更。スキップする区切り文字がruneとして渡されるようになります。
  4. 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言語の公式ドキュメント
  • Go言語のブログ記事
  • GitHubのコミット履歴
  • UnicodeおよびUTF-8に関する一般的な技術資料
  • RFC 1521, RFC 2045, RFC 822 (MIMEおよびメール関連の標準)
  • XML Character Range (XML関連の標準)