Goでのパニックの処理

前書き

プログラムで発生するエラーは、プログラマが予期したものとそうでないものの2つの大きなカテゴリに分類されます。 error handlingに関する以前の2つの記事で取り上げたerrorインターフェースは、Goプログラムを作成するときに予想されるエラーを主に処理します。 errorインターフェースでは、関数呼び出しからエラーが発生するというまれな可能性を認識できるため、そのような状況で適切に対応できます。

パニックはエラーの2番目のカテゴリに分類されますが、これはプログラマが予期しないものです。 これらの予期しないエラーにより、プログラムは自発的に終了し、実行中のGoプログラムを終了します。 よくある間違いは、多くの場合、パニックの原因となります。 このチュートリアルでは、一般的な操作によってGoでパニックが発生する可能性があるいくつかの方法を調べ、それらのパニックを回避する方法についても説明します。 また、deferステートメントとrecover関数を使用して、実行中のGoプログラムを予期せず終了する前にパニックをキャプチャします。

パニックを理解する

Goには、自動的にパニックを返し、プログラムを停止する特定の操作があります。 一般的な操作には、容量を超えてarrayにインデックスを付ける、型アサーションを実行する、nilポインターでメソッドを呼び出す、ミューテックスを誤って使用する、閉じたチャネルを操作するなどがあります。 これらの状況のほとんどは、プログラムのコンパイル中にコンパイラが検出できない能力があるというプログラミング中のミスに起因します。

パニックには問題の解決に役立つ詳細が含まれているため、開発者は通常、プログラムの開発中に間違いを犯したことを示すためにパニックを使用します。

アウトオブバウンズパニック

スライスの長さまたはアレイの容量を超えてインデックスにアクセスしようとすると、Goランタイムはパニックを生成します。

次の例では、lenビルトインによって返されるスライスの長さを使用して、スライスの最後の要素にアクセスしようとする一般的な間違いを犯しています。 このコードを実行して、パニックが発生する理由を確認してください。

package main

import (
    "fmt"
)

func main() {
    names := []string{
        "lobster",
        "sea urchin",
        "sea cucumber",
    }
    fmt.Println("My favorite sea creature is:", names[len(names)])
}

これにより、次の出力が得られます。

Outputpanic: runtime error: index out of range [3] with length 3

goroutine 1 [running]:
main.main()
    /tmp/sandbox879828148/prog.go:13 +0x20

パニックの出力の名前はヒントを提供します:panic: runtime error: index out of range。 3つの海の生き物でスライスを作成しました。 次に、len組み込み関数を使用して、スライスの長さでそのスライスにインデックスを付けることにより、スライスの最後の要素を取得しようとしました。 スライスと配列はゼロベースであることを忘れないでください。したがって、最初の要素はゼロであり、このスライスの最後の要素はインデックス2にあります。 3番目のインデックス3でスライスにアクセスしようとしているため、スライスの境界を超えているため、スライスに返す要素はありません。 ランタイムには、不可能なことを行うように要求したため、終了して終了する以外のオプションはありません。 また、Goはコンパイル中にこのコードがこれを実行しようとすることを証明できないため、コンパイラはこれをキャッチできません。

また、後続のコードが実行されなかったことにも注意してください。 これは、パニックがGoプログラムの実行を完全に停止するイベントだからです。 生成されるメッセージには、パニックの原因の診断に役立つ複数の情報が含まれています。

パニックの解剖学

パニックは、パニックの原因を示すメッセージと、コード内のどこでパニックが発生したかを特定するのに役立つstack traceで構成されます。

パニックの最初の部分はメッセージです。 常に文字列panic:で始まり、その後にパニックの原因に応じて変化する文字列が続きます。 前の演習のパニックには次のメッセージがあります。

panic: runtime error: index out of range [3] with length 3

panic:プレフィックスに続く文字列runtime error:は、パニックが言語ランタイムによって生成されたことを示しています。 このパニックは、スライスの長さ3の範囲外のインデックス[3]を使用しようとしたことを示しています。

このメッセージに続くのはスタックトレースです。 スタックトレースは、パニックが生成されたときに実行されていたコード行と、そのコードが以前のコードによってどのように呼び出されたかを正確に特定するためのマップを形成します。

goroutine 1 [running]:
main.main()
    /tmp/sandbox879828148/prog.go:13 +0x20

前の例のこのスタックトレースは、プログラムが行番号13のファイル/tmp/sandbox879828148/prog.goからパニックを生成したことを示しています。 また、このパニックはmainパッケージのmain()関数で発生したこともわかります。

