spec: remove string(int)
要約
概要
string(int)変換(整数値を1文字のUnicode文字列に変換する組み込み変換)を、byteまたはrune型の整数に限定し、それ以外の整数型からの変換を言語仕様上禁止する提案です。長らくgo vetの警告としてのみ存在した制限を、Go 1.28以降の言語仕様レベルに格上げする内容です。
ステータス変更
active → likely_accept
2026年9月9日の提案レビュー会議で、コアチームはこの変更に賛成の姿勢を示し、「基本的に現行のgo vetチェック(stringintconv)と同じ挙動に合わせる」という方針を確認しました。細かい論点として「string(0)のような型なし定数リテラルの扱いをどうするか」が議論され、通常の型のデフォルト化ルールに従わせた上でintを拒否する形にすることで合意しました。具体的な仕様文言はGriesemer氏が執筆することになり、実際にCL 830044(spec変更)とCL 832604(go/types, types2の実装)が提出されたことを受け、9月16日にAclements氏が「likely accept」と判定しました。
技術的背景
現状の問題点
現行のGo仕様では任意の整数型をstringに変換でき、これはUTF-8エンコードされたコードポイントに変換されます。しかし以下のような問題があります。
i := 3
s := string(i) // 意図: "3" を期待しがちだが、実際は制御文字(ETX, U+0003)になる
- サロゲート領域(
0xD800〜0xDFFF)の値は正当なUTF-8にできないため、代わりに置換文字�になるなど直感的でない挙動がある。 - シフト演算の結果を渡すと定数の扱いが複雑化する(関連issue #26096)。
- 「int→string変換で数値の文字列表現が得られる」と誤解するバグが繰り返し報告されている(Python等の
str(42)との混同)。 - 2016年頃から議論が続き、2019年にGo vetの
stringintconvチェック(#32479)が追加されたことで、実運用上の危険性は大幅に緩和されていました。
提案された解決策
Go 1.28以降の言語バージョンにおいて、string(x)のxが整数型の場合、byte(uint8)またはrune(int32)型(およびそれらのエイリアス、または型なしrune定数)でなければコンパイルエラーとする、という仕様変更です。仕様文言の差分(CL 830044より抜粋)は以下の通りです。
<li>
- x is an integer or a slice of bytes or runes
- and T is a string type.
+ x is an integer of type byte or rune
+ or an untyped rune constant
+ or a slice of bytes or runes and T is a string type.
</li>
重要なのは、これが言語バージョン依存の変更である点です。go.modで古い言語バージョンを指定しているモジュールは従来通り動作し、新しい言語バージョン(1.28以降)を指定した場合のみ制限が適用されます。これにより既存コードへの後方互換性への影響を最小化しています。
これによって何ができるようになるか
これは「新機能の追加」ではなく「危険な変換の禁止」による安全性向上が主目的です。開発者にとってのメリットは以下の通りです。
- 意図しない
string(int)のバグ(数値の文字列表現化を期待して実は制御文字になる、など)をコンパイル時に検出できる。従来はgo vetを明示的に実行しないと検出できなかった。 byte/runeからの変換(string(os.PathSeparator)のような一般的で正当な用法)は引き続き許可されるため、実用上のワークアラウンドはほぼ不要。- IDEやリンターに頼らず、コンパイラレベルでより分かりやすいエラーメッセージが得られる。
コード例
// Before (Go 1.27以前、またはgo.modでlanguage version < go1.28を指定):
i := 3
s := string(i) // コンパイルは通るが、"3"ではなく制御文字(ETX)になる意図しない結果
// go vetを実行すれば stringintconv 警告が出るが、通常のビルドでは検出されない
// After (go.modで go 1.28 以上を指定した場合):
i := 3
s := string(i) // コンパイルエラー: cannot convert i (variable of type int) to type string
// 正しい書き方(数値を文字列表現にしたい場合)
s := strconv.Itoa(i) // "3"
// byte/runeへの変換は引き続き有効
var b byte = 65
s2 := string(b) // "A" — これは今後も許可される
var r rune = '文'
s3 := string(r) // "文" — これも許可される
議論のハイライト
- 10年以上に渡る慎重な議論: 2013年の提案当初は「Go 1互換性を壊す」懸念から長期保留されていましたが、2019年に
go vetのstringintconvチェック(#32479)が導入されたことで実運用データが蓄積され、議論が再燃しました。 - byte/runeの特例扱いの根拠: Ian Lancetaylor氏や多くのコメンターが、
string(os.PathSeparator)やstring('x')のような一般的で正当な用法を壊さないため、byteとrune型だけは除外すべきと主張し、これが最終的な提案の核となりました。 - 2026年7月の一度目の判定は「likely decline」だった点が特筆されます。Griesemer氏は当初「vetチェックが既に十分機能しており、仕様変更のメリットが小さい」として却下寄りの判断を示しましたが、Aclements氏の「言語バージョンに紐付ければ既存コードを一切壊さずに、コンパイルエラーとしてより強い保証が得られる」という指摘により方針が転換し、「likely accept」へ再評価されました。
- 型なし定数の扱いの明確化:
string(0)のような型なし整数定数の扱いについて、通常の型のデフォルト化規則に従わせつつintを拒否するという整合的なアプローチが採用されました。 - エイリアス関係の確認: 提案後の議論で
rune/int32、byte/uint8がエイリアス(同一の型)であることを踏まえ、string(int32(1))やstring(uint8(1))もstring(rune(1))やstring(byte(1))と同様に許可されることがGriesemer氏により明確化されました。
関連リンク
- Proposal Issue github.com/golang/go
- Review Comment proposal review meeting
- 関連Issue #30614: path/filepath: add SeparatorString
- Proposal Issue #3939
- Review Minutes (issue #33502)
- 関連Issue #32479: cmd/vet: add check for `string(int)`
- 関連Issue #26096: shift expressions involving `string(int)`
- 関連Issue #29012: separate rune and int32 types