M.Hiroi's Home Page

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

応用編 : パッケージ


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

はじめに

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

たとえば、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 の作成

それでは簡単な例題として、パズルを解くときによく使う関数をまとめたパッケージ 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 tools
$ ls
go.mod  tools.go  tools_test.go
$ go test
PASS
ok      tools   0.003s

テストが正常に終了すれば 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 日