[インデックス 1794] ファイルの概要
このコミットは、Go言語の標準ライブラリの一部である tabwriter
パッケージに対する変更です。tabwriter
パッケージは、テキストを整形してタブ区切りの列をきれいに揃えるためのユーティリティを提供します。このコミットの主な目的は、tabwriter.Writer
構造体の初期化パラメータを簡素化し、将来の拡張性を高めることです。具体的には、複数のブーリアンフラグを単一の uint
型のフラグフィールドに統合しています。
コミット
commit 6906e3b884ad1c7890ee9c05c2fcbb44a8d173b9
Author: Robert Griesemer <gri@golang.org>
Date: Tue Mar 10 16:30:26 2009 -0700
- incorporate suggestions from previous code review
R=rsc
DELTA=64 (18 added, 3 deleted, 43 changed)
OCL=26046
CL=26058
---
src/lib/tabwriter/tabwriter.go | 43 +++++++++++++++++---------\
src/lib/tabwriter/tabwriter_test.go | 60 ++++++++++++++++++-------------------\
2 files changed, 59 insertions(+), 44 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6906e3b884ad1c7890ee9c05c2fcbb44a8d173b9
元コミット内容
このコミットは、以前のコードレビューで指摘された改善提案を取り入れたものです。具体的な変更内容は、tabwriter.Writer
構造体の align_left
(左寄せ) と filter_html
(HTMLタグフィルタリング) という2つのブーリアンフィールドを、flags
という単一の uint
型フィールドに置き換えることです。これにより、Init
関数および New
(後に NewWriter
に改名) 関数のシグネチャが簡素化され、新しいフォーマットオプションを追加する際の柔軟性が向上します。
変更の背景
Go言語の初期開発段階では、API設計の試行錯誤が頻繁に行われていました。このコミットが行われた2009年3月は、Go言語が一般に公開される前の時期であり、標準ライブラリのAPIが活発に開発・改善されていた時期です。
tabwriter
パッケージの Writer
構造体には、当初 align_left
と filter_html
という2つのブーリアンフィールドがありました。これらのフィールドは、それぞれセルのアライメントとHTMLタグの処理を制御していました。しかし、このようなブーリアンフラグが複数ある場合、以下のような問題が生じます。
- 関数のシグネチャの複雑化:
Init
関数やコンストラクタ関数 (New
) の引数リストが長くなり、可読性が低下します。 - 拡張性の低さ: 新しいオプションを追加するたびに、構造体に新しいブーリアンフィールドを追加し、関連する関数のシグネチャを変更する必要があり、APIの破壊的変更につながる可能性があります。
- オプションの組み合わせの管理: 複数のブーリアンフラグの組み合わせによっては、意味のない、あるいは矛盾する状態が発生する可能性があります。
これらの問題を解決し、よりクリーンで拡張性の高いAPIを提供するために、コードレビューでブーリアンフラグをビットフラグに統合する提案がなされたと考えられます。これにより、将来的に新しいフォーマットオプションが追加された場合でも、既存のAPIを変更することなく、新しいフラグを追加するだけで対応できるようになります。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と一般的なプログラミングの知識が必要です。
- Go言語の構造体 (struct): 複数のフィールド(変数)をまとめた複合データ型です。
tabwriter.Writer
はこの構造体の一例です。 - Go言語のメソッド: 構造体に関連付けられた関数です。
Init
やWrite
はWriter
構造体のメソッドです。 - Go言語のインターフェース (interface): 型が満たすべき振る舞いを定義します。
io.Write
はWrite
メソッドを持つ型が満たすべきインターフェースです。 - ビットフラグ (Bit Flags): 複数のブーリアン状態を単一の整数型(この場合は
uint
)の各ビットに対応させて表現する手法です。各ビットが特定のオプションのオン/オフを示します。- ビット演算子:
|
(ビットOR): 複数のフラグを組み合わせる際に使用します。例:FlagA | FlagB
&
(ビットAND): 特定のフラグが設定されているかを確認する際に使用します。例:flags & FlagA != 0
^
(ビットXOR): ビットを反転させる際に使用します。このコミットでは^AlignRight
のように使用されており、これはAlignRight
以外のすべてのビットを立てる(反転させる)ことを意味します。<<
(左シフト): ビットを左にシフトすることで、2のべき乗を表現します。1 << iota
のように使用されます。
- ビット演算子:
iota
キーワード: Go言語の定数宣言で使用される特殊な識別子です。const
ブロック内で使用され、連続する定数に自動的にインクリメントされる値を割り当てます。デフォルトでは0から始まり、各const
定義で1ずつ増加します。これにより、ビットフラグの値を簡単に定義できます。const ( FlagA = 1 << iota // FlagA = 1 (0001) FlagB // FlagB = 2 (0010) FlagC // FlagC = 4 (0100) )
panic
とos.Error
: Go言語の初期のエラーハンドリングは、現在のようなerror
インターフェースが確立される前であり、os.Error
という具体的な型が使われていました。panic
は回復不可能なエラーを示すために使用されます。tabwriter
パッケージの機能:- タブ区切りテキストの整形: 入力ストリームからタブ (
\t
) で区切られたテキストを読み込み、出力ストリームに整形された形で書き出します。 - 列の自動調整: 各列の幅を自動的に計算し、最も長い要素に合わせて調整することで、きれいに揃った表形式の出力を生成します。
- パディング: 列の間に指定されたパディング文字(例: スペース、ドット)を挿入します。
- アライメント: 各セルの内容を左寄せまたは右寄せに設定できます。
- HTMLタグのフィルタリング: HTMLタグを無視したり、HTMLエンティティを単一の文字として扱ったりするオプションがあります。
- タブ区切りテキストの整形: 入力ストリームからタブ (
技術的詳細
このコミットの技術的な核心は、tabwriter.Writer
構造体の内部状態管理の変更と、それに伴うAPIの調整です。
-
Writer
構造体の変更:- 削除:
align_left bool
,filter_html bool
- 追加:
flags uint
この変更により、Writer
構造体のメモリフットプリントがわずかに削減され、複数のブーリアンフラグを個別に管理する手間がなくなります。
- 削除:
-
フラグ定数の導入:
const
ブロック内でiota
を使用して、FilterHTML
とAlignRight
というビットフラグ定数が定義されました。const ( // Ignore html tags and treat entities (starting with '&' // and ending in ';') as single characters (width = 1). FilterHTML = 1 << iota; // 1 (0001) // Force right-alignment of cell content. // Default is left-alignment. AlignRight; // 2 (0010) )
FilterHTML = 1 << iota
はiota
が0なので1 << 0
、つまり1
となります。AlignRight
はiota
が1にインクリメントされるため、1 << 1
、つまり2
となります。 これにより、FilterHTML
は1番目のビット、AlignRight
は2番目のビットに対応するようになります。
-
Init
関数のシグネチャ変更:- 変更前:
func (b *Writer) Init(output io.Write, cellwidth, padding int, padchar byte, align_left, filter_html bool) *Writer
- 変更後:
func (b *Writer) Init(output io.Write, cellwidth, padding int, padchar byte, flags uint) *Writer
align_left
とfilter_html
の2つのブーリアン引数が、単一のflags uint
引数に置き換えられました。これにより、関数の引数が減り、より簡潔になりました。
- 変更前:
-
Init
関数内のフラグ処理ロジック:padchar == '\t'
の場合の特殊処理:tabwriter
は、パディング文字としてタブ (\t
) が指定された場合、強制的に左寄せになります。以前はb.align_left = align_left || padchar == '\t'
で処理されていましたが、新しいフラグシステムではflags &= uint(t)
のようにビット演算でAlignRight
フラグをクリアしています。t := ^AlignRight
はAlignRight
のビットを反転させることで、AlignRight
以外のすべてのビットが1になるマスクを作成します。このマスクとflags
をビットAND (&
) することで、AlignRight
フラグが強制的に0に設定されます。コメントにある// TODO 6g bug
は、当時のGoコンパイラ (6g) のバグに対するワークアラウンドを示唆している可能性があります。b.flags = flags
で、渡されたフラグがWriter
構造体に設定されます。
-
writeLines
メソッド内のアライメント処理: 以前はif b.align_left
でアライメントを分岐していましたが、switch
ステートメントとビットAND演算 (b.flags & AlignRight != 0
) を使用して、AlignRight
フラグが設定されているかどうかで右寄せ/左寄せを判断するように変更されました。default: // align left
は、AlignRight
フラグが設定されていない場合のデフォルトの動作として左寄せを意味します。case b.flags & AlignRight != 0: // align right
は、AlignRight
フラグが設定されている場合に右寄せのロジックを実行します。
-
Write
メソッド内のHTMLフィルタリング処理: 以前はif b.filter_html
でHTMLフィルタリングを分岐していましたが、if b.flags & FilterHTML != 0
のようにビットAND演算を使用して、FilterHTML
フラグが設定されているかどうかでフィルタリングを判断するように変更されました。 -
New
関数の改名とシグネチャ変更:- 変更前:
func New(writer io.Write, cellwidth, padding int, padchar byte, align_left, filter_html bool) *Writer
- 変更後:
func NewWriter(writer io.Write, cellwidth, padding int, padchar byte, flags uint) *Writer
関数名がNew
からNewWriter
に変更されました。これは、Go言語の慣例として、構造体名を省略せずにコンストラクタ関数に含めることが推奨されるようになったためと考えられます。また、Init
関数と同様に、引数が単一のflags uint
に変更されました。
- 変更前:
-
テストファイルの変更 (
tabwriter_test.go
):check
関数のシグネチャも、align_left, filter_html bool
からflags uint
に変更されました。これにより、テストケースの呼び出しも新しいフラグ形式に更新されています。 例えば、true, false
(左寄せ、HTMLフィルタなし) は0
(デフォルトフラグなし) に、true, true
(左寄せ、HTMLフィルタあり) はtabwriter.FilterHTML
に、false, false
(右寄せ、HTMLフィルタなし) はtabwriter.AlignRight
にそれぞれ置き換えられています。
これらの変更は、APIの整合性を高め、将来の機能追加に対する柔軟性を提供することを目的としています。
コアとなるコードの変更箇所
src/lib/tabwriter/tabwriter.go
--- a/src/lib/tabwriter/tabwriter.go
+++ b/src/lib/tabwriter/tabwriter.go
@@ -99,8 +99,7 @@ type Writer struct {
cellwidth int;
padding int;
padbytes [8]byte;
- align_left bool;
- filter_html bool;
+ flags uint;
// current state
thtml_char byte; // terminating char of html tag/entity, or 0 ('>', ';', or 0)
@@ -144,6 +143,18 @@ func (b *Writer) addLine() {
}
+// Formatting can be controlled with these flags.
+const (
+// Ignore html tags and treat entities (starting with '&'
+// and ending in ';') as single characters (width = 1).
+ FilterHTML = 1 << iota;
+
+// Force right-alignment of cell content.
+// Default is left-alignment.
+ AlignRight;
+)
+
+
// A Writer must be initialized with a call to Init. The first parameter (output)
// specifies the filter output. The remaining parameters control the formatting:
//
@@ -155,11 +166,9 @@ func (b *Writer) addLine() {
// and cells are left-aligned independent of align_left
// (for correct-looking results, cellwidth must correspond
// to the tab width in the viewer displaying the result)
-// align_left alignment of cell content
-// filter_html ignores html tags and treats entities (starting with '&'
-// and ending in ';') as single characters (width = 1)
+// flags formatting control
//
-func (b *Writer) Init(output io.Write, cellwidth, padding int, padchar byte, align_left, filter_html bool) *Writer {
+func (b *Writer) Init(output io.Write, cellwidth, padding int, padchar byte, flags uint) *Writer {
if cellwidth < 0 {
panic("negative cellwidth");
}
@@ -172,8 +181,12 @@ func (b *Writer) Init(output io.Write, cellwidth, padding int, padchar byte, ali
for i := len(b.padbytes) - 1; i >= 0; i-- {
b.padbytes[i] = padchar;
}
- b.align_left = align_left || padchar == '\t'; // tab enforces left-alignment
- b.filter_html = filter_html;
+ if padchar == '\t' {
+ // tab enforces left-alignment
+ t := ^AlignRight; // TODO 6g bug
+ flags &= uint(t);
+ }
+ b.flags = flags;
b.buf.Init(1024);
b.lines_size.Init(0);
@@ -256,7 +269,9 @@ func (b *Writer) writeLines(pos0 int, line0, line1 int) (pos int, err *os.Error)
for j := 0; j < line_size.Len(); j++ {
s, w := line_size.At(j), line_width.At(j);
- if b.align_left {
+ switch {
+ default: // align left
+
err = b.write0(b.buf.slice(pos, pos + s));
if err != nil {
goto exit;
@@ -269,7 +284,7 @@ func (b *Writer) writeLines(pos0 int, line0, line1 int) (pos int, err *os.Error)
}
}
- } else { // align right
+ case b.flags & AlignRight != 0: // align right
if j < b.widths.Len() {
err = b.writePadding(w, b.widths.At(j));
@@ -433,7 +448,7 @@ func (b *Writer) Write(buf []byte) (written int, err *os.Error) {
}
case '<', '&':
- if b.filter_html {
+ if b.flags & FilterHTML != 0 {
b.append(buf[i0 : i]);
i0 = i;
b.width += unicodeLen(b.buf.slice(b.pos, b.buf.Len()));
@@ -467,9 +482,9 @@ func (b *Writer) Write(buf []byte) (written int, err *os.Error) {
}
-// New allocates and initializes a new tabwriter.Writer.
+// NewWriter allocates and initializes a new tabwriter.Writer.
// The parameters are the same as for the the Init function.
//
-func New(writer io.Write, cellwidth, padding int, padchar byte, align_left, filter_html bool) *Writer {
- return new(Writer).Init(writer, cellwidth, padding, padchar, align_left, filter_html)
+func NewWriter(writer io.Write, cellwidth, padding int, padchar byte, flags uint) *Writer {
+ return new(Writer).Init(writer, cellwidth, padding, padchar, flags)
}
src/lib/tabwriter/tabwriter_test.go
--- a/src/lib/tabwriter/tabwriter_test.go
+++ b/src/lib/tabwriter/tabwriter_test.go
@@ -71,12 +71,12 @@ func verify(t *testing.T, w *tabwriter.Writer, b *buffer, src, expected string)
}
-func check(t *testing.T, tabwidth, padding int, padchar byte, align_left, filter_html bool, src, expected string) {
+func check(t *testing.T, tabwidth, padding int, padchar byte, flags uint, src, expected string) {
var b buffer;
b.init(1000);
var w tabwriter.Writer;
- w.Init(&b, tabwidth, padding, padchar, align_left, filter_html);
+ w.Init(&b, tabwidth, padding, padchar, flags);
// write all at once
b.clear();
@@ -105,109 +105,109 @@ func check(t *testing.T, tabwidth, padding int, padchar byte, align_left, filter
func Test(t *testing.T) {
check(
- t, 8, 1, '.', true, false,
+ t, 8, 1, '.', 0,
"",
""
);
check(
- t, 8, 1, '.', true, false,
+ t, 8, 1, '.', 0,
"\n\n\n",
"\n\n\n"
);
check(
- t, 8, 1, '.', true, false,
+ t, 8, 1, '.', 0,
"a\nb\nc",
"a\nb\nc"
);
check(
- t, 8, 1, '.', true, false,
+ t, 8, 1, '.', 0,
"\t", // '\t' terminates an empty cell on last line - nothing to print
""
);
check(
- t, 8, 1, '.', false, false,
+ t, 8, 1, '.', tabwriter.AlignRight,
"\t", // '\t' terminates an empty cell on last line - nothing to print
""
);
check(
- t, 8, 1, '.', true, false,
+ t, 8, 1, '.', 0,
"*\t*",
"**"
);
check(
- t, 8, 1, '.', true, false,
+ t, 8, 1, '.', 0,
"*\t*\n",
"*.......*\n"
);
check(
- t, 8, 1, '.', true, false,
+ t, 8, 1, '.', 0,
"*\t*\t",
"*.......*"
);
check(
- t, 8, 1, '.', false, false,
+ t, 8, 1, '.', tabwriter.AlignRight,
"*\t*\t",
".......**"
);
check(
- t, 8, 1, '.', true, false,
+ t, 8, 1, '.', 0,
"\t\n",
"........\n"
);
check(
- t, 8, 1, '.', true, false,
+ t, 8, 1, '.', 0,
"a) foo",
"a) foo"
);
check(
- t, 8, 1, ' ', true, false,
+ t, 8, 1, ' ', 0,
"b) foo\tbar", // "bar" is not in any cell - not formatted, just flushed
"b) foobar"
);
check(
- t, 8, 1, '.', true, false,
+ t, 8, 1, '.', 0,
"c) foo\tbar\t",
"c) foo..bar"
);
check(
- t, 8, 1, '.', true, false,
+ t, 8, 1, '.', 0,
"d) foo\tbar\n",
"d) foo..bar\n"
);
check(
- t, 8, 1, '.', true, false,
+ t, 8, 1, '.', 0,
"e) foo\tbar\t\n",
"e) foo..bar.....\n"
);
check(
- t, 8, 1, '.', true, true,
+ t, 8, 1, '.', tabwriter.FilterHTML,
"e) f<o\t<b>bar</b>\t\n",
"e) f<o..<b>bar</b>.....\n"
);
check(
- t, 8, 1, '*', true, false,
+ t, 8, 1, '*', 0,
"Hello, world!\n",
"Hello, world!\n"
);
check(
- t, 0, 0, '.', true, false,
+ t, 0, 0, '.', 0,
"1\t2\t3\t4\n"
"11\t222\t3333\t44444\n",
@@ -216,19 +216,19 @@ func Test(t *testing.T) {
);
check(
- t, 5, 0, '.', true, false,
+ t, 5, 0, '.', 0,
"1\t2\t3\t4\n",
"1....2....3....4\n"
);
check(
- t, 5, 0, '.', true, false,
+ t, 5, 0, '.', 0,
"1\t2\t3\t4\t\n",
"1....2....3....4....\n"
);
check(
- t, 8, 1, '.', true, false,
+ t, 8, 1, '.', 0,
"本\tb\tc\n"
"aa\t本本本\tcccc\tddddd\n"
"aaa\tbbbb\n",
@@ -239,7 +239,7 @@ func Test(t *testing.T) {
);
check(
- t, 8, 1, ' ', false, false,
+ t, 8, 1, ' ', tabwriter.AlignRight,
"a\tè\tc\t\n"
"aa\tèèè\tcccc\tddddd\t\n"
"aaa\tèèèè\t\n",
@@ -250,7 +250,7 @@ func Test(t *testing.T) {
);
check(
- t, 2, 0, ' ', true, false,
+ t, 2, 0, ' ', 0,
"a\tb\tc\n"
"aa\tbbb\tcccc\n"
"aaa\tbbbb\n",
@@ -261,7 +261,7 @@ func Test(t *testing.T) {
);
check(
- t, 8, 1, '_', true, false,
+ t, 8, 1, '_', 0,
"a\tb\tc\n"
"aa\tbbb\tcccc\n"
"aaa\tbbbb\n",
@@ -272,7 +272,7 @@ func Test(t *testing.T) {
);
check(
- t, 4, 1, '-', true, false,
+ t, 4, 1, '-', 0,
"4444\t日本語\t22\t1\t333\n"
"999999999\t22\n"
"7\t22\n",
@@ -291,7 +291,7 @@ func Test(t *testing.T) {
);
check(
- t, 4, 3, '.', true, false,
+ t, 4, 3, '.', 0,
"4444\t333\t22\t1\t333\n"
"999999999\t22\n"
"7\t22\n",
@@ -310,7 +310,7 @@ func Test(t *testing.T) {
);
check(
- t, 8, 1, '\t', true, true,
+ t, 8, 1, '\t', tabwriter.FilterHTML,
"4444\t333\t22\t1\t333\n"
"999999999\t22\n"
"7\t22\n",
@@ -329,7 +329,7 @@ func Test(t *testing.T) {
);
check(
- t, 0, 2, ' ', false, false,
+ t, 0, 2, ' ', tabwriter.AlignRight,
".0\t.3\t2.4\t-5.1\t\n"
"23.0\t12345678.9\t2.4\t-989.4\t\n"
"5.1\t12.0\t2.4\t-7.0\t\n"
コアとなるコードの解説
このコミットのコアとなる変更は、tabwriter.Writer
構造体の設計思想の転換にあります。
-
Writer
構造体フィールドの集約:align_left bool
とfilter_html bool
という個別のブーリアンフィールドが削除され、代わりにflags uint
という単一のuint
型フィールドが導入されました。- これにより、
Writer
構造体の定義がより簡潔になり、将来的に新しいフォーマットオプションを追加する際に、既存の構造体定義を変更する必要がなくなります。新しいオプションは、新しいビットフラグ定数として追加するだけで済みます。
-
ビットフラグ定数の定義と
iota
の活用:const ( FilterHTML = 1 << iota; AlignRight; )
iota
はGo言語の定数宣言で連続する値を自動的に生成する特殊な識別子です。FilterHTML = 1 << iota
では、iota
が0
なので1 << 0
、つまり1
(バイナリで0001
) となります。AlignRight
では、iota
が自動的に1
にインクリメントされるため、1 << 1
、つまり2
(バイナリで0010
) となります。- このように、各フラグが異なるビット位置に対応するように定義することで、複数のオプションを単一の
uint
値にパックできます。
-
Init
関数の引数と内部ロジックの変更:Init
関数のシグネチャが(..., align_left, filter_html bool)
から(..., flags uint)
に変更されました。これにより、関数の呼び出しがより簡潔になり、可読性が向上します。- 内部では、渡された
flags
値がb.flags
に直接代入されます。 padchar == '\t'
の場合の特殊処理 (t := ^AlignRight; flags &= uint(t);
) は、パディング文字がタブの場合に強制的に左寄せにするためのものです。^AlignRight
はAlignRight
以外のすべてのビットが1であるマスクを生成し、これとflags
をビットANDすることで、AlignRight
ビットが強制的に0に設定されます。これは、タブによるパディングが常に左寄せを意味するというtabwriter
の設計上の制約を反映しています。
-
writeLines
メソッドでのアライメント処理の変更:- 以前は
if b.align_left
で条件分岐していましたが、switch
ステートメントとb.flags & AlignRight != 0
というビットAND演算を使用して、AlignRight
フラグが設定されているかどうかでアライメントを決定するように変更されました。 b.flags & AlignRight != 0
は、flags
の中にAlignRight
ビットが立っているかどうかを効率的にチェックします。この結果がtrue
であれば右寄せ、false
であればdefault
ケース(左寄せ)が実行されます。
- 以前は
-
Write
メソッドでのHTMLフィルタリング処理の変更:- 同様に、
if b.filter_html
からif b.flags & FilterHTML != 0
に変更され、FilterHTML
フラグの有無でHTMLフィルタリングのロジックが実行されるようになりました。
- 同様に、
-
New
からNewWriter
への改名:- Go言語の慣例として、コンストラクタ関数は
New
の後に構造体名を続ける形式 (NewWriter
) が推奨されるため、関数名が変更されました。これはAPIの整合性を高めるための変更です。
- Go言語の慣例として、コンストラクタ関数は
これらの変更は、tabwriter
パッケージの内部実装をより堅牢で拡張性の高いものにし、同時に外部から利用するAPIをよりクリーンで理解しやすいものにすることを目的としています。ビットフラグの利用は、複数のブーリアンオプションを効率的に管理するための一般的なパターンであり、Go言語の標準ライブラリでも広く採用されています。
関連リンク
- Go言語の
tabwriter
パッケージのドキュメント (現在のバージョン): https://pkg.go.dev/text/tabwriter - Go言語の
iota
キーワードに関する公式ドキュメント: https://go.dev/ref/spec#Iota - Go言語のビット演算子に関する公式ドキュメント: https://go.dev/ref/spec#Operators
参考にした情報源リンク
- Go言語の公式ドキュメント (上記リンクを含む)
- Go言語の初期のコミット履歴 (GitHub)
- ビットフラグに関する一般的なプログラミングの概念
- Go言語のコードレビュー文化とAPI設計の進化に関する一般的な知識