Goでのパニックの処理
序章
プログラムで発生するエラーは、プログラマーが予期したエラーと予期しなかったエラーの2つの大きなカテゴリに分類されます。 エラー処理に関する以前の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プログラムの実行を完全に停止するイベントであるためです。 生成されるメッセージには、パニックの原因を診断するのに役立つ複数の情報が含まれています。
パニックの解剖学
パニックは、パニックの原因を示すメッセージと、コード内のどこでパニックが発生したかを特定するのに役立つスタックトレースで構成されます。
パニックの最初の部分はメッセージです。 常に文字列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
前の例のこのスタックトレースは、プログラムがファイル/tmp/sandbox879828148/prog.go
の行番号13からパニックを生成したことを示しています。 また、このパニックは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
に再割り当てします。 最後に、変数s
でSayHello
メソッドを呼び出そうとします。 サミーから友好的なメッセージを受け取る代わりに、無効なメモリアドレスにアクセスしようとしたというパニックを受け取ります。 s
変数はnil
であるため、SayHello
関数が呼び出されると、*Shark
タイプのフィールドName
にアクセスしようとします。 。 これはポインターレシーバーであり、この場合のレシーバーはnil
であるため、nil
ポインターを逆参照できないため、パニックになります。
この例では、s
をnil
に明示的に設定していますが、実際には、これはそれほど明白ではありません。 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を使用すると、呼び出し元の関数の実行が完了するまで、関数呼び出しの実行を延期できます。 遅延関数は、パニックが存在する場合でも実行され、パニックの混沌とした性質を防ぐための安全メカニズムとして使用されます。 関数は、通常どおりに呼び出してから、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
関数内で、最初にdefer
メッセージ"hello from the deferred function!"
を出力する無名関数の呼び出しを行います。 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
を呼び出します。 この関数内で、defer
は、divideByZero
の実行中に発生する可能性のあるパニックの処理を担当する無名関数の呼び出しです。 この延期された無名関数内で、recover
組み込み関数を呼び出し、それが返すエラーを変数に割り当てます。 divideByZero
がパニックになっている場合は、このerror
の値が設定されます。それ以外の場合は、nil
になります。 err
変数をnil
と比較することで、パニックが発生したかどうかを検出できます。この場合、他の場合と同様に、log.Println
関数を使用してパニックをログに記録します。 error
。
この延期された無名関数に続いて、定義した別の関数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
関数は、除数b
が0
と等しいかどうかをチェックするように変更されました。 そうである場合、nil
の引数を持つpanic
ビルトインを使用してパニックを生成します。 今回の出力には、divide
によって作成されたにもかかわらず、パニックが発生したことを示すログメッセージは含まれていません。 このサイレントな動作が、panic
組み込み関数への引数がnil
でないことを確認することが非常に重要である理由です。
結論
Goでpanic
を作成する方法と、recover
ビルトインを使用してそれらを回復する方法をいくつか見てきました。 必ずしも自分でpanic
を使用する必要はありませんが、パニックから適切に回復することは、Goアプリケーションを本番環境に対応させるための重要なステップです。
Goシリーズのコーディング方法シリーズ全体を調べることもできます。