序章

プログラムで発生するエラーは、プログラマーが予期したエラーと予期しなかったエラーの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)])
}

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

Output
panic: 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()
}

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

Output
panic: 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!")
}

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

Output
panic: 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!")
}

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

Output
hello 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
}

この例では、次のように出力されます。

Output
2009/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
}

これは出力します:

Output
we survived dividing by zero!

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

結論

Goでpanicを作成する方法と、recoverビルトインを使用してそれらを回復する方法をいくつか見てきました。 必ずしも自分でpanicを使用する必要はありませんが、パニックから適切に回復することは、Goアプリケーションを本番環境に対応させるための重要なステップです。

Goシリーズのコーディング方法シリーズ全体を調べることもできます。