Menu

Go言語のパッケージとimport:公開名、複数ファイルの扱い

Goのパッケージの仕組みを解説します。package句、標準ライブラリとモジュールのパッケージのインポート、公開名と非公開名、パッケージの複数ファイルへの分割、internalパッケージ、インポートの別名、ブランクインポート、そしてgo run main.goで出る「undefined」エラーまで。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

Goのファイルはすべて、1行目で宣言したパッケージに属します。あるパッケージのコードが別のパッケージを使うには、それをインポートし、名前の前にパッケージ名を付けます。パッケージの外から使えるのは、大文字で始まる名前だけです。

fmtmathstrings は標準ライブラリのパッケージです。Goに同梱されているので、「追加する」とは import ブロックに名前を書くことを意味します。何もダウンロードされません。fmt.Printlnmath.Sqrtstrings.Repeat が大文字で始まっているのは、公開されているからです。

package句

どの .go ファイルも、コメントを除く最初の行は package name です。1つのディレクトリ内のファイルはすべて同じ名前を使う必要があり、それらが集まって1つのパッケージになります。2つの異なるパッケージ名が混在するディレクトリはコンパイルできません。

found packages greeting (b.go) and greet (greet.go) in /home/ana/shop/greet

(唯一の例外はテストファイルで、package name_test を使えます。)

package main は特別で、実行ファイルを生成し、プログラムはその func main() から始まります。それ以外のパッケージはすべて、インポートして使うためのライブラリです。

慣習として、パッケージ名はディレクトリパスの最後の要素と同じにします。shop/greet にあるコードは package greet です。パッケージ名は短く、小文字の1単語で、アンダースコアも大文字小文字の混在も使いません。string_utilsgreetHelpers ではなく、strconvhttptestgreet のようにします。

パッケージをインポートする

インポートパスは文字列です。標準ライブラリの場合はGoのソースツリー内でのパッケージのパス、それ以外はモジュールパスで始まります。

import (
	"fmt"           // standard library
	"net/http"      // standard library, nested path
	"math/rand/v2"  // standard library, version 2 of math/rand

	"github.com/google/uuid"   // another module (needs go get)
	"example.com/shop/greet"   // a package in your own module
)

パス はパッケージの置き場所で、名前 はコードで入力するものです。net/http の名前は httpmath/rand/v2 の名前は rand です。http.Get と書き、net/http.Get とは決して書きません。

gofmtgoimports はブロックを並べ替え、慣習として標準ライブラリのインポートとそれ以外を空行で区切ります。

使っていないインポートはコンパイルエラーになります。

./main.go:5:2: "os" imported and not used

Goをサポートしているエディタは保存時に goimports を実行し、足りない標準パッケージのインポートを追加し、使っていないものを削除してくれます。

公開名と非公開名

Goの可視性はたった1つのルールで決まります。名前が大文字で始まれば公開される。これは関数、型、変数、定数、構造体のフィールド、メソッドに当てはまります。publicprivateprotected といったキーワードはありません。

名前パッケージの外から見えるか
func Hello()見える
func hello()見えない
type User struct見える
User.Name フィールド見える
User.email フィールド見えない
const MaxSize見える
var defaultTimeout見えない

別のパッケージの非公開名を使おうとすると、undefined エラーになります。

./main.go:11:20: undefined: greet.prefix

このルールは1つのファイルの中でも意味を持ちます。他のパッケージがあなたの型を調べるからです。encoding/json は公開されたフィールドしか見られません。

token は何の警告もなくJSONから外されます。そのフィールドが秘密のデータなら便利な機能ですが、出力したかったフィールドを大文字にし忘れた場合は予想外の結果になります。

パッケージを複数のファイルに分ける

パッケージはいくつのファイルに分けても構いません。同じ package 行を持つディレクトリ内のファイルは、公開・非公開を問わず、他のファイルで宣言されたすべての名前を何もインポートせずに参照できます。

shop/
├── go.mod        (module example.com/shop)
├── main.go
└── math.go
// main.go
package main

import "fmt"

func main() {
	fmt.Println(total(2, 3))
}
// math.go
package main

func total(a, b int) int { return a + b }

main.gototal を直接呼んでいます。同じパッケージのファイル間に #include もインポートもありません。

go run main.goで出る「undefined」エラー

これはGoで最も検索されているパッケージの問題で、原因は1つです。main を含むファイルだけを実行すると、そのファイルだけがコンパイルされます。

go run main.go
# command-line-arguments
./main.go:6:14: undefined: total

command-line-arguments は、ファイルのリストからビルドされたパッケージにGoが付ける名前で、そのリストに math.go は入っていません。代わりにパッケージ全体を実行します。

go run .
5

