CLOVER🍀

That was when it all began.

Linuxのカーネルパラメーターを表示・変更する

これは、なにをしたくて書いたもの?

Linuxカーネルパラメーターを表示したり変更したりするやり方を、いつも忘れるのでいい加減にメモしておこうかなと。

環境

今回の確認環境は、こちらです。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.1 LTS
Release:    20.04
Codename:   focal


$ uname -srvmpio
Linux 5.4.0-64-generic #72-Ubuntu SMP Fri Jan 15 10:27:54 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

Ubuntu Linux 20.04 LTSです。

sysctlとsysctl.conf

今回のインプットは、こちら。

sysctlsysctl.confのドキュメントです。

Man page of SYSCTL

Man page of SYSCTL.CONF

sysctl(8) - Linux manual page

sysctl.conf(5) - Linux manual page

ヘルプの確認。

$ sysctl --help

Usage:
 sysctl [options] [variable[=value] ...]

Options:
  -a, --all            display all variables
  -A                   alias of -a
  -X                   alias of -a
      --deprecated     include deprecated parameters to listing
  -b, --binary         print value without new line
  -e, --ignore         ignore unknown variables errors
  -N, --names          print variable names without values
  -n, --values         print only values of the given variable(s)
  -p, --load[=<file>]  read values from file
  -f                   alias of -p
      --system         read values from all system directories
  -r, --pattern <expression>
                       select setting that match expression
  -q, --quiet          do not echo variable set
  -w, --write          enable writing a value to variable
  -o                   does nothing
  -x                   does nothing
  -d                   alias of -h

 -h, --help     display this help and exit
 -V, --version  output version information and exit

For more details see sysctl(8).

また、/etc/sysctl*ディレクトリやファイルはこのようになっています。

$ find /etc/sysctl* -type f
/etc/sysctl.conf
/etc/sysctl.d/README.sysctl
/etc/sysctl.d/10-magic-sysrq.conf
/etc/sysctl.d/10-kernel-hardening.conf
/etc/sysctl.d/10-console-messages.conf
/etc/sysctl.d/10-zeropage.conf
/etc/sysctl.d/10-network-security.conf
/etc/sysctl.d/10-ptrace.conf
/etc/sysctl.d/10-link-restrictions.conf
/etc/sysctl.d/10-ipv6-privacy.conf

現在のカーネルパラメーターを表示する

現在のカーネルパラメーターをすべて表示するには、sysctl -aを使います。

$ sudo sysctl -a

なお、パラメーターを表示するのにsudoを付けなくてもいいのですが、一部権限で見れないものがあったりするので、今回は一律
sudoを付けています。

特定のパラメーターを表示する場合は、sysctlの後にその名前を指定します。

$ sudo sysctl vm.max_map_count
vm.max_map_count = 65530

今回のエントリでは、カーネルパラメーターの例としてはvm.max_map_countを使うことにします。

-nを付けると、値だけになります。

$ sudo sysctl -n vm.max_map_count
65530

-Nを付けると、名前だけになります。

$ sudo sysctl -N vm.max_map_count
vm.max_map_count

また、/proc/sys配下を見てもOKです。

$ cat /proc/sys/vm/max_map_count 
65530

現在の値の確認は、こんな感じですね。

カーネルパラメーターを変更する

次に、カーネルパラメーターの変更を行います。

カーネルパラメーターを変更する際には、その変更を永続化するケースと、一時的な変更(再起動すると戻ってしまう)とするケースの
2つがあります。

変更を永続化する

変更を永続化する場合、/etc/sysctl.confに対象のパラメーターと値を記述します。

こんな感じです。項目名 = 値で指定します。=の前後にスペースがあってもかまいません。

/etc/sysctl.conf

vm.max_map_count = 262144

もちろん、書いただけでは変わりません。

$ sudo sysctl vm.max_map_count
vm.max_map_count = 65530

sysctl -pでの反映が必要です。

$ sudo sysctl -p

反映されました。

$ sudo sysctl vm.max_map_count
vm.max_map_count = 262144

OSを再起動しても、/etc/sysctl.confに記載した内容になっています。

$ sudo reboot

...


$ sudo sysctl vm.max_map_count
vm.max_map_count = 262144

ちなみに、もとに戻したい場合は/etc/sysctl.confファイルに書いた項目を削除してsysctl -pを実行してもすぐには意味がありません。

