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

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

このコミットは、Go言語の標準ライブラリであるfmtパッケージにおいて、文字の表現にint型ではなくrune型を使用するように変更したものです。これにより、Unicode文字の扱いがより正確かつ自然になります。FormatterおよびScannerインターフェースの定義も変更され、既存のクライアントコードに影響を与える可能性があるため、govetによるチェックが推奨されています。

コミット

  • コミットハッシュ: 4e4eca261817c80c77ebe0b8522df8b0746cf10b
  • Author: Russ Cox rsc@golang.org
  • Date: Tue Oct 25 22:21:33 2011 -0700

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

https://github.com/golang/go/commit/4e4eca261817c80c77ebe0b8522df8b0746cf10b

元コミット内容

fmt: use rune

Lots of internal edits.

Formatter and Scanner interfaces change
(clients to be checked by govet).

R=r
CC=golang-dev
https://golang.org/cl/5305045

変更の背景

Go言語は設計当初からUnicodeを強く意識しており、文字列はUTF-8でエンコードされたバイト列として扱われます。しかし、個々の文字(Unicodeコードポイント)を扱う際には、初期のGoではint型が使われることがありました。これは、Goのint型が少なくとも32ビット幅を持つことが保証されており、Unicodeのすべてのコードポイント(U+0000からU+10FFFFまで)を表現できるためです。

しかし、int型は汎用的な整数型であり、それが文字を表すのか、単なる数値を表すのかがコードの可読性や意図の明確さの点で曖昧になるという問題がありました。Go言語には、Unicodeコードポイントを明示的に表すための組み込み型としてrune型が存在します。runeint32のエイリアスであり、その目的は「Unicodeコードポイント」を表現することに特化しています。

このコミットの背景には、fmtパッケージのような低レベルのテキスト処理を行う部分において、文字を扱う際にはその意図を明確にし、コードの堅牢性を高める目的があったと考えられます。intruneに置き換えることで、開発者はその変数がUnicodeコードポイントを扱っていることを一目で理解できるようになり、誤用を防ぐ効果も期待できます。また、Go言語全体のAPI設計の一貫性を保つ上でも重要な変更でした。

前提知識の解説

Go言語におけるrune

Go言語において、runeはUnicodeコードポイントを表す組み込み型です。これはint32のエイリアスとして定義されており、Goの文字列がUTF-8でエンコードされているのに対し、runeは個々のUnicode文字(コードポイント)を表現するために使用されます。

  • string: UTF-8でエンコードされたバイト列のシーケンス。文字列の長さはバイト数で決まり、文字数とは異なる場合がある。
  • rune: 1つのUnicodeコードポイントを表す。int32型であり、すべてのUnicode文字を表現できる。

例えば、日本語の「あ」はUTF-8では3バイトですが、runeとしては1つのコードポイントとして扱われます。

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	s := "こんにちは"
	fmt.Printf("String: %s, Bytes: %d\n", s, len(s)) // String: こんにちは, Bytes: 15

	// runeとしてイテレート
	for i, r := range s {
		fmt.Printf("Index: %d, Rune: %c, Unicode: %U, Size: %d\n", i, r, r, utf8.RuneLen(r))
	}
	// 出力例:
	// Index: 0, Rune: こ, Unicode: U+3053, Size: 3
	// Index: 3, Rune: ん, Unicode: U+3093, Size: 3
	// Index: 6, Rune: に, Unicode: U+306B, Size: 3
	// Index: 9, Rune: ち, Unicode: U+3061, Size: 3
	// Index: 12, Rune: は, Unicode: U+306F, Size: 3
}

fmtパッケージ

fmtパッケージは、Go言語におけるフォーマットI/Oを実装するパッケージです。C言語のprintfscanfに似た機能を提供し、Goのデータ型を文字列に変換したり、文字列からデータ型に変換したりする機能を提供します。fmt.Printffmt.Sprintffmt.Scanなどが含まれます。

FormatterインターフェースとScannerインターフェース

