[インデックス 15957] ファイルの概要
このコミットは、Go言語の公式ドキュメントツールであるgodoc
の内部クリーンアップとリファクタリングに関するものです。具体的には、テキストセグメントを表現するためのデータ構造と、それを扱うSelection
インターフェースの改善が行われています。これにより、コードの可読性、型安全性、および効率が向上しています。
コミット
commit 2180506169e448ce1473b25875195f3681291f54
Author: Robert Griesemer <gri@golang.org>
Date: Tue Mar 26 13:12:38 2013 -0700
godoc: internal cleanup: remove a TODO
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/8005044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2180506169e448ce1473b25875195f3681291f54
元コミット内容
godoc: internal cleanup: remove a TODO
このコミットは、godoc
ツール内の内部的なクリーンアップであり、既存のTODOコメントを削除することを目的としています。TODOコメントは、テキストセグメントを扱うSelection
インターフェースの現在の実装が非効率であり、改善の余地があることを示していました。このコミットでは、そのTODOコメントで指摘されていた問題に対処するための具体的なコード変更が行われています。
変更の背景
この変更の背景には、godoc
のformat.go
ファイルにおけるテキストセグメントの表現と処理に関する既存の課題がありました。以前のSelection
型は、[]int
([a, b)
形式のオフセットペア)を返す関数として定義されており、これは以下の問題点を抱えていました。
- 非効率性: 各セグメントを
[]int
スライスとして返すため、セグメントごとに新しいスライスが割り当てられ、ガベージコレクションのオーバーヘッドが発生する可能性がありました。TODOコメントにも「It's more efficient to return a pair (a, b int) instead of creating lots of slices.」と明記されていました。 - 終了の不明瞭さ:
Selection
の終了を示すためにnil
スライスを返す必要がありましたが、これはGoのイテレータパターンとしてはあまり明確ではありませんでした。TODOコメントには「Need to determine how to indicate the end of a Selection.」とも書かれていました。 - 型安全性と可読性:
[]int
という汎用的な型では、それがテキストセグメントの開始と終了オフセットを表すという意図がコードを読むだけでは直感的に理解しにくい側面がありました。
これらの問題を解決し、コードベースの品質と保守性を向上させるために、このリファクタリングが実施されました。
前提知識の解説
godoc
: Go言語のソースコードからドキュメントを生成し、表示するためのツールです。Goの標準ライブラリの一部として提供されており、Goのコードベースを理解する上で非常に重要な役割を果たします。src/cmd/godoc/format.go
:godoc
ツール内で、テキストのフォーマットやハイライト表示に関連するロジックを扱うファイルです。特に、コード内の特定の範囲(行、トークン、リンクなど)を選択し、それらを整形して出力する機能に関わっています。- テキストセグメント: テキスト内の連続した範囲を指します。通常、開始オフセットと終了オフセット(または開始インデックスと終了インデックス)のペアで表現されます。例えば、
[a, b)
はインデックスa
からインデックスb-1
までの範囲を示します。 - イテレータパターン: コレクションの要素に順次アクセスするためのデザインパターンです。このコミットでは、
Selection
関数がイテレータとして機能し、連続するテキストセグメントを順に返します。 - Go言語の構造体(
struct
): 複数のフィールド(プロパティ)をまとめた複合データ型です。関連するデータを一つの論理的な単位として扱うために使用されます。 - Go言語のメソッド: 構造体や任意の型に関連付けられた関数です。レシーバ引数(
func (seg *Segment) ...
のseg *Segment
部分)を通じて、その型のインスタンスのデータにアクセスしたり、操作したりできます。 - ゼロ値: Go言語において、変数を宣言した際に明示的に初期化しない場合、その型に応じたデフォルト値(ゼロ値)が自動的に割り当てられます。数値型は0、ブール型は
false
、文字列型は空文字列、ポインタやスライス、マップ、チャネルなどはnil
です。このコミットでは、Segment
構造体のゼロ値が「ready-to-use empty segment」として扱われるように設計されています。
技術的詳細
このコミットの主要な技術的変更点は、テキストセグメントの表現方法とSelection
インターフェースのセマンティクスを改善したことです。
-
Segment
構造体の導入:- 以前は
[]int{start, end}
というスライスで表現されていたテキストセグメントが、新しく定義されたSegment
構造体(struct { start, end int }
)に置き換えられました。 - これにより、セグメントの意図が明確になり、コードの可読性が向上しました。
Segment
構造体は値型であるため、スライスのようにヒープ割り当てを頻繁に行う必要がなくなり、パフォーマンスの向上が期待できます。
- 以前は
-
Segment.isEmpty()
メソッドの追加:Segment
構造体にisEmpty() bool
メソッドが追加されました。このメソッドはseg.start >= seg.end
の場合にtrue
を返します。- このメソッドは、セグメントが空であるかどうかを明確に判断するための標準的な方法を提供します。特に、
Selection
イテレータの終了条件として利用されます。
-
Selection
型の変更:Selection
型は、以前のfunc() []int
からfunc() Segment
に変更されました。- これにより、
Selection
はSegment
構造体のインスタンスを返すようになり、型安全性が向上しました。 Selection
のセマンティクスも更新されました。「連続する、重複しない、空でないセグメントを返し、その後、無限に続く空のセグメントを返す。最初の空のセグメントが選択の終了を示す。」と定義されました。これは、Goのイテレータパターンにおける一般的な「done」シグナル(例えば、io.Reader
がio.EOF
を返すように)を、Segment
のゼロ値(isEmpty()
がtrue
を返す)で表現するものです。
-
merger
構造体と関連ロジックの更新:merger
構造体のsegments
フィールドが[][]int
から[]Segment
に変更されました。merger.next()
メソッド内のセグメントへのアクセスが、seg[0]
やseg[1]
からseg.start
やseg.end
に直接変更されました。これにより、コードの意図がより明確になりました。- セグメントを「消費」するロジックも、
m.segments[index][0] = infinity
からm.segments[index].start = infinity
のように、Segment
のフィールドを直接操作するように変更されました。 newMerger
関数も、Segment{infinity, infinity}
で初期化するように変更されました。
-
lineSelection
,tokenSelection
,makeSelection
関数の更新:- これらの関数は、
Selection
型を返すため、内部でSegment
構造体を構築し、それを返すように変更されました。 - 特に
makeSelection
関数では、入力の[][]int
からSegment
を生成する際に、m[0] < m[1]
(つまりstart < end
)の条件で空でないセグメントのみを返すように修正されました。これにより、無効なセグメントが処理されるのを防ぎます。
- これらの関数は、
これらの変更により、godoc
の内部コードはより堅牢で、理解しやすく、そして潜在的にパフォーマンスが向上しました。
コアとなるコードの変更箇所
変更はsrc/cmd/godoc/format.go
ファイルに集中しています。
--- a/src/cmd/godoc/format.go
+++ b/src/cmd/godoc/format.go
@@ -23,15 +23,21 @@ import (
// ----------------------------------------------------------------------------
// Implementation of FormatSelections
-// A Selection is a function returning offset pairs []int{a, b}
-// describing consecutive non-overlapping text segments [a, b).
-// If there are no more segments, a Selection must return nil.
+// A Segment describes a text segment [start, end).
+// The zero value of a Segment is a ready-to-use empty segment.
//
-// TODO It's more efficient to return a pair (a, b int) instead
-// of creating lots of slices. Need to determine how to
-// indicate the end of a Selection.
+type Segment struct {
+ start, end int
+}
+
+func (seg *Segment) isEmpty() bool { return seg.start >= seg.end }
+
+// A Selection is an "iterator" function returning a text segment.
+// Repeated calls to a selection return consecutive, non-overlapping,
+// non-empty segments, followed by an infinite sequence of empty
+// segments. The first empty segment marks the end of the selection.
//
-type Selection func() []int
+type Selection func() Segment
// A LinkWriter writes some start or end "tag" to w for the text offset offs.
// It is called by FormatSelections at the start or end of each link segment.
@@ -141,17 +147,17 @@ func FormatSelections(w io.Writer, text []byte, lw LinkWriter, links Selection,
//
type merger struct {\n selections []Selection
-\tsegments [][]int // segments[i] is the next segment of selections[i]
+\tsegments []Segment // segments[i] is the next segment of selections[i]
}\n
const infinity int = 2e9
func newMerger(selections []Selection) *merger {
-\tsegments := make([][]int, len(selections))
+\tsegments := make([]Segment, len(selections))
\tfor i, sel := range selections {
-\t\tsegments[i] = []int{infinity, infinity}
+\t\tsegments[i] = Segment{infinity, infinity}
\t\tif sel != nil {
-\t\t\tif seg := sel(); seg != nil {
+\t\t\tif seg := sel(); !seg.isEmpty() {
\t\t\t\tsegments[i] = seg
\t\t\t}\n \t\t}\n@@ -170,12 +176,12 @@ func (m *merger) next() (index, offs int, start bool) {
\tindex = -1
\tfor i, seg := range m.segments {
\t\tswitch {\n-\t\tcase seg[0] < offs:\n-\t\t\toffs = seg[0]\n+\t\tcase seg.start < offs:\n+\t\t\toffs = seg.start
\t\t\tindex = i
\t\t\tstart = true
-\t\tcase seg[1] < offs:\n-\t\t\toffs = seg[1]\n+\t\tcase seg.end < offs:\n+\t\t\toffs = seg.end
\t\t\tindex = i
\t\t\tstart = false
\t\t}\n@@ -188,18 +194,17 @@ func (m *merger) next() (index, offs int, start bool) {
\t// either way it is ok to consume the start offset: set it
\t// to infinity so it won\'t be considered in the following
\t// next call
-\tm.segments[index][0] = infinity
+\tm.segments[index].start = infinity
\tif start {\n \t\treturn
\t}\n \t// end offset found - consume it
-\tm.segments[index][1] = infinity
+\tm.segments[index].end = infinity
\t// advance to the next segment for that selection
\tseg := m.selections[index]()
-\tif seg == nil {\n-\t\treturn
+\tif !seg.isEmpty() {\n+\t\tm.segments[index] = seg
\t}\n-\tm.segments[index] = seg
\treturn
}\n \n@@ -209,7 +214,7 @@ func (m *merger) next() (index, offs int, start bool) {
// lineSelection returns the line segments for text as a Selection.\n func lineSelection(text []byte) Selection {\n \ti, j := 0, 0\n-\treturn func() (seg []int) {\n+\treturn func() (seg Segment) {\n \t\t// find next newline, if any\n \t\tfor j < len(text) {\n \t\t\tj++\n@@ -219,7 +224,7 @@ func lineSelection(text []byte) Selection {\n \t\t}\n \t\tif i < j {\n \t\t\t// text[i:j] constitutes a line\n-\t\t\tseg = []int{i, j}\n+\t\t\tseg = Segment{i, j}\n \t\t\ti = j\n \t\t}\n \t\treturn\n@@ -234,7 +239,7 @@ func tokenSelection(src []byte, sel token.Token) Selection {\n \tfset := token.NewFileSet()\n \tfile := fset.AddFile(\"\", fset.Base(), len(src))\n \ts.Init(file, src, nil, scanner.ScanComments)\n-\treturn func() (seg []int) {\n+\treturn func() (seg Segment) {\n \t\tfor {\n \t\t\tpos, tok, lit := s.Scan()\n \t\t\tif tok == token.EOF {\n@@ -242,7 +247,7 @@ func tokenSelection(src []byte, sel token.Token) Selection {\n \t\t\t}\n \t\t\toffs := file.Offset(pos)\n \t\t\tif tok == sel {\n-\t\t\t\tseg = []int{offs, offs + len(lit)}\n+\t\t\t\tseg = Segment{offs, offs + len(lit)}\n \t\t\t\tbreak\n \t\t\t}\n \t\t}\n@@ -251,13 +256,20 @@ func tokenSelection(src []byte, sel token.Token) Selection {\n }\n \n // makeSelection is a helper function to make a Selection from a slice of pairs.\n+// Pairs describing empty segments are ignored.\n+//\n func makeSelection(matches [][]int) Selection {\n-\treturn func() (seg []int) {\n-\t\tif len(matches) > 0 {\n-\t\t\tseg = matches[0]\n-\t\t\tmatches = matches[1:]\n+\ti := 0\n+\treturn func() Segment {\n+\t\tfor i < len(matches) {\n+\t\t\tm := matches[i]\n+\t\t\ti++\n+\t\t\tif m[0] < m[1] {\n+\t\t\t\t// non-empty segment\n+\t\t\t\treturn Segment{m[0], m[1]}\n+\t\t\t}\n \t\t}\n-\t\treturn\n+\t\treturn Segment{}\n \t}\n }\n \n```
## コアとなるコードの解説
このコミットの核心は、`Selection`インターフェースのセマンティクスと、それをサポートする`Segment`構造体の導入にあります。
1. **`Segment`構造体と`isEmpty()`メソッド**:
```go
type Segment struct {
start, end int
}
func (seg *Segment) isEmpty() bool { return seg.start >= seg.end }
```
`Segment`は、テキストの開始オフセットと終了オフセットを保持するシンプルな構造体です。`isEmpty()`メソッドは、`start`が`end`以上の場合に`true`を返します。これは、セグメントが有効な範囲を持たない(空であるか、開始が終了より後である)ことを示し、`Selection`イテレータの終了条件として機能します。
2. **`Selection`型の定義変更**:
```go
type Selection func() Segment
```
`Selection`は、引数を取らず`Segment`を返す関数型として再定義されました。この関数はイテレータとして機能し、呼び出されるたびに次のテキストセグメントを返します。セグメントがもうない場合、`isEmpty()`が`true`を返す`Segment`のゼロ値を返します。これにより、以前の`nil`スライスを返すよりも明確で型安全な終了シグナルが提供されます。
3. **`merger`構造体と`next()`メソッドの変更**:
`merger`構造体は複数の`Selection`をマージして、テキスト全体でソートされたセグメントのストリームを生成するために使用されます。
```go
type merger struct {
selections []Selection
segments []Segment // segments[i] is the next segment of selections[i]
}
```
`segments`フィールドが`[]Segment`型に変更され、各`Selection`から取得した次のセグメントを保持します。
`merger.next()`メソッドでは、セグメントの開始/終了オフセットへのアクセスが`seg[0]`や`seg[1]`から`seg.start`や`seg.end`に直接変更されました。これにより、コードの可読性が向上し、`Segment`構造体の恩恵を最大限に活用しています。
また、`seg == nil`のチェックが`!seg.isEmpty()`に変更され、新しいセマンティクスに適合しています。
4. **`lineSelection`, `tokenSelection`, `makeSelection`関数の適応**:
これらの関数は、それぞれ行、トークン、または指定されたマッチングに基づいて`Selection`を生成します。
```go
func lineSelection(text []byte) Selection {
// ...
return func() (seg Segment) {
// ...
if i < j {
seg = Segment{i, j} // Segment構造体を構築
i = j
}
return
}
}
```
これらの関数は、内部で`Segment{start, end}`を構築し、それを`Selection`関数が返すように変更されました。特に`makeSelection`では、空のセグメント(`m[0] < m[1]`が`false`の場合)をスキップし、有効なセグメントのみを返すようにロジックが修正されています。
これらの変更は、`godoc`の内部でテキストセグメントを扱う方法を標準化し、より堅牢で効率的なイテレータパターンを確立することを目的としています。
## 関連リンク
* Go言語の公式ドキュメント: [https://golang.org/doc/](https://golang.org/doc/)
* `godoc`コマンドのドキュメント: [https://pkg.go.dev/cmd/godoc](https://pkg.go.dev/cmd/godoc)
* Go言語のコードレビューシステム (Gerrit): [https://go-review.googlesource.com/](https://go-review.googlesource.com/)
* このコミットのGerritチェンジリスト: [https://golang.org/cl/8005044](https://golang.org/cl/8005044)
## 参考にした情報源リンク
* Go言語の公式ドキュメント
* Go言語のソースコード(特に`src/cmd/godoc/format.go`)
* Go言語におけるイテレータパターンの一般的な実装方法に関する知識
* Go言語の構造体とメソッドに関する知識
* Gitのコミットと差分(diff)の読み方に関する知識