デフォルト値でよければ/etc/sysctl.confに記載した項目を削除してsysctl -pに変更後、OSを再起動します。
それ以外の値にしたかったら、あらためて設定しましょう。

Ubuntu Linux(およびDebian)の場合

Ubuntu Linux(およびDebian)の場合、/etc/sysctl.dディレクトリ配下に.conf拡張子のファイルを作成してカーネルパラメーターを
設定することもできます。
ファイル名はなんでもよいのですが、拡張子は.confである必要があります。ファイルの書き方はsysctl.confと同じです。

/etc/sysctl.d/README.sysctl

Kernel system variables configuration files

Files found under the /etc/sysctl.d directory that end with .conf are
parsed within sysctl(8) at boot time.  If you want to set kernel variables
you can either edit /etc/sysctl.conf or make a new file.

The filename isn't important, but don't make it a package name as it may clash
with something the package builder needs later. It must end with .conf though.

My personal preference would be for local system settings to go into
/etc/sysctl.d/local.conf but as long as you follow the rules for the names
of the file, anything will work. See sysctl.conf(8) man page for details
of the format.

After making any changes, please run "service procps reload" (or, from
a Debian package maintainer script "deb-systemd-invoke restart procps.service").

今回は、/etc/sysctl.d/local.confというファイルで用意します。

/etc/sysctl.d/local.conf

vm.max_map_count = 262144

この変更を反映するには、sysctl -pを実行しても効果がありません。procpsというサービスを再起動します。

$ sudo systemctl restart procps

反映されました。

$ sudo sysctl vm.max_map_count
vm.max_map_count = 262144

再起動しても、ファイルに記述していればその設定は残ります。

$ sudo reboot

...

$ sudo sysctl vm.max_map_count
vm.max_map_count = 262144

記載した項目と値を削除してprocpsを再起動しても効果がないのは、sysctl -pの時と同じです。

デフォルト値に戻すのならOSを再起動、そうでないなら値を改めて設定しましょう。

変更を一時的なものにする

次は、カーネルパラメーターを変更しますが、その変更は一時的なものにします。要するに、再起動すると元に戻ります。

まずは、今の値を表示。

$ sudo sysctl vm.max_map_count
vm.max_map_count = 65530

一時的にカーネルパラメーターを変更するには、sysctl -w [項目名]=[値]で指定します。sysctl.confと違い、こちらは=の前後に
スペースがあってはいけません。

$ sudo sysctl -w vm.max_map_count=262144
## または 
### $ sudo sysctl vm.max_map_count=262144
vm.max_map_count = 262144

以前は-wオプションが必要だみたいなことが書かれていたのですが、今のmanを見るとその記述はありませんし、実際に反映も
行われますね。

variable=value

  To set a key, use the form variable=value where variable is the key and value is the value to set it to.  If the value contains quotes or  characters  which  are parsed by the shell, you may need to enclose the value in double quotes.  

確認。

$ sudo sysctl vm.max_map_count
vm.max_map_count = 262144

値が変わりました。

再起動すると、デフォルト値に戻ることも確認できます。

$ sudo reboot

...

$ sudo sysctl vm.max_map_count
vm.max_map_count = 65530

ちなみに、即時に変更しつつその内容を永続化する場合は、以下のようにsysctl -wの内容を/etc/sysctl.confに追記するという
方法もあるようです。

$ sudo sh -c 'sysctl -w vm.max_map_count=262144 >> /etc/sysctl.conf'

$ sudo sysctl vm.max_map_count
vm.max_map_count = 262144


$ sudo reboot

...

$ sudo sysctl vm.max_map_count
vm.max_map_count = 262144

sysctl -wを使う方法以外では、/proc/sys配下に直接書き込むという方法もあります。

$ sudo sh -c 'echo 262144 > /proc/sys/vm/max_map_count'

確認。

$ sudo sysctl vm.max_map_count
vm.max_map_count = 262144

こちらも、再起動するとデフォルト値に戻ります。

$ sudo reboot

...

$ sudo sysctl vm.max_map_count
vm.max_map_count = 65530

ひととおり確認できたのではないでしょうか。

Goのエラーに関するAPIを学ぶ

これは、なにをしたくて書いたもの?

Goの本を読んだり、サンプルコードを見ていたりすると、こういうのを目にするのですが。

    file, err = os.Create(filename)
    if err == nil {
        return
    }

nilかどうかの判定はさておき…。

