概要
- 前回の続き
- 今回はエラー処理について学習
- 例外の構文がないため、エラーを検知する特別な仕組みはない(java等のcatch句)
- Goを学ぶ前に聞いたことがあり、唯一不便そうだなと思った。実際どうなのだろうか。
参考
エラー処理の基本
- 関数を呼び出した際、戻り値としてerrorを返す。
- errorには、正常終了した際はnilを、問題が発生したら値を返す
- 呼び出し側で、nilをチェックすることで、エラーをハンドリングする
- Java等のように、例外をスローするのではなく、エラーを返すようになっている
- Goでは、例外のような余分なコードパスを増やすことを良しとしないから。
- また、Goは宣言した変数は使用しないといけない。といった制約上の理由もあるから。
エラー処理
func doubleEven(i int) (int, error) {
if i % 2 != 0 {
// 文字列を返す場合は、errors.NewでOK
// return 0, errors.New("偶数ではありません")
// fmt.Errorfでフォーマットも使用できる
return 0, fmt.Errorf("%dは偶数ではありません", i)
}
return i * 2, nil
}
func main() {
i := 17
double, err := doubleEven(i)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Printf("%dの2倍は %d\n", i, double)
}
センチネルエラー
- プログラムがエラーを判別するための値(センチネル値)を使用するエラー
- 処理を開始、継続できないときに発生させる
- Goでは、一般的ではないが、一部で存在する(スライス範囲外のインデックスアクセス時など)
- パッケージレベルで宣言されている変数で、読み取り専用で扱うルール(変更はできてしまうがNG)
- 「==」を使用してチェックする
センチネルエラー
...
if err == zip.ErrFormat {
fmt.println("zip形式ではありません")
}
独自のエラー定義
- errorはインターフェースなので、文字列以外の情報以外も定義できる
- 独自のエラー型を使う場合は初期化していないインスタンスを返してはいけない。
- インターフェースの初期値はnilでないから。エラーを設定していな場合でも、エラーと判定されてしまう。
独自エラー型
// ステータスの独自列挙型(iota)
type Status int
const (
InvalidLogin Status = iota + 1
NotFound
)
// 独自のエラー型定義
type StatusErr struct {
Status Status
Message string
}
func (se StatusErr) Error() string {
return se.Message
}
func GenerateError(flag bool) error {
if flag {
return StatusErr{
Status: NotFound,
}
}
return nil
}
エラーのラップ
- 返されたエラーに対して、付加情報を設定したエラーを定義することをエラーのラップと呼ぶ。
-
fmt.Errorf
の末尾に%w
を付与することで、前のエラーをラップする - ラップされたエラーを表示するためには、
Unwrap
を使用する - カスタマイズしたエラーの場合は、上記の
Unwrap
も実装する必要がある
wrap
type StatusErr struct {
Status Status
Message string
Err error // ラップするエラー
}
func (se StatusErr) Error() string {
return se.Message
}
// アンラップを追加
func (se StatusErr) Unwrap() error {
return se.Err
}
IsとAs
- 戻されたエラーがラップしたエラーや、センチネルエラーの場合は
==
による比較ができないので、errors.Is
を使用する
errors.Is
func fileChecker (name string) error{
f,err := os.Open(name)
if err != nil {
return fmt.Errorf("in fileChekcer: %w", err);
}
f.Close()
return nil
}
func main() {
err := fileChecker("notfound.txt")
if err != nil {
// 対象のerrorと比較するインスタンスを指定する
if errors.Is(err, os.ErrNotExist) {
fmt.Println("not exisit")
}
}
}
- 戻されたエラーが特定の型にマッチするかをチェックする場合は、Asを使用する。
- 対象のエラーと比較する型の変数ポインタを指定する
As
err := TestAsError()
var myErr, MyErr
if errors.As(err, &myErr) {
fmt.Println(myErr.code)
}
パニックとリカバー
- メモリ不足など、Goのランタイムが処理を行えないとき、パニックが生成される
- パニックが起こると、実行中の関数は即座に終了し、deferを実行し、呼び出し元に返す(呼び出し元も同様の処理)
- パニックを補足する方法としてリカバーがある。defer内で呼び出すことで処理を継続できる
- Javaなどの例外(try-catch)と似ているが、危機的な状況のみでパニックは発生するので、安易にリカバーを使用し処理継続すべきではない。
panic&recover
func myDiv(i int) {
// defer内でリカバー処理を記述する
defer func() {
if v := recover(); v != nil {
fmt.Println("処理継続", v)
}
}()
fmt.Println(60 / i)
}
func main() {
for _, val := range []int{1,2,0,4} {
// 0除算でパニックとなるがリカバーするため、処理が継続される。
//60
//30
//処理継続 runtime error: integer divide by zero
//15
myDiv(val)
}
}
エラー時のスタックトレース
- デフォルトでGoは、エラー時のスタックトレースを表示しない。
- パニック&リカバーは表示される。
- そのため、手動でエラーをラップしてコールスタックを積み上げることが必要
- サードパーティライブラリを入れるか、
fmt.Printf
で%*v
を指定することでも表示できる
おわりに
- Goには例外の特別な仕組みはない。シンプルにerrorを返し、エラーの有無をチェックする。
- Javaなどの例外方式(スロー宣言、try-catch)に慣れてしまっているので、正直使いづらさを感じてしまう。(通常の戻り値判定とエラー判定を混同してしまう等)
- パニック、リカバーの形式がJavaなどの例外処理と似ており、スタックトレースも出るため、こちらの方が使いやすさを感じてしまった。
- Goの開発を行う上で、一番違和感を感じる箇所かもしれない。