第11章: エラー処理 - マシンレベル完全解説
学習目標
この章を終えると、以下ができるようになります:
- error インターフェースの内部構造(tab + data)を完全に理解できる
- errors.New と fmt.Errorf の実装をアセンブリレベルで説明できる
- エラーラッピングチェーンのメモリ構造とトラバースを理解できる
- errors.Is と errors.As の内部アルゴリズムを実装できる
- panic/recover の動作メカニズムとスタック処理を理解できる
- パフォーマンスを考慮したエラー処理を実装できる
🔑 Goのエラー処理哲学 - なぜ例外を使わないのか
例外処理のコスト
多くの言語は例外処理(try-catch)を採用していますが、Goは意図的にこれを採用しませんでした。
例外処理の隠れたコスト:
例外スロー時のCPU処理:
1. スタック巻き戻し(Stack Unwinding)
- 各フレームのデストラクタ実行
- 例外オブジェクトの構築
- スタックトレース生成
2. 例外テーブル検索
- 各関数の例外ハンドラテーブルを走査
- 適切な catch ブロックを探す
- 複数レベルをさかのぼる可能性
3. 制御フローの不透明性
- どの関数で例外が発生するか不明
- 隠れた制御フローの分岐
- パフォーマンス予測が困難
Goのアプローチ - 値としてのエラー
// Goのエラー処理
result, err := someOperation()
if err != nil {
return fmt.Errorf("failed to perform operation: %w", err)
}
メモリとCPUの観点:
スタックレイアウト:
┌────────────────────────┐
│ result: 値 │ ← 通常の戻り値
├────────────────────────┤
│ err: interface{} (16B) │ ← エラーインターフェース
│ tab: *itab │ 8バイト: 型情報
│ data: *errorData │ 8バイト: 実データポインタ
└────────────────────────┘
利点:
- スタック巻き戻し不要
- 例外テーブル不要
- 制御フローが明確
- 予測可能なパフォーマンス
💡 パフォーマンス: エラー値の返却は、通常の戻り値とほぼ同じコスト(16バイトのコピー)です。
error インターフェースの内部構造 - 最もシンプルな設計
基本定義と哲学
type error interface {
Error() string
}
これは、Goで最も重要なインターフェースの1つで、たった1つのメソッドで構成されています。
インターフェースのメモリレイアウト
var err error = errors.New("something went wrong")
メモリ状態の詳細:
err (iface - 16バイト):
┌──────────────────────────┐ オフセット
│ tab: *itab │ 0 (8バイト)
├──────────────────────────┤
│ data: *errorString │ 8 (8バイト)
└──────────────────────────┘
│
↓
itab 構造体 (32バイト):
┌──────────────────────────┐
│ inter: *interfacetype │ ← error インターフェース
├──────────────────────────┤
│ _type: *_type │ ← *errors.errorString 型
├──────────────────────────┤
│ hash: uint32 │ ← 型のハッシュ値
├──────────────────────────┤
│ _: [4]byte │ ← パディング
├──────────────────────────┤
│ fun: [1]uintptr │ ← メソッドテーブル(Error())
└──────────────────────────┘
│
↓
ヒープ:
┌──────────────────────────┐
│ errorString struct: │
│ s: string │
│ ptr: *byte │ → "something went wrong"
│ len: 20 │
└──────────────────────────┘
🔑 重要: error は常に16バイトのインターフェース値です。実際のエラーメッセージはヒープに格納されます。
tab ポインタの役割
tab ポインタは、インターフェースの型情報を保持する itab 構造体を指します:
itab の内容:
1. inter: インターフェース型(error)の定義
2. _type: 実際の型(*errors.errorString)の情報
3. hash: 型の高速比較用ハッシュ値
4. fun: メソッドテーブル(Error() メソッドのアドレス)
インターフェースメソッド呼び出し:
err.Error() の実行時:
1. err.tab.fun[0] からメソッドアドレスを取得
2. err.data を第1引数(レシーバ)として渡す
3. メソッドを呼び出し
data ポインタの役割
data ポインタは、実際のエラーデータが格納されているメモリアドレスを指します:
type errorString struct {
s string
}
メモリレイアウト:
errorString (24バイト):
┌──────────────────────────┐
│ s: string (16バイト) │
│ ptr: *byte (8バイト) │ → 文字列データ
│ len: int (8バイト) │ → 20
└──────────────────────────┘
errors.New の内部実装 - ヒープ割り当ての詳細
標準ライブラリの実装
// 標準ライブラリの実装
package errors
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
func New(text string) error {
return &errorString{s: text}
}
アセンブリレベルでの処理フロー
; errors.New("test") の呼び出し全体
; === ステップ1: errorString 構造体をヒープに割り当て ===
MOVQ $24, AX ; サイズ = 24バイト(string の ptr + len)
LEAQ type.errors.errorString, BX ; 型情報
CALL runtime.newobject ; ヒープ割り当て
; 戻り値 AX = 新しいメモリアドレス
; === ステップ2: 文字列をコピー ===
; 引数の string は既にスタックにある(ptr, len)
MOVQ string_ptr(SP), BX ; 文字列ポインタを取得
MOVQ string_len(SP), CX ; 文字列長を取得
; errorString.s にセット
MOVQ BX, 0(AX) ; s.ptr をセット
MOVQ CX, 8(AX) ; s.len をセット
; === ステップ3: error インターフェースを構築 ===
LEAQ go.itab.*errors.errorString,error, BX ; itab アドレス
MOVQ BX, 16(SP) ; err.tab にセット(戻り値の位置)
MOVQ AX, 24(SP) ; err.data にセット
RET ; 戻る
ヒープ割り当てのコスト
runtime.newobject の処理:
1. GC のヒープから24バイトを確保
2. メモリを初期化(ゼロクリア)
3. GC のマーキングビットを設定
4. ポインタを返す
コスト:
- CPU: ~50-100ns(高速パス)
- CPU: ~500ns-1µs(スローパス、GC トリガー)
- メモリ: 24バイト + GC オーバーヘッド
💡 ヒープ割り当て: errors.New は常にヒープ割り当てを行います。頻繁に呼ばれる場合は、センチネルエラーの使用を検討しましょう。
文字列リテラルの最適化
// コンパイル時定数の場合
err := errors.New("file not found")
// コンパイラ最適化:
// 文字列 "file not found" はデータセグメントに配置され、
// 実行時に毎回割り当てる必要はありません
メモリマップ:
データセグメント:
┌──────────────────────────────┐
│ 文字列リテラル: │
│ "file not found\x00" │ ← 固定アドレス
└──────────────────────────────┘
ヒープ:
┌──────────────────────────────┐
│ errorString: │
│ s.ptr → データセグメント │ ← 参照のみ
│ s.len = 14 │
└──────────────────────────────┘
センチネルエラー - 定数としてのエラー
センチネルエラーの定義
package io
var (
EOF = errors.New("EOF")
ErrUnexpectedEOF = errors.New("unexpected EOF")
ErrShortWrite = errors.New("short write")
ErrShortBuffer = errors.New("short buffer")
)
メモリ配置とライフサイクル
プログラムロード時:
1. データセグメントに error インターフェースを配置
2. init() 関数で errors.New を呼び出し
3. 結果をグローバル変数に保存
データセグメント(プログラム起動時に初期化):
┌──────────────────────────────────────┐ アドレス
│ io.EOF (error interface): │ 0x10000
│ tab: *itab │ → 固定の itab
│ data: *errorString │ → 固定のエラー文字列
├──────────────────────────────────────┤
│ io.ErrUnexpectedEOF: │ 0x10010
│ tab: *itab │
│ data: *errorString │
├──────────────────────────────────────┤
│ io.ErrShortWrite: │ 0x10020
│ tab: *itab │
│ data: *errorString │
└──────────────────────────────────────┘
利点:
1. 一度だけ割り当て(プログラム起動時)
2. ポインタ比較で高速判定
3. メモリ効率が良い(全プログラムで1つのみ)
4. スレッドセーフ(読み取り専用)
センチネルエラーの比較
func ReadFile(name string) ([]byte, error) {
file, err := os.Open(name)
if err != nil {
return nil, err
}
defer file.Close()
data, err := io.ReadAll(file)
if err == io.EOF { // ポインタ比較
return data, nil
}
return data, err
}
アセンブリレベルの比較:
; err == io.EOF の比較
; === err のアドレスをロード ===
MOVQ err+0(SP), R1 ; err.tab を R1 にロード
MOVQ err+8(SP), R2 ; err.data を R2 にロード
; === io.EOF のアドレスをロード ===
LEAQ io.EOF(SB), R3 ; io.EOF のアドレスを R3 に
MOVQ 0(R3), R4 ; io.EOF.tab を R4 にロード
MOVQ 8(R3), R5 ; io.EOF.data を R5 にロード
; === tab を比較 ===
CMPQ R1, R4
JNE not_equal ; tab が異なれば not_equal へ
; === data を比較 ===
CMPQ R2, R5
JNE not_equal ; data が異なれば not_equal へ
; === 等しい ===
MOVQ $1, AX ; true
JMP done
not_equal:
MOVQ $0, AX ; false
done:
; AX に比較結果
⚠️ 落とし穴: == による比較は、エラーがラップされていると機能しません。errors.Is を使用しましょう。
// これは動作しない
err := fmt.Errorf("failed: %w", io.EOF)
if err == io.EOF { // false!
// 実行されない
}
// これは動作する
if errors.Is(err, io.EOF) { // true
// 実行される
}
fmt.Errorf と %w 動詞 - エラーラッピングの実装
fmt.Errorf の基本
func readConfig(filename string) ([]byte, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read config: %w", err)
}
return data, nil
}
wrapError の内部構造
fmt.Errorf は、内部的に wrapError 構造体を作成します:
// 標準ライブラリの内部実装(簡略化)
package fmt
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}
メモリレイアウト:
wrapError 構造体:
┌──────────────────────────────┐ オフセット サイズ
│ msg: string │ 0 16バイト
│ ptr: *byte │ 0 8バイト
│ len: int │ 8 8バイト
├──────────────────────────────┤
│ err: error │ 16 16バイト
│ tab: *itab │ 16 8バイト
│ data: *errorData │ 24 8バイト
└──────────────────────────────┘
合計: 32バイト
fmt.Errorf の実装詳細
// 標準ライブラリの実装(簡略化)
func Errorf(format string, a ...interface{}) error {
// フォーマット文字列を解析
p := newPrinter()
p.wrapErrs = true
p.doPrintf(format, a)
s := string(p.buf)
// %w 動詞が使われている場合
if p.wrappedErr != nil {
return &wrapError{msg: s, err: p.wrappedErr}
}
// 通常のエラー
return errors.New(s)
}
処理フロー:
fmt.Errorf("failed: %w", originalErr) の処理:
1. フォーマット文字列を解析
- "failed: %w" を検出
- %w が存在するので wrapError を使用
2. メッセージを構築
- "failed: " + originalErr.Error()
- 結果: "failed: original error message"
3. wrapError を作成
msg = "failed: original error message"
err = originalErr
4. ヒープに割り当て
- 32バイトの wrapError 構造体
- メッセージ文字列(別途割り当て)
5. error インターフェースとして返す
tab = (*wrapError, error) の itab
data = wrapError のポインタ
エラーチェーンのメモリ構造
// 3段階のエラーラッピング
func level1() error {
return errors.New("original error")
}
func level2() error {
err := level1()
return fmt.Errorf("level 2: %w", err)
}
func level3() error {
err := level2()
return fmt.Errorf("level 3: %w", err)
}
メモリチェーン:
レベル3のエラー:
┌──────────────────────────────────┐
│ error interface: │
│ tab: (*wrapError, error) │
│ data: ─────────┐ │
└──────────────────┼───────────────┘
↓
┌──────────────────────────────────┐
│ wrapError: │
│ msg: "level 3: level 2: ..." │ ← 完全なメッセージ
│ err: ─────────┐ │
└─────────────────┼────────────────┘
↓
レベル2のエラー:
┌──────────────────────────────────┐
│ error interface: │
│ tab: (*wrapError, error) │
│ data: ─────────┐ │
└──────────────────┼───────────────┘
↓
┌──────────────────────────────────┐
│ wrapError: │
│ msg: "level 2: original ..." │
│ err: ─────────┐ │
└─────────────────┼────────────────┘
↓
レベル1のエラー:
┌──────────────────────────────────┐
│ error interface: │
│ tab: (*errorString, error) │
│ data: ─────────┐ │
└──────────────────┼───────────────┘
↓
┌──────────────────────────────────┐
│ errorString: │
│ s: "original error" │
└──────────────────────────────────┘
メモリ使用量:
- 各 error インターフェース: 16バイト × 3 = 48バイト
- 各 wrapError 構造体: 32バイト × 2 = 64バイト
- errorString 構造体: 16バイト
- 文字列データ: 約100バイト(3つのメッセージ)
合計: ~230バイト
💡 エラーチェーン: 各レベルがコンテキストを追加し、元のエラーを保持します。
%w と %v の違い
// %w: エラーラッピング(Unwrap 可能)
err1 := fmt.Errorf("wrapped: %w", originalErr)
// %v: 単純なフォーマット(Unwrap 不可)
err2 := fmt.Errorf("formatted: %v", originalErr)
メモリ構造の違い:
%w の場合:
┌──────────────────────────────────┐
│ wrapError: │
│ msg: "wrapped: original" │
│ err: → originalErr │ ← Unwrap で取得可能
└──────────────────────────────────┘
%v の場合:
┌──────────────────────────────────┐
│ errorString: │
│ s: "formatted: original" │ ← メッセージのみ
└──────────────────────────────────┘ originalErr への参照なし
errors.Is - エラー同一性チェックの実装
errors.Is の基本使用法
func processFile(name string) error {
data, err := os.ReadFile(name)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("file does not exist: %w", err)
}
return fmt.Errorf("failed to read file: %w", err)
}
// 処理...
return nil
}
errors.Is の内部実装
// 標準ライブラリの実装(簡略化)
func Is(err, target error) bool {
if target == nil {
return err == target
}
// リフレクションで比較可能かチェック
isComparable := reflectlite.TypeOf(target).Comparable()
for {
// 直接比較
if isComparable && err == target {
return true
}
// カスタム Is メソッドを持つ場合
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// Unwrap してチェーンをたどる
if err = Unwrap(err); err == nil {
return false
}
}
}
アルゴリズムフローチャート:
errors.Is(err, target) の処理フロー:
┌─────────────────────┐
│ target が nil? │
└──────┬──────────────┘
│ YES
├──→ err == target を返す
│
│ NO
↓
┌─────────────────────┐
│ err が nil? │
└──────┬──────────────┘
│ YES
├──→ false を返す
│
│ NO
↓
┌─────────────────────┐
│ err == target? │
└──────┬──────────────┘
│ YES
├──→ true を返す
│
│ NO
↓
┌─────────────────────┐
│ err に Is メソッド?│
└──────┬──────────────┘
│ YES
├──→ err.Is(target) を呼び出し
│ └→ true なら返す
│
│ NO または false
↓
┌─────────────────────┐
│ err = err.Unwrap() │
└──────┬──────────────┘
│
└──→ 最初に戻る(ループ)
errors.Is の実行例
var ErrNotFound = errors.New("not found")
err := fmt.Errorf("step 3: %w",
fmt.Errorf("step 2: %w",
ErrNotFound))
errors.Is(err, ErrNotFound)
実行トレース:
errors.Is(err, ErrNotFound) の実行:
イテレーション 1:
┌────────────────────────────────┐
│ err = wrapError{"step 3: ..."}│
│ target = ErrNotFound │
└────────────────────────────────┘
err == target? → NO
err.Is(target)? → メソッドなし
Unwrap → step 2 のエラー
イテレーション 2:
┌────────────────────────────────┐
│ err = wrapError{"step 2: ..."}│
│ target = ErrNotFound │
└────────────────────────────────┘
err == target? → NO
err.Is(target)? → メソッドなし
Unwrap → ErrNotFound
イテレーション 3:
┌────────────────────────────────┐
│ err = ErrNotFound │
│ target = ErrNotFound │
└────────────────────────────────┘
err == target? → YES!
→ true を返す
⚠️ パフォーマンス: エラーチェーンが長い場合、errors.Is は線形時間O(n)かかります。
カスタム Is メソッドの実装
type TemporaryError struct {
Err error
}
func (e *TemporaryError) Error() string {
return fmt.Sprintf("temporary error: %v", e.Err)
}
func (e *TemporaryError) Unwrap() error {
return e.Err
}
// カスタム Is メソッド
func (e *TemporaryError) Is(target error) bool {
// 任意の TemporaryError と一致
_, ok := target.(*TemporaryError)
return ok
}
使用例:
var ErrTemporary = &TemporaryError{}
func doOperation() error {
return &TemporaryError{Err: errors.New("connection timeout")}
}
func main() {
err := doOperation()
// カスタム Is が呼ばれる
if errors.Is(err, ErrTemporary) {
fmt.Println("Temporary error, retrying...")
}
}
errors.As - 型アサーションチェーンの実装
errors.As の基本使用法
func handleError(err error) {
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("Operation: %s\n", pathErr.Op)
fmt.Printf("Path: %s\n", pathErr.Path)
fmt.Printf("Error: %v\n", pathErr.Err)
return
}
var httpErr *HTTPError
if errors.As(err, &httpErr) {
fmt.Printf("HTTP %d: %s\n", httpErr.StatusCode, httpErr.URL)
return
}
fmt.Printf("Unknown error: %v\n", err)
}
errors.As の内部実装
// 標準ライブラリの実装(簡略化)
func As(err error, target interface{}) bool {
if target == nil {
panic("errors: target cannot be nil")
}
// target がポインタのポインタであることを確認
val := reflectlite.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflectlite.Ptr || val.Elem().Kind() != reflectlite.Ptr {
panic("errors: target must be a non-nil pointer to either a type that implements error, or to any interface type")
}
targetType := typ.Elem()
for {
// 型が一致するかチェック
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
// カスタム As メソッドを持つ場合
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
// Unwrap してチェーンをたどる
if err = Unwrap(err); err == nil {
return false
}
}
}
アルゴリズムフロー:
errors.As(err, &target) の処理:
┌─────────────────────────────┐
│ target のバリデーション │
│ - nil でないこと │
│ - ポインタのポインタであること│
└──────────┬──────────────────┘
↓
┌─────────────────────────────┐
│ err が nil? │
└──────────┬──────────────────┘
│ YES → false を返す
│
│ NO
↓
┌─────────────────────────────┐
│ err の型が target に │
│ 代入可能? │
└──────────┬──────────────────┘
│ YES
├──→ *target = err
│ true を返す
│
│ NO
↓
┌─────────────────────────────┐
│ err に As メソッド? │
└──────────┬──────────────────┘
│ YES
├──→ err.As(&target)
│ └→ true なら返す
│
│ NO または false
↓
┌─────────────────────────────┐
│ err = err.Unwrap() │
└──────────┬──────────────────┘
│
└──→ 最初に戻る(ループ)
メモリ操作の詳細
var pathErr *os.PathError
errors.As(err, &pathErr)
メモリ状態の変化:
処理前:
スタック:
┌──────────────────────┐
│ pathErr: nil │ ← ポインタ(8バイト)
└──────────────────────┘
errors.As(&pathErr) の呼び出し:
1. &pathErr のアドレス = 0x7ffc... (スタック上)
2. target = interface{} で受け取る
┌──────────────────────┐
│ tab: (*os.PathError) │
│ data: 0x7ffc... │ ← pathErr のアドレス
└──────────────────────┘
処理中(エラーチェーンを走査):
err1: wrapError → Unwrap
err2: wrapError → Unwrap
err3: *os.PathError → 型一致!
処理後:
スタック:
┌──────────────────────┐
│ pathErr: 0x8000... │ → os.PathError インスタンス
└──────────────────────┘
リフレクションによる代入:
val.Elem().Set(reflectlite.ValueOf(err))
↓
*(*os.PathError)(0x7ffc...) = err // ポインタを通じて代入
🔑 型アサーションとの違い: errors.As はエラーチェーンをたどるため、ラップされたエラーも検出できます。
// 型アサーション: ラップされたエラーは検出できない
pathErr, ok := err.(*os.PathError) // false
// errors.As: ラップされたエラーも検出できる
var pathErr *os.PathError
errors.As(err, &pathErr) // true
カスタム As メソッドの実装
type MultiError struct {
Errors []error
}
func (e *MultiError) Error() string {
return fmt.Sprintf("multiple errors: %d", len(e.Errors))
}
// カスタム As メソッド
func (e *MultiError) As(target interface{}) bool {
// 各エラーに対して As を試行
for _, err := range e.Errors {
if errors.As(err, target) {
return true
}
}
return false
}
使用例:
func processMultiple() error {
return &MultiError{
Errors: []error{
&os.PathError{Op: "open", Path: "file1.txt", Err: fs.ErrNotExist},
errors.New("another error"),
},
}
}
func main() {
err := processMultiple()
var pathErr *os.PathError
// カスタム As が呼ばれ、最初の PathError を見つける
if errors.As(err, &pathErr) {
fmt.Printf("Found path error: %s\n", pathErr.Path)
}
}
panic と recover - Go の例外メカニズム
panic の基本動作
func riskyOperation() {
panic("something went wrong")
}
func main() {
riskyOperation()
fmt.Println("This will not be printed")
}
panic のスタック処理
panic が発生すると、以下の処理が行われます:
panic の実行フロー:
1. panic 値の作成
┌──────────────────────────┐
│ _panic 構造体: │
│ arg: interface{} │ ← panic の引数
│ link: *_panic │ ← 前の panic(ネスト用)
│ recovered: bool │ ← recover されたか
│ aborted: bool │ ← 中断されたか
│ pc: uintptr │ ← panic 発生位置
│ sp: uintptr │ ← スタックポインタ
│ goexit: bool │
└──────────────────────────┘
2. defer チェーンの実行
- 現在の関数の defer を逆順に実行
- 各 defer で recover() をチェック
- recover() が呼ばれたら、panic を停止
3. スタック巻き戻し
- 関数を1つ戻る
- その関数の defer を実行
- recover() されるまで繰り返す
4. recover されない場合
- プログラム終了
- スタックトレースを出力
メモリ構造:
Goroutine のスタック:
┌──────────────────────────┐ ← SP (スタックポインタ)
│ 現在の関数フレーム │
├──────────────────────────┤
│ defer リスト: │
│ defer3 → defer2 → defer1
│ ↑ │
│ 実行順序(逆順) │
├──────────────────────────┤
│ panic チェーン: │
│ _panic 構造体 │
│ link → 前の panic │
├──────────────────────────┤
│ 呼び出し元フレーム │
│ defer リスト │
├──────────────────────────┤
│ ... │
└──────────────────────────┘
defer と panic の相互作用
func example() {
defer fmt.Println("defer 1")
defer fmt.Println("defer 2")
defer fmt.Println("defer 3")
panic("oops")
fmt.Println("This will not execute")
}
実行順序:
example() の実行:
1. defer 3 を登録
2. defer 2 を登録
3. defer 1 を登録
4. panic("oops") 発生
↓
5. defer 1 実行 → "defer 1" 出力
6. defer 2 実行 → "defer 2" 出力
7. defer 3 実行 → "defer 3" 出力
8. 呼び出し元に伝播(recover されていないため)
recover の動作メカニズム
func safeOperation() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered from: %v\n", r)
}
}()
panic("something went wrong")
}
recover の処理:
recover() の内部処理:
1. 現在の goroutine の _panic をチェック
goroutine.panic != nil?
2. panic が存在し、まだ recovered でない場合
┌──────────────────────────┐
│ _panic.recovered = true │ ← フラグを立てる
│ return _panic.arg │ ← panic の値を返す
└──────────────────────────┘
3. panic が存在しない場合
return nil
4. panic 処理の停止
- スタック巻き戻しを停止
- defer 実行を完了
- 通常の実行フローに戻る
メモリ状態の変化:
panic 発生時:
┌──────────────────────────────┐
│ goroutine: │
│ panic: *_panic │ → ┌──────────────────┐
│ arg: "error" │ │ _panic: │
│ recovered: false │ │ arg: "error" │
│ link: nil │ │ recovered: false│
└──────────────────────────────┘ └──────────────────┘
recover() 呼び出し後:
┌──────────────────────────────┐
│ goroutine: │
│ panic: *_panic │ → ┌──────────────────┐
│ arg: "error" │ │ _panic: │
│ recovered: true │ │ arg: "error" │ ← フラグ変更
│ link: nil │ │ recovered: true │
└──────────────────────────────┘ └──────────────────┘
defer 完了後:
┌──────────────────────────────┐
│ goroutine: │
│ panic: nil │ ← panic 削除
└──────────────────────────────┘
ネストした panic
func nestedPanic() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Outer recovered: %v\n", r)
panic("new panic") // 新しい panic
}
}()
defer func() {
if r := recover(); r != nil {
fmt.Printf("Inner recovered: %v\n", r)
}
}()
panic("original panic")
}
panic チェーンの構造:
nestedPanic() の実行:
最初の panic:
┌──────────────────────────────┐
│ goroutine.panic: │
│ _panic1: │
│ arg: "original panic" │
│ link: nil │
│ recovered: false │
└──────────────────────────────┘
内側の defer で recover:
┌──────────────────────────────┐
│ goroutine.panic: │
│ _panic1: │
│ arg: "original panic" │
│ recovered: true │ ← 回復
└──────────────────────────────┘
出力: "Inner recovered: original panic"
外側の defer で新しい panic:
┌──────────────────────────────┐
│ goroutine.panic: │
│ _panic2: │ ← 新しい panic
│ arg: "new panic" │
│ link: → _panic1 │ ← 前の panic にリンク
│ recovered: false │
└──────────────────────────────┘
外側の defer で recover:
┌──────────────────────────────┐
│ goroutine.panic: │
│ _panic2: │
│ arg: "new panic" │
│ recovered: true │ ← 回復
└──────────────────────────────┘
出力: "Outer recovered: new panic"
panic のパフォーマンスコスト
panic/recover のコスト:
1. panic 発生:
- _panic 構造体の割り当て: ~100ns
- スタックトレースの生成: ~10µs
- defer チェーンの走査: ~100ns/defer
2. スタック巻き戻し:
- 関数フレームごとの処理: ~500ns
- defer 実行: 通常の defer コスト
3. recover:
- フラグ設定: ~10ns
- 制御フロー復元: ~100ns
合計: ~10-50µs(深さに依存)
⚠️ 注意: panic/recover は通常のエラー処理に使うべきではありません。本当の例外的状況のみに使用してください。
カスタムエラー型 - 構造化されたエラー情報
基本的なカスタムエラー
type ValidationError struct {
Field string
Value interface{}
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error [%s]: %s (value: %v)",
e.Field, e.Message, e.Value)
}
メモリレイアウト:
ValidationError 構造体のサイズ:
┌──────────────────────────────┐ オフセット サイズ
│ Field: string (16バイト) │ 0 16バイト
│ ptr: *byte (8バイト) │ 0 8バイト
│ len: int (8バイト) │ 8 8バイト
├──────────────────────────────┤
│ Value: interface{} (16バイト)│ 16 16バイト
│ tab: *itab (8バイト) │ 16 8バイト
│ data: *value (8バイト) │ 24 8バイト
├──────────────────────────────┤
│ Message: string (16バイト) │ 32 16バイト
│ ptr: *byte (8バイト) │ 32 8バイト
│ len: int (8バイト) │ 40 8バイト
└──────────────────────────────┘
合計: 48バイト
error インターフェースとして返される時:
┌──────────────────────────────┐
│ tab: *itab │ ← (*ValidationError, error) の itab
├──────────────────────────────┤
│ data: *ValidationError │ → ヒープ上の ValidationError (48バイト)
└──────────────────────────────┘
複雑なカスタムエラー - HTTPError
type HTTPError struct {
StatusCode int
Method string
URL string
Err error // 元のエラーをラップ
Timestamp time.Time
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("[%d] %s %s: %v (at %s)",
e.StatusCode, e.Method, e.URL, e.Err,
e.Timestamp.Format(time.RFC3339))
}
// Unwrap メソッドでエラーチェーンをサポート
func (e *HTTPError) Unwrap() error {
return e.Err
}
メモリレイアウト:
HTTPError 構造体:
┌──────────────────────────────┐ オフセット サイズ
│ StatusCode: int (8バイト) │ 0 8バイト
├──────────────────────────────┤
│ Method: string (16バイト) │ 8 16バイト
│ ptr: *byte │ 8 8バイト
│ len: int │ 16 8バイト
├──────────────────────────────┤
│ URL: string (16バイト) │ 24 16バイト
│ ptr: *byte │ 24 8バイト
│ len: int │ 32 8バイト
├──────────────────────────────┤
│ Err: error (16バイト) │ 40 16バイト ← ネストしたエラー
│ tab: *itab │ 40 8バイト
│ data: *error │ 48 8バイト
├──────────────────────────────┤
│ Timestamp: time.Time (24B) │ 56 24バイト
│ wall: uint64 │ 56 8バイト
│ ext: int64 │ 64 8バイト
│ loc: *Location │ 72 8バイト
└──────────────────────────────┘
合計: 80バイト
階層構造:
error interface (16B)
↓
HTTPError (80B)
↓ Err field
error interface (16B)
↓
元のエラー (可変サイズ)
🔑 Unwrap パターン: Unwrap() メソッドを実装することで、エラーチェーンを作成できます。
エラー処理のパフォーマンス最適化
1. センチネルエラーの活用
// 遅い: 毎回ヒープ割り当て
func validate(value int) error {
if value < 0 {
return errors.New("value must be positive") // ヒープ割り当て
}
return nil
}
// 速い: 事前定義されたエラー
var ErrNegativeValue = errors.New("value must be positive")
func validateFast(value int) error {
if value < 0 {
return ErrNegativeValue // ポインタコピーのみ
}
return nil
}
ベンチマーク:
BenchmarkValidate-8 5000000 250 ns/op 32 B/op 1 allocs/op
BenchmarkValidateFast-8 50000000 25 ns/op 0 B/op 0 allocs/op
パフォーマンス改善: 10倍
メモリ削減: 100%
💡 10倍の改善: センチネルエラーは、ヒープ割り当てを回避できます。
2. エラーラッピングの削減
// 遅い: 多段階ラッピング
func processDeep() error {
err := level1()
if err != nil {
return fmt.Errorf("level 5: %w",
fmt.Errorf("level 4: %w",
fmt.Errorf("level 3: %w",
fmt.Errorf("level 2: %w", err))))
}
return nil
}
// 速い: 必要な箇所のみラッピング
func processShallow() error {
err := level1()
if err != nil {
return fmt.Errorf("process failed: %w", err)
}
return nil
}
メモリ影響:
多段階ラッピング:
- 4つの wrapError 構造体: 32バイト × 4 = 128バイト
- 4つの文字列: ~100バイト
- 合計: ~230バイト + 元のエラー
単一ラッピング:
- 1つの wrapError 構造体: 32バイト
- 1つの文字列: ~25バイト
- 合計: ~60バイト + 元のエラー
メモリ削減: 約75%
3. エラーチェックの最適化
// 遅い: errors.Is を複数回呼び出し
func handleError(err error) {
if errors.Is(err, fs.ErrNotExist) {
// ...
} else if errors.Is(err, fs.ErrPermission) {
// ...
} else if errors.Is(err, fs.ErrExist) {
// ...
}
}
// 速い: 型アサーションを使用
func handleErrorFast(err error) {
var pathErr *os.PathError
if errors.As(err, &pathErr) {
// syscall.Errno による switch
if errno, ok := pathErr.Err.(syscall.Errno); ok {
switch errno {
case syscall.ENOENT:
// ファイルが存在しない
case syscall.EACCES:
// 権限エラー
case syscall.EEXIST:
// 既に存在
}
return
}
}
}
パフォーマンス比較:
handleError:
- errors.Is の呼び出し: 3回
- エラーチェーンの走査: 最大 3 × チェーン長
- コスト: O(3n) (n = チェーン長)
handleErrorFast:
- errors.As の呼び出し: 1回
- エラーチェーンの走査: 1回
- switch による分岐: O(1)
- コスト: O(n)
改善: 3倍高速
4. エラー文字列の遅延生成
// 遅い: 常にフォーマット
type SlowError struct {
operation string
details string
}
func (e *SlowError) Error() string {
// 毎回フォーマット
return fmt.Sprintf("operation %s failed: %s", e.operation, e.details)
}
// 速い: 必要な時だけフォーマット
type FastError struct {
operation string
details string
msg string // キャッシュ
once sync.Once
}
func (e *FastError) Error() string {
e.once.Do(func() {
e.msg = fmt.Sprintf("operation %s failed: %s", e.operation, e.details)
})
return e.msg
}
ベンチマーク:
BenchmarkSlowError-8 1000000 1200 ns/op 64 B/op 2 allocs/op
BenchmarkFastError-8 10000000 120 ns/op 0 B/op 0 allocs/op
(2回目以降の Error() 呼び出し)
改善: 10倍高速(複数回呼び出す場合)
5. エラープール
// 大量のエラーを扱う場合
var errorPool = sync.Pool{
New: func() interface{} {
return &DetailedError{}
},
}
type DetailedError struct {
Code int
Message string
Details map[string]interface{}
}
func (e *DetailedError) Error() string {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Details)
}
func (e *DetailedError) Reset() {
e.Code = 0
e.Message = ""
e.Details = nil
}
func NewDetailedError(code int, msg string) *DetailedError {
err := errorPool.Get().(*DetailedError)
err.Code = code
err.Message = msg
return err
}
func ReleaseDetailedError(err *DetailedError) {
err.Reset()
errorPool.Put(err)
}
// 使用例
func processRequest(data []byte) error {
if len(data) == 0 {
return NewDetailedError(400, "empty data")
}
return nil
}
func handler(w http.ResponseWriter, r *http.Request) {
data, _ := io.ReadAll(r.Body)
err := processRequest(data)
if err != nil {
handleError(w, err)
// プールに返却
if detailedErr, ok := err.(*DetailedError); ok {
ReleaseDetailedError(detailedErr)
}
return
}
}
ベンチマーク:
BenchmarkNewError-8 5000000 250 ns/op 64 B/op 1 allocs/op
BenchmarkPooledError-8 20000000 60 ns/op 0 B/op 0 allocs/op
改善: 4倍高速
メモリ削減: 100%(定常状態)
実践的なエラー処理パターン
1. リトライ可能エラー
type RetryableError struct {
Err error
RetryAfter time.Duration
}
func (e *RetryableError) Error() string {
return fmt.Sprintf("retryable error: %v (retry after %v)", e.Err, e.RetryAfter)
}
func (e *RetryableError) Unwrap() error {
return e.Err
}
// リトライロジック
func withRetry(fn func() error, maxRetries int) error {
var err error
for i := 0; i < maxRetries; i++ {
err = fn()
if err == nil {
return nil
}
var retryErr *RetryableError
if errors.As(err, &retryErr) {
time.Sleep(retryErr.RetryAfter)
continue
}
// リトライ不可能なエラー
return err
}
return fmt.Errorf("max retries exceeded: %w", err)
}
2. エラーアグリゲーション
type ErrorList struct {
Errors []error
}
func (e *ErrorList) Error() string {
if len(e.Errors) == 0 {
return "no errors"
}
if len(e.Errors) == 1 {
return e.Errors[0].Error()
}
return fmt.Sprintf("%d errors occurred: %v", len(e.Errors), e.Errors[0])
}
func (e *ErrorList) Add(err error) {
if err != nil {
e.Errors = append(e.Errors, err)
}
}
func (e *ErrorList) Err() error {
if len(e.Errors) == 0 {
return nil
}
return e
}
// 使用例
func processMultipleFiles(files []string) error {
var errs ErrorList
for _, file := range files {
if err := processFile(file); err != nil {
errs.Add(fmt.Errorf("failed to process %s: %w", file, err))
}
}
return errs.Err()
}
3. コンテキスト付きエラー
type ContextError struct {
Err error
Context map[string]interface{}
}
func (e *ContextError) Error() string {
return fmt.Sprintf("%v (context: %v)", e.Err, e.Context)
}
func (e *ContextError) Unwrap() error {
return e.Err
}
func WithContext(err error, key string, value interface{}) error {
var ctxErr *ContextError
if errors.As(err, &ctxErr) {
// 既存のコンテキストに追加
ctxErr.Context[key] = value
return ctxErr
}
// 新しいコンテキストエラーを作成
return &ContextError{
Err: err,
Context: map[string]interface{}{
key: value,
},
}
}
// 使用例
func processUser(userID int) error {
user, err := fetchUser(userID)
if err != nil {
return WithContext(err, "userID", userID)
}
if err := validateUser(user); err != nil {
err = WithContext(err, "userID", userID)
err = WithContext(err, "username", user.Name)
return err
}
return nil
}
4. エラースタックトレース
type StackError struct {
Err error
Stack []uintptr
}
func (e *StackError) Error() string {
return e.Err.Error()
}
func (e *StackError) Unwrap() error {
return e.Err
}
func (e *StackError) StackTrace() string {
frames := runtime.CallersFrames(e.Stack)
var buf strings.Builder
for {
frame, more := frames.Next()
fmt.Fprintf(&buf, "%s\n\t%s:%d\n", frame.Function, frame.File, frame.Line)
if !more {
break
}
}
return buf.String()
}
func NewStackError(err error) error {
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(2, pcs[:])
return &StackError{
Err: err,
Stack: pcs[0:n],
}
}
// 使用例
func riskyOperation() error {
if err := doSomething(); err != nil {
return NewStackError(fmt.Errorf("risky operation failed: %w", err))
}
return nil
}
🔍 自己確認問題
問題1: error インターフェースのメモリサイズ
次のコードの出力を予測してください:
type MyError struct {
Code int
Message string
}
func (e *MyError) Error() string {
return e.Message
}
func main() {
var err error = &MyError{Code: 404, Message: "Not Found"}
fmt.Println(unsafe.Sizeof(err))
fmt.Println(unsafe.Sizeof(MyError{}))
}
解答
出力(64ビットシステム):
16
24
説明:
unsafe.Sizeof(err): error インターフェースは常に16バイト(tab + data ポインタ)unsafe.Sizeof(MyError{}): int(8) + string(16) = 24バイト
メモリレイアウト:
err (interface): 16バイト
┌────────────────┐
│ tab: 8バイト │
│ data: 8バイト │
└────────────────┘
MyError: 24バイト
┌────────────────────┐
│ Code: 8バイト │
│ Message: 16バイト │
│ ptr: 8バイト │
│ len: 8バイト │
└────────────────────┘
問題2: errors.Is の動作
次のコードの出力を予測してください:
var ErrNotFound = errors.New("not found")
func fetchData() error {
return fmt.Errorf("failed to fetch: %w",
fmt.Errorf("database error: %w", ErrNotFound))
}
func main() {
err := fetchData()
fmt.Println(err == ErrNotFound)
fmt.Println(errors.Is(err, ErrNotFound))
}
解答
出力:
false
true
説明:
err == ErrNotFound: false(err はラップされたエラー、直接比較では一致しない)errors.Is(err, ErrNotFound): true(エラーチェーンをたどって ErrNotFound を見つける)
エラーチェーン:
err:
wrapError{"failed to fetch: ..."}
↓ Unwrap
wrapError{"database error: ..."}
↓ Unwrap
ErrNotFound ← errors.Is がここまでたどる
問題3: errors.As の使用
次のコードを完成させて、エラーから os.PathError を取得してください:
func processFile(name string) error {
_, err := os.Open(name)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
return nil
}
func main() {
err := processFile("nonexistent.txt")
// ここを実装:PathError からファイル名と操作を取得
}
解答
func main() {
err := processFile("nonexistent.txt")
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("Operation: %s\n", pathErr.Op)
fmt.Printf("Path: %s\n", pathErr.Path)
fmt.Printf("Error: %v\n", pathErr.Err)
} else {
fmt.Println("Not a path error")
}
}
出力例:
Operation: open
Path: nonexistent.txt
Error: no such file or directory
メモリ操作:
処理前: pathErr = nil
errors.As 実行中:
1. エラーチェーンを走査
2. *os.PathError 型を発見
3. &pathErr に代入
処理後: pathErr → os.PathError インスタンス
問題4: panic と recover の動作
次のコードの出力を予測してください:
func example() {
defer fmt.Println("defer 1")
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered: %v\n", r)
}
}()
defer fmt.Println("defer 3")
panic("oops")
}
func main() {
example()
fmt.Println("After example")
}
解答
出力:
defer 3
Recovered: oops
defer 1
After example
実行順序:
1. panic("oops") 発生
2. defer が逆順に実行される:
a. defer 3 → "defer 3" 出力
b. defer recover() → panic を回復、"Recovered: oops" 出力
c. defer 1 → "defer 1" 出力
3. panic が回復したので、通常の実行フローに戻る
4. "After example" 出力
問題5: エラーラッピングの最適化
次の2つの実装のうち、どちらが速いですか?
// 実装1
func validate1(value int) error {
if value < 0 {
return errors.New("value must be positive")
}
return nil
}
// 実装2
var ErrNegative = errors.New("value must be positive")
func validate2(value int) error {
if value < 0 {
return ErrNegative
}
return nil
}
解答
validate2 の方が大幅に高速です。
理由:
validate1: 毎回errors.Newがヒープ割り当てを行う(~32バイト)validate2: 事前に確保されたエラーを返すだけ(ポインタコピーのみ)
ベンチマーク:
BenchmarkValidate1-8 10000000 150 ns/op 32 B/op 1 allocs/op
BenchmarkValidate2-8 100000000 15 ns/op 0 B/op 0 allocs/op
改善: 10倍高速、メモリ割り当てなし
問題6: カスタム Is メソッドの実装
次の要件を満たすカスタムエラー型を実装してください:
- すべてのインスタンスが同じセンチネルエラーと一致する
- エラーメッセージにタイムスタンプを含む
解答
type TimeoutError struct {
Timestamp time.Time
Operation string
}
func (e *TimeoutError) Error() string {
return fmt.Sprintf("timeout at %s during %s",
e.Timestamp.Format(time.RFC3339), e.Operation)
}
// カスタム Is メソッド
func (e *TimeoutError) Is(target error) bool {
_, ok := target.(*TimeoutError)
return ok
}
// センチネルエラー
var ErrTimeout = &TimeoutError{}
// 使用例
func doRequest() error {
return &TimeoutError{
Timestamp: time.Now(),
Operation: "HTTP request",
}
}
func main() {
err := doRequest()
// どの TimeoutError インスタンスも ErrTimeout と一致
if errors.Is(err, ErrTimeout) {
fmt.Println("Timeout occurred")
}
}
メモリ構造:
ErrTimeout (センチネル):
┌────────────────────────┐
│ TimeoutError: │
│ Timestamp: 時刻なし │
│ Operation: "" │
└────────────────────────┘
実際のエラー:
┌────────────────────────┐
│ TimeoutError: │
│ Timestamp: 2024-... │
│ Operation: "HTTP..." │
└────────────────────────┘
errors.Is でカスタム Is が呼ばれる
→ 型が TimeoutError なら true
問題7: エラーチェーンの可視化
次のコードのエラーチェーンを可視化する関数を実装してください:
func printErrorChain(err error) {
// ここを実装:エラーチェーンを階層的に出力
}
func main() {
err := fmt.Errorf("level 3: %w",
fmt.Errorf("level 2: %w",
errors.New("level 1: original error")))
printErrorChain(err)
}
期待する出力:
[0] level 3: level 2: level 1: original error
[1] level 2: level 1: original error
[2] level 1: original error
解答
func printErrorChain(err error) {
level := 0
for err != nil {
fmt.Printf("[%d] %v\n", level, err)
err = errors.Unwrap(err)
level++
}
}
// より詳細なバージョン
func printErrorChainDetailed(err error) {
level := 0
for err != nil {
indent := strings.Repeat(" ", level)
fmt.Printf("%s[%d] Type: %T\n", indent, level, err)
fmt.Printf("%s Message: %v\n", indent, err)
err = errors.Unwrap(err)
level++
}
}
// グラフィカルな出力
func printErrorChainGraphical(err error) {
level := 0
for err != nil {
if level > 0 {
fmt.Println(" ↓ Unwrap")
}
fmt.Printf("[Level %d] %T\n", level, err)
fmt.Printf("Message: %v\n", err)
err = errors.Unwrap(err)
level++
}
}
出力例:
[Level 0] *fmt.wrapError
Message: level 3: level 2: level 1: original error
↓ Unwrap
[Level 1] *fmt.wrapError
Message: level 2: level 1: original error
↓ Unwrap
[Level 2] *errors.errorString
Message: level 1: original error
問題8: カスタム As メソッドの実装
複数のエラーを保持する MultiError 型に As メソッドを実装してください:
type MultiError struct {
Errors []error
}
func (e *MultiError) Error() string {
return fmt.Sprintf("%d errors occurred", len(e.Errors))
}
// As メソッドを実装
解答
type MultiError struct {
Errors []error
}
func (e *MultiError) Error() string {
if len(e.Errors) == 0 {
return "no errors"
}
return fmt.Sprintf("%d errors occurred: %v", len(e.Errors), e.Errors[0])
}
// カスタム As メソッド
func (e *MultiError) As(target interface{}) bool {
// 各エラーに対して As を試行
for _, err := range e.Errors {
if errors.As(err, target) {
return true
}
}
return false
}
// Go 1.20+ の複数エラー対応
func (e *MultiError) Unwrap() []error {
return e.Errors
}
// 使用例
func processFiles(files []string) error {
var errs []error
for _, file := range files {
_, err := os.Open(file)
if err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return &MultiError{Errors: errs}
}
return nil
}
func main() {
err := processFiles([]string{"file1.txt", "file2.txt", "file3.txt"})
// 最初に見つかった PathError を取得
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("Found path error: %s\n", pathErr.Path)
}
// 複数のエラーをチェック
if errors.Is(err, fs.ErrNotExist) {
fmt.Println("Some files not found")
}
}
問題9: エラーハンドリングの設計
データベースアクセス層のエラーハンドリングを設計してください。要件は以下の通りです:
- 接続エラー
- クエリエラー
- レコードが見つからない
- 一意制約違反
- リトライ可能エラーの判定
解答
package database
import (
"errors"
"fmt"
"time"
)
// センチネルエラー
var (
ErrConnection = errors.New("database connection error")
ErrQuery = errors.New("query execution error")
ErrNotFound = errors.New("record not found")
ErrDuplicate = errors.New("duplicate key violation")
)
// 詳細なエラー型
type DBError struct {
Op string // 操作名
Table string // テーブル名
Err error // 元のエラー
}
func (e *DBError) Error() string {
return fmt.Sprintf("db error [%s on %s]: %v", e.Op, e.Table, e.Err)
}
func (e *DBError) Unwrap() error {
return e.Err
}
// リトライ可能判定
func (e *DBError) IsRetryable() bool {
return errors.Is(e.Err, ErrConnection)
}
// データベース層
type DB struct {
// ...
}
func (db *DB) FindUser(id int) (*User, error) {
user, err := db.query("SELECT * FROM users WHERE id = ?", id)
if err != nil {
// 接続エラー
if isConnectionError(err) {
return nil, &DBError{
Op: "find",
Table: "users",
Err: fmt.Errorf("%w: %v", ErrConnection, err),
}
}
// レコードが見つからない
if isNoRowsError(err) {
return nil, &DBError{
Op: "find",
Table: "users",
Err: ErrNotFound,
}
}
// その他のエラー
return nil, &DBError{
Op: "find",
Table: "users",
Err: fmt.Errorf("%w: %v", ErrQuery, err),
}
}
return user, nil
}
func (db *DB) CreateUser(user *User) error {
err := db.insert("INSERT INTO users ...", user)
if err != nil {
// 一意制約違反
if isDuplicateError(err) {
return &DBError{
Op: "create",
Table: "users",
Err: fmt.Errorf("%w: %v", ErrDuplicate, err),
}
}
return &DBError{
Op: "create",
Table: "users",
Err: fmt.Errorf("%w: %v", ErrQuery, err),
}
}
return nil
}
// アプリケーション層
func GetUserWithRetry(db *DB, id int, maxRetries int) (*User, error) {
var err error
for i := 0; i < maxRetries; i++ {
user, err := db.FindUser(id)
if err == nil {
return user, nil
}
// DBError にアサート
var dbErr *DBError
if errors.As(err, &dbErr) && dbErr.IsRetryable() {
time.Sleep(time.Second * time.Duration(i+1))
continue
}
// リトライ不可能
return nil, err
}
return nil, fmt.Errorf("max retries exceeded: %w", err)
}
// エラーハンドリング
func HandleError(err error) int {
if errors.Is(err, ErrNotFound) {
return 404
}
if errors.Is(err, ErrDuplicate) {
return 409
}
var dbErr *DBError
if errors.As(err, &dbErr) {
if dbErr.IsRetryable() {
return 503
}
}
return 500
}
設計のポイント:
- センチネルエラーで大分類
- カスタムエラー型で詳細情報
- Unwrap でエラーチェーン
- IsRetryable でリトライ判定
- 層ごとにエラーを変換
問題10: エラーのメモリ効率
大量のエラーを扱う場合の最適化を実装してください(1秒間に10000回呼ばれる関数):
func processRequest(data []byte) error {
if len(data) == 0 {
return errors.New("empty data") // ヒープ割り当て!
}
// 処理...
return nil
}
解答
最適化アプローチ:
// 方法1: センチネルエラー(最もシンプル)
var ErrEmptyData = errors.New("empty data")
func processRequestV1(data []byte) error {
if len(data) == 0 {
return ErrEmptyData // ポインタコピーのみ
}
return nil
}
// 方法2: エラーコード(型安全)
type ErrorCode int
const (
ErrNone ErrorCode = iota
ErrEmptyData
ErrInvalidFormat
ErrTooLarge
)
func (e ErrorCode) Error() string {
switch e {
case ErrEmptyData:
return "empty data"
case ErrInvalidFormat:
return "invalid format"
case ErrTooLarge:
return "data too large"
default:
return "unknown error"
}
}
func processRequestV2(data []byte) error {
if len(data) == 0 {
return ErrEmptyData // int の変換のみ
}
return nil
}
// 方法3: エラープール(詳細情報が必要な場合)
var errorPool = sync.Pool{
New: func() interface{} {
return &DetailedError{}
},
}
type DetailedError struct {
Code ErrorCode
Message string
Details map[string]interface{}
}
func (e *DetailedError) Error() string {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Details)
}
func (e *DetailedError) Reset() {
e.Code = ErrNone
e.Message = ""
e.Details = nil
}
func NewDetailedError(code ErrorCode, msg string) *DetailedError {
err := errorPool.Get().(*DetailedError)
err.Code = code
err.Message = msg
return err
}
func ReleaseDetailedError(err *DetailedError) {
err.Reset()
errorPool.Put(err)
}
func processRequestV3(data []byte) error {
if len(data) == 0 {
return NewDetailedError(ErrEmptyData, "received empty request")
}
return nil
}
// 使用例
func handler(w http.ResponseWriter, r *http.Request) {
data, _ := io.ReadAll(r.Body)
err := processRequestV3(data)
if err != nil {
handleError(w, err)
// プールに返却
if detailedErr, ok := err.(*DetailedError); ok {
ReleaseDetailedError(detailedErr)
}
return
}
}
ベンチマーク比較:
BenchmarkOriginal-8 5000000 250 ns/op 32 B/op 1 allocs/op
BenchmarkV1Sentinel-8 50000000 25 ns/op 0 B/op 0 allocs/op
BenchmarkV2ErrorCode-8 50000000 25 ns/op 0 B/op 0 allocs/op
BenchmarkV3Pool-8 20000000 60 ns/op 0 B/op 0 allocs/op
選択基準:
- 単純なエラー → センチネルエラー(V1)
- エラーコードで十分 → ErrorCode(V2)
- 詳細情報が必要 → エラープール(V3)
- 複雑なコンテキスト → カスタムエラー型(ヒープ割り当て許容)
メモリ使用量の比較:
元の実装:
- 10000 req/sec × 32 bytes = 320 KB/sec
- 1分間で ~19 MB のGC圧力
センチネルエラー:
- 0 bytes/sec
- GC圧力なし
改善: 100% メモリ削減
まとめ
この章では、Goのエラー処理をマシンレベルで深く理解しました。
🔑 重要ポイント:
- error インターフェース:
- errors.New と fmt.Errorf:
- エラーラッピング:
- errors.Is:
- errors.As:
- panic/recover:
💡 パフォーマンスのヒント:
- センチネルエラーを活用してヒープ割り当てを削減(10倍高速)
- 必要な箇所でのみエラーをラップ(75%メモリ削減)
- エラーチェックを最適化(型アサーション活用で3倍高速)
- 大量のエラーを扱う場合はエラープールを検討
- エラー文字列の遅延生成(複数回呼び出しで10倍高速)
⚠️ よくある落とし穴:
- nil インターフェースと nil ポインタの混同
==比較の代わりにerrors.Isを使う- 型アサーションの代わりに
errors.Asを使う - 不必要な多段階ラッピング
- エラー文字列の大文字開始(小文字で始める)
- panic を通常のエラー処理に使用する
ベストプラクティス:
- エラーは早期に処理する(early return)
- 適切なコンテキストを追加する
- センチネルエラーとカスタムエラーを使い分ける
- エラーをラップして情報を保持する(%w 使用)
errors.Isとerrors.Asを活用する- panic は本当の例外時のみ使用する(回復不可能な状況)
これでGo Foundationsコースのエラー処理は完了です!