go-defensive について
go-defensiveは、GoのAPI境界における防御的プログラミングの実践をまとめたガイドです。データの意図しない共有、リソースリーク、エラー処理のリスクを抑えたい開発者向けに、コピー、後処理、列挙値、乱数、panicの扱いを説明します。
主な機能
- 01API境界でスライスとマップをコピー
- 02deferによるリソース解放
- 03可変なグローバル状態の回避
- 04列挙値を非ゼロから開始
- 05暗号学的乱数と通常の乱数を区別
- 06panicを制限しrecoverを適切に使用
活用シーン
- 01API境界におけるデータ安全性のレビュー
- 02ファイル、ロック、キャンセル処理の後始末確認
- 03列挙値、グローバル状態、panicの使い方の評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ cxuu/ golang-skills --skill go-defensiveインストール後、Claude Code で/go-defensiveを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/BOUNDARY-COPYING.mdGLOBAL-STATE.mdMUST-FUNCTIONS.mdPANIC-RECOVER.mdTIME-ENUMS-TAGS.md詳細
Go の防御的プログラミングパターン
互換性:暗号関連の例では
crypto/rand.Textを使用する場合があります。この機能には Go 1.24 以降が必要です。
リソースのルーティング
references/BOUNDARY-COPYING.md- API 境界を越えてスライス/マップをコピーする場合に読みます。references/GLOBAL-STATE.md- パッケージグローバル変数を導入または削除する場合に読みます。references/MUST-FUNCTIONS.md- エラー時に panic するヘルパーが許容されるか判断する場合に読みます。references/PANIC-RECOVER.md- panic、recover、またはクラッシュの封じ込めを評価する場合に読みます。references/TIME-ENUMS-TAGS.md- 時刻型、列挙型のゼロ値、または構造体タグを扱う場合に読みます。
防御的プログラミングのチェックリストの優先順位
API 境界でコードを堅牢化する場合は、次の順に確認します。
Reviewing an API boundary?
├─ 1. Error handling → Return errors; don't panic (see go-error-handling)
├─ 2. Input validation → Copy slices/maps received from callers
├─ 3. Output safety → Copy slices/maps before returning to callers
├─ 4. Resource cleanup → Use defer for Close/Unlock/Cancel
├─ 5. Interface checks → Route compile-time assertions to go-interfaces
├─ 6. Time correctness → Use time.Time and time.Duration, not int/float
├─ 7. Enum safety → Start iota at 1 so zero-value is invalid
└─ 8. Crypto safety → crypto/rand for keys, never math/rand
クイックリファレンス
| パターン | ルール | 詳細 |
|---|---|---|
| 境界でのコピー | 受け取り時と返却時にスライス/マップをコピーする | BOUNDARY-COPYING.md |
| defer によるクリーンアップ | os.Open の直後に defer f.Close() を記述する | 以下を参照 |
| インターフェースの確認 | コンパイル時の実装確認アサーション | go-interfaces を参照 |
| 時刻型 | time.Time / time.Duration を使い、生の int は使わない | TIME-ENUMS-TAGS.md |
| 列挙型の開始値 | iota + 1 を使い、ゼロを無効値にする | 以下を参照 |
| 暗号用乱数 | キーには crypto/rand を使い、math/rand は使わない | 以下を参照 |
| Must 関数 | 初期化時のみ使用し、失敗時は panic する | MUST-FUNCTIONS.md |
| panic/recover | パッケージ境界を越えて panic を公開しない | PANIC-RECOVER.md |
| 可変グローバル変数 | 依存性注入に置き換える | 以下を参照 |
インターフェースへの準拠を確認する
コンパイル時のインターフェースアサーションについては、go-interfaces に任せてください。 このスキルは、API 境界における堅牢性のリスクに気づくためだけに使用します。アサーションが適切な場面と、具体的なアサーションの形式はインターフェーススキルが扱います。
境界でスライスとマップをコピーする
スライスとマップには、基になるデータへのポインターが含まれています。意図しない変更を防ぐため、API 境界でコピーします。
// Receiving: copy incoming slice
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
// Returning: copy map before returning
result := make(map[string]int, len(s.counters))
for k, v := range s.counters { result[k] = v }
defer でクリーンアップする
defer を使って、ファイルやロックなどのリソースをクリーンアップします。複数の return 経路でクリーンアップ漏れが発生するのを防げます。
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
defer のオーバーヘッドは無視できる程度です。分かりやすさのため、os.Open の直後に
defer f.Close() を記述します。defer された関数の引数は、その関数の実行時ではなく、
defer の実行時に評価されます。複数の defer は LIFO 順に実行されます。
構造体フィールドのタグ
推奨事項:マーシャルまたはアンマーシャルされる構造体には、必ず明示的なフィールドタグを追加してください。
type User struct {
Name string `json:"name" yaml:"name"`
Email string `json:"email" yaml:"email"`
}
フィールドタグはシリアライズの契約です。タグを更新せずに構造体フィールドの名前を変更すると、 ワイヤー互換性が気づかれないまま損なわれます。シリアライズ境界を越える型では、タグを公開 API の一部として扱ってください。
列挙型は 1 から始める
未初期化の値と有効な値を区別するため、列挙型はゼロ以外の値から始めます。
const (
Add Operation = iota + 1 // Add=1, zero value = uninitialized
Subtract
Multiply
)
例外:ゼロが妥当なデフォルト値である場合(例:LogToStdout = iota)。
時刻、構造体タグ、埋め込み
可変グローバル変数を避ける
パッケージレベルの変数を変更するのではなく、依存関係を注入します。これにより、グローバル変数の保存と復元をせずにコードをテストできます。
type signer struct {
now func() time.Time // injected; tests replace with fixed time
}
func newSigner() *signer {
return &signer{now: time.Now}
}
暗号用乱数
キーの生成に math/rand または math/rand/v2 を使用しないでください。これはセキュリティ上の懸念です。時刻をシードにするジェネレーターは、出力を予測できます。
import "crypto/rand"
func Key() string { return rand.Text() }
テキスト出力には crypto/rand.Text を直接使うか、ランダムなバイト列を
encoding/hex または encoding/base64 でエンコードします。
Panic と Recover
本当に回復不能な状況でのみ panic を使用してください。ライブラリ関数では panic を避けるべきです。
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
主なルール:
- パッケージ境界を越えて panic を公開しないでください。必ずエラーに変換してください。
- ライブラリが自身をセットアップできない場合は、
init()で panic してもかまいません。 - サーバーのゴルーチンハンドラーで panic を隔離するには recover を使用してください。
Must 関数
Must 関数はエラー時に panic します。失敗するとプログラムを実行できない場合の、プログラム初期化時に限って使用してください。
var validID = regexp.MustCompile(`^[a-z][a-z0-9-]{0,62}$`)
var tmpl = template.Must(template.ParseFiles("index.html"))
関連するスキル
- エラー処理:エラーを返すかパニックを起こすかの選択、または境界でエラーをラップする場合は、go-error-handlingを参照してください
- 並行処理の安全性:ミューテックス、アトミック操作、チャネルで共有状態を保護する場合は、go-concurrencyを参照してください
- インターフェースのチェック:コンパイル時のインターフェース充足チェックを追加する場合は、go-interfacesを参照してください
- データ構造のコピー:スライス/マップの内部構造やポインターのエイリアスを扱う場合は、go-data-structuresを参照してください