M.Hiroi's Home Page

Go Language Programming

お気楽 Go 言語プログラミング入門

[ PrevPage | Golang | NextPage ]

エラー処理

今回は Go 言語のエラー処理について説明します。他のプログラミング言語、たとえばC++ や Java などには、try, throw, catch といったエラーを処理するための構文が用意されています。これを「例外処理」といいます。「例外=エラー処理」と考えてもらってもかまいません。最近は例外処理を持っているプログラミング言語が多くなりました。

Go 言語の場合、関数は多値を返すことができるので、エラーは簡単に報告することができます。このため、Go 言語には例外処理専用の構文はないのですが、組み込み関数 panic, recover と defer 文を使って例外処理と似たような動作を行わせることができます。

●エラーの返し方

Go 言語にはエラーを表すインターフェース error が定義されています。Go 言語でエラーを報告する場合、error を多値で返すことがよく行われます。

リスト : error の定義

type error interface {
    Error() string
}

メソッド Error はエラーメッセージを文字列として返します。Print や Println は error を受け取るとメソッド Error を呼び出して、返り値の文字列を表示します。

パッケージ errors の関数 New もしくはパッケージ fmt の関数 Errorf を使うと、簡単にエラーを返すことができます。

func New(text string) error

関数 errors.New は引数 text を格納した構造体 (非公開) へのポインタを返します。この構造体にはメソッド Error が定義されているので、error 型として扱うことができます。

func Errorf(format string, args... interface{}) error

関数 fmt.Errorf は fmt.Sprintf と errors.New を組み合わせたもので、書式文字列 format に従って変数 args を整形し、その結果を文字列に変換して errors.New に渡します。

簡単な例として、階乗を求める関数 fact で引数の範囲チェックを行うことにします。次のリストを見てください。

リスト : エラーの返し方 (sample1501.go)

package main

import (
    "fmt"
    "errors"
)

// 階乗
func fact(n int) (int, error) {
    if n < 0 {
        return 0, errors.New("fact : domain error")
    }
    a := 1;
    for ; n > 1; n-- {
        a *= n
    }
    return a, nil
}

func main() {
    for x := 10; x >= -1; x-- {
        v, err := fact(x)
        if err != nil {
            fmt.Println(err)
        } else {
            fmt.Println(v)
        }
    }
}

関数 fact は int と error を多値で返します。引数 n が負の場合、int のゼロ値 0 と error を返します。error は errors.New で生成します。n が正常の範囲であれば、階乗を計算して、その結果 a と nil を返します。main では fact の返り値 err をチェックして、err が nil でなければ fmt.Println(err) でエラーメッセージを表示します。

それでは実行してみましょう。

$ go run sample1501.go
3628800
362880
40320
5040
720
120
24
6
2
1
1
fact : domain error

このように、Go 言語は多値を使ってエラーを返すことができます。

なお、自分でエラーを定義することも簡単にできます。次のリストを見てください。

リスト : エラーの返し方 (2)

package main

import "fmt"

// エラーの定義
type MyError struct {
    msg string
}

// エラーの生成
func newMyError(s string) *MyError {
    err := new(MyError) //
    err.msg = s         //
    return err          // return &MyError{s} でもよい
}

// メソッドの定義
func (err *MyError) Error() string {
    return err.msg
}

// 階乗
func fact(n int) (int, error) {
    if n < 0 {
        return 0, newMyError("fact : domain error")
    }
    a := 1;
    for ; n > 1; n-- {
        a *= n
    }
    return a, nil
}

func main() {
    for x := 10; x >= -1; x-- {
        v, err := fact(x)
        if err != nil {
            fmt.Println(err)
        } else {
            fmt.Println(v)
        }
    }
}

エラーメッセージを格納する構造体 MyError を定義します。関数 newMyError は new で MyError のメモリを確保し、引数の文字列 s をフィールド変数 msg にセットします。なお、new を使わずに &MyError{s} を return で返しても、メモリを動的に確保することができます。

メソッド Error は MyError のフィールド変数 msg の値を返すだけです。これで MyError は error と同じ型として扱うことができます。関数 fact では、errors.New のかわりに newMyError を呼び出してエラーを生成します。

●panic