スタックトレースは、プログラム内のgoroutineごとに1つずつ、別々のブロックに分割されます。 すべてのGoプログラムの実行は、Goコードの一部をそれぞれ独立して同時に実行できる1つ以上のゴルーチンによって実現されます。 各ブロックはヘッダーgoroutine X [state]:で始まります。 ヘッダーには、ゴルーチンのID番号と、パニックが発生したときの状態が示されます。 スタックトレースには、ヘッダーの後に、パニックが発生したときにプログラムが実行していた関数と、関数が実行されたファイル名と行番号が表示されます。

前の例のパニックは、スライスへの範囲外アクセスによって生成されました。 パニックは、設定されていないポインターでメソッドが呼び出されたときにも生成されます。

無受信機

Goプログラミング言語には、実行時にコンピューターのメモリに存在する特定のタイプのインスタンスを参照するポインターがあります。 ポインターは、何も指していないことを示す値nilを想定できます。 nilであるポインターでメソッドを呼び出そうとすると、Goランタイムはパニックを生成します。 同様に、インターフェイス型の変数も、メソッドが呼び出されるとパニックを引き起こします。 これらの場合に生成されるパニックを確認するには、次の例を試してください。

package main

import (
    "fmt"
)

type Shark struct {
    Name string
}

func (s *Shark) SayHello() {
    fmt.Println("Hi! My name is", s.Name)
}

func main() {
    s := &Shark{"Sammy"}
    s = nil
    s.SayHello()
}

生成されるパニックは次のようになります。

Outputpanic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xdfeba]

goroutine 1 [running]:
main.(*Shark).SayHello(...)
    /tmp/sandbox160713813/prog.go:12
main.main()
    /tmp/sandbox160713813/prog.go:18 +0x1a

この例では、Sharkという構造体を定義しました。 Sharkには、ポインターレシーバーで定義されたSayHelloという1つのメソッドがあり、呼び出されたときに標準出力にグリーティングを出力します。 main関数の本体内で、このShark構造体の新しいインスタンスを作成し、&演算子を使用してそのインスタンスへのポインターを要求します。 このポインタはs変数に割り当てられます。 次に、ステートメントs = nilを使用して、s変数を値nilに再割り当てします。 最後に、変数sSayHelloメソッドを呼び出そうとします。 サミーからフレンドリーメッセージを受信する代わりに、無効なメモリアドレスにアクセスしようとしたというパニックを受信します。 s変数はnilであるため、SayHello関数が呼び出されると、*SharkタイプのフィールドNameにアクセスしようとします。 これはポインターレシーバーであり、この場合のレシーバーはnilであるため、nilポインターを逆参照できないため、パニックになります。

この例では、snilに明示的に設定していますが、実際には、これはそれほど明白ではありません。 nil pointer dereferenceに関連するパニックが発生した場合は、作成した可能性のあるポインター変数が適切に割り当てられていることを確認してください。

nilポインターと範囲外アクセスから生成されるパニックは、ランタイムによって生成される2つの一般的なパニックです。 組み込み関数を使用して、手動でパニックを生成することもできます。

panic組み込み関数の使用

panic組み込み関数を使用して、独自のパニックを生成することもできます。 パニックが生成するメッセージである引数として単一の文字列を取ります。 通常、このメッセージは、エラーを返すようにコードを書き換えるよりも冗長です。 さらに、これを独自のパッケージ内で使用して、パッケージのコードを使用するときに間違いを犯した可能性があることを開発者に示すことができます。 可能な限り、ベストプラクティスは、パッケージのコンシューマーにerror値を返そうとすることです。

このコードを実行して、別の関数から呼び出された関数から生成されたパニックを確認します。

package main

func main() {
    foo()
}

func foo() {
    panic("oh no!")
}

生成されるパニック出力は次のようになります。

Outputpanic: oh no!

goroutine 1 [running]:
main.foo(...)
    /tmp/sandbox494710869/prog.go:8
main.main()
    /tmp/sandbox494710869/prog.go:4 +0x40

ここでは、文字列"oh no!"で組み込みのpanicを呼び出す関数fooを定義します。 この関数は、main関数によって呼び出されます。 出力にメッセージpanic: oh no!があり、スタックトレースに、スタックトレースに2行の単一のゴルーチンが表示されていることに注目してください。1つはmain()関数用、もう1つはfoo()関数用です。

パニックが発生すると、プログラムが終了するように見えることがわかりました。 これにより、適切に閉じる必要がある開いているリソースがある場合に問題が発生する可能性があります。 Goは、パニックが発生している場合でも、一部のコードを常に実行するメカニズムを提供します。

遅延機能

プログラムには、ランタイムによってパニックが処理されている間でも、適切にクリーンアップする必要があるリソースがあります。 Goでは、呼び出し元の関数の実行が完了するまで、関数呼び出しの実行を延期できます。 遅延機能は、パニックが発生している場合でも実行され、パニックの混againstとした性質から保護するための安全メカニズムとして使用されます。 関数は、通常どおりに呼び出してから、defer sayHello()のように、ステートメント全体の前にdeferキーワードを付けることで延期されます。 この例を実行して、パニックが発生した場合でもメッセージがどのように印刷されるかを確認します。

