fzfはコマンドラインのあいまい検索ツールで、大量の候補行から高速に絞り込む必要があります。同じ検索クエリを何度も打つと同じ計算を繰り返すのは無駄なので、このファイルでは「どのChunk(データの塊)に対してどのクエリ文字列を検索したか」をキャッシュしておく仕組みを作っています。複数のゴルーチン(Go言語の軽量スレッド)から同時に読み書きされるため、排他制御(ミューテックス)の使い方を学ぶ好例です。

コード

// ChunkBitmap is a bitmap with one bit per item in a chunk.
type ChunkBitmap [chunkBitWords]uint64

// queryCache associates query strings to bitmaps of matching items
type queryCache map[string]ChunkBitmap

// ChunkCache associates Chunk and query string to bitmaps
type ChunkCache struct {
	mutex sync.Mutex
	cache map[*Chunk]*queryCache
}

// NewChunkCache returns a new ChunkCache
func NewChunkCache() *ChunkCache {
	return &ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
}

// …略…

// Add stores the bitmap for the given chunk and key
func (cc *ChunkCache) Add(chunk *Chunk, key string, bitmap ChunkBitmap, matchCount int) {
	if len(key) == 0 || !chunk.IsFull() || matchCount > queryCacheMax {
		return
	}

	cc.mutex.Lock()
	defer cc.mutex.Unlock()

	qc, ok := cc.cache[chunk]
	if !ok {
		cc.cache[chunk] = &queryCache{}
		qc = cc.cache[chunk]
	}
	(*qc)[key] = bitmap
}

// Lookup returns the bitmap for the exact key
func (cc *ChunkCache) Lookup(chunk *Chunk, key string) *ChunkBitmap {
	if len(key) == 0 || !chunk.IsFull() {
		return nil
	}

	cc.mutex.Lock()
	defer cc.mutex.Unlock()

	qc, ok := cc.cache[chunk]
	if ok {
		if bm, ok := (*qc)[key]; ok {
			return &bm
		}
	}
	return nil
}

引用はリポジトリの実物と機械で照合しています。「// …略…」は省略した行です。

上から順に読む

type ChunkBitmap [chunkBitWords]uint64

`type 名前 元の型` は「新しい型を定義する」構文です。ここでは `[chunkBitWords]uint64` という「要素数chunkBitWords個のuint64配列」に `ChunkBitmap` という名前を付けています。`[N]T` はGoの固定長配列で、Nは配列の長さ、Tは要素の型です(スライス`[]T`とは違い、長さが型の一部として固定されます)。uint64は64ビットの符号なし整数で、ここでは1つのuint64の各ビットを「候補行がマッチしたか」のフラグとして使うビットマップになっています。

type queryCache map[string]ChunkBitmap

`map[K]V` はキーの型がK、値の型がVの辞書(連想配列)です。ここではキーが検索クエリ文字列(string)、値が先ほど定義したChunkBitmapで、「このクエリで検索したらこのビットマップになった」という対応を保存する型に`queryCache`という名前を付けています。

type ChunkCache struct {
	mutex sync.Mutex
	cache map[*Chunk]*queryCache
}

`struct { ... }` は複数のフィールドをまとめた構造体です。1つ目のフィールド`mutex`は`sync.Mutex`という型で、複数のゴルーチン(Goの軽量スレッド)が同時にデータを触らないようにロックをかけるための道具です。2つ目の`cache`は`map[*Chunk]*queryCache`、つまりキーが`*Chunk`(Chunk構造体へのポインタ)、値が`*queryCache`(queryCacheマップへのポインタ)の辞書です。`*`が型名の前につくと「その型の値が格納されているメモリ上の場所を指すポインタ」を意味します。Chunk自体をキーではなく「Chunkへのポインタ」をキーにすることで、同じ内容でも別のChunkインスタンスは別物として区別できます。

func NewChunkCache() *ChunkCache {
	return &ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
}

これは`ChunkCache`を新しく作るためのコンストラクタ関数です。`&ChunkCache{...}`の`&`は「この構造体の値を作ってそのアドレス(ポインタ)を返す」という意味で、`{sync.Mutex{}, make(...)}`は構造体の各フィールドをフィールド定義の順番通りに初期化する書き方(複合リテラル)です。`sync.Mutex{}`はロック済みでない初期状態のミューテックス、`make(map[*Chunk]*queryCache)`は空のmapを作る組み込み関数`make`の呼び出しです。mapはnilのままだと書き込めないので、`make`で必ず初期化してから使います。