組み込み関数 panic はプログラムの実行を中断するランタイムエラーを生成します。

panic(err interface{})

panic に渡された引数 err はプログラムをエラー終了するとき画面に出力されます。

簡単な例を示しましょう。次のリストを見てください。

リスト : panic の使用例 (sample1502.go)

package main

import "fmt"

// 階乗
func fact(n int) (int, error) {
    if n < 0 {
        panic("fact : domain error")
    }
    a := 1;
    for ; n > 1; n-- {
        a *= n
    }
    return a, nil
}

func main() {
    for x := 10; x >= -1; x-- {
        v, err := fact(x)
        if err != nil {
            fmt.Println(err)
        } else {
            fmt.Println(v)
        }
    }
}

関数 fact は引数 n が負の場合、panic でランタイムエラーを生成します。panic を実行すると、画面にエラー情報を表示して、プログラムの実行を終了します。

それでは実行してみましょう。

$ go run sample1502.go
3628800
362880
40320
5040
720
120
24
6
2
1
1
panic: fact : domain error

・・・エラー情報 (省略) ・・・

exit status 2

●defer 文

defer 文は関数の終了時に実行する処理を指定します。

defer 処理

defer 文で指定する処理は関数またはメソッド呼び出しでなければなりません。

簡単な例を示しましょう。

リスト : defer 文の使用例 (sample1503.go)

package main

import "fmt"

func bar() {
    defer fmt.Println("bar end")
    fmt.Println("bar start!")
}

func foo() {
    defer fmt.Println("foo end")
    fmt.Println("foo start!")
    bar()
}

func main(){
    foo()
}
$ go run sample1503.go
foo start!
bar start!
bar end
foo end

関数 foo と bar は defer でメッセージを表示する処理を指定します。foo から bar を呼び出して、bar の実行が終了すると defer で指定した処理が実行されて bar end が表示されます。そのあと foo に戻ってきて foo の実行が終了すると、defer で指定した処理が実行されて foo end が表示されます。

defer 文の指定は、ランタイムエラーで関数の実行が中断された場合にも有効です。次のリストを見てください。

リスト : defer 文の使用例 (sample1504.go)

package main

import "fmt"

func baz(){
    panic("oops!")
}

func bar() {
    defer fmt.Println("bar end")
    fmt.Println("bar start!")
    baz()
}

func foo() {
    defer fmt.Println("foo end")
    fmt.Println("foo start!")
    bar()
}

func main(){
    foo()
}
$ go run sample1504.go
foo start!
bar start!
bar end
foo end
panic: oops!

・・・省略・・・

exit status 2

関数 bar から関数 baz を呼び出し、baz の中で panic を実行します。ランタイムエラーが発生すると、Go 言語は関数の呼び出し履歴 (スタック) をたどり、呼び出した関数に defer 文の記述があれば、その処理を実行します。

baz で panic を実行したとき、関数は main -> foo -> bar -> baz の順番で呼び出されているので、baz, bar, foo, main の順番で defer 文の記述があるかチェックします。bar と foo には defer 文があるので、bar の defer 文の処理を実行して bar end が表示され、そのあと foo end が表示されます。最後に、ランタイムエラーのメッセージが表示されてプログラムの実行が終了します。

●ランタイムエラーの捕捉

ところで、ランタイムエラーが発生するたびにプログラムをエラー終了するのではなく、致命的なエラーでなければプログラムの実行を継続したい場合もあるでしょう。Go 言語では組み込み関数 recover と defer 文を使ってランタイムエラーを捕まえることができます。

func recover() interface{}

recover は defer 文で指定した処理の中で使います。recover を実行すると、関数の呼び出し履歴をたどるのをやめて、panic に渡された引数を返します。エラーが発生していない場合、recover は nil を返します。defer 文の処理が終了すると、その関数の呼び出し元に戻ってプログラムの実行を継続します。

簡単な例を示しましょう。次のリストを見てください。

リスト : recover の使用例 (sample1505.go)

package main

import "fmt"

func baz(){
    panic("oops!")
}

func bar() {
    defer fmt.Println("bar end")
    fmt.Println("bar start!")
    baz()
}

func foo() {
    defer func(){
        fmt.Println("foo end")
        err := recover()
        if err != nil {
            fmt.Println(err)
        }
    }()
    fmt.Println("foo start!")
    bar()
}