package main

import "fmt"

func main() {
    defer func() {
        fmt.Println("hello from the deferred function!")
    }()

    panic("oh no!")
}

この例から生成される出力は次のようになります。

Outputhello from the deferred function!
panic: oh no!

goroutine 1 [running]:
main.main()
    /Users/gopherguides/learn/src/github.com/gopherguides/learn//handle-panics/src/main.go:10 +0x55

この例のmain関数内で、最初にメッセージ"hello from the deferred function!"を出力する無名関数の呼び出しをdeferします。 main関数は、panic関数を使用してすぐにパニックを引き起こします。 このプログラムの出力では、遅延関数が実行され、そのメッセージが出力されることが最初にわかります。 これに続いて、mainで生成されたパニックが発生します。

遅延機能は、パニックの驚くべき性質に対する保護を提供します。 遅延関数内で、Goは別の組み込み関数を使用してGoプログラムを終了するパニックを停止する機会も提供します。

パニックの処理

パニックには、recoverの組み込み関数という単一の回復メカニズムがあります。 この関数を使用すると、コールスタックの途中でパニックをインターセプトし、プログラムが予期せず終了するのを防ぐことができます。 使用には厳格なルールがありますが、本番アプリケーションでは非常に貴重です。

これはbuiltinパッケージの一部であるため、追加のパッケージをインポートせずにrecoverを呼び出すことができます。

package main

import (
    "fmt"
    "log"
)

func main() {
    divideByZero()
    fmt.Println("we survived dividing by zero!")

}

func divideByZero() {
    defer func() {
        if err := recover(); err != nil {
            log.Println("panic occurred:", err)
        }
    }()
    fmt.Println(divide(1, 0))
}

func divide(a, b int) int {
    return a / b
}

この例は出力します:

Output2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero
we survived dividing by zero!

この例のmain関数は、定義した関数divideByZeroを呼び出します。 この関数内で、divideByZeroの実行中に発生する可能性のあるパニックの処理を担当する無名関数への呼び出しをdeferします。 この据え置き無名関数内で、recover組み込み関数を呼び出し、それが返すエラーを変数に割り当てます。 divideByZeroがパニックになっている場合は、このerror値が設定されます。それ以外の場合は、nilになります。 err変数をnilと比較することで、パニックが発生したかどうかを検出できます。この場合、他のerrorと同様に、log.Println関数を使用してパニックをログに記録します。 )s。

この据え置き匿名関数に続いて、定義した別の関数divideを呼び出し、fmt.Printlnを使用してその結果を出力しようとします。 指定された引数により、divideはゼロによる除算を実行し、パニックが発生します。

この例の出力では、最初にパニックを回復する無名関数からのログメッセージが表示され、次にメッセージwe survived dividing by zero!が表示されます。 recoverの組み込み関数が、Goプログラムを終了させる壊滅的なパニックを阻止したおかげで、実際にこれを実行しました。

recover()から返されるerr値は、panic()の呼び出しに提供された値とまったく同じです。 したがって、パニックが発生していない場合にのみ、err値がゼロになるようにすることが重要です。

recoverによるパニックの検出

recover関数は、エラーの値に依存して、パニックが発生したかどうかを判断します。 panic関数の引数は空のインターフェイスであるため、任意のタイプにすることができます。 空のインターフェイスを含むすべてのインターフェイスタイプのゼロ値はnilです。 この例で示されているように、panicへの引数としてnilを回避するように注意する必要があります。

package main

import (
    "fmt"
    "log"
)

func main() {
    divideByZero()
    fmt.Println("we survived dividing by zero!")

}

func divideByZero() {
    defer func() {
        if err := recover(); err != nil {
            log.Println("panic occurred:", err)
        }
    }()
    fmt.Println(divide(1, 0))
}

func divide(a, b int) int {
    if b == 0 {
        panic(nil)
    }
    return a / b
}

これは出力されます:

Outputwe survived dividing by zero!

この例は、recoverを含む前の例と同じですが、若干の変更が加えられています。 divide関数は、その除数b0と等しいかどうかをチェックするように変更されました。 そうである場合、nilの引数でpanic組み込みを使用してパニックを生成します。 今回の出力には、divideによって作成されたにもかかわらず、パニックが発生したことを示すログメッセージは含まれていません。 このサイレントな動作が、panic組み込み関数への引数がnilではないことを確認することが非常に重要である理由です。

結論

panic+`s can be created in Go and how they can be recovered from using the `+recoverが組み込まれているいくつかの方法を見てきました。 必ずしも自分でpanicを使用するとは限りませんが、パニックから適切に回復することは、Goアプリケーションを本番環境に対応させるための重要なステップです。

our entire How To Code in Go seriesを探索することもできます。