エラーの中身や種類に踏み込みたい場合は?とか、自分でエラーを定義する場合は?あたりが気になるので、
ちょっと調べてみることにしました。

インプット、それから

今回のエントリの主な情報源は、以下です。

The Go Programming Language Specification / Errors

Effective Go / Errors

errors - The Go Programming Language

Working with Errors in Go 1.13 - The Go Blog

言語仕様、Effective Goそれぞれのエラーのセクション、errorsパッケージ、それからGo 1.13以降のエラーを扱うことに
ついてのブログです。

このあたりを見たり、実際に動かしてみていろいろ考えたのですが、こんな感じでしょうか?

  • 自分でエラーを作る、作らないに関係なく
    • エラーに関する条件判定にはerros.Isを使う
    • 具体的なエラーの型の変数に束縛する際には、erros.Asを使う
  • 自分でエラーを作る場合
    • erros.Newから始める
    • 他のエラーを原因とする、簡単なエラーを作成する場合はfmt.Errorf%w書式を使う
    • エラーの内容が固定できる場合は、Errで始まる名前でパッケージからエクスポートしておく(※1)
    • 関数呼び出し時にエラーのインスタンスを作成する場合は、error.Isで比較可能な手段を提供しておく(※2)
      • Variableと直接比較できないから
      • エラーの発生原因等、比較可能な情報を使ってerror.Isさせることになる?
    • 自分でエラーに関する構造体を定義する場合は、Error、少なくともUnwrapメソッドを作成する
      • 型の名前はErrorで終わるようにする
      • 比較をカスタマイズする場合はIsAsの作成も考える

Goを見ている感じ、※1の場合は通常の型として、※2の場合はエラーの型をポインタとすることになるのかな?という
印象を持ちました。

エラーとは?

Goの言語仕様に関する内容を見てみます。

The Go Programming Language Specification / Errors

Goでいうエラーとは、以下のように定義されたerrorインターフェースです。要するに、stringを返すErrorメソッドを
実装しなさい、と。

type error interface {
    Error() string
}

Goで使う多数の関数のうち、エラーが発生する可能性があるものは、成功した時の値とerrorインスタンスを返すような
複数の戻り値を返せる定義になっています。

errorを返せる定義になっている関数の戻り値は、先に返されたerrornilかどうかを確認するところからスタート、となります。

ここで、Effective Goを見てみます。

Effective Go / Errors

errornilでない場合で、さらにエラーの中身に踏み込む場合(起こった事象や原因を知りたい場合など)は、
アサーションを使って具体的な型の変数に代入してフィールドを見たりします。

for try := 0; try < 2; try++ {
    file, err = os.Create(filename)
    if err == nil {
        return
    }
    if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
        deleteTempFiles()  // Recover some space.
        continue
    }
    return
}

これが、まずは基本(?)みたいですね。

errorsパッケージとfmtパッケージ

次に、errorsパッケージを見てみます。

errors - The Go Programming Language

errosパッケージには、errorを扱う関数が含まれています。

  • 文字列(メッセージ)からerrorを作成するNew
  • errorに別のerrorが含まれる場合、それを取り出すUnwrap
  • errorを(Unwrapすることも含めて)特定のerrorと比較するIs
  • errorを(Unwrapしていき)特定の型の引数に割り当てるAs

https://github.com/golang/go/blob/go1.15.6/src/errors/errors.go

https://github.com/golang/go/blob/go1.15.6/src/errors/wrap.go

また、fmtパッケージのErrorf関数を使うことでもerrorを作成できます。

Package fmt / func Errorf