func main(){
    fmt.Println("main start!")
    foo()
    fmt.Println("main end")
}
$ go run sample1505.go
main start!
foo start!
bar start!
bar end
foo end
oops!
main end

関数 foo の defer 文の処理は匿名関数で、その中で recover を呼び出しています。関数 baz で panic を実行すると、foo の recover で捕捉されて、変数 err には "oops!" がセットされます。foo end と oops! を表示したあと、foo を呼び出した main に戻ってプログラムの実行を継続するので、main end が表示されます。recover でエラーを捕捉しないとプログラムはエラー終了するので、main end は表示されません。

●返り値がある関数でエラーを捕捉する

返り値がある関数でエラーを捕捉する場合は注意が必要です。関数 foo を次のように修正します。

リスト : 返り値がある関数でエラーを捕捉 (sample1506.go)

func foo(n int) int {
    defer func(){
        fmt.Println("foo end")
        err := recover()
        if err != nil {
            fmt.Println(err)
        }
    }()
    fmt.Println("foo start!")
    bar()
    return n * n
}

func main(){
    fmt.Println("main start!")
    fmt.Println(foo(10))
    fmt.Println("main end")
}
$ go run sample1506.go
main start!
foo start!
bar start!
bar end
foo end
oops!
0
main end

関数 foo は整数値 n * n を返します。ところが、bar の実行中にランタイムエラーが発生すると、return n * n は実行されません。この場合、foo の返り値はゼロ値 (0) になります。エラーを捕捉したあと、ゼロ値以外の値を返したい場合は、返り値の型で変数名を指定して、その変数に値をセットします。次の例を見てください。

リスト : 返り値がある関数でエラーを捕捉 (sample1507.go)

func foo(n int) (m int) {
    defer func(){
        fmt.Println("foo end")
        err := recover()
        if err != nil {
            fmt.Println(err)
            m = -1
        }
    }()
    fmt.Println("foo start!")
    bar()
    m = n * n
    return
}
$ go run sample1507.go
main start!
foo start!
bar start!
bar end
foo end
oops!
-1
main end

foo は変数 m の値を返します。bar の実行でエラーがない場合は n * n を返します。エラーが発生した場合は defer 文の匿名関数の中で m に -1 をセットします。これで -1 を返すことができます。

●特定のエラーを捕捉する

特定のエラーだけを捕捉してプログラムの実行を継続することもできます。次のリストを見てください。

リスト : 特定のエラーを捕捉する (sample1508.go)

package main

import "fmt"

// エラーの定義
type MyError struct {
    msg string
}

// エラーの生成
func newMyError(s string) *MyError {
    err := new(MyError)
    err.msg = s
    return err
}

// メソッドの定義
func (err *MyError) Error() string {
    return err.msg
}

func baz1(){
    panic(newMyError("oops!"))
}

func baz2(){
    panic("oops!")
}

func bar(f func()) {
    defer fmt.Println("bar end")
    fmt.Println("bar start!")
    f()
}

func foo(f func()) {
    defer func(){
        fmt.Println("foo end")
        err := recover()
        if err != nil {
            v := err.(*MyError)
            fmt.Println(v)
        }
    }()
    fmt.Println("foo start!")
    bar(f)
}

func main(){
    fmt.Println("main start!")
    foo(baz1)
    foo(baz2)
    fmt.Println("main end")
}

関数 foo の defer 文の匿名関数でエラーを捕捉します。このとき、型アサーションを使って err が *MyError かチェックします。そうであれば、エラーメッセージを表示してプログラムの実行を継続します。err が *MyError でなければ、型アサーションに失敗してエラーが発生するので、プログラムはエラー終了します。

それでは実行してみましょう。

C>go run sample1508.go
main start!
foo start!
bar start!
bar end
foo end
oops!
foo start!
bar start!
bar end
foo end
panic: oops! [recovered]
        panic: interface conversion: interface{} is string, not *main.MyError

・・・省略・・・

exit status 2

foo(baz1) の呼び出しは MyError を返すので、oops! を表示したあとプログラムの実行は継続されます。このあと、foo(baz2) が呼び出されますが、baz2 では panic に文字列 "oops!" を渡して呼び出しているので、foo の defer 文で型アサーションに失敗します。エラー情報を表示したあと、プログラムはエラー終了するので main end は表示されません。


