golang-safety について
golang-safety は、通常のアプリケーションコードにおける正しさと防御的な実装を扱う Go のコーディングスキルです。nil 値、コレクションの挙動、数値範囲、リソース管理に着目し、パニックや気づきにくいデータ破損を防ぎます。
主な機能
- 01nil とインターフェースの安全性
- 02スライスとマップの使用指針
- 03数値変換と浮動小数点の確認
- 04リソース管理と defer の指針
- 05防御的コピーと安全な初期化
活用シーン
- 01nil に起因するパニックをコードで確認
- 02数値変換のオーバーフローを調査
- 03内部コレクションの意図しない変更を防止
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ samber/ cc-skills-golang --skill golang-safetyインストール後、Claude Code で/golang-safetyを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/evals.jsonreferences/nil-safety.mdslice-map-safety.md詳細
ペルソナ: あなたは防御的な Go エンジニニアです。nil、容量、数値範囲に関する未検証の前提はすべて、潜在的なクラッシュの原因として扱います。
Go の安全性:正確性と防御的コーディング
通常の(悪意のある攻撃を想定しない)コードにおけるプログラマーのミス、バグ、パニック、気付かれないデータ破損を防ぎます。セキュリティは攻撃者に対処し、安全性は自分たち自身に対処します。
ベストプラクティスの概要
- 型の集合が既知の場合は、
anyよりジェネリクスを優先します。コンパイラーが実行時のパニックではなく、不一致を検出します。 - 型アサーションは常に安全な方法で行います。通常のインターフェースではカンマ ok イディオム(
v, ok := x.(T))を使います。Go 1.25 以降のリフレクションでは、value.Interface().(T)よりreflect.TypeAssert[T](value)を優先します。 - インターフェース内の型付き nil ポインターは
== nilになりません。型記述子があるため、nil ではないと判定されます。 - nil マップへの書き込みはパニックを起こします。使用前に必ず初期化してください。
appendはバッキング配列を再利用することがあります。容量に余裕があると、2 つのスライスがメモリーを共有し、互いのデータを気付かないうちに破損させます。- エクスポートされた関数からは防御的コピーを返します。そうしないと、呼び出し元が内部データを変更できます。
deferはループの各反復ではなく、関数の終了時に実行されます。ループ本体を関数に切り出してください。- 整数変換では値が黙って切り捨てられます。
int64からint32への変換では、エラーなしに値が折り返します。 - 浮動小数点演算は正確ではありません。イプシロン比較または
math/bigを使用してください。 - 有用なゼロ値を設計します。nil のマップフィールドは最初の書き込みでパニックを起こすため、遅延初期化を使用してください。
- 遅延初期化には
sync.Onceを使用します。並行処理下でも初期化が正確に一度だけ行われることを保証します。
nil の安全性
nil に関連するパニックは、Go で最もよくあるクラッシュです。
nil インターフェースの落とし穴
インターフェースは(型、値)を格納します。インターフェースが nil になるのは、型と値の両方が nil の場合のみです。型付き nil ポインターを返すと型記述子が設定されるため、nil ではなくなります。
// ✗ Dangerous — interface{type: *MyHandler, value: nil} is not == nil
func getHandler() http.Handler {
var h *MyHandler // nil pointer
if !enabled {
return h // interface{type: *MyHandler, value: nil} != nil
}
return h
}
// ✓ Good — return nil explicitly
func getHandler() http.Handler {
if !enabled {
return nil // interface{type: nil, value: nil} == nil
}
return &MyHandler{}
}
nil のマップ、スライス、チャネルの動作
| 型 | nil へのインデックスアクセス | nil への書き込み | nil の Len/Cap | nil に対する Range |
|---|---|---|---|---|
| Map | ゼロ値 | パニック | 0 | 0 回の反復 |
| Slice | パニック | パニック | 0 | 0 回の反復 |
| Channel | 永久にブロック | 永久にブロック | 0 | 永久にブロック |
// ✗ Bad — nil map panics on write
var m map[string]int
m["key"] = 1
// ✓ Good — initialize or lazy-init in methods
m := make(map[string]int)
func (r *Registry) Add(name string, val int) {
if r.items == nil { r.items = make(map[string]int) }
r.items[name] = val
}
nil レシーバー、ジェネリクスにおける nil、nil インターフェースのパフォーマンスについては、nil の安全性の詳細 を参照してください。
スライスとマップの安全性
スライスのエイリアシング — append の落とし穴
容量に余裕がある場合、append はバッキング配列を再利用します。その場合、2 つのスライスはメモリーを共有します。
// ✗ Dangerous — a and b share backing array
a := make([]int, 3, 5)
b := append(a, 4)
b[0] = 99 // also modifies a[0]
// ✓ Good — full slice expression forces new allocation
b := append(a[:len(a):len(a)], 4)
マップへの並行アクセス
マップに並行アクセスしてはなりません。同期プリミティブについては samber/cc-skills-golang@golang-concurrency を参照してください。
range の落とし穴、サブスライスによるメモリー保持、slices.Clone/maps.Clone については、スライスとマップの詳細 を参照してください。
数値の安全性
暗黙的な型変換では値が黙って切り捨てられる
// ✗ Bad — silently wraps around if val > math.MaxInt32 (3B becomes -1.29B)
var val int64 = 3_000_000_000
i32 := int32(val) // -1294967296 (silent wraparound)
// ✓ Good — check before converting
if val > math.MaxInt32 || val < math.MinInt32 {
return fmt.Errorf("value %d overflows int32", val)
}
i32 := int32(val)
浮動小数点数の比較
// ✗ Bad — floating point arithmetic is not exact
var a, b, c float64 = 0.1, 0.2, 0.3
a+b == c // false
// ✓ Good — use epsilon comparison
const epsilon = 1e-9
math.Abs((a+b)-c) < epsilon // true
ゼロ除算
整数のゼロ除算はパニックを起こします。浮動小数点数のゼロ除算では +Inf、-Inf、または NaN が生成されます。
func avg(total, count int) (int, error) {
if count == 0 {
return 0, errors.New("division by zero")
}
return total / count, nil
}
セキュリティの脆弱性としての整数オーバーフローについては、samber/cc-skills-golang@golang-security スキルのセクションを参照してください。
リソースの安全性
ループ内の defer — リソースの蓄積
defer はループの反復時ではなく、関数 の終了時に実行されます。関数が戻るまでリソースが蓄積します。
// ✗ Bad — all files stay open until function returns
for _, path := range paths {
f, _ := os.Open(path)
defer f.Close() // deferred until function exits
process(f)
}
// ✓ Good — extract to function so defer runs per iteration
for _, path := range paths {
if err := processOne(path); err != nil { return err }
}
func processOne(path string) error {
f, err := os.Open(path)
if err != nil { return err }
defer f.Close()
return process(f)
}
ゴルーチンリーク
ゴルーチンのライフサイクルとリークの防止については、samber/cc-skills-golang@golang-concurrency を参照してください。
不変性と防御的コピー
スライスやマップを返すエクスポートされた関数では、防御的コピーを返すべきです。
構造体の内部データを保護する
// ✗ Bad — exported slice field, anyone can mutate
type Config struct {
Hosts []string
}
// ✓ Good — unexported field with accessor returning a copy
type Config struct {
hosts []string
}
func (c *Config) Hosts() []string {
return slices.Clone(c.hosts)
}
初期化の安全性
ゼロ値の設計
「初期化し忘れ」によるバグを防ぐため、var x MyType が安全に使える型を設計します。
var mu sync.Mutex // ✓ usable at zero value
var buf bytes.Buffer // ✓ usable at zero value
// ✗ Bad — nil map panics on write
type Cache struct { data map[string]any }
遅延初期化に sync.Once を使用する
type DB struct {
once sync.Once
conn *sql.DB
}
func (db *DB) connection() *sql.DB {
db.once.Do(func() {
db.conn, _ = sql.Open("postgres", connStr)
})
return db.conn
}
init() 関数の落とし穴
明示的なコンストラクターを優先し、init() を避けるべき理由については、samber/cc-skills-golang@golang-design-patterns を参照してください。
リンターで強制する
多くの安全性に関する落とし穴は、リンターによって自動的に検出されます:errcheck、forcetypeassert、nilerr、govet、staticcheck。設定と使用方法については、samber/cc-skills-golang@golang-lint スキルを参照してください。
Go 1.25+ のリフレクション型アサーション
リフレクションコードでは、value.Interface().(T) よりも reflect.TypeAssert[T] を優先してください。
v := reflect.ValueOf(x)
if s, ok := reflect.TypeAssert[string](v); ok {
use(s)
}
よくある間違い
| 間違い | 修正方法 |
|---|---|
型アサーションをそのまま使う v := x.(T) | 型が一致しないとパニックが発生し、プログラムがクラッシュします。v, ok := x.(T) を使って適切に処理してください |
| インターフェースを返す関数から型付き nil を返す | インターフェースは (型, nil) を保持するため、nil と等しくありません。nil の場合は型なしの nil を返してください |
| nil マップに書き込む | nil マップには保存領域がなく、書き込むとパニックが発生します。make(map[K]V) で初期化するか、遅延初期化してください |
append は常にコピーすると仮定する | 容量に余裕がある場合、スライスは同じバッキング配列を共有します。コピーを強制するには s[:len(s):len(s)] を使ってください |
ループ内の defer | defer はループの各反復ではなく関数の終了時に実行されるため、リソースが蓄積します。本体を別の関数に切り出してください |
境界チェックなしで int64 から int32 に変換する | 値は警告なくラップします(3B → -1.29B)。最初に math.MaxInt32/math.MinInt32 と比較してください |
浮動小数点数を == で比較する | IEEE 754 の表現は正確ではありません(0.1+0.2 != 0.3)。math.Abs(a-b) < epsilon を使ってください |
| ゼロチェックなしで整数除算を行う | ゼロによる整数除算はパニックを引き起こします。除算の前に if divisor == 0 で確認してください |
| 内部のスライス/マップ参照を返す | 呼び出し元は共有されたバッキング配列を介して、構造体の内部を変更できます。防御的コピーを返してください |
順序を前提とした複数の init() | ファイル間での init() の実行順序は未規定です。→ samber/cc-skills-golang@golang-design-patterns を参照し、明示的なコンストラクターを使用してください |
| nil チャネルで永久にブロックする | nil チャネルは送受信のどちらでもブロックします。使用前に必ず初期化してください |
関連項目
- → 並行アクセスのパターンと同期プリミティブについては、
samber/cc-skills-golang@golang-concurrencyスキルを参照してください - → スライス/マップの内部構造、容量の拡張、container/ パッケージについては、
samber/cc-skills-golang@golang-data-structuresスキルを参照してください - → nil エラーインターフェースの落とし穴については、
samber/cc-skills-golang@golang-error-handlingスキルを参照してください - → セキュリティに関わる安全性の問題(メモリ安全性、整数オーバーフロー)については、
samber/cc-skills-golang@golang-securityスキルを参照してください - → パニックやデータ競合のデバッグについては、
samber/cc-skills-golang@golang-troubleshootingスキルを参照してください - → これらのガイドラインを使った CI での AI 主導の自動コードレビューについては、
samber/cc-skills-golang@golang-continuous-integrationスキルを参照してください