プログラムを作っていると、以前作った関数と同じ処理が必要になる場合があります。いちばんてっとり早い方法はソースファイルからその関数をコピーすることですが、賢明な方法とはいえません。このような場合、自分で作成した関数をライブラリとしてまとめておくと便利です。ライブラリの作成で問題になるのが「名前の衝突」です。複数のライブラリを使うときに、同じ名前の関数や変数が存在すると、そのライブラリは正常に動作しないでしょう。この問題は「パッケージ (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.23.2
ディレクトリ 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 $ 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 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 (https://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.23.2 $ 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 $ cat go.mod module sample1 go 1.23.2 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 $ ls ~/go/pkg/mod/rsc.io/quote@v1.5.2 LICENSE README.md buggy go.mod quote.go quote_test.go
それでは簡単な例題として、パズルを解くときによく使う関数をまとめたパッケージ tools を作成してみましょう。関数の一覧表を示します。
関数名 | 機能 |
---|---|
func Equal(xs, ys []int) bool | xs と ys が等しければ true を返す |
func Dup(xs []int) []int | xs を複製する |
func Member(n int, xs []int) bool | n は xs に含まれているか |
func Position(n int, xs []int) int | n と等しい要素の位置を求める |
func Count(n int, xs []int) int | n と等しい要素を数える |
func Map(f func(int) int, xs []int) []int | xs の要素に関数 f を適用し、その結果を新しいスライスに格納して返す (マッピング) |
func Filter(f func(int) bool, xs []int) []int | 関数 f が真を返す要素を新しいスライスに格納して返す (フィルター) |
func Fold(f func(int, int) int, a int, xs []int) int | xs の要素を関数 f で畳み込む |
func Remove(n int, xs []int) []int | n と等しい要素を取り除いた新しいスライスを返す |
func Permutation(f func([]int), n int, xs []int) | xs から n 個の要素を選ぶ順列を生成する |
func PermGen(n int, xs []int) <-chan []int | xs から n 個の要素を選ぶ順列を生成するチャネルを返す |
func Combination(f func([]int), n int, xs []int) | xs から n 個の要素を選ぶ組み合わせを生成する |
func CombGen(n int, xs []int) <-chan []int | xs から 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 tools $ ls go.mod tools.go tools_test.go $ go test PASS ok tools 0.003s
テストが正常に終了すれば PASS と表示されます。
このほかにも、パッケージ testing にはログやベンチマークをとる機能があります。また、コマンド go test にはいろいろなオプションが用意されています。詳細は Go 言語のマニュアルをお読みください。
// // 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 }
// // 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) } }