初版 2014 年 4 月 20 日
改訂 2021 年 12 月 18 日

パッケージ

プログラムを作っていると、以前作った関数と同じ処理が必要になる場合があります。いちばんてっとり早い方法はソースファイルからその関数をコピーすることですが、賢明な方法とはいえません。このような場合、自分で作成した関数をライブラリとしてまとめておくと便利です。ライブラリの作成で問題になるのが「名前の衝突」です。複数のライブラリを使うときに、同じ名前の関数や変数が存在すると、そのライブラリは正常に動作しないでしょう。この問題は「パッケージ (package)」を使うと解決することができます。

●モジュールとパッケージの基本

昔の Go 言語は環境変数 GOPATH で指定したディレクトリでパッケージの作成を行っていました。これを GOPATH モードと呼びます。Ver 1.11 から Go Modules というツールが導入され、GOPATH 以外のディレクトリでもパッケージの作成やバージョン管理が行えるようになりました。これをモジュール対応モード (module-aware mode) と呼びます。最近では Go Modules を使ってパッケージを管理する方法が主流のようです。本稿でも Go Modules を使うことにします。

Go 言語のパッケージはディレクトリで管理します。ディレクトリを作成して、そこにソースファイルを格納すれば、それをパッケージとして使用することができます。このとき、ディレクトリ名がパッケージ名になります。ディレクトリ名とソースファイル名は異なっていてもかまいません。ディレクトリの中に複数のソースファイルがある場合、それらをまとめたものが一つのパッケージになります。

そして、複数のパッケージや main パッケージのソースファイルなどをまとめたものを「モジュール (module)」といいます。ローカルな開発環境では、ディレクトリがモジュールに対応します。たとえば、ディレクトリ sample の中でアプリケーションを開発することにしましょう。ディレクトリをモジュールとして使うには、コマンド go mod init を使います。

go mod init モジュール名

開発するアプリケーションが非公開の場合、モジュール名はディレクトリ名と同じでかまいません。なお、GitHub などで公開する場合、公開先のパス (たとえば、github.com/ユーザ名/リポジトリ名 など) がモジュール名になりますが、アプリケーションの公開は本稿の範囲を超えるので、説明は割愛させていただきます。

それでは実際に試してみましょう。

$ mkdir sample
$ cd sample
$ ls
$ go mod init sample
go: creating new go.mod: module sample
$ ls
go.mod
$ cat go.mod
module sample

go 1.17

ディレクトリ sample の中で go mod init sample を実行すると、go.mod というファイルが生成されます。この中にモジュールの情報が保存されます。これでディレクトリ sample をモジュールとして扱うことができます。

次は sample にパッケージ foo と bar を追加します。

リスト : foo/foo.go

package foo

import "fmt"

const (
    A = 10
)

func Test() {
    fmt.Println("package foo")
}
リスト : bar/bar.go

package bar

import "fmt"

const (
    A = 100
)

func Test() {
    fmt.Println("package bar")
}

ディレクトリ sample の中にサブディレクトリ foo を作成し、その中にファイル foo.go を格納します。同様に、サブディレクトリ bar を作成して、ファイル bar.go を格納します。ファイルの先頭には package 文でパッケージ名を記述します。foo.go と bar.go は main パッケージではないので、関数 main は必要ありません。パッケージの中で英大文字から始まる名前 (関数、変数、定数、構造体名、フィールド名など) が外部に公開されます。これを「エクスポート (export)」といいます。

パッケージを利用する場合は import 文を使います。パッケージ foo と bar はモジュール sample の中にあるので、次のように指定します。

リスト : パッケージのインポート

import (
    "sample/foo"
    "sample/bar"
)

パッケージの指定はディレクトリのパス指定と同じです。ただし、パスの区切り記号は Windows でもスラッシュ / を使ってください。"foo", "bar" のように指定すると、Go 言語は標準ライブラリの中から foo と bar を探します。また、ver 1.10 までは相対パス指定 (. や ..) を使うことができましたが、新しいバージョンではエラーになります。ご注意くださいませ。

