👁

Goモジュールとパッケージ

この章では、Goのモジュール管理とパッケージ分割・外部パッケージの取り込みについて学び、ハンズオン形式で学習します。これにより、go mod による依存管理、自作パッケージの分割とエクスポート規約、外部パッケージの導入までが身につきます。

1. 本章の概要

1.1 本章の目的

複数のファイルにまたがるコードや、外部のライブラリを使うプログラムを書くには、「このプロジェクトはどのモジュールで、何に依存しているか」を管理する仕組みが必要です。Goでは Goモジュールgo mod)でこれを扱います。本章では、Goモジュールの作成と依存管理、自作パッケージの分割とエクスポート規則、そして外部パッケージの導入までを扱います。

1.2 ハンズオンの流れ

go mod init コマンドから始めて、モジュール構成を作ったうえで、自作パッケージの分割と外部パッケージの導入を順に体験します。

1.3 事前準備

必要なツール

この章では、以下のツールを使用します。まだインストールしていない場合は、リンク先の手順に沿って準備をお願いします。

ツール名 関連箇所 理由
Visual Studio Code Visual Studio Codeのインストール Goのコードを記述するエディタとして使用する
Go Goのインストール 本章のサンプルコードを実行する言語処理系として使用する

2. モジュールとパッケージ

これまでの章では、main.go 1ファイルにすべてのコードを書いてきました。学習用のサンプルなら見通せますが、実際のプログラムでは扱う関数や構造体が増え、1ファイルが数百行・数千行に膨らんでいくと、目的のコードを探すだけで一苦労になります。

たとえば以下のように、計算まわり・文字列まわり・ユーザ情報などを1ファイルに詰め込むと、何をやっているファイルかがひと目では分からなくなります。

package main

import "fmt"

// 計算まわり
func Add(a, b int) int      { return a + b }
func Multiply(a, b int) int { return a * b }

// 文字列まわり
func Shout(s string) string   { return s + "!!!" }
func Whisper(s string) string { return "(" + s + ")" }

// ユーザ情報
type User struct{ Name string }

func main() {
    fmt.Println(Add(3, 5))
    fmt.Println(Shout("hello"))
    fmt.Println(User{Name: "Alice"})
}

関数や構造体などの機能単位で ファイルを分けたい、というのが自然な要求です。Goでは、このファイル分割を パッケージ という単位で行います。同じフォルダに置いたファイルは1つのパッケージとして扱われ、フォルダを分けることで別のパッケージに切り離せます。

そして、複数のパッケージからなるプロジェクト全体を1つの管理単位としてまとめ、外部ライブラリへの依存も記録する仕組みが モジュール です。

モジュールとパッケージの関係は、モジュール(プロジェクト全体の管理単位)の中に複数のパッケージ(フォルダ)が並ぶ 入れ子構造 になります。GitHub の1リポジトリがモジュール、その中のフォルダがパッケージ、と対応させて考えると分かりやすいです。たとえば、冒頭の肥大化した1ファイル(計算・文字列・ユーザ情報)を機能ごとに分けるなら、以下のような構成になります。

github.com/example/myapp/  ← モジュール(1リポジトリ相当)
├── go.mod
├── main.go
├── calculator/            ← パッケージ(フォルダ単位)
│   └── calculator.go
├── text/                  ← パッケージ(フォルダ単位)
│   └── text.go
└── user/                  ← パッケージ(フォルダ単位)
    └── user.go

上位の管理単位(モジュール)と、その中で責務ごとにまとめるコード群(パッケージ)を組み合わせることで、大きなプロジェクトでも整理された状態を保てます。

まずはモジュールの作り方(go mod init コマンド)と依存管理ファイル(go.mod / go.sum)から見ていきます。

2.1 go mod initによるモジュール作成

新しいプロジェクトを始めるときは、プロジェクトのフォルダの中で go mod init コマンドを実行し、そのフォルダをモジュールとして初期化します。任意の場所に myapp フォルダを作成し、Visual Studio Codeの「ファイル」→「フォルダーを開く」から作成した myapp フォルダを開きます。以降の操作は、Visual Studio Codeのターミナルから行います。

myapp/  ← このフォルダを作成

モジュール名は、他のプロジェクトからこのモジュールを取り込むときに、そのモジュールを指し示す 識別子 になります。他のモジュールと名前が衝突しないよう、世界で一意にできる リポジトリのパス(例: github.com/<ユーザ名>/<リポジトリ名>)をそのまま使うのが慣例です。この形にしておくと、go get コマンドで外部から取得するときにも、モジュール名がそのまま入手先の URL として機能します。ここでは仮の名前として github.com/example/myapp にしておきます(example はドキュメント例示用の予約ドメインです)。あとから外部に公開する予定がなくても、この形で名前を付けておくと将来の衝突を避けられます。