func (cc *ChunkCache) Add(chunk *Chunk, key string, bitmap ChunkBitmap, matchCount int) {

`func (cc *ChunkCache) Add(...)`の`(cc *ChunkCache)`部分は「レシーバ」と呼ばれ、この関数が`ChunkCache`型のメソッドであることを示します。`cc`はこのメソッドの中で使う「自分自身」を指す変数名(他言語のthis/selfに相当)です。ポインタ型`*ChunkCache`をレシーバにしているのは、メソッドの中でccのフィールド(mutexやcache)を実際に書き換えたいからです。値型(`ChunkCache`)をレシーバにすると呼び出し時にコピーが作られてしまい、元の構造体は変更されません。

if len(key) == 0 || !chunk.IsFull() || matchCount > queryCacheMax {
		return
	}

`||`は論理OR演算子で、「いずれか1つでも真ならこのif全体が真になる」という意味です。`len(key) == 0`はキー文字列が空かどうか、`!chunk.IsFull()`の`!`は否定演算子で「chunk.IsFull()の結果を反転させる」ので「chunkがまだ満杯になっていない」という条件です。3つのどれかに当てはまったら、キャッシュする価値がない(クエリが空、Chunkが未完成、マッチ数が多すぎる)と判断して`return`で関数を早期終了します。こうして本処理の前に無効なケースを弾く書き方は「ガード節」と呼ばれ、Goでは頻出のイディオムです。

cc.mutex.Lock()
	defer cc.mutex.Unlock()

`Lock()`はミューテックスに鍵をかけ、他のゴルーチンが同時にこの後の処理へ入れないようにします。`defer`は「この関数がreturnする直前(正常終了でもpanicでも)に、指定した処理を実行する」という予約です。ここでは`Lock()`した直後に`defer cc.mutex.Unlock()`と書くことで、「関数のどこでreturnしても必ず鍵を開け忘れない」ことを保証しています。ロックしたら次の行に必ずdeferでUnlockを書く、というのはGoでミューテックスを使うときの定番パターンです。

qc, ok := cc.cache[chunk]
	if !ok {
		cc.cache[chunk] = &queryCache{}
		qc = cc.cache[chunk]
	}

Goのmapから値を取り出すとき`v, ok := m[key]`のように2つの値を受け取れます。1つ目`v`(ここでは`qc`)はその値(キーが無い場合は型のゼロ値)、2つ目`ok`は「そのキーが実際にmapに存在したか」を示す真偽値です。`if !ok`は「そのchunk用のqueryCacheがまだ存在しなかったら」という意味で、その場合は`&queryCache{}`で空のqueryCacheを作ってそのポインタをmapに登録し、`qc`をその新しいポインタで更新しています。

(*qc)[key] = bitmap

`qc`は`*queryCache`型、つまりqueryCache(mapの一種)へのポインタです。ポインタが指す先の値そのものを扱いたいときは`*qc`のように`*`を前置してデリファレンス(参照先の取り出し)します。`(*qc)[key] = bitmap`は「qcが指しているqueryCacheマップに対して、keyというキーでbitmapを保存する」という代入文です。丸括弧`()`で囲っているのは、`*`と`[key]`の優先順位を明示して「まずデリファレンスしてから添字アクセスする」ことを示すためです。

if bm, ok := (*qc)[key]; ok {
			return &bm
		}

`if 初期化文; 条件 { ... }`はGoのif文の特殊な形で、セミコロン`;`の前に変数を宣言してから、その変数を使った条件判定ができます。ここでは`(*qc)[key]`でmapから値を取り出しつつ`ok`(存在したかどうか)も受け取り、そのokが真のときだけ`return &bm`しています。`bm`はこのif文のスコープ内だけで有効なローカル変数ですが、`&bm`でそのアドレスを返しても問題ありません。Goのコンパイラは「関数を抜けた後も使われる変数」を自動的にヒープに確保してくれる(エスケープ解析)ため、C言語のように「ローカル変数のアドレスを返すと危険」という心配は不要です。

← 2026.09.19 の号を読む