Goでエラーを処理する

堅牢なコードは、不適切なユーザー入力、ネットワーク接続の障害、ディスクの障害などの予期しない状況に正しく対応する必要があります。 Error handlingは、プログラムが予期しない状態にあることを識別し、後でデバッグするために診断情報を記録する手順を実行するプロセスです。

開発者が特殊な構文でエラーを処理する必要がある他の言語とは異なり、Goのエラーは、他の値と同様に関数から返されるタイプerrorの値です。 Goでエラーを処理するには、関数が返す可能性のあるこれらのエラーを調べ、エラーが発生したかどうかを判断し、データを保護し、エラーが発生したことをユーザーまたはオペレーターに伝える適切なアクションを実行する必要があります。

エラーを作成する

エラーを処理する前に、まずいくつかを作成する必要があります。 標準ライブラリには、エラーを作成するための2つの組み込み関数errors.Newfmt.Errorfが用意されています。 これらの関数の両方を使用すると、後でユーザーに表示できるカスタムエラーメッセージを指定できます。

errors.Newは、単一の引数を取ります。これは、ユーザーに何が悪かったのかを警告するためにカスタマイズできる文字列としてのエラーメッセージです。

次の例を実行して、標準出力に出力されるerrors.Newによって作成されたエラーを確認してください。

package main

import (
    "errors"
    "fmt"
)

func main() {
    err := errors.New("barnacles")
    fmt.Println("Sammy says:", err)
}
OutputSammy says: barnacles

標準ライブラリのerrors.New関数を使用して、文字列"barnacles"をエラーメッセージとして持つ新しいエラーメッセージを作成しました。 ここでは、Go Programming Language Style Guideが示すように、エラーメッセージに小文字を使用するという規則に従いました。

最後に、fmt.Println関数を使用して、エラーメッセージを"Sammy says:"と組み合わせました。

fmt.Errorf関数を使用すると、エラーメッセージを動的に作成できます。 最初の引数は、文字列の場合は%s、整数の場合は%dなどのプレースホルダー値を持つエラーメッセージを含む文字列です。 fmt.Errorfは、このフォーマット文字列に続く引数を次の順序でこれらのプレースホルダーに補間します。

package main

import (
    "fmt"
    "time"
)

func main() {
    err := fmt.Errorf("error occurred at: %v", time.Now())
    fmt.Println("An error happened:", err)
}
OutputAn error happened: Error occurred at: 2019-07-11 16:52:42.532621 -0400 EDT m=+0.000137103

fmt.Errorf関数を使用して、現在の時刻を含むエラーメッセージを作成しました。 fmt.Errorfに提供したフォーマット文字列には、フォーマット文字列の後に指定された最初の引数にデフォルトのフォーマットを使用するようにfmt.Errorfに指示する%vフォーマットディレクティブが含まれています。 その引数は、標準ライブラリのtime.Now関数によって提供される現在の時刻になります。 前の例と同様に、エラーメッセージを短いプレフィックスと組み合わせ、fmt.Println関数を使用して結果を標準出力に出力します。

エラー処理

通常、このように作成されたエラーは、前の例のように、他の目的ですぐに使用されることはありません。 実際には、エラーを作成し、何か問題が発生したときに関数からエラーを返すのがはるかに一般的です。 次に、その関数の呼び出し元は、ifステートメントを使用して、エラーが存在するかどうか、またはnil(初期化されていない値)を確認します。

次の例には、常にエラーを返す関数が含まれています。 プログラムを実行すると、今回は関数がエラーを返していても、前の例と同じ出力が生成されることに注意してください。 別の場所でエラーを宣言しても、エラーのメッセージは変わりません。

package main

import (
    "errors"
    "fmt"
)

func boom() error {
    return errors.New("barnacles")
}

func main() {
    err := boom()

    if err != nil {
        fmt.Println("An error occurred:", err)
        return
    }
    fmt.Println("Anchors away!")
}
OutputAn error occurred: barnacles

ここでは、errors.Newを使用して作成した単一のerrorを返すboom()という関数を定義します。 次に、この関数を呼び出し、err := boom()
という行でエラーをキャプチャします。このエラーを割り当てたら、if err != nil条件付きで存在するかどうかを確認します。 ここでは、常にboom()からerrorを返すため、条件は常にtrueに評価されます。

これは常に当てはまるとは限らないため、エラーが存在しない場合(nil)とエラーが存在する場合をロジック処理することをお勧めします。 エラーが存在する場合、前の例で行ったように、fmt.Printlnを使用してプレフィックスとともにエラーを出力します。 最後に、returnステートメントを使用してfmt.Println("Anchors away!")の実行をスキップします。これは、エラーが発生しなかった場合にのみ実行されるためです。