foo と bar を一つのディレクトリに格納することもできます。次の図を見てください。

./
    baz/
        foo/
            foo.go
        bar/
            bar.go

ディレクトリ baz の中にディレクトリ foo と bar があり、その中にソースファイル foo.go と bar.go があります。この場合、import 文のパスを次のように指定します。

import (
    "sample/baz/foo"
    "sample/baz/bar"
)

この場合でもパッケージ名は foo と bar になるので、アクセス方法は今までと同じです。baz.foo.Test() や baz.bar.A のように baz を指定する必要はありません。

●簡単な実行例

簡単な実行例を示しましょう。

リスト : パッケージの使用例 (main.go)

package main

import (
    "fmt"
    "sample/foo"
    "sample/bar"
)

func main() {
    foo.Test()
    fmt.Println("foo.A =", foo.A)
    bar.Test()
    fmt.Println("bar.A =", bar.A)
}
$ ls -R
.:
bar  foo  go.mod  main.go

./bar:
bar.go

./foo:
foo.go
$ go build
$ ls
bar  foo  go.mod  main.go  sample
$ ./sample
package foo
foo.A = 10
package bar
bar.A = 100

$ rm sample
$ ls
bar  foo  go.mod  main.go
$ go env GOPATH
/home/mhiroi/go
$ ls ~/go/bin
$ go install
$ ls ~/go/bin
sample
$ ls
bar  foo  go.mod  main.go
$ ~/go/bin/sample
package foo
f.A = 10
package bar
A = 100

$ go build main.go
$ ls
bar  foo  go.mod  main  main.go
$ ./main
package foo
foo.A = 10
package bar
bar.A = 100
$ go run main.go
package foo
foo.A = 10
package bar
bar.A = 100

最初に import で foo と bar をインポートします。あとは、パッケージ名 + "." + 名前で、関数や定数 (変数) にアクセスすることができます。foo.Test を実行すれば、package foo と表示され、bar.A の値を表示すると 100 になります。このように、パッケージを使うことで名前の衝突を回避することができます。

プログラムのコンパイルはコマンド go build を使うと簡単です。この場合、main パッケージが複数のファイルに分割されていてもコンパイルすることができます。実行ファイル名はデフォルトでモジュール名と同じになるようです。実行ファイル名はオプション -o で変更することができます。

go install を実行すると、main.go をコンパイルして、作成した sample を GOPATH のサブディレクトリ bin に格納します。GOPATH はデフォルトで ~/go に設定されます。go build や go install は基本的にディレクトリにあるソースファイルをすべてコンパイルします。もちろん go buiid main.go でもコンパイルできますし、go run main.go でもプログラムを実行することができます。

●import 文の使い方

import 文にはパッケージに別名を付ける機能があります。

import 別名 "パッケージ名"

たとえば、import f "sample/foo" とすると、foo.Test(), foo.A は f.Test(), f.A と書くことができます。

また、別名にピリオド ( . ) を指定すると、インポートするパッケージで定義されている名前をそのまま自分のパッケージで利用することができます。たとえば、import . "sample/bar" とすると、bar.Test(), bar.A は Test(), A と書くことができます。名前の衝突がない場合は、パッケージ名を付けなくてすむので便利です。

簡単な例を示します。

リスト : パッケージに別名を付ける

package main

import (
    "fmt"
    f "sample/foo"
    . "sample/bar"
)

func main() {
    f.Test()
    fmt.Println("f.A =", f.A)
    Test()
    fmt.Println("A =", A)
}
$ go run main.go
package foo
f.A = 10
package bar
A = 100

パッケージ foo には f という別名を付け、パッケージ bar で公開されている名前は main パッケージにそのまま取り込みます。したがって、f.Test() は package foo を表示し、Test() は package bar を表示します。また、f.A の値は 10 になり、A の値は 100 になります。

●外部パッケージのインポート

Go 言語には優れた標準ライブラリが添付されていますが、それだけではなく、サードパーティーが開発したライブラリ (モジュールやパッケージ) も多数公開されています。go.dev (本家) の Packages (pkg.go.dev) でパッケージを検索することができます。また、Go Modules を使うと外部パッケージのインポートも簡単に行うことができます。