fmtパッケージには、カスタムのフォーマットやスキャン動作を定義するためのインターフェースが用意されています。

  • Formatterインターフェース: type Formatter interface { Format(f State, c int) } このインターフェースを実装する型は、fmt.Printfなどのフォーマット関数でどのように表示されるかを制御できます。変更前はc intでしたが、このコミットでc runeに変更されました。cはフォーマット動詞(例: %c, %d, %sなど)を表します。

  • Scannerインターフェース: type Scanner interface { Scan(state ScanState, verb int) os.Error } このインターフェースを実装する型は、fmt.Scanなどのスキャン関数でどのように入力が読み取られるかを制御できます。変更前はverb intでしたが、このコミットでverb runeに変更されました。verbはスキャン動詞(例: %c, %d, %sなど)を表します。

govetツール

govetはGo言語の静的解析ツールで、Goのソースコードを検査し、疑わしい構成要素(バグの可能性のあるコード)を報告します。このコミットのように、インターフェースのシグネチャが変更された場合、govetは古いインターフェースを実装しているが新しいインターフェースに適合していないコードを検出するのに役立ちます。

技術的詳細

このコミットの主要な技術的変更は、fmtパッケージ内で文字(特にフォーマット動詞やスキャン動詞)を表現するために使用されていたint型を、よりセマンティックなrune型に置き換えたことです。

具体的には、以下の点が変更されました。

  1. Formatterインターフェースの変更: Format(f State, c int) から Format(f State, c rune) へと変更されました。これにより、カスタムフォーマッタを実装する際に、フォーマット動詞が明確にruneとして扱われるようになります。

  2. Scannerインターフェースの変更: Scan(state ScanState, verb int) os.Error から Scan(state ScanState, verb rune) os.Error へと変更されました。これにより、カスタムスキャナを実装する際に、スキャン動詞が明確にruneとして扱われるようになります。

  3. 内部関数のシグネチャ変更: fmtパッケージ内の多くのプライベート関数やヘルパー関数で、文字や動詞を表す引数の型がintからruneに変更されました。例えば、add(c int)add(c rune)に、badVerb(verb int)badVerb(verb rune)に、fmtInt64(v int64, verb int)fmtInt64(v int64, verb rune)に変更されています。

  4. ss構造体のフィールド変更: src/pkg/fmt/scan.go内のss構造体(スキャナの状態を保持する)で、peekRune intprevRune intがそれぞれpeekRune runeprevRune runeに変更されました。これは、読み取り中のルックアヘッド文字や直前の文字をruneとして保持するためです。

  5. 型変換の明示化: 一部の箇所では、int(a)のような明示的な型変換がrune(a)に変更されています。これは、数値がUnicodeコードポイントとして扱われることをより明確にするためです。例えば、unicode.IsPrint(int(a))unicode.IsPrint(rune(a))に変更されています。

これらの変更は、Go言語の型システムをより厳密に適用し、コードの意図を明確にするためのものです。intruneは基底型が同じint32であるため、多くの場合はコンパイルエラーにはなりませんが、インターフェースのシグネチャ変更は互換性を破る変更であり、既存のコードが新しいインターフェースに適合するように修正する必要があることを意味します。そのため、コミットメッセージに「(clients to be checked by govet)」と明記されています。

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

このコミットでは、以下の5つのファイルが変更されています。

  • src/pkg/fmt/fmt_test.go
  • src/pkg/fmt/format.go
  • src/pkg/fmt/print.go
  • src/pkg/fmt/scan.go
  • src/pkg/fmt/scan_test.go

