go/constant パッケージに、文字列定数を実体化せずに長さを取得できる StringLen 関数を追加
go/constant
概要
go/constant パッケージに StringLen 関数が追加された。Value が文字列定数を表す場合に、その値を実際の Go 文字列として構築(マテリアライズ)せずに長さだけを int64 で返す。Value が Unknown の場合は 0 を返し、それ以外の種類の場合は panic する。
従来、文字列定数の長さを知るには StringVal で文字列値を取得してから len() を呼ぶ必要があった。しかし go/constant の内部では、+ による連結で作られた文字列定数は連結木として遅延的に保持されており、StringVal を呼ぶとその木を辿って一つの文字列に実体化するため、大きな連結が絡む場合は巨大なメモリアロケーションが発生し得た。StringLen は木を実体化せず再帰的に長さだけを合算するため、この問題を避けられる。
導入経緯
本提案は、関連 issue #78346 で報告された、文字列定数の連鎖倍加(b = a + a, c = b + b, ...)が []rune 変換などと組み合わさるとコンパイラが OOM(メモリ枯渇)を起こす問題への対応として提出された。go/types 側で文字列長をチェックしてガードを掛けたいが、既存の StringVal では長さを知るためにまず文字列そのものを実体化する必要があり、チェック自体が OOM の原因になってしまう。StringLen はこの矛盾を解消するための最小限の API として提案された。
議論タイムライン
- 2026-05-06: proposal review ミーティングにて
@aclementsが likely accept と判定。 - 2026-05-13: 異論がないまま最終コメント期間が終了し、accepted となった。
議論のハイライト
- 返り値の型:
@mateusz834が 32bit プラットフォームではintで十分ではないかと指摘したが、@griesemerと@ianlancetaylorが「32bit ホストで 64bit ターゲット向けプログラムを解析する場合、連結木として保持される文字列は実体化しない限り理論上math.MaxInt32を超えうる」と説明し、int64が採用された。 - メソッド vs 関数:
Valueインターフェースは非公開メソッドimplementsValue()を持つためメソッドとして追加することも技術的には可能だったが、go/constantの他の機能が一貫して関数スタイルであることから、@griesemerの判断で関数として追加された。 - 実装 CL は提案と並行して go.dev/cl/772320 としてすでに提出されていた。
使用例
Before
package main
import (
"fmt"
"go/constant"
)
func stringLength(v constant.Value) int {
s := constant.StringVal(v) // 巨大な連結文字列だと実体化コストが高い
return len(s)
}
func main() {
v := constant.MakeString("hello")
fmt.Println(stringLength(v))
}
After
package main
import (
"fmt"
"go/constant"
)
func stringLength(v constant.Value) int64 {
return constant.StringLen(v) // 文字列を実体化せずに長さだけ取得
}
func main() {
v := constant.MakeString("hello")
fmt.Println(stringLength(v))
}
実装解説
stringVal は「単一文字列(s を持ち l・r が nil)」または「2つの stringVal の連結(l・r が非nil)」のいずれかを表す木構造として定義されている(value.go;l=90)。
StringVal が呼び出す (*stringVal).string() は、この木を appendReverse で辿って全ての葉の文字列を集めてから strings.Join で連結し、結果を x.s にキャッシュして木を平坦化する(value.go;l=138)。この連結処理自体が実体化コストの正体である。
一方 StringLen は *stringVal に対して新設の (*stringVal).len() を呼ぶだけで、こちらは文字列を構築せず、l・r が非nilなら両者の len() を再帰的に足し合わせ、葉ノードでは len(x.s) を返す(value.go;l=625)。木構造そのものは書き換えないため、後で StringVal を呼んだ場合の平坦化・キャッシュ動作には影響しない。