簡単な例題として、quote package (pkg.go.dev/rsc.io/quote) を使ってみましょう。これはモジュール機能のデモンストレーション用に公開されているパッケージです。詳細は quote package のドキュメントをお読みください。

プログラムは次のようになります。

リスト : 外部ライブラリのインポート

package main

import (
    "fmt"
    "rsc.io/quote"
)

func main() {
    fmt.Println(quote.Hello())
}

import "rsc.io/quote" で外部パッケージ rsc.io/quote をインポートすることができます。pkg.dev.go で公開されているパッケージは、パスに pkg.go.dev を付けなくてもインポートできるようです。ですが、実際にパッケージがインストールされていないとコンパイルでエラーになります。パッケージのインストールには次のコマンドを使います。

go mod tidy

go mod tidy はソースファイルの import を解析して、必要となるパッケージをファイル go.mod に書き込みます。このとき、足りないパッケージは自動的にインストールされます。

それでは実際に試してみましょう。

$ mkdir sample1
$ cd sample1
$ go mod init sample1
go: creating new go.mod: module sample1
$ emacs main.go
... 省略 ...

$ ls
go.mod  main.go
$ cat go.mod
module sample1

go 1.17
$ go mod tidy
go: finding module for package rsc.io/quote
go: downloading rsc.io/quote v1.5.2
go: found rsc.io/quote in rsc.io/quote v1.5.2
go: downloading rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
mhiroi@DESKTOP-FQK6237:~/work/go/sample1$ cat go.mod
module sample1

go 1.17

require rsc.io/quote v1.5.2

require (
        golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect
        rsc.io/sampler v1.3.0 // indirect
)
$ go run main.go
こんにちは世界。

quote をインストールするとき、quote が必要とするパッケージも自動的にインストールされます。ダウンロードしたパッケージは GOPATH のサブディレクトリ pkg/mod にインストールされます。

$ go env GOPATH
/home/mhiroi/go
$ ls ~/go
bin  pkg
$ ls ~/go/pkg
mod  sumdb
$ ls ~/go/pkg/mod
cache  golang.org  rsc.io
$ ls ~/go/pkg/mod/rsc.io
quote@v1.5.2  sampler@v1.3.0
mhiroi@DESKTOP-FQK6237:~/work/go/sample1$ ls ~/go/pkg/mod/rsc.io/quote@v1.5.2
LICENSE  README.md  buggy  go.mod  quote.go  quote_test.go

●パッケージ tools の作成

それでは簡単な例題として、パズルを解くときによく使う関数をまとめたパッケージ tools を作成してみましょう。関数の一覧表を示します。

表 : tools の関数
関数名機能
func Equal(xs, ys []int) boolxs と ys が等しければ true を返す
func Dup(xs []int) []intxs を複製する
func Member(n int, xs []int) booln は xs に含まれているか
func Position(n int, xs []int) intn と等しい要素の位置を求める
func Count(n int, xs []int) intn と等しい要素を数える
func Map(f func(int) int, xs []int) []intxs の要素に関数 f を適用し、その結果を新しいスライスに格納して返す (マッピング)
func Filter(f func(int) bool, xs []int) []int関数 f が真を返す要素を新しいスライスに格納して返す (フィルター)
func Fold(f func(int, int) int, a int, xs []int) intxs の要素を関数 f で畳み込む
func Remove(n int, xs []int) []intn と等しい要素を取り除いた新しいスライスを返す
func Permutation(f func([]int), n int, xs []int)xs から n 個の要素を選ぶ順列を生成する
func PermGen(n int, xs []int) <-chan []intxs から n 個の要素を選ぶ順列を生成するチャネルを返す
func Combination(f func([]int), n int, xs []int)xs から n 個の要素を選ぶ組み合わせを生成する
func CombGen(n int, xs []int) <-chan []intxs から n 個の要素を選ぶ組み合わせを生成するチャネルを返す

プログラムは簡単なので説明は割愛します。詳細は プログラムリスト をお読みください。

●パッケージのテスト

パッケージを作成したら、それが正常に動作するかテストしましょう。Go 言語の場合、パッケージ testing とコマンド go test を使って、簡単にパッケージのテストを行うことができます。