以下のコマンドで、Go がインストールされていることを確認します。

go version

以下のように Go のバージョンが表示されれば、インストールは確認できています。

go version go1.x.x darwin/arm64

バージョンが表示されない場合は、Goのインストール を先に実施してください。

インストールが確認できたら、以下のコマンドで、モジュールを初期化します。

go mod init github.com/example/myapp

以下のような実行結果が表示されます。

go: creating new go.mod: module github.com/example/myapp

指定したモジュール名で go.mod が新しく作られたことが分かります。フォルダの中身は以下のようになります。

myapp/
└── go.mod  ← 自動生成される

2.2 go.mod

生成された go.mod の中身を開くと、以下のようになっています。

module github.com/example/myapp

go 1.22

コードを解説します。

module github.com/example/myapp

このモジュールの名前を宣言する行です。他のパッケージからこのモジュールを取り込むときに、この名前がベースとして使われます(取り込む書き方は、本章後半の「パッケージの分割」で import として扱います)。

go 1.22

このプロジェクトが対応する Go のバージョンを示します。ここに書かれる値は、go mod init コマンドを実行したときにインストールされている Go 本体のバージョンから自動で決まるため、実際に表示される値は環境によって変わります(go version コマンドで手元のバージョンを確認できます)。以降、外部パッケージを導入すると require の行が自動で追記されていきます。

なお、go.mod と対になる go.sum(依存モジュールのチェックサムを記録するファイル)は、外部パッケージを取り込んだ時点で自動生成されます。実物が現れる本章後半の「外部パッケージの導入」で詳しく扱います。

3. パッケージの分割

前節までで myapp モジュールを初期化しました。ここからは、モジュールの中身を パッケージ に分割し、機能ごとにフォルダを分けて整理していきます。冒頭で触れたとおり、Goでは同じフォルダに置いたファイルが1つのパッケージとして扱われ、フォルダを分けることで別のパッケージに切り離せます。ここでは、myapp の中に計算ロジック専用の calculator パッケージを追加します。

3.1 フォルダ構成

次のような構成で、計算ロジックを別パッケージに切り出す例を見ます。

myapp/
├── go.mod
├── main.go
└── calculator/
    └── calculator.go

main.go はエントリポイント、calculator/calculator.go は計算ロジックを分離したパッケージです。以降、提供側の calculator/calculator.go → 利用側の main.go の順に実装していきます。

3.2 提供側の実装

本節では、まずcalculator パッケージが Add / Multiply 関数を提供する側の実装を書きます。ファイルの先頭に package パッケージ名 を宣言することで、そのファイルの所属パッケージを示します。書き方は以下のとおりです。

package パッケージ名

// 関数などの定義

以下が calculator/calculator.go の実装です。

package calculator

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

func Multiply(a, b int) int {
    return a * b
}

コードを解説します。

package calculator

calculator/calculator.go の先頭で package calculator を宣言しています。これで、このフォルダのファイルはすべて calculator パッケージに属することを Go に伝えます。

3.3 利用側の実装と実行

続いて、main.go から calculator パッケージを呼び出す側のコードを書きます。他パッケージの関数を呼ぶには、import にモジュール名 + フォルダ名を書き、パッケージ名.関数名 の形で呼び出します。書き方は以下のとおりです。

import "モジュール名/フォルダ名"

パッケージ名.関数名(引数)

以下が main.go の実装です。

package main

import (
    "fmt"

    "github.com/example/myapp/calculator"
)

func main() {
    fmt.Println(calculator.Add(3, 5))
    fmt.Println(calculator.Multiply(3, 5))
}

実行すると以下のような実行結果が表示されます。

8
15

calculator.Add(3, 5) の結果 8calculator.Multiply(3, 5) の結果 15 が出力されました。パッケージを分けても、正しく import して呼び出せていることが分かります。

コードを解説します。

import (
    "fmt"

    "github.com/example/myapp/calculator"
)

main.go からは、importモジュール名 + フォルダ名github.com/example/myapp/calculator)を書くことで、calculator パッケージを読み込めます。標準ライブラリの fmt と分けて書くのが慣習で、間に空行を1つ入れます。

fmt.Println(calculator.Add(3, 5))

