詳細検索

Golangのエラー処理に対するより洗練された代替手段

アバター
著者: Chen Ziyu

Golangのエラー処理に対するより洗練された代替手段
Englishから翻訳 • 原文を読む

最近、Colorkrewの新しいプロジェクトに参加しました。このプロジェクトはGolangをバックエンド言語として使っています。チームのほとんど全員、私自身も含めてGolangの経験がなかったため、ゼロから始めなければなりませんでした。Golangを学ぶことはこれまでのところやりがいのある経験です。何しろ、今や最も需要の高いプログラミング言語の一つですから。しかし、Golangでコードを書くことがすべて順風満帆というわけではありません。私は意見を持たないとはいえ、それでも不満は少なく、最大の不満はGolangの推奨されるエラー処理方法です。

Golangの公式チュートリアルによると、エラーが発生した場合、関数は通常の返される値とともにエラーを返すことになっています。以下はGolangのチュートリアルからの例で、わずかな調整を加えています:

パッケージメイン

インポート(
    「エラーズ」
    「FMT」
    「ログ」
)

ハローは名前のある人物に挨拶を返す。
func Hello(名前文字列) (*string, error) {
    if name == "" {
        返す nil、エラー。New("empty name")
    }

    メッセージ := fmt。Sprintf(「こんにちは、%v.ようこそ!」、名前)
    返信&メッセージ、なし
}

「Hello」関数は「name」という文字列をパラメータとして受け取ります。与えられた「name」を使って「メッセージ」を作成し、返します。「name」が空文字列であることは許されず、エラーとみなされます。「Hello」関数はまず、名前が空文字列かどうかを確認します。文字列が空なら、'Hello'は「nil」と「empty name」エラーを返します。そうでなければ、「name」パラメータと空のエラー('nil')で構成された実際のメッセージを返します。

Golangは関数の返される値の一部としてエラーを呼び出し元に返す必要があるため、Golangのエンジニアはエラーを処理するまで層ごとに返さなければなりません。その結果、Golangコードは必然的に以下の繰り返しのエラーボイラープレートを伴います。

もし err != nil {
    返す0、えっと
}

このようなエラー処理スタイルは、関数がエラーを発生させてキャッチブロックで処理する標準的なプログラミング言語の慣習とは異なります。主流の慣習では、エンジニアがエラーを層ごとに渡すことは求められません。エンジニアは任意のレイヤーでエラーをキャッチ・処理できるため、冗長さや反復性の少ないコードベースが実現します。

Golangの弁護としては、エラー処理の哲学により、エンジニアがすべてのエラーをどこでどのように処理するかを決めることが求められます。これにより、プログラマーの監督によるシステム障害の発生確率が低減されます。また、エラーを関数の返される値の一部として扱うことは、純粋関数を書くエンジニアにとって有利であり、純粋関数よりも予測可能で、不純関数よりも予測可能になります。エラーを上げるとプログラムの通常の流れから逸脱し副作用が生じ、コードの予測可能性が低くなります。エラーを返すことでこの問題が解消され、純粋関数の作成が容易になり、エンジニアが簡単に推論できるコードが得られます。

上記のエラーのボイラープレートをコピー&ペーストすればするほど、関数型プログラミングの原則を破らずにエラーを扱うより洗練された方法があるのか疑問に思いました。そしてある日、この質問の答えは非常にシンプルだと気づきました。Scalaのような関数型プログラミング言語でエラー処理によく使われる「Either」を使うことです。

一体「Either」とは何でしょうか?「Either」は互いに素な和集合であり、そのインスタンスは「Left」または「Right」のいずれかになり得ます。従来、誤り情報は「Left」に、有効な値は「Right」に保存します。言い換えれば、「Either」が「Left」の場合、それはエラーメッセージを含むエラーです。「Either」が「Right」の場合、有効な値を含みます。

上記の「Hello」関数をScalaで「Either」で書き換えてみましょう。

def hello(名前:String):Either[String, String] = {
    もし (name.isEmpty()) {
        return Left("empty name")
    }
    返す 右(「こんにちは、$name。ようこそ!」)
}

