前書き
プログラムで発生するエラーは、プログラマが予期したものとそうでないものの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
に再割り当てします。 最後に、変数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では、呼び出し元の関数の実行が完了するまで、関数呼び出しの実行を延期できます。 遅延機能は、パニックが発生している場合でも実行され、パニックの混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
関数は、その除数b
が0
と等しいかどうかをチェックするように変更されました。 そうである場合、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を探索することもできます。