変更の概要は以下の通りです。

  • fmt_test.go: Formatterインターフェースを実装するテスト用の型(F, flagPrinter, PanicF)のFormatメソッドのシグネチャがc intからc runeに変更されています。
  • format.go: fmt構造体の内部メソッド(integer, fmt_c64, fmt_c128)で、文字や動詞を表す引数の型がintからruneに変更されています。また、unicode.IsPrintutf8.RuneLenutf8.EncodeRuneへの引数もint(a)からrune(a)に変更されています。
  • print.go: StateインターフェースとFormatterインターフェースの定義が変更され、intruneに置き換えられています。また、pp構造体の多くのメソッド(add, badVerb, fmtBool, fmtInt64, fmtUint64, fmtFloat32, fmtFloat64, fmtComplex64, fmtComplex128, fmtString, fmtBytes, fmtPointer, catchPanic, handleMethods, printField, printValue, printReflectValue)の引数verbcの型がintからruneに変更されています。fmtC関数では、int64からruneへの変換が明示的に行われています。
  • scan.go: ScanStateインターフェースとScannerインターフェースの定義が変更され、intruneに置き換えられています。ss構造体のpeekRuneprevRuneフィールドの型がintからruneに変更されています。ReadRunegetRunemustReadRuneTokennotSpacereadRuneskipSpacetokenconsumepeeknotEOFokVerbscanBoolgetBasescanRunescanIntscanUintscanComplexconvertStringquotedStringhexDigitscanOnedoScanなどの多くの関数やメソッドで、文字や動詞を表す引数や変数の型がintからruneに変更されています。
  • scan_test.go: Scannerインターフェースを実装するテスト用の型(Xs, IntString, TwoLines, RecursiveInt)のScanメソッドのシグネチャがverb intからverb runeに変更されています。また、TwoLinescharsスライスが[]intから[]runeに変更されています。

全体として、fmtパッケージのフォーマットおよびスキャン処理において、文字やフォーマット/スキャン動詞を扱うすべての箇所でint型がrune型に置き換えられています。

コアとなるコードの解説

以下に、変更の代表的な箇所をいくつか抜粋して解説します。

src/pkg/fmt/fmt_test.go