他パッケージの関数は パッケージ名.関数名 の形で呼び出します。ここでは calculator.Add(3, 5)calculator パッケージの Add を呼び出しています。

4. 外部パッケージの導入

前節までで、自作パッケージを作って呼び出す仕組みを扱いました。実際には、ゼロからすべてを自作するのではなく、コミュニティが公開している 外部パッケージ(他の人が作って公開しているパッケージ)を組み合わせて開発するのが一般的です。Goでは、HTTPクライアント・Webフレームワーク・DBドライバ・UUID生成など、よく使われる機能が数多く外部パッケージとして公開されており、go get コマンド1つで自分のプロジェクトに取り込めます。ここでは、外部パッケージを取り込んで自分のコードから呼び出す流れを、実際に UUID 生成のライブラリで体験します。

4.1 go get

go get は、外部パッケージを取り込むためのコマンドです。ここでは UUID 生成ライブラリを例に、コマンドの実行から、生成される go.sum ファイル、実際にコードから呼び出すところまでを扱います。

コマンドの実行

例として、github.com/google/uuid パッケージを導入してみます。UUID(Universally Unique Identifier)は、世界中で重複しないように設計された識別子で、ユーザIDやセッションIDなど、他と衝突しない一意な文字列が必要な場面で使われます。このパッケージは、その UUID を Go で生成するためのライブラリです。

外部パッケージを取り込むには、go get コマンドにパッケージ名を渡して実行します。

go get github.com/google/uuid

以下のような実行結果が表示されます。

go: added github.com/google/uuid v1.6.0

指定したパッケージがバージョン付きで追加されたことが分かります。go.mod に依存が追記され、あわせて新しく go.sum ファイルが生成されます。

go.sum の生成

先ほど go get コマンドを実行したことで、myapp/ に新しく go.sum というファイルが生成されました。

myapp/
├── go.mod
└── go.sum   ← go get で新しく生成される

go.sum は、依存する外部モジュールごとに 中身のチェックサム(ハッシュ値)を記録するファイルです。ダウンロードしたモジュールから計算されるハッシュがここに書き込まれ、次回以降のビルド時に、取得したモジュールから再計算したハッシュと照合されます。一致しなければビルドを中断するため、悪意のある差し替えや偶発的なファイル破損に気づかず使ってしまう事故を防げます。

go.sumgo get や後述する go mod tidy コマンドを実行したタイミングで自動的に作成・更新されるため、手で編集する必要はありません。

go.modgo.sum はペアで 依存管理を担う2ファイル として、どちらも Git 管理対象に含めます。チーム全員が同じチェックサムを共有することで、環境ごとに異なる中身のモジュールが混ざる状況を避けられます。

取り込んだパッケージの呼び出し

取り込んだ外部パッケージは、import に書くだけで自分のコードから呼び出せます。次のコードは、uuid.New() で新しい UUID を生成して表示する例です。

package main

import (
    "fmt"

    "github.com/google/uuid"
)

func main() {
    id := uuid.New()
    fmt.Println(id)
}

以下のような実行結果が表示されます(実行のたびに UUID は変わります)。

7f8a3c4e-9b2d-4f1a-8e5c-0d1f2b3c4e5d

uuid.New() が返した新しい UUID の文字列表現が出力されました。実行のたびに異なる値が生成されます。

コードを解説します。

import (
    "fmt"

    "github.com/google/uuid"
)

importgithub.com/google/uuid を追加することで、外部パッケージ uuid を使えるようになります。標準ライブラリ(fmt)と外部パッケージの間には空行を1つ入れる慣習です。

id := uuid.New()
fmt.Println(id)

uuid.New() を呼ぶと新しい UUID が生成されます。fmt.Println で表示すると、実行のたびに異なる UUID 文字列が出力されます。

4.2 go mod tidy

go mod tidy は、コードの import 状況に合わせて go.modgo.sum双方向に同期 するコマンドです。使われている依存だけを残し、使われていない依存は削除、まだ go.mod に載っていない import があれば追加します。先ほど導入した uuid を使って、削除と追加の両方の動きを確認します。

使わなくなった依存の削除

本節では、まず削除方向の動きを見ていきます。main.go から uuidimport と使用箇所を取り除き、以下の内容に書き換えます。

package main

import "fmt"

func main() {
    fmt.Println("hello")
}

この段階では、main.go から uuid は参照されていませんが、go.mod にはまだ require github.com/google/uuid ... の行が残ったままです。go mod tidy コマンドを実行します。

go mod tidy

実行後、go.mod を開いて中身を確認します。