Scalaでは、returnキーワードを省略し、Hello関数を以下のように書き換えることもできます。
def hello(名前:String):Either[String, String] = {
もし (name.isEmpty()) {
左(「空名」)
} そうでなければ {
さて(「こんにちは、$name。ようこそ!」) 
//     }
// }

最初の顕著な違いは、Scalaの「hello」関数は返される値が1つしか持たないのに対し、Golang関数は2つあることです。次に、Scalaの「hello」関数は2つのタイプのいずれかを返すことが確実です。エラーメッセージ付きの「Left」か、有効なウェルカムメッセージを含む「Right」です。Golangの「Hello」関数を呼び出す際には同じ確信度はありません。定義によれば、返される値の組み合わせは「(nil, error)」と「(message, nil)」の2つしかありません。しかし技術的には、型ヒントから推測できる組み合わせがさらに2つあります:'(nil, nil)'と「(message, error)'です。Golangの「Hello」関数を呼び出す際には、4つの可能な組み合わせすべてを考慮する必要があるため、Scalaの「Either」よりも複雑になります。

関数の返り値を簡略化するだけでなく、エラー処理も効率化します。前述の例をさらに詳しく説明しましょう。新しい関数「VIPHello」を作成し、「name」というパラメータを受け取り、それで「Hello」関数を呼び出します。「Hello」関数がエラーを返すと、 'VIPHello' はそのエラーを返します。「Hello」関数が有効なメッセージを返す場合、'VIPHello' はメッセージに追加のテキストを追加し、その結果のメッセージを返します。Golang では次のように実装します。

パッケージメイン

インポート(
    「エラーズ」
    「FMT」
    「ログ」
)

ハローは名前のある人物に挨拶を返す。
func Hello(名前文字列) (*string, error) {
    if name == "" {
        返す nil、エラー。New("empty name")
    }

    メッセージ := fmt。Sprintf(「こんにちは、%v.ようこそ!」、名前)
    返信&メッセージ、なし
}

func VIPHello(名前文字列) (*string, error) {
    helloMessage、えっと := Hello(name)
    もし err != nil {
        返す0、えっと
    }

    メッセージ := fmt。Sprintf("%v 心からの感謝を受け取ってください"、*helloMessage)
    返信&メッセージ、なし
}

ご覧の通り、先ほど述べたエラーボイラープレートを使う必要があります。誤りを呼び出すたびにエラーボイラープレートをコピー&ペーストしなければならないと、コードがどれほど冗長になるか想像してみてください。一方で、『Either』とその『map』メソッドのおかげで、同じ関数をScalaでより洗練された方法で実装できます。

def hello(名前:String):Either[String, String] = {
    もし (name.isEmpty()) {
        return Left("empty name")
    }
    返す 右(「こんにちは、$name。ようこそ!」)
}

def vipHello(名前:String):Either[String, String] = {
    返答 hello(name).map((right) => s"$right 心からの感謝をお受け取りください」)
}

シンプルでありながら(Scalaプログラマーにとっては)読みやすい一言文!仕組みを説明しましょう。もし「hello」関数の返される値が「Left」なら、「map」メソッドは単に返します。Eitherの「map」メソッドの魔法は、返される「hello」関数の値が「Right」であるときに起こります。「Right」から有効なメッセージを抽出し、渡された関数の指定に従って追加メッセージを付け加え、結果として得られたメッセージを「Right」でラップしてから返します。この関数はGolangの対応関数と同じことを、よりきれいかつ洗練的に実現します。さらに重要なのは、エラーのボイラープレートをあちこちにコピー&ペーストする必要がないことです!

Golangのエラー処理スタイルに対する私の不満を聞いてくださり、ありがとうございます!私はGolangのシンプルさや卓越した性能など、他の多くの面にもまだ魅力を感じていますが、あちこちでエラーのボイラープレートをコピー&ペーストしたり、有効な値とエラーを別々に返す関数を書かなければならないのは残念です。

この記事が、有能なGolangプログラマーの皆さんに、Golangのエラー処理スタイルを再考し、『Anyther』をGolangに導入する可能性を探るきっかけになればと思います。

Related Articles