次期 Go 言語で導入される(かもしれない)新しいエラー・ハンドリングについて予習する

今回は「次期 Go 言語で導入される(かもしれない)総称型について予習する」の続き。

次期 Go 言語で追加される(かもしれない)仕様についてもう一度挙げておこう。

この記事ではエラー・ハンドリングについて予習してみる。 はっきり言って私は物凄く期待している。 総称型なんか後回しにしてこっちを先に実現してほしい。

なお “Go 2” の提案はまだドラフト段階なので大幅に変更になったり場合によっては立ち消えになる可能性もある。 なので,この記事では深いところまで踏み込まずフワっとした説明になるけど,あしからずご了承の程を。

追記 2019-08-24

Go 1.13 で後半の “Wrapper interface” に関連する仕様追加が行われた。 詳しくは以下の記事を参照のこと。

Check 式(Check Expression)と Handle 構文(Handle Statement)

まずはファイルをコピーする簡単なコマンドを書いてみよう。 ちなみにこれは完全に動くコードである。

package main

import (
    "flag"
    "fmt"
    "io"
    "os"
)

func copyFile(src, dst string) error {
    r, err := os.Open(src)
    if err != nil {
        return err
    }
    defer r.Close()

    w, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer w.Close()

    if _, err := io.Copy(w, r); err != nil {
        return err
    }
    return nil
}

func main() {
    flag.Parse()
    if flag.NArg() != 2 {
        fmt.Println(os.ErrInvalid)
        return
    }
    if err := copyFile(flag.Arg(0), flag.Arg(1)); err != nil {
        fmt.Println(err)
        return
    }
    return
}

この中で

if err != nil {
    ...
}

という記述が多数見られるのが分かると思う。 Go 言語は C++ や Java で言うところの例外処理の仕組みを持っていない1 ためにこのような記述になるのだが,こうした単純な繰り返しの記述は Go 言語プログラマの間でも不評のようだ。

これを解消するのが check 式と handle 構文である。

たとえば,関数の返り値に error を含む場合

v1, v2, ..., vn, err := foo()

check 式を使って error を検知し残りの返り値を返す事ができる。

v1, v2, ..., vn := chack foo()

検知した error はどうなるかというと直近の handle 構文で指定された処理へ飛ぶ2

handle err {
    ...
}

では check と handle を使って先程の copyFile() 関数を書き直してみよう。

func copyFile(src, dst string) error {
    handle err {
        return err
    }

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    defer w.Close()

    check io.Copy(w, r)
    return nil
}

随分とスッキリした。 こうかはばつぐんだ!

Check は式なので

res := check foo(check bar())

といった書き方もできる。 関数の返り値が error とタプルになっている場合は(いったん変数に流し込んだり)スマートでない記述になっているので,これは嬉しい。

Handle 構文はいくつでも書くことができる。 たとえば

func process(user string, files chan string) (n int, err error) {
    handle err { return 0, fmt.Errorf("process: %v", err)  }      // handler A
    for i := 0; i < 3; i++ {
        handle err { err = fmt.Errorf("attempt %d: %v", i, err) } // handler B
        handle err { err = moreWrapping(err) }                    // handler C

        check do(something())  // check 1: handler chain C, B, A
    }
    check do(somethingElse())  // check 2: handler chain A
}

のように書けるらしい。 Handle 構文の処理はスタック状に積まれていく感じかな。

Wrapper interface

たとえば os.PathError は以下のように内部に error 情報を持っている。

// PathError records an error and the operation and file path that caused it.
type PathError struct {
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

エラーハンドリングを行う際は,この内部の error を見て原因となるエラー情報を取得することができる。 このような構造になっている error オブジェクトは多そうである。 そこで errors パッケージに Wrapper interface を追加することを考える。

package errors

// A Wrapper is an error implementation
// wrapping context around another error.
type Wrapper interface {
    // Unwrap returns the next error in the error chain.
    // If there is no next error, Unwrap returns nil.
    Unwrap() error
}

error オブジェクトが Unwrap() 関数を用意していれば,この関数を使って原因となる error オブジェクトを取得できるというわけだ。

func (e *PathError) Unwrap() error { return e.Err }

これを踏まえて以下の関数も用意する。

func As(type E)(err error) (e E, ok bool) {
    for {
        if e, ok := err.(E); ok {
            return e, true
        }
        wrapper, ok := err.(Wrapper)
        if !ok {
            return e, false
        }
        err = wrapper.Unwrap()
        if err == nil {
            return e, false
        }
    }
}

この As() 関数は総称型 E を含んでいる点に注目。 これを使えば

if pe, ok := errors.As(*os.PathError)(err); ok {
    if errno, ok := errors.As(syscall.Errno)(pe.Err); ok {
        switch errno {
        case syscall.ENOENT:
            fmt.Fprintln(os.Stderr, "ファイルが存在しない")
        case syscall.ENOTDIR:
            fmt.Fprintln(os.Stderr, "ディレクトリが存在しない")
        default:
            fmt.Fprintln(os.Stderr, "Errno =", errno)
        }
    } else {
        fmt.Fprintln(os.Stderr, "その他の PathError")
    }
}

という感じでハンドリングできるだろう。

ホンマこれ早めに実現しないかなぁ。

ブックマーク

参考図書

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-07-13)
Kindle版
B099928SJD (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)


  1. Go 言語の panic は例外処理に似た大域脱出の機能を持っているが,本来はリカバリ不能なエラーや障害が発生した際に迅速にプロセスを終了させるための仕組みなので,例外処理のような使い方をすべきではないとされている。 ↩︎

  2. 直近に handle 構文がない場合には error を吸い込んだまま何もせずにスルーするようだ。 ↩︎