module github.com/example/myapp

go 1.22

require github.com/google/uuid の行が消えています。あわせて go.sum からも該当のチェックサム行が削除されます。実際の import に登場しない依存が、go.mod / go.sum から自動的に整理されたことが分かります。

import からの依存追加

次に、追加方向を確認します。まだ go.mod に載っていない import を書いてから go mod tidy コマンドを実行すると、依存が自動で go.mod / go.sum に追加されます。試しに main.go に先ほどの uuidimport を戻します。

package main

import (
    "fmt"

    "github.com/google/uuid"
)

func main() {
    id := uuid.New()
    fmt.Println(id)
}

この段階では go.moduuid の依存はありません。go mod tidy コマンドを実行します。

go mod tidy

以下のような実行結果が表示されます。

go: finding module for package github.com/google/uuid
go: found github.com/google/uuid in github.com/google/uuid v1.6.0

実行後、go.mod を開いて中身を確認します。

module github.com/example/myapp

go 1.22

require github.com/google/uuid v1.6.0

require github.com/google/uuid v1.6.0 の行が新しく追記されています。あわせて go.sum にもチェックサムが記録されます。import に書いた段階ではまだ取得されていなかった依存が、go mod tidy コマンドによってダウンロード・記録されたことが分かります。

このように、go mod tidy コマンドはコード中の import を基準に依存を揃えてくれるため、go get コマンドを明示的に呼ばなくても、import を書いてから go mod tidy コマンドを実行するだけで新しい外部パッケージを取り込めます。実装が一段落したタイミングで go mod tidy コマンドを実行しておくと、依存の記述が実際の import 状況と揃った状態を保てます。

5. アクセス制御

Goには、パッケージ内で定義した関数や型を、パッケージの外から呼び出せるようにするか/内部でだけ使えるようにするかを制御する仕組みがあります。ここでは、その仕組みを実際にコードで確認していきます。

5.1 呼び出しの制御

Goでは、識別子の先頭文字が大文字か小文字か によって、パッケージの外から呼び出せるかどうかが決まります。大文字始まりの識別子は エクスポート されてパッケージ外から参照でき、小文字始まりの識別子は 非エクスポート となり、そのパッケージの中でしか使えません。書き方は以下のとおりです。

// 大文字始まり → エクスポート(他パッケージから参照可)
func Xxx() { ... }

// 小文字始まり → 非エクスポート(パッケージ内でのみ参照可)
func xxx() { ... }

以降、エクスポート識別子・非エクスポート識別子の順に、実際にコードで挙動を確認していきます。

5.2 エクスポート識別子

先の ## パッケージの分割 で、calculator.Addmain.go から呼び出せることは確認しました。ここでは新しく大文字始まりの Subtract 関数を追加して、同じようにエクスポート識別子として外から呼び出せることを試してみます。

calculator/calculator.goSubtract 関数を追加して、以下の内容に書き換えます。

package calculator

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

// 大文字始まり → エクスポート(外から呼び出せる)
func Subtract(a, b int) int {
    return a - b
}

main.goSubtract を呼び出すコードに書き換えます。

package main

import (
    "fmt"

    "github.com/example/myapp/calculator"
)

func main() {
    fmt.Println(calculator.Subtract(10, 3))
}

実行すると以下のような結果が表示されます。

7

Subtract は大文字始まりでエクスポートされているため、calculator パッケージの外にある main.go からも calculator.Subtract(...) の形で参照でき、呼び出しが成功します。これがエクスポート識別子の実例です。

動作確認が終わったら、calculator.goSubtract 関数と main.go の該当行は削除して元の状態に戻しておきます。

5.3 非エクスポート識別子

小文字始まりの識別子は、同一パッケージ内からは自由に使えます が、パッケージの外からは呼び出せません。実際に両方の挙動を試してみます。

検証用コードの準備

calculator/calculator.go に、小文字始まりの multiply 関数と、それを内部で呼び出すエクスポート関数 Square を追加して、以下のように書き換えます。

package calculator

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

// 小文字始まり → 非エクスポート
func multiply(a, b int) int {
    return a * b
}

// エクスポート関数 → 同一パッケージ内なので multiply を呼び出せる
func Square(n int) int {
    return multiply(n, n)
}

Square は同じ calculator パッケージ内にあるため、非エクスポートの multiply を問題なく呼び出せる形になっています。この状態で、パッケージ内からの呼び出しとパッケージ外からの呼び出しを、それぞれ試していきます。

パッケージ内からの呼び出し