テストプログラムはパッケージとは別のファイルに書きます。ファイル名はパッケージ名の後ろに _test.go を付けたものになります。たとえば、tools.go のテストプログラムはファイル tools_test.go に書きます。テストを実行する関数は次のように定義します。

func TestXxx(t *testing.T)

関数名は Test の後ろに英小文字以外から始まる名前 Xxx を付けます。たとえば、関数の単体テストであれば Xxx に関数名を付けます。今回はパッケージ tools のテストなので、関数名は TestTools としました。

引数の型 *testing.T にはテストで使用するメソッドが定義されていて、それらを使ってエラーの報告やテストの記録を残すことができます。たとえば、テストをパスしなかった場合は、メソッド Error でエラーを報告します。次のリストを見てください。

リスト : tools のテスト (tools_test.go)

package tools

import "testing"

func TestTools(t *testing.T) {
    a := []int{1,2,3,4,5}
    if Equal(a, []int{1,2,3,4,5}) != true {
        t.Error("Equal: not true")
    }
    if Equal(a, []int{1,2,0,4,5}) != false {
        t.Error("Equal: not false")
    }
    if Equal(a, []int{1,2,3,4,5,6}) != false {
        t.Error("Equal: not false")
    }
    if Equal(a, Dup(a)) != true {
        t.Error("Dup: error")
    }

    ・・・省略・・・

}

tools_test.go の package には tools.go と同じパッケージ名 tools を指定します。そして、パッケージ testing を import してください。

二つのスライスの等値を判定する関数 Equal に、等しいスライスを渡します。結果が true でなければテストには不合格です。t.Error でテストに失敗したことを報告します。異なるスライスを Equal に渡して、true が帰ってきた場合もテストは失敗です。同様に、t.Error で不合格を通知します。このように、Go 言語のテストプログラムは特別な構文を使うのではなく、Go 言語の普通のプログラムと変わりありません。基本的には、関数の返り値をチェックして、結果が異なればメソッドでエラーを報告していくだけです。

あとのプログラムは簡単なので説明は割愛します。詳細は プログラムリスト2 をお読みください。

テストの実行は簡単です。tools ディレクトリで go test を実行するだけです。

$ cd puzzle/tools
$ ls
tools.go  tools_test.go
$ go test
PASS
ok      puzzle/tools    0.014s

テストが正常に終了すれば PASS と表示されます。

このほかにも、パッケージ testing にはログやベンチマークをとる機能があります。また、コマンド go test にはいろいろなオプションが用意されています。詳細は Go 言語のマニュアルをお読みください。

●参考 URL

  1. Tutorial: Create a Go module, (本家)
  2. build-web-application-with-golang (日本語訳), 11.3 Goでどのようにテストを書くか
  3. Jxck_ - Qiita, Go の Test に対する考え方

●プログラムリスト

//
// tools.go : パッケージ tools
//
//            Copyright (C) 2014-2021 Makoto Hiroi
//

package tools

// xs と ys は等しいか
func Equal(xs, ys []int) bool {
    if len(xs) != len(ys) {
        return false
    }
    for i := 0; i < len(xs); i++ {
        if xs[i] != ys[i] {
            return false
        }
    }
    return true
}

// xs を複製する
func Dup(xs []int) []int {
    ys := make([]int, len(xs))
    copy(ys, xs)
    return ys
}

// n は xs に含まれるか
func Member(n int, xs []int) bool {
    for _, x := range xs {
        if n == x {
            return true
        }
    }
    return false
}

// n と等しい要素の位置を求める
func Position(n int, xs []int) int {
    for i, x := range xs {
        if n == x {
            return i
        }
    }
    return -1
}

// n と等しい要素を数える
func Count(n int, xs []int) int {
    c := 0
    for _, x := range xs {
        if n == x {
            c++
        }
    }
    return c
}

// マッピング
func Map(f func(int) int, xs []int) []int {
    ys := make([]int, len(xs))
    for i, x := range xs {
        ys[i] = f(x)
    }
    return ys
}

// フィルター
func Filter(f func(int) bool, xs []int) []int {
    ys := make([]int, 0, len(xs))
    for _, x := range xs {
        if f(x) {
            ys = append(ys, x)
        }
    }
    return ys
}