Errorf関数は指定した文字列とその中に含まれる書式からerrorを作成しますが、この時に%w書式とその引数としてerror
含めることにより、errors.Unwrapすることができるerror(原因となるerrorを含んだerrorを作成します。

https://github.com/golang/go/blob/go1.15.6/src/fmt/errors.go

エラーの原因を含めない場合は、fmt.Errorferrors.Newと完全に同等です。

では、このあたりのAPIを使ってちょっと確認していってみましょう。

環境

今回の環境は、こちら。

$ go version
go version go1.15.6 linux/amd64

モジュール作成。

$ go mod init error-handling
go: creating new go.mod: module error-handling

確認は、テストコードで行います。
go.mod

module error-handling

go 1.15

require github.com/stretchr/testify v1.7.0

標準パッケージに定義されているエラーで確認してみる

まずは、標準パッケージに定義されているエラーで確認してみましょう。

簡単に起こせるエラーでパッと思いつくものとして、開こうとしたファイルがなかった場合、をお題にしましょう。

Package os / func Open

エラーが起こった場合、*PathErrorが返ってくるようです。

Package os / type PathError

テストコードの雛形を用意。
file_read_error_test.go

package main

import (
    "errors"
    "github.com/stretchr/testify/assert"
    "os"
    "syscall"
    "testing"
)

// ここに、テストを書く!

最初はシンプルに。返ってきたerrornilかどうかだけ確認。

func TestOpenFileSuccess(t *testing.T) {
    file, err := os.Open("file_read_error_test.go")

    if err == nil {
        assert.Equal(t, "file_read_error_test.go", file.Name())
    } else {
        assert.Failf(t, "can't open file", "target file = %s", "file_read_error_test.go")
    }
}

よくありそうなコードです。

次に、型アサーションを使って*PathErrorとして扱ってみます。

The Go Programming Language Specification / Type assertions

func TestOpenFileFailureTypeAssertion1(t *testing.T) {
    _, err := os.Open("not_found_file")

    if err == nil {
        assert.FailNow(t, "file exists")
    }

    if pathError, isPathError := err.(*os.PathError); isPathError {
        assert.Equal(t, "open", pathError.Op)
        assert.Equal(t, "not_found_file", pathError.Path)
        assert.Equal(t, "no such file or directory", pathError.Err.Error())

        assert.Equal(t, syscall.ENOENT, pathError.Err)

        errno, isErrno := pathError.Err.(syscall.Errno)
        assert.True(t, isErrno)
        assert.True(t, errno.Is(os.ErrNotExist))
    } else {
        assert.FailNow(t, "not PathError")
    }
}

*PathError型にできてしまえば、いくつかフィールドがあるのでもう少し詳細な内容を確認できます。

       assert.Equal(t, "open", pathError.Op)
        assert.Equal(t, "not_found_file", pathError.Path)
        assert.Equal(t, "no such file or directory", pathError.Err.Error())

また*PathErrorErrというフィールドにエラーの発生原因を持っていて、こちらでosパッケージを使ってエラーの種類を
確認できます。

       assert.Equal(t, syscall.ENOENT, pathError.Err)

        errno, isErrno := pathError.Err.(syscall.Errno)
        assert.True(t, isErrno)
        assert.True(t, errno.Is(os.ErrNotExist))

osパッケージに定義されているエラーは、Variablesを見るとわかります。

Package os / Variables

今回はファイルが存在しないので、ErrNotExistとなります。

なお、これらのエラーはsyscallパッケージのErrnoという型(実体はuintptr)です。

Package syscall / type Errno

型の判定は、Type switchesを使う方法もあるでしょう。

The Go Programming Language Specification / Type switches

func TestOpenFileFailureTypeAssertion2(t *testing.T) {
    _, err := os.Open("not_found_file")

    if err == nil {
        assert.FailNow(t, "file exists")
    }

    switch err := err.(type) {
    case *os.PathError:
        assert.Equal(t, "open", err.Op)
        assert.Equal(t, "not_found_file", err.Path)
        assert.Equal(t, "no such file or directory", err.Err.Error())

        assert.Equal(t, syscall.ENOENT, err.Err)

        switch errno := err.Err.(type) {
        case syscall.Errno:
            assert.True(t, errno.Is(os.ErrNotExist))
        default:
            assert.FailNow(t, "not Errno")
        }

        switch err.Err {
        case syscall.ENOENT:
            // no-op
        default:
            assert.FailNow(t, "not ENOENT")
        }
    default:
        assert.FailNow(t, "not PathError")
    }
}

この部分に関しては、値でも比較できます。

       switch err.Err {
        case syscall.ENOENT:
            // no-op
        default:
            assert.FailNow(t, "not ENOENT")
        }

次に、これらをerrorsパッケージを使って書き換えていきましょう。

errors - The Go Programming Language

Asから。Asを使用すると、エラー(とその内部的に持っている原因となったエラー)に対して指定した変数の型に合致するものが
あれば、その変数に値を設定しtrueを返します。そうでなければfalseを返します。

Package errors / func As

こんな感じですね。先ほどの*PathErrorに対する型アサーションを書き換えたものです。

func TestOpenFileFailureUsingErrorsAs1(t *testing.T) {
    _, err := os.Open("not_found_file")

    if err == nil {
        assert.FailNow(t, "file exists")
    }

    var pathError *os.PathError
    if errors.As(err, &pathError) {
        assert.Equal(t, "open", pathError.Op)
        assert.Equal(t, "not_found_file", pathError.Path)
        assert.Equal(t, "no such file or directory", pathError.Err.Error())

        assert.Equal(t, syscall.ENOENT, pathError.Err)

        var errno syscall.Errno
        if errors.As(err, &errno) {
            assert.True(t, errno.Is(os.ErrNotExist))
        } else {
            assert.FailNow(t, "not Errno")
        }
    } else {
        assert.FailNow(t, "not PathError")
    }
}

内部的に持っているエラー…チェインされたエラーに対しても見れるという話なので、*PathErrorからいきなりsyscall.Errnoまで
引き抜くことも可能です。

func TestOpenFileFailureUsingErrorsAs2(t *testing.T) {
    _, err := os.Open("not_found_file")

    if err == nil {
        assert.FailNow(t, "file exists")
    }

    var errno syscall.Errno
    if errors.As(err, &errno) {
        assert.True(t, errno.Is(os.ErrNotExist))
    } else {
        assert.FailNow(t, "not Errno")
    }
}

次は、Isです。Isを使うと、エラー(とその内部的に持っている原因となったエラー)に対して、引数で指定したerror
合致するものがあれば、trueを返します。そうでなければfalseを返します。

Package errors / func Is

要するに、Asで変数に値を束縛しない版みたいなものです。

シンプルに、条件分岐で使うのでしょう。

func TestOpenFileFailureUsingErrorsIs(t *testing.T) {
    _, err := os.Open("not_found_file")

    if err == nil {
        assert.FailNow(t, "file exists")
    }

    if errors.Is(err, os.ErrNotExist) {
        pathError, _ := err.(*os.PathError)
        assert.Equal(t, "no such file or directory", pathError.Err.Error())
    } else {
        assert.FailNow(t, "not PathError")
    }
}

*PathErrorの場合、値として比較するerrorがないのでos.ErrNotExistで比較しました。これでtrueになります。

   if errors.Is(err, os.ErrNotExist) {

最後はUnwrapです。Unwrapを使うと、指定したerrorUnwrapメソッドを実装していた場合、Unwrapメソッドの
呼び出し結果を返します。要するに、errorの原因が返ります。Unwrapメソッドを実装していなかった場合は、nil
返ります。

Package errors / func Unwrap

こんな感じです。

func TestOpenFileFailureUsingErrorsUnwrap(t *testing.T) {
    _, err := os.Open("not_found_file")

    if err == nil {
        assert.FailNow(t, "file exists")
    }

    unwrapped := errors.Unwrap(err)

    if errno, isErrno := unwrapped.(syscall.Errno); isErrno {
        assert.True(t, errno.Is(os.ErrNotExist))
    } else {
        assert.FailNow(t, "not PathError")
    }
}

ファイル未存在の場合の*PathError型の変数をerrors.Unwrapに渡すと、その原因(syscall.Errno)が取得できます。

errorsパッケージの使い方はこんな感じです。

ところで、今回のお題では*PathErrorsyscall.Errnoos.ErrNotExistと3つ扱うことになっていろいろややこしかったのですが、
改めてと見ると

という定義になっています。いずれも、errorインターフェースの定義を満たしています。

*PathErrorErrフィールドをUnwrapで返すようになっていたことから、

https://github.com/golang/go/blob/go1.15.6/src/os/error.go#L58

*PathError → 原因 → syscall.Errno → 実体の型 or 原因 → os.ErrNotExistみたいな関係になっているのかなと思ったのですが。

そうではなく、syscall.ErrnoIsで受け取ったerrorの値を使った比較処理を行っていたので

https://github.com/golang/go/blob/go1.15.6/src/syscall/syscall_unix.go#L126-L136

*PathErrorUnwrapした結果を==で直接os.ErrNotExistと比較してもtrueにならずに、だいぶ混乱しました。
errors.Isで比較すると、trueになるのに…と。

Isの使い方をちゃんと見た感じですね…。

また、Errnoの実体が持つエラーに割り当てられた数字は、以下で確認でき、

Package syscall / Constants

https://github.com/golang/go/blob/go1.15.6/src/syscall/tables_js.go#L102-L228

os.Err〜に割り当てられる実体は、こちらを見ると確認できます。

https://github.com/golang/go/blob/go1.15.6/src/os/error.go#L29-L34

だいぶ、型に振り回されましたね…。

自分でエラーを定義する

ここまでは、標準パッケージに定義されているエラーを使いましたが、ここからは自分でエラーを定義していきましょう。

コードの雛形を書いていきます。

エラーを定義する方。
myerrors.go

package main

import (
    "errors"
)

// ここに、エラーを定義していく

テストコードを書く方。
myerrors_test.go

package main
import (
    "errors"
    "fmt"
    "github.com/stretchr/testify/assert"
    "os"
    "syscall"
    "testing"
)

// ここに、テストを書く!

自分でエラーを定義する1番シンプルなパターンとしては、errors.Newfmt.Errorfを使う方法かな、と。

Package errors / func New

Package fmt / func Errorf

errors.Newは文字列からerrorを作成し、fmt.Errorfは書式も使えつつ文字列からerrorを作成します。

テストコードで定義すると、こんな感じに。

func TestCreateError(t *testing.T) {
    error1 := errors.New("Oops!!")

    assert.Error(t, error1)
    assert.Equal(t, "Oops!!", error1.Error())
    assert.Nil(t, errors.Unwrap(error1))

    error2 := fmt.Errorf("Oops!!")

    assert.Error(t, error2)
    assert.Equal(t, "Oops!!", error2.Error())
    assert.Nil(t, errors.Unwrap(error2))
}

どちらもErrorメソッドを実装しています。この場合は、errors.Unwrapを呼び出しても双方nilです。

ただ、fmt.Errorfの場合、書式に%wを含め、そこにerrorをあてはめるとUnwrapで指定したerrorが返るように構築されます。

こんな感じですね。

func TestWrapedError1(t *testing.T) {
    _, err := os.Open("not_found_file")

    wrappedError := fmt.Errorf("Open file error, reason [%w]", err)

    assert.Error(t, wrappedError)
    assert.Equal(t, "Open file error, reason [open not_found_file: no such file or directory]", wrappedError.Error())
    assert.Equal(t, "*fmt.wrapError", fmt.Sprintf("%T", wrappedError))

    causeError := errors.Unwrap(wrappedError)
    assert.True(t, err == causeError)

    assert.True(t, errors.Is(wrappedError, os.ErrNotExist))
    assert.ErrorIs(t, wrappedError, os.ErrNotExist)
}

ファイル未存在のエラーを元にして%wに与え

   _, err := os.Open("not_found_file")

    wrappedError := fmt.Errorf("Open file error, reason [%w]", err)

これをUnwrapすると%wに指定したものと同じであることが確認できます。

   causeError := errors.Unwrap(wrappedError)
    assert.True(t, err == causeError)

あまり意味はないですが、%wだけを与えてUnwrapで返させることもできます。

func TestWrapedError2(t *testing.T) {
    _, err := os.Open("not_found_file")

    wrappedError := fmt.Errorf("%w", err)

    assert.Error(t, wrappedError)
    assert.Equal(t, "open not_found_file: no such file or directory", wrappedError.Error())
    assert.Equal(t, "*fmt.wrapError", fmt.Sprintf("%T", wrappedError))

    causeError := errors.Unwrap(wrappedError)
    assert.True(t, err == causeError)

    assert.True(t, errors.Is(wrappedError, os.ErrNotExist))
    assert.ErrorIs(t, wrappedError, os.ErrNotExist)
}

myerrors.go側に、Variableとしてerrorを定義(エクスポート)し、関数呼び出しの結果としてerrorを返すようにしてみましょう。

var (
    ErrMySimpleError = errors.New("My Error")
)

func GetMySimpleErr() error {
    return ErrMySimpleError
}

Variableとしてエクスポートするerrorの名前は、Errで始めるのが通例のようです。

テストコードはこんな感じで。

func TestGetMySimpleErr(t *testing.T) {
    err := GetMySimpleErr()

    assert.Equal(t, ErrMySimpleError, err)

    assert.Equal(t, "My Error", err.Error())
    assert.Equal(t, "*errors.errorString", fmt.Sprintf("%T", err))
}

最後に、エラーを構造体で定義してみます。

2つ用意して、片方はエラーの原因を持てるようにしました。ところで、エラーとなる型の名前はErrorで終わらせるのが通例の
ようです。1とか2とか付けてしまいました…。

type MyError1 struct {
    Message string
}

func (e MyError1) Error() string {
    return e.Message
}

func (e MyError1) Is(target error) bool {
    return errors.Is(e, target)
}

type MyError2 struct {
    Message string
    Cause   error
}

func (e *MyError2) Error() string {
    return e.Message + ": " + e.Cause.Error()
}

func (e *MyError2) Unwrap() error {
    return e.Cause
}

Errorメソッドを実装するのはerrorとしての必須条件ですが、MyError1の方はIsを、MyError2の方はUnwrapを実装して
おきました。

MyError1の方は、Variableとして固定で定義します。

var (
    ErrMyError = MyError1{Message: "Oops!!"}
)

MyError2の方は、実行時にエラーの原因を含んで動的に作成することにします。

これら2つのエラーを、引数の指定でどちらかのエラー(もしくは正常に値を返す)を返すような関数を作成します。

func Execute(word string, cause error) (string, error) {
    switch word {
    case "Error1":
        return "NG", ErrMyError
    case "Error2":
        e := &MyError2{Message: "Oops!!", Cause: cause}
        return "NG", e
    default:
        return "OK", nil
    }
}

MyError2の方は、この関数呼び出し時に構造体のインスタンスを作成していますが、ポインタを返すようにしました。
*PathErrorと同じですね。

https://github.com/golang/go/blob/go1.15.6/src/os/file_unix.go#L210

他のパッケージもいくつか見たのですが、関数の呼び出し時にエラー発生時の情報を含めるなどの理由でインスタンスを作ったものは、
ポインタを返しているように思います。それに習いました。

テストコードで確認してみます。

MyError1の確認。

func TestMyCustomError1(t *testing.T) {
    _, err := os.Open("not_found_file")

    result, myerr := Execute("Error1", err)

    assert.Equal(t, "NG", result)

    assert.True(t, myerr == ErrMyError)

    assert.Equal(t, "Oops!!", myerr.Error())
    assert.True(t, errors.Is(myerr, ErrMyError))
    assert.Nil(t, errors.Unwrap(myerr))

    var e MyError1
    assert.True(t, errors.As(myerr, &e))
}

関数の戻り値となったerrorを、==errors.Isで比較できます。

   assert.True(t, myerr == ErrMyError)

    assert.True(t, errors.Is(myerr, ErrMyError))

Unwrapは実装していないのでnilですね。

   assert.Nil(t, errors.Unwrap(myerr))

errors.Asで変数に束縛もできます。

   var e MyError1
    assert.True(t, errors.As(myerr, &e))

続いて、MyError2を使う方。

func TestMyCustomError2(t *testing.T) {
    _, err := os.Open("not_found_file")

    _, myerr := Execute("Error2", err)

    assert.Equal(t, "Oops!!: open not_found_file: no such file or directory", myerr.Error())

    assert.True(t, errors.Is(myerr, os.ErrNotExist))
    assert.True(t, errors.Unwrap(myerr) == err)

    var e *MyError2
    assert.True(t, errors.As(myerr, &e))

    var errno syscall.Errno
    assert.True(t, errors.As(myerr, &errno))
}

Isメソッドは実装していないのですが、普通に使えます。

   assert.True(t, errors.Is(myerr, os.ErrNotExist))

Unwrapメソッドを実装していれば、そちらも見てくれるので。

https://github.com/golang/go/blob/go1.15.6/src/errors/wrap.go#L39-L59

単純比較、IsUnwrapと推移していくので、比較に特殊な条件がなければUnwrapを実装しておくのがよいのでしょう。

errors.Unwrapでは、エラーの原因が取得できます。これは、Unwrapメソッドを実装したからですね。

   assert.True(t, errors.Unwrap(myerr) == err)

errors.Asでの変数への束縛。

   var e *MyError2
    assert.True(t, errors.As(myerr, &e))

こちらも、Asメソッドを実装していなくてもUnwrapメソッドの呼び出し結果を使ってくれます。

https://github.com/golang/go/blob/go1.15.6/src/errors/wrap.go#L77-L101

*PathErrorの時と同じように、errorsAsで一気にsyscall.Errno`を取得することもできます。

   var errno syscall.Errno
    assert.True(t, errors.As(myerr, &errno))

こんな感じでしょうか。

まとめ

Goのエラーに関するAPIがよくわからなかったので、標準ライブラリの使い方、自分でエラーを定義する場合を含めて、
いろいろ試してみました。

ある程度使い方がわかったかな?という気がします。