[.note]#Note:最後の例に示されているif err != nil構造は、Goプログラミング言語でのエラー処理の主力製品です。 関数がエラーを生成する可能性がある場合は常に、ifステートメントを使用してエラーが発生したかどうかを確認することが重要です。 このように、慣用的なGoコードでは、当然、最初のインデントレベルに“happy path”ロジックがあり、2番目のインデントレベルにすべての「悲しいパス」ロジックがあります。

ifステートメントにオプションの代入句があり、関数の呼び出しとそのエラーの処理を簡略化するために使用できます。

次のプログラムを実行して、前の例と同じ出力を確認しますが、今回は複合ifステートメントを使用して定型文を減らします。

package main

import (
    "errors"
    "fmt"
)

func boom() error {
    return errors.New("barnacles")
}

func main() {
    if err := boom(); err != nil {
        fmt.Println("An error occurred:", err)
        return
    }
    fmt.Println("Anchors away!")
}
OutputAn error occurred: barnacles

前と同じように、常にエラーを返す関数boom()があります。 boom()から返されたエラーをifステートメントの最初の部分としてerrに割り当てます。 ifステートメントの2番目の部分では、セミコロンに続いて、そのerr変数が使用可能になります。 エラーが存在するかどうかを確認し、以前に行ったように短いプレフィックス文字列でエラーを出力します。

このセクションでは、エラーのみを返す関数を処理する方法を学びました。 これらの関数は一般的ですが、複数の値を返す可能性のある関数からのエラーを処理できることも重要です。

値とともにエラーを返す

単一のエラー値を返す関数は、多くの場合、データベースへの行の挿入など、何らかのステートフルな変更をもたらす関数です。 また、関数が正常に完了した場合に値を返し、その関数が失敗した場合に潜在的なエラーを返す関数を記述することも一般的です。 Goは、関数が複数の結果を返すことを許可します。これを使用して、値とエラータイプを同時に返すことができます。

複数の値を返す関数を作成するには、関数のシグネチャの括弧内に、返された各値のタイプをリストします。 たとえば、stringerrorを返すcapitalize関数は、func capitalize(name string) (string, error) {}を使用して宣言されます。 (string, error)の部分は、この関数がstringerrorをこの順序で返すことをGoコンパイラーに通知します。

次のプログラムを実行して、stringerrorの両方を返す関数からの出力を確認します。

package main

import (
    "errors"
    "fmt"
    "strings"
)

func capitalize(name string) (string, error) {
    if name == "" {
        return "", errors.New("no name provided")
    }
    return strings.ToTitle(name), nil
}

func main() {
    name, err := capitalize("sammy")
    if err != nil {
        fmt.Println("Could not capitalize:", err)
        return
    }

    fmt.Println("Capitalized name:", name)
}
OutputCapitalized name: SAMMY

capitalize()は、文字列(大文字にする名前)を受け取り、文字列とエラー値を返す関数として定義します。 main()では、capitalize()を呼び出し、関数から返された2つの値をname変数とerr変数に、左側のコンマで区切って割り当てます。 :=演算子。 この後、前の例のようにif err != nilチェックを実行し、エラーが存在する場合はfmt.Printlnを使用してエラーを標準出力に出力します。 エラーがなかった場合は、Capitalized name: SAMMYを出力します。

name, err := capitalize("sammy")の文字列"sammy"を空の文字列("")に変更してみてください。代わりに、エラーCould not capitalize: no name providedが表示されます。

関数の呼び出し元がnameパラメーターに空の文字列を指定すると、capitalize関数はエラーを返します。 nameパラメータが空の文字列でない場合、capitalize()strings.ToTitleを使用してnameパラメータを大文字にし、エラー値としてnilを返します。

この例に続く、Goコードに特有の、Goコンパイラーによって強制されない、いくつかの微妙な規則があります。 関数がエラーを含む複数の値を返す場合、規則では、最後の項目としてerrorを返すように要求されます。 複数の戻り値を持つ関数からerrorを返す場合、慣用的なGoコードも各非エラー値をzero valueに設定します。 ゼロ値は、たとえば、文字列の場合は空の文字列、整数の場合は0、構造体タイプの場合は空の構造体、インターフェイスとポインタのタイプの場合はnilです。 ゼロ値については、tutorial on variables and constantsで詳しく説明します。

ボイラープレートの削減