--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -73,7 +73,7 @@ type C struct {
 
 type F int
 
-func (f F) Format(s State, c int) {
+func (f F) Format(s State, c rune) {
 	Fprintf(s, "<%c=F(%d)>", c, int(f))
 }

ここでは、Formatterインターフェースを実装するF型のFormatメソッドのシグネチャが変更されています。引数cintからruneになりました。これにより、フォーマット動詞がruneとして渡されることが明確になります。

src/pkg/fmt/format.go

--- a/src/pkg/fmt/format.go
+++ b/src/pkg/fmt/format.go
@@ -242,8 +242,8 @@ func (f *fmt) integer(a int64, base uint64, signedness bool, digits string) {
 	}
 
 	// If we want a quoted char for %#U, move the data up to make room.
-\tif f.unicode && f.uniQuote && a >= 0 && a <= unicode.MaxRune && unicode.IsPrint(int(a)) {
-\t\truneWidth := utf8.RuneLen(int(a))\n+\tif f.unicode && f.uniQuote && a >= 0 && a <= unicode.MaxRune && unicode.IsPrint(rune(a)) {
+\t\truneWidth := utf8.RuneLen(rune(a))\n \t\twidth := 1 + 1 + runeWidth + 1 // space, quote, rune, quote
 \t\tcopy(buf[i-width:], buf[i:])   // guaranteed to have enough room.
 \t\ti -= width
@@ -253,7 +253,7 @@ func (f *fmt) integer(a int64, base uint64, signedness bool, digits string) {
 \t\tj++
 \t\tbuf[j] = '\''
 \t\tj++
-\t\tutf8.EncodeRune(buf[j:], int(a))\n+\t\tutf8.EncodeRune(buf[j:], rune(a))\n \t\tj += runeWidth
 \t\tbuf[j] = '\''
 \t}
@@ -400,7 +400,7 @@ func (f *fmt) fmt_G32(v float32) { f.plusSpace(strconv.Ftoa32(v, 'G', doPrec(f,\
  func (f *fmt) fmt_fb32(v float32) { f.padString(strconv.Ftoa32(v, 'b', 0)) }
 
  // fmt_c64 formats a complex64 according to the verb.
-func (f *fmt) fmt_c64(v complex64, verb int) {
+func (f *fmt) fmt_c64(v complex64, verb rune) {
 	f.buf.WriteByte('(')
 	r := real(v)
 	for i := 0; ; i++ {

unicode.IsPrintutf8.RuneLenutf8.EncodeRuneといったUnicode関連の関数に渡す引数が、int(a)からrune(a)に明示的に変更されています。これは、aが数値としてではなく、Unicodeコードポイントとして扱われるべきであることを明確に示しています。また、fmt_c64などの内部メソッドのverb引数もintからruneに変更されています。

src/pkg/fmt/print.go

--- a/src/pkg/fmt/print.go
+++ b/src/pkg/fmt/print.go
@@ -51,7 +51,7 @@ type State interface {
 // The implementation of Format may call Sprintf or Fprintf(f) etc.
 // to generate its output.
 type Formatter interface {
-\tFormat(f State, c int)\n+\tFormat(f State, c rune)\n }\n \n // Stringer is implemented by any value that has a String method,
@@ -159,7 +159,7 @@ func (p *pp) Flag(b int) bool {
 	return false
 }
 
-func (p *pp) add(c int) {
+func (p *pp) add(c rune) {
 	p.buf.WriteRune(c)
 }

Formatterインターフェースの定義自体が変更されています。これは、このインターフェースを実装するすべての型に影響を与える重要な変更です。また、pp構造体のaddメソッドのように、文字をバッファに追加する内部関数もintからruneを受け取るように変更されています。

src/pkg/fmt/scan.go

--- a/src/pkg/fmt/scan.go
+++ b/src/pkg/fmt/scan.go
@@ -32,7 +32,7 @@ type ScanState interface {
 	// If invoked during Scanln, Fscanln, or Sscanln, ReadRune() will
 	// return EOF after returning the first '\n' or when reading beyond
 	// the specified width.
-\tReadRune() (rune int, size int, err os.Error)\n+\tReadRune() (r rune, size int, err os.Error)\n \t// UnreadRune causes the next call to ReadRune to return the same rune.
 \tUnreadRune() os.Error
 \t// SkipSpace skips space in the input. Newlines are treated as space
@@ -47,7 +47,7 @@ type ScanState interface {
 	// EOF.  The returned slice points to shared data that may be overwritten
 	// by the next call to Token, a call to a Scan function using the ScanState
 	// as input, or when the calling Scan method returns.
-\tToken(skipSpace bool, f func(int) bool) (token []byte, err os.Error)\n+\tToken(skipSpace bool, f func(rune) bool) (token []byte, err os.Error)\n \t// Width returns the value of the width option and whether it has been set.
 \t// The unit is Unicode code points.
 \tWidth() (wid int, ok bool)
@@ -62,7 +62,7 @@ type ScanState interface {
 // receiver, which must be a pointer to be useful.  The Scan method is called
 // for any argument to Scan, Scanf, or Scanln that implements it.
 type Scanner interface {
-\tScan(state ScanState, verb int) os.Error\n+\tScan(state ScanState, verb rune) os.Error\n }\n \n // Scan scans text read from standard input, storing successive
@@ -149,8 +149,8 @@ const eof = -1
 type ss struct {
 	rr       io.RuneReader // where to read input
 	buf      bytes.Buffer  // token accumulator
-\tpeekRune int           // one-rune lookahead\n-\tprevRune int           // last rune returned by ReadRune\n+\tpeekRune rune          // one-rune lookahead\n+\tprevRune rune          // last rune returned by ReadRune\n 	count    int           // runes consumed so far.
 	atEOF    bool          // already read EOF
 	ssave

ScanStateScannerインターフェースの定義が変更され、intruneに置き換えられています。特にReadRuneメソッドの戻り値の型名がrune intからr runeに変更されている点に注目です。これは、戻り値がrune型であることをより明確にするための変更です。また、ss構造体の内部状態を保持するpeekRuneprevRunerune型に変更されています。

これらの変更は、Go言語のfmtパッケージがUnicode文字をより正確かつ意図的に扱うように進化していることを示しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント(fmtパッケージ、unicodeパッケージ、utf8パッケージに関する情報)
  • Go言語のrune型に関する解説記事
  • Go言語のインターフェースに関する解説記事
  • Go言語のgovetツールに関する情報