[インデックス 10105] ファイルの概要
このコミットは、Go言語の標準ライブラリであるbufio
パッケージとio
パッケージにおいて、Unicode文字(コードポイント)を扱う際の型定義をint
から組み込み型rune
へ変更するものです。具体的には、ReadRune
およびWriteRune
関数のシグネチャが更新され、よりGo言語のイディオムに沿った記述になっています。
コミット
commit 9f6d036f333effed712e54c0e087b4be616d2dab
Author: Russ Cox <rsc@golang.org>
Date: Tue Oct 25 22:23:34 2011 -0700
bufio, io: use rune
ReadRune, WriteRune change signature.
R=golang-dev, gri, r
CC=golang-dev
https://golang.org/cl/5314043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9f6d036f333effed712e54c0e087b4be616d2dab
元コミット内容
bufio
およびio
パッケージにおいて、rune
型を使用するように変更。
ReadRune
およびWriteRune
関数のシグネチャが変更されました。
変更の背景
Go言語では、文字列はUTF-8でエンコードされたバイトのシーケンスとして扱われます。しかし、Unicodeのコードポイント(文字)を直接扱う必要がある場合、Goにはrune
という組み込み型が用意されています。rune
は実際にはint32
のエイリアスであり、単一のUnicodeコードポイントを表します。
このコミット以前は、bufio.Reader.ReadRune()
やbufio.Writer.WriteRune()
、そしてio.RuneReader
インターフェースのReadRune()
メソッドの戻り値や引数で、Unicodeコードポイントをint
型として扱っていました。これは機能的には問題ありませんでしたが、Go言語の設計思想やイディオムからすると、Unicodeコードポイントを明示的に示すrune
型を使用する方が、コードの意図が明確になり、可読性や保守性が向上します。
この変更は、Go言語がUnicodeとUTF-8を第一級市民として扱うという設計哲学をさらに強化し、APIの一貫性を高めるためのものです。これにより、開発者はUnicode文字を扱う際に、より直感的で型安全なコードを書くことができるようになります。
前提知識の解説
Go言語におけるrune
型
Go言語において、string
型は不変のバイトスライスであり、通常はUTF-8でエンコードされたテキストを表します。しかし、string
を直接イテレートすると、バイト単位で処理されるため、マルチバイト文字(UTF-8で2バイト以上を占める文字)を正しく扱うことができません。
ここで登場するのがrune
型です。rune
はGoの組み込み型で、単一のUnicodeコードポイントを表すために使用されます。その実体はint32
のエイリアスであり、Unicodeの各文字に割り当てられた数値(コードポイント)を格納します。
例:
A
(U+0041) はrune(65)
あ
(U+3042) はrune(12354)
for range
ループで文字列をイテレートすると、各要素はrune
型として取得され、UTF-8のデコードが自動的に行われます。
s := "Hello, 世界"
for i, r := range s {
fmt.Printf("Index: %d, Rune: %c (U+%04X)\n", i, r, r)
}
出力:
Index: 0, Rune: H (U+0048)
Index: 1, Rune: e (U+0065)
Index: 2, Rune: l (U+006C)
Index: 3, Rune: l (U+006C)
Index: 4, Rune: o (U+006F)
Index: 5, Rune: , (U+002C)
Index: 6, Rune: (U+0020)
Index: 7, Rune: 世 (U+4E16)
Index: 10, Rune: 界 (U+754C)
(インデックスが飛んでいるのは、マルチバイト文字が複数のバイトを占めるためです。)
UTF-8エンコーディング
UTF-8は、Unicode文字を可変長のバイトシーケンスで表現するエンコーディング方式です。ASCII文字は1バイトで表現され、それ以外の文字は2バイトから4バイトで表現されます。この可変長エンコーディングにより、ASCIIとの互換性を保ちつつ、世界中のあらゆる文字を効率的に表現できます。
Go言語の標準ライブラリには、unicode/utf8
パッケージがあり、UTF-8エンコーディングとデコーディングのためのユーティリティ関数が提供されています。例えば、utf8.DecodeRune
はバイトスライスからrune
とバイトサイズを抽出し、utf8.EncodeRune
はrune
をバイトスライスにエンコードします。
bufio
パッケージ
bufio
パッケージは、I/O操作をバッファリングすることで効率化するための機能を提供します。これにより、ディスクやネットワークへのアクセス回数を減らし、パフォーマンスを向上させることができます。
bufio.Reader
: バッファリングされた読み込みを提供します。ReadByte
、ReadRune
、ReadLine
などのメソッドがあります。bufio.Writer
: バッファリングされた書き込みを提供します。WriteByte
、WriteRune
、WriteString
などのメソッドがあります。
io
パッケージ
io
パッケージは、I/Oプリミティブの基本的なインターフェースを提供します。Reader
、Writer
、Closer
などのインターフェースが定義されており、これらを実装することで、様々なI/Oソース(ファイル、ネットワーク接続、メモリなど)に対して統一的な方法で操作を行うことができます。
このコミットで関連するのは、io.RuneReader
インターフェースです。これはReadRune()
メソッドを持つインターフェースで、Unicodeコードポイントを読み込む機能を提供します。
技術的詳細
このコミットの主要な変更点は、bufio
およびio
パッケージ内のReadRune
およびWriteRune
関連の関数シグネチャと変数宣言において、int
型を使用していた箇所を明示的にrune
型に変更したことです。
具体的には、以下の変更が行われました。
-
bufio.Reader.ReadRune()
のシグネチャ変更: 変更前:func (b *Reader) ReadRune() (rune int, size int, err os.Error)
変更後:func (b *Reader) ReadRune() (r rune, size int, err os.Error)
戻り値の最初の要素の名前がrune
からr
に変更され、型がint
からrune
に明示されました。 -
bufio.Writer.WriteRune()
のシグネチャ変更: 変更前:func (b *Writer) WriteRune(rune int) (size int, err os.Error)
変更後:func (b *Writer) WriteRune(r rune) (size int, err os.Error)
引数の名前がrune
からr
に変更され、型がint
からrune
に明示されました。 -
io.RuneReader
インターフェースのシグネチャ変更: 変更前:type RuneReader interface { ReadRune() (rune int, size int, err os.Error) }
変更後:type RuneReader interface { ReadRune() (r rune, size int, err os.Error) }
インターフェースのメソッドシグネチャも同様に変更されました。 -
内部変数およびテストコードでの型変更: 上記のシグネチャ変更に伴い、
bufio.go
内のReadRune
およびWriteRune
の実装、そしてbufio_test.go
内のテストコードにおいて、rune
型の値を格納する変数がint
型からrune
型に修正されました。例えば、int(b.buf[b.r])
のような明示的な型変換が不要になり、直接rune(b.buf[b.r])
と記述できるようになりました。また、rune >= 0x80
のような比較もr >= 0x80
に変更されています。
これらの変更は、Go言語の型システムをより厳密に適用し、コードの意図を明確にするためのものです。rune
型を明示的に使用することで、開発者はその変数がUnicodeコードポイントを表していることを一目で理解でき、誤った型での操作を防ぐことができます。
コアとなるコードの変更箇所
src/pkg/bufio/bufio.go
--- a/src/pkg/bufio/bufio.go
+++ b/src/pkg/bufio/bufio.go
@@ -208,7 +208,7 @@ func (b *Reader) UnreadByte() os.Error {
// ReadRune reads a single UTF-8 encoded Unicode character and returns the
// rune and its size in bytes.
-func (b *Reader) ReadRune() (rune int, size int, err os.Error) {
+func (b *Reader) ReadRune() (r rune, size int, err os.Error) {
for b.r+utf8.UTFMax > b.w && !utf8.FullRune(b.buf[b.r:b.w]) && b.err == nil {
b.fill()
}
@@ -216,14 +216,14 @@ func (b *Reader) ReadRune() (rune int, size int, err os.Error) {
if b.r == b.w {
return 0, 0, b.readErr()
}
- rune, size = int(b.buf[b.r]), 1
- if rune >= 0x80 {
- rune, size = utf8.DecodeRune(b.buf[b.r:b.w])
+ r, size = rune(b.buf[b.r]), 1
+ if r >= 0x80 {
+ r, size = utf8.DecodeRune(b.buf[b.r:b.w])
}
b.r += size
b.lastByte = int(b.buf[b.r-1])
b.lastRuneSize = size
- return rune, size, nil
+ return r, size, nil
}
// UnreadRune unreads the last rune. If the most recent read operation on
@@ -497,9 +497,9 @@ func (b *Writer) WriteByte(c byte) os.Error {
// WriteRune writes a single Unicode code point, returning
// the number of bytes written and any error.
-func (b *Writer) WriteRune(rune int) (size int, err os.Error) {
- if rune < utf8.RuneSelf {
- err = b.WriteByte(byte(rune))
+func (b *Writer) WriteRune(r rune) (size int, err os.Error) {
+ if r < utf8.RuneSelf {
+ err = b.WriteByte(byte(r))
if err != nil {
return 0, err
}
@@ -516,10 +516,10 @@ func (b *Writer) WriteRune(rune int) (size int, err os.Error) {
if n < utf8.UTFMax {
// Can only happen if buffer is silly small.
- return b.WriteString(string(rune))
+ return b.WriteString(string(r))
}
}
- size = utf8.EncodeRune(b.buf[b.n:], rune)
+ size = utf8.EncodeRune(b.buf[b.n:], r)
b.n += size
return size, nil
}
src/pkg/io/io.go
--- a/src/pkg/io/io.go
+++ b/src/pkg/io/io.go
@@ -194,7 +194,7 @@ type ByteScanner interface {
// and returns the rune and its size in bytes. If no character is
// available, err will be set.
type RuneReader interface {
- ReadRune() (rune int, size int, err os.Error)
+ ReadRune() (r rune, size int, err os.Error)
}
// RuneScanner is the interface that adds the UnreadRune method to the
src/pkg/bufio/bufio_test.go
テストコードも同様に、rune
型を使用するように変更されています。
--- a/src/pkg/bufio/bufio_test.go
+++ b/src/pkg/bufio/bufio_test.go
@@ -195,14 +195,14 @@ func readRuneSegments(t *testing.T, segments []string) {
want := strings.Join(segments, "")
r := NewReader(&StringReader{data: segments})
for {
- rune, _, err := r.ReadRune()
+ r, _, err := r.ReadRune()
if err != nil {
if err != os.EOF {
return
}
break
}
- got += string(rune)
+ got += string(r)
}
if got != want {
t.Errorf("segments=%v got=%s want=%s", segments, got, want)
@@ -233,24 +233,24 @@ func TestUnreadRune(t *testing.T) {
r := NewReader(&StringReader{data: segments})
// Normal execution.
for {
- rune, _, err := r.ReadRune()
+ r1, _, err := r.ReadRune()
if err != nil {
if err != os.EOF {
t.Error("unexpected EOF")
}
break
}
- got += string(rune)
+ got += string(r1)
// Put it back and read it again
if err = r.UnreadRune(); err != nil {
t.Error("unexpected error on UnreadRune:", err)
}
- rune1, _, err := r.ReadRune()
+ r2, _, err := r.ReadRune()
if err != nil {
t.Error("unexpected error reading after unreading:", err)
}
- if rune != rune1 {
- t.Errorf("incorrect rune after unread: got %c wanted %c", rune1, rune)
+ if r1 != r2 {
+ t.Errorf("incorrect rune after unread: got %c wanted %c", r2, r1)
}
}
if got != data {
@@ -339,25 +339,25 @@ func TestReadWriteRune(t *testing.T) {
w := NewWriter(byteBuf)
// Write the runes out using WriteRune
buf := make([]byte, utf8.UTFMax)
- for rune := 0; rune < NRune; rune++ {
- size := utf8.EncodeRune(buf, rune)
- nbytes, err := w.WriteRune(rune)
+ for r := rune(0); r < NRune; r++ {
+ size := utf8.EncodeRune(buf, r)
+ nbytes, err := w.WriteRune(r)
if err != nil {
- t.Fatalf("WriteRune(0x%x) error: %s", rune, err)
+ t.Fatalf("WriteRune(0x%x) error: %s", r, err)
}
if nbytes != size {
- t.Fatalf("WriteRune(0x%x) expected %d, got %d", rune, size, nbytes)
+ t.Fatalf("WriteRune(0x%x) expected %d, got %d", r, size, nbytes)
}
}
w.Flush()
r := NewReader(byteBuf)
// Read them back with ReadRune
- for rune := 0; rune < NRune; rune++ {
- size := utf8.EncodeRune(buf, rune)
+ for r1 := rune(0); r1 < NRune; r1++ {
+ size := utf8.EncodeRune(buf, r1)
nr, nbytes, err := r.ReadRune()
- if nr != rune || nbytes != size || err != nil {
- t.Fatalf("ReadRune(0x%x) got 0x%x,%d not 0x%x,%d (err=%s)", r, nr, nbytes, r, size, err)
+ if nr != r1 || nbytes != size || err != nil {
+ t.Fatalf("ReadRune(0x%x) got 0x%x,%d not 0x%x,%d (err=%s)", r1, nr, nbytes, r1, size, err)
}
}
}
コアとなるコードの解説
bufio.go
の変更点
ReadRune
関数の変更
// ReadRune reads a single UTF-8 encoded Unicode character and returns the
// rune and its size in bytes.
-func (b *Reader) ReadRune() (rune int, size int, err os.Error) {
+func (b *Reader) ReadRune() (r rune, size int, err os.Error) {
// ... (バッファリング処理は省略)
if b.r == b.w {
return 0, 0, b.readErr()
}
- rune, size = int(b.buf[b.r]), 1
- if rune >= 0x80 {
- rune, size = utf8.DecodeRune(b.buf[b.r:b.w])
+ r, size = rune(b.buf[b.r]), 1 // ここでバイトをruneに変換
+ if r >= 0x80 { // ASCII範囲外の文字(マルチバイト文字の可能性)
+ r, size = utf8.DecodeRune(b.buf[b.r:b.w]) // UTF-8デコード
}
b.r += size
b.lastByte = int(b.buf[b.r-1])
b.lastRuneSize = size
- return rune, size, nil
+ return r, size, nil
}
- シグネチャの変更: 戻り値の最初の要素の名前が
rune
からr
に変更され、型がint
からrune
に明示されました。これにより、この関数が返すのがUnicodeコードポイントであることがより明確になります。 - 変数宣言と代入: 以前は
rune, size = int(b.buf[b.r]), 1
のようにint
に型変換していましたが、変更後はr, size = rune(b.buf[b.r]), 1
と直接rune
型として扱っています。これは、byte
(uint8
のエイリアス)がrune
(int32
のエイリアス)に暗黙的に変換可能であるためです。 - 条件分岐:
if rune >= 0x80
がif r >= 0x80
に変更されました。これは、ASCII文字(0x00-0x7F)は1バイトで表現され、それ以外のマルチバイト文字は0x80以上の値を持つというUTF-8の特性に基づいています。
WriteRune
関数の変更
// WriteRune writes a single Unicode code point, returning
// the number of bytes written and any error.
-func (b *Writer) WriteRune(rune int) (size int, err os.Error) {
- if rune < utf8.RuneSelf { // utf8.RuneSelfは0x80
- err = b.WriteByte(byte(rune))
+func (b *Writer) WriteRune(r rune) (size int, err os.Error) {
+ if r < utf8.RuneSelf { // ASCII文字の場合
+ err = b.WriteByte(byte(r)) // 1バイトとして書き込む
if err != nil {
return 0, err
}
return 1, nil
}
// ... (バッファリング処理は省略)
if n < utf8.UTFMax {
// Can only happen if buffer is silly small.
- return b.WriteString(string(rune))
+ return b.WriteString(string(r)) // runeをstringに変換して書き込む
}
- size = utf8.EncodeRune(b.buf[b.n:], rune) // runeをUTF-8バイトにエンコード
+ size = utf8.EncodeRune(b.buf[b.n:], r) // runeをUTF-8バイトにエンコード
b.n += size
return size, nil
}
- シグネチャの変更: 引数の名前が
rune
からr
に変更され、型がint
からrune
に明示されました。 - 条件分岐と型変換:
if rune < utf8.RuneSelf
がif r < utf8.RuneSelf
に変更されました。utf8.RuneSelf
はASCII文字の最大値(0x80)であり、これより小さい値は1バイトで表現できるASCII文字であることを示します。 WriteString
への変換:return b.WriteString(string(rune))
がreturn b.WriteString(string(r))
に変更されました。string(rune)
は単一のrune
からなる文字列を生成します。utf8.EncodeRune
の引数:utf8.EncodeRune(b.buf[b.n:], rune)
がutf8.EncodeRune(b.buf[b.n:], r)
に変更されました。これは、rune
をUTF-8バイトシーケンスにエンコードする関数です。
io.go
の変更点
RuneReader
インターフェースの変更
type RuneReader interface {
- ReadRune() (rune int, size int, err os.Error)
+ ReadRune() (r rune, size int, err os.Error)
}
io.RuneReader
インターフェースのReadRune
メソッドのシグネチャも、bufio.Reader.ReadRune
と同様に、戻り値の最初の要素の名前と型が変更されました。これにより、io
パッケージレベルでもrune
型が明示的に使用されるようになり、インターフェースの一貫性が保たれます。
これらの変更は、Go言語の型システムをより効果的に活用し、Unicode文字の扱いに関するコードの明確性と堅牢性を向上させるための重要なステップです。
関連リンク
- Go言語の
rune
型について: https://go.dev/blog/strings bufio
パッケージのドキュメント: https://pkg.go.dev/bufioio
パッケージのドキュメント: https://pkg.go.dev/iounicode/utf8
パッケージのドキュメント: https://pkg.go.dev/unicode/utf8
参考にした情報源リンク
- Go言語の公式ドキュメントおよびブログ記事
- Go言語のソースコード (特に
src/pkg/bufio/
とsrc/pkg/io/
) - UnicodeおよびUTF-8に関する一般的な知識
- コミットメッセージに記載されているGoのコードレビューシステム (Gerrit) のリンク: https://golang.org/cl/5314043 (現在はGoのGerritインスタンスは廃止され、GitHubに移行しています。このリンクは当時のレビューへの参照です。)
- GitHubのコミットページ: https://github.com/golang/go/commit/9f6d036f333effed712e54c0e087b4be616d2dab