まず、main.go から Square を呼び出して、Square の中から multiply が呼び出せていることを確認します。

package main

import (
    "fmt"

    "github.com/example/myapp/calculator"
)

func main() {
    fmt.Println(calculator.Square(4))
}

実行すると以下のような結果が表示されます。

16

Square(4) は内部で multiply(4, 4) を呼び、その結果 16 が返っています。非エクスポートの multiply も、同じパッケージ内からcalculator/calculator.goSquare から)は問題なく呼び出せていることが分かります。

パッケージ外からの呼び出し

次に、main.go を書き換えて、multiplyパッケージ外から直接 呼び出そうとしてみます。

package main

import (
    "fmt"

    "github.com/example/myapp/calculator"
)

func main() {
    fmt.Println(calculator.multiply(3, 5))
}

この状態で実行しようとすると、以下のようなコンパイルエラーが表示されます。

./main.go:10:17: cannot refer to unexported name calculator.multiply
./main.go:10:17: undefined: calculator.multiply

multiplyunexported name(非エクスポート識別子)として、calculator パッケージの外にある main.go からは参照できずコンパイルが通りません。

まとめると、multiplycalculator パッケージ内からは呼び出せるSquare 経由で確認)ものの、パッケージ外の main.go からは呼び出せない(コンパイルエラー)ということが実際のコードで確認できました。

動作確認が終わったら、calculator.gomultiply / Square 関数と、main.go の該当行はどちらも削除して元の状態に戻しておきます。

5.4 適用範囲

エクスポート/非エクスポートのルールは関数だけでなく、変数・定数・構造体の型・構造体のフィールド など、Go の識別子全般に同じように適用されます。それぞれ、書き方を対比で見ていきます。

変数

パッケージレベルで宣言する変数も同じルールに従います。

// 大文字始まり → エクスポート
var Xxx = ...

// 小文字始まり → 非エクスポート
var xxx = ...

定数

const で宣言する定数も同様です。

// 大文字始まり → エクスポート
const Xxx = ...

// 小文字始まり → 非エクスポート
const xxx = ...

構造体の型

構造体の型定義も、型名の先頭文字でエクスポート/非エクスポートが決まります。

// 大文字始まり → エクスポート
type Xxx struct{ ... }

// 小文字始まり → 非エクスポート
type xxx struct{ ... }

構造体のフィールド

構造体のフィールドは、フィールド名の先頭文字で フィールドごと にエクスポート範囲が決まります。

type User struct {
    Name string  // 大文字 → エクスポート(他パッケージから u.Name で参照可)
    age  int     // 小文字 → 非エクスポート(同一パッケージ内でのみ参照可)
}

この User 型を他のパッケージから使うと、u.Name は参照できますが、u.age は先ほどの multiply と同じ理由でコンパイルエラーになります。構造体を設計するときは、外部に触ってほしいフィールドだけを大文字始まりにする、という書き分けが基本になります。

命名だけでエクスポート範囲が決まるため、public / private などのアクセス修飾子キーワードは Go に存在しません。「外に見せるものは大文字始まりで書く」という命名規約1つでアクセス制御ができる、シンプルな設計になっています。

6. まとめ

この章では、Goのモジュールとパッケージ、そしてアクセス制御を学びつつ、実際にモジュール作成・自作パッケージの分割・外部パッケージの導入を体験しました。

  • go mod init コマンドで go.mod を作り、モジュール名はGitHubパスなどの慣例で命名する
  • 同じフォルダのファイルが1つのパッケージになり、フォルダを分けることで別パッケージにできる
  • 外部パッケージは go get コマンドで明示的に取り込むほか、import を書いてから go mod tidy コマンドを実行しても追加でき、使わなくなった依存も go mod tidy コマンドで自動的に整理される
  • 依存管理は go.mod(バージョン記述)と go.sum(チェックサム)の2ファイルで行い、どちらもGit管理対象として扱う
  • 大文字始まりの識別子だけがエクスポートされる(外から参照できる)という命名規約で、アクセス制御が決まる

次の章では、Goの代表的な標準ライブラリを学びつつ、実際に fmtosstringsbufioiotimeencoding/json の使い方を体験します。

この教材は役に立ちましたか?

いいねをたくさんいただけると、制作者の励みになり、より多くのセクションが作れるようになります。

感想を一言(任意)

いただいたコメントは次の制作のヒントになります。ぜひお気軽にご投稿ください。

このコメントは他の受講生には公開されません。DevOps Camp運営が、教材改善のために確認します。

0 / 2000