Goのosパケージのerrorを1.12と1.13で比較する

Goの1.13でGo2で提案されていたerrorsの変更が入り(全部ではない?)、ErrorValueFAQに移行方法などが書いてあります。その中でosパッケージに触れてありました。移行サンプルとして挙げられているわけではないですが、独自の型を作成しているライブラリがどうやって移行しているのか気になったので調べてみました。
調べてみた結果、なんか複雑になったな、というのが感想です...
今回のerrorの変更と独自エラー型との相性はあんまりよくない気がします。 前の記事では(Goによる並行処理)ではエラーは独自エラーでラップしたほうがいいという感じでしたが...

ErrorValueFAQにも下記のようにエラーをラップすべきかというのは2つのトレードオフであると書かれています。 そして、errorsは中間的な選択だそうです。

I'm writing new code, with no clients. Should I wrap returned errors or not?
Since you have no clients, you aren't constrained by backwards compatibility. But you still need to balance two opposing considerations:

Giving client code access to underlying errors can help it make decisions, which can lead to better software.
Every error you expose becomes part of your API: your clients may come to rely on it, so you can't change it.
For each error you return, you have to weigh the choice between helping your clients and locking yourself in. Of course, this choice is not unique to errors; as a package author, you make many decisions about whether a feature of your code is important for clients to know or an implementation detail.

With errors, though, there is an intermediate choice: you can expose error details to people reading your code's error messages without exposing the errors themselves to client code. One way to do that is to put the details in a string using fmt.Errorf with %s or %v. Another is to write a custom error type, add the details to the string returned by its Error method, and avoid defining an Unwrap method.

1.12

package os

import (
    "errors"
)

var (
    ErrPermission = errors.New("permission denied")
)

type SyscallError struct {
    Syscall string
    Err     error
}

func (e *SyscallError) Error() string { return e.Syscall + ": " + e.Err.Error() }

func NewSyscallError(syscall string, err error) error {
    if err == nil {
        return nil
    }
    return &SyscallError{syscall, err}
}

func IsPermission(err error) bool {
    return isPermission(err)
}

// underlyingError returns the underlying error for known os error types.
func underlyingError(err error) error {
    switch err := err.(type) {
    case *PathError:
        return err.Err
    case *LinkError:
        return err.Err
    case *SyscallError:
        return err.Err
    }
    return err
}
// +build aix darwin dragonfly freebsd js,wasm linux nacl netbsd openbsd solaris

package os

import "syscall"

func isPermission(err error) bool {
    err = underlyingError(err)
    return err == syscall.EACCES || err == syscall.EPERM || err == ErrPermission
}

1.13

package os

import (
    "internal/oserror"
)

var (
    ErrPermission = errPermission() // "permission denied"
)

func errPermission() error { return oserror.ErrPermission }

type SyscallError struct {
    Syscall string
    Err     error
}

func (e *SyscallError) Error() string { return e.Syscall + ": " + e.Err.Error() }

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

func NewSyscallError(syscall string, err error) error {
    if err == nil {
        return nil
    }
    return &SyscallError{syscall, err}
}

func IsPermission(err error) bool {
    return underlyingErrorIs(err, ErrPermission)
}

func underlyingErrorIs(err, target error) bool {
    // Note that this function is not errors.Is:
    // underlyingError only unwraps the specific error-wrapping types
    // that it historically did, not all errors.Wrapper implementations.
    err = underlyingError(err)
    if err == target {
        return true
    }
    // To preserve prior behavior, only examine syscall errors.
    e, ok := err.(syscallErrorType)
    return ok && e.Is(target)
}

// underlyingError returns the underlying error for known os error types.
func underlyingError(err error) error {
    switch err := err.(type) {
    case *PathError:
        return err.Err
    case *LinkError:
        return err.Err
    case *SyscallError:
        return err.Err
    }
    return err
}
// +build !plan9

package os

import "syscall"

type syscallErrorType = syscall.Errno
package oserror

import "errors"

var (
    ErrPermission = errors.New("permission denied")
)
package syscall

import (
    "internal/oserror"
)

type Errno uintptr

func (e Errno) Is(target error) bool {
    switch target {
    case oserror.ErrPermission:
        return e == EACCES || e == EPERM
    case oserror.ErrExist:
        return e == EEXIST || e == ENOTEMPTY
    case oserror.ErrNotExist:
        return e == ENOENT
    }
    return false
}

ちなみに、PathErrorはUnwapメソッドが増えているだけです。

1.12

type PathError struct {
    Op   string
    Path string
    Err  error
}

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

1.13

type PathError struct {
    Op   string
    Path string
    Err  error
}

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

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