これらの規則を順守することは、関数から返される値が多数ある状況では退屈になる可能性があります。 anonymous functionを使用して、定型文を減らすことができます。 無名関数は、変数に割り当てられたプロシージャです。 前の例で定義した関数とは対照的に、それらは宣言した関数内でのみ使用できます。これにより、再利用可能なヘルパーロジックの短い断片として機能するのに最適です。

次のプログラムは、大文字の名前の長さを含めるように最後の例を修正します。 返される値は3つあるため、エラーを処理するのに役立つ匿名関数がなければ面倒になります。

package main

import (
    "errors"
    "fmt"
    "strings"
)

func capitalize(name string) (string, int, error) {
    handle := func(err error) (string, int, error) {
        return "", 0, err
    }

    if name == "" {
        return handle(errors.New("no name provided"))
    }

    return strings.ToTitle(name), len(name), nil
}

func main() {
    name, size, err := capitalize("sammy")
    if err != nil {
        fmt.Println("An error occurred:", err)
    }

    fmt.Printf("Capitalized name: %s, length: %d", name, size)
}
OutputCapitalized name: SAMMY, length: 5

main()内で、capitalizeから返された3つの引数をそれぞれnamesize、およびerrとしてキャプチャします。 次に、err変数がnilと等しくないかどうかを確認することにより、capitalizeerrorを返したかどうかを確認します。 これは、capitalizeによって返される他の値を使用する前に行うことが重要です。これは、無名関数handleがそれらをゼロ値に設定する可能性があるためです。 文字列"sammy"を指定したためエラーは発生しなかったため、大文字の名前とその長さを出力します。

もう一度、"sammy"を空の文字列("")に変更して、出力されたエラーケース(An error occurred: no name provided)を確認できます。

capitalize内で、handle変数を無名関数として定義します。 単一のエラーを受け取り、capitalizeの戻り値と同じ順序で同じ値を返します。 handleは、これらの値をゼロ値に設定し、引数として渡されたerrorを最終的な戻り値として転送します。 これを使用して、errorをパラメーターとしてhandleの呼び出しの前にreturnステートメントを使用することにより、capitalizeで発生したエラーを返すことができます。

関数を定義した方法であるため、capitalizeは常に3つの値を返す必要があることに注意してください。 関数が返す可能性のあるすべての値を処理したくない場合があります。 幸いなことに、これらの値を割り当て側で使用する方法には柔軟性があります。

マルチリターン関数からのエラーの処理

関数が多くの値を返す場合、Goでは各値を変数に割り当てる必要があります。 最後の例では、capitalize関数から返される2つの値の名前を指定してこれを行います。 これらの名前はコンマで区切って、:=演算子の左側に表示する必要があります。 capitalizeから返された最初の値はname変数に割り当てられ、2番目の値(error)は変数errに割り当てられます。 場合によっては、エラー値のみに関心があります。 特別な_変数名を使用して、関数が返す不要な値を破棄できます。

次のプログラムでは、capitalize関数を含む最初の例を変更して、空の文字列("")を渡すことでエラーを生成しました。 このプログラムを実行して、_変数で最初の戻り値を破棄することにより、エラーだけを調べる方法を確認してください。

package main

import (
    "errors"
    "fmt"
    "strings"
)

func capitalize(name string) (string, error) {
    if name == "" {
        return "", errors.New("no name provided")
    }
    return strings.ToTitle(name), nil
}

func main() {
    _, err := capitalize("")
    if err != nil {
        fmt.Println("Could not capitalize:", err)
        return
    }
    fmt.Println("Success!")
}
OutputCould not capitalize: no name provided

今回はmain()関数内で、大文字の名前(最初に返されるstring)をアンダースコア変数(_)に割り当てます。 同時に、capitalizeによって返されるerrorerr変数に割り当てます。 次に、エラーがif err != nil条件付きで存在するかどうかを確認します。 行_, err := capitalize("")capitalizeへの引数として空の文字列をハードコーディングしているため、この条件は常にtrueに評価されます。 これにより、ifステートメントの本体内のfmt.Println関数の呼び出しによって出力される出力"Could not capitalize: no name provided"が生成​​されます。 この後のreturnは、fmt.Println("Success!")をスキップします。

結論

標準ライブラリを使用してエラーを作成する多くの方法と、慣用的な方法でエラーを返す関数を作成する方法を見てきました。 このチュートリアルでは、標準ライブラリのerrors.New関数とfmt.Errorf関数を使用して、さまざまなエラーを正常に作成できました。 今後のチュートリアルでは、独自のカスタムエラータイプを作成して、より豊富な情報をユーザーに伝える方法を検討します。