// 畳み込み
func Fold(f func(int, int) int, a int, xs []int) int {
    for _, x := range xs {
        a = f(a, x)
    }
    return a
}

// n と等しい要素を取り除く
func Remove(n int, xs []int) []int {
    return Filter(func(x int) bool { return x != n }, xs)
}

// 順列の生成
func permSub(f func([]int), n int, xs, ys []int) {
    if n == 0 {
        f(ys)
    } else {
        for _, x := range xs {
            permSub(f, n - 1, Remove(x, xs), append(ys, x))
        }
    }
}

func Permutation(f func([]int), n int, xs []int) {
    permSub(f, n, xs, make([]int, 0, n))
}

func PermGen(n int, xs []int) <-chan []int {
    ch := make(chan []int)
    go func(){
        Permutation(func(ys []int){ ch <- Dup(ys) }, n, xs)
        close(ch)
    }()
    return ch
}

// 組み合わせの生成
func combSub(f func([]int), n int, xs, ys []int) {
    switch {
    case n == 0:
        f(ys)
    case len(xs) == n:
        f(append(ys, xs...))
    default:
        combSub(f, n - 1, xs[1:], append(ys, xs[0]))
        combSub(f, n, xs[1:], ys)
    }
}

func Combination(f func([]int), n int, xs []int) {
    combSub(f, n, xs, make([]int, 0, n))
}

func CombGen(n int, xs []int) <-chan []int {
    ch := make(chan []int)
    go func(){
        Combination(func(ys []int){ ch <- Dup(ys) }, n, xs)
        close(ch)
    }()
    return ch
}

●プログラムリスト2

//
// tools_test.go : パッケージ tools の簡単なテスト
//
//                 Copyright (C) 2014-2021 Makoto Hiroi
//
package tools

import "testing"

func TestTools(t *testing.T) {
    a := []int{1,2,3,4,5}
    if Equal(a, []int{1,2,3,4,5}) != true {
        t.Error("Equal: not true")
    }
    if Equal(a, []int{1,2,0,4,5}) != false {
        t.Error("Equal: not false")
    }
    if Equal(a, []int{1,2,3,4,5,6}) != false {
        t.Error("Equal: not false")
    }
    if Equal(a, Dup(a)) != true {
        t.Error("Dup: error")
    }
    if Member(5, a) != true {
        t.Error("Member: not found 5")
    }
    if Member(0, a) != false {
        t.Error("Member: found 0")
    }
    if Position(5, a) != 4 {
        t.Error("Position: not found 5")
    }
    if Position(0, a) != -1 {
        t.Error("Position: found 0")
    }
    if Count(5, a) != 1 {
        t.Error("Count: not found 5")
    }
    if Count(0, a) != 0 {
        t.Error("Count: found 0")
    }
    square := func(x int) int { return x * x }
    b := Map(square, a)
    for i := 0; i < len(a); i++ {
        if b[i] != a[i] * a[i] {
            t.Error("Map: square error")
        }
    }
    isOdd := func(x int) bool { return x % 2 == 1 }
    c := Filter(isOdd, a)
    if len(c) != 3 || c[0] != 1 || c[1] != 3 || c[2] != 5 {
        t.Error("Filter: isOdd error")
    }
    add := func(x, y int) int { return x + y }
    if Fold(add, 0, a) != 15 {
        t.Error("Fold: add error")
    }
    d := make([][]int, 0, 24)
    for x := range PermGen(4, []int{1,2,3,4}) {
        d = append(d, x)
    }
    if len(d) != 24 ||
       !Equal(d[0], []int{1,2,3,4}) ||
       !Equal(d[23], []int{4,3,2,1}) {
        t.Errorf("PermGem: error %d, %v", len(d), d)
    }
    e := make([][]int, 0, 10)
    for x := range CombGen(3, []int{1,2,3,4,5}){
        e = append(e, x)
    }
    if len(e) != 10 ||
       !Equal(e[0], []int{1,2,3}) ||
       !Equal(e[9], []int{3,4,5}) {
        t.Errorf("CombGen: error %d, %v", len(e), e)
    }
}

初版 2014 年 4 月 20 日
改訂 2021 年 12 月 18 日

Copyright (C) 2014-2021 Makoto Hiroi
All rights reserved.

[ PrevPage | Golang | NextPage ]