go build main.go でも同じことが起きます。go run .go build(または go build ./cmd/app)を使えば、この問題は起こりません。go run . でも解決しない場合は、両方のファイルが package main になっているか、どちらかがビルド制約(//go:build)や _test.go の接尾辞で除外されていないかを確認してください。

自作のパッケージ

モジュールのサブディレクトリは、別のパッケージになります。go.mod のモジュールパスにディレクトリを付けてインポートします。

shop/
├── go.mod          (module example.com/shop)
├── main.go
└── greet/
    └── greet.go
// greet/greet.go
package greet

const prefix = "Hi, "

// Hello returns a greeting for name.
func Hello(name string) string {
	return prefix + name
}
// main.go
package main

import (
	"fmt"

	"example.com/shop/greet"
)

func main() {
	fmt.Println(greet.Hello("Ana"))
}
Hi, Ana

ここでつまずきやすい点が2つあります。インポートは相対パスではなく モジュールパスにディレクトリを付けたもの です。モジュールモードでは "./greet" は使えません。そして prefix は小文字なので main からは見えません。パッケージのAPIに含まれるのは Hello だけです。

internalパッケージ

internal という名前のディレクトリは、その中身をインポートできる相手を制限します。shop/internal/store のコードは shop/ をルートとするどのパッケージからもインポートできますが、その外からはインポートできません。別のモジュールが試みると次のエラーになります。

use of internal package example.com/shop/internal/store not allowed

自分のパッケージ間では共有したいけれど、外の世界には約束したくないコードに internal/ を使います。外部のコードが依存できないので、APIを自由に変更できます。

インポートの別名

パスの前に名前を書くと、インポートに別のローカル名を付けられます。よくある理由は、同じ名前のパッケージが2つあることです。

どちらのパッケージも名前は rand なので、少なくとも一方に別名が必要です。別名は衝突するときと、名前がわかりにくいときに使います。短くするためだけにパッケージ名を変えるのはやめましょう。読む人は strconv を知っていても、あなたの sc は知りません。

ブランクインポートとドットインポート

ブランクインポート(_)。 副作用のためだけにパッケージをインポートします。パッケージの変数と init 関数は実行されますが、名前は何も使いません。データベースドライバや画像デコーダはこの方法で自身を登録します。

import (
	"database/sql"

	_ "github.com/lib/pq" // registers the "postgres" driver with database/sql
)

_ がないと、使っていないインポートとしてエラーになります。

ドットインポート(.)。 パッケージの公開名を直接ファイルのスコープに入れるので、fmt.Println の代わりに Println と書けます。名前がどこから来たのかが見えなくなるため、いくつかのテストヘルパーのパターン以外では推奨されません。

インポートの循環

Goでは、2つのパッケージが直接、または連鎖を通じて互いをインポートすることはできません。

package example.com/shop
	imports example.com/shop/greet from main.go
	imports example.com/shop/other from c.go
	imports example.com/shop/greet from o.go: import cycle not allowed

エラーには連鎖が表示されます。ほとんどの場合、次の2つのどちらかで解決します。

  1. 両方のパッケージが必要とするコードを、両方がインポートする3つ目のより低レベルなパッケージに移す。
  2. パッケージAがBの何かを呼ぶだけなら、Aに小さなインターフェースを定義し、Bの型にそれを満たさせる。これでAはBをインポートしなくなります。

循環が起きるのはたいてい、本来ひとまとまりであるべきコードの層の間にパッケージの境界を引いているときです。分け方を見直すきっかけにもなります。

よくある間違い

  • 1つのファイルだけを実行する。 複数ファイルのパッケージで go run main.go を使っている。go run . を使います。
  • 相対インポート。 モジュールモードでは import "./utils" は失敗します。"example.com/yourmodule/utils" を使います。
  • 公開するつもりの名前が小文字。 他のパッケージが必要とする関数、フィールド、メソッドは大文字で始める必要があります。
  • ディレクトリと一致しないパッケージ名。 文法上は問題ありませんが、インポートパスは utils なのにコードでは helpers.X となり混乱します。同じにしておきましょう。
  • 何でも入れるパッケージ名。 utilcommonmisc は中身について何も語らず、インポート循環を引き寄せるように育ちがちです。moneyauthslug のように、提供するものの名前を付けましょう。

よくある質問

同じパッケージにある関数が、なぜundefinedと言われるのですか?

おそらく go run main.go を実行しています。ファイル名を指定するとそのファイルだけがコンパイルされるので、同じパッケージの他のファイルで定義した関数が見つからず undefined: total になります。代わりに、ディレクトリ内のすべての .go ファイルをコンパイルする go run . を実行してください。すべてのファイルの package 行が同じかどうかも確認しましょう。

Goでfmtパッケージを追加するには?

fmt は標準ライブラリの一部でGoに同梱されているので、インストールするものはありません。package 行の後に import "fmt" を追加し、fmt.Println("hi") のように fmt. を付けて関数を呼びます。stringsostimenet/http など他の標準パッケージも同じです。

自作のパッケージをインポートするには?

モジュールパスにディレクトリを付けてインポートします。example.com/shop という名前のモジュールで greet フォルダにあるパッケージは、import "example.com/shop/greet" でインポートし、greet.Hello() として使います。インポートする側から見えるのは、大文字で始まる名前だけです。

Goの公開(exported)と非公開(unexported)の違いは何ですか?

大文字で始まる名前(HelloTotalUser.Name)は公開され、他のパッケージから使えます。小文字の名前(hellototaluser.name)は非公開で、そのパッケージの中からしか見えません。publicprivate といったキーワードはありません。

「import cycle not allowed」はどう直せばよいですか?

2つのパッケージが直接、または連鎖を通じて互いをインポートしており、Goはこれを禁止しています。共有するコードを、両方がインポートする3つ目のパッケージに移すか、振る舞いを必要とする側のパッケージに小さなインターフェースを定義して、もう一方をインポートしなくて済むようにします。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める