パッケージとモジュール

レッスン11:パッケージとモジュール

現実世界のアナロジー: 大きな図書館を想像してください。各本はカテゴリー別に異なる棚に置かれています — 文学、科学、歴史... これが「パッケージ」です。図書館の索引システムは各棚がどこにあるかを教えてくれます — これが「モジュール」です。すべての本を一つの部屋に詰め込む必要はありません。カテゴリー分けすることで効率的に必要なものを見つけられます。Goのパッケージシステムは、コードの世界における図書館の分類です。


コアコンセプト

コンセプト 説明
パッケージ Goソースコードの組織単位。ディレクトリ内のすべての .go ファイルが同じパッケージに属する
モジュール 関連パッケージの集合で、go.mod ファイルによって定義される。依存関係管理の最小単位
import 他のパッケージからエクスポートされた識別子を使用する
エクスポートルール 大文字で始まる識別子は外部パッケージからアクセス可能。小文字はパッケージプライベート
internal パッケージ 特別なディレクトリ名。internalパッケージは親ディレクトリツリーのコードからのみインポート可能
go get リモートリポジトリからサードパーティパッケージをダウンロードしてインストール

パッケージとモジュールの関係

my-module/              ← モジュールルート、go.mod を含む
├── go.mod
├── main.go             ← package main
├── mathutil/           ← サブパッケージ mathutil
│   └── calc.go         ← package mathutil
└── internal/           ← internal パッケージ
    └── secret.go       ← package internal

基本構文と使い方

1. パッケージ宣言

すべてのGoソースファイルの最初の行package 宣言でなければなりません。

GO
package main // 実行可能プログラムのエントリーパッケージ

package mathutil // ユーティリティパッケージ
💡 ヒント: 同じディレクトリ内のすべての .go ファイルは同じパッケージ名を宣言する必要があります(通常はディレクトリ名と一致)。

2. モジュールの初期化

BASH
# プロジェクトルートディレクトリで実行
go mod init my-module

# go.mod ファイルが生成される:
# module my-module
#
# go 1.24
💡 ヒント: モジュールパスは通常リポジトリアドレスです。例えば github.com/username/my-module のようにすると、他の人が参照しやすくなります。

3. 依存関係の整理

BASH
# 不足している依存関係を追加し、未使用のものを削除
go mod tidy
💡 ヒント: 新しいサードパーティパッケージを導入するたびに go mod tidy を実行してください。

4. import 文

GO
import "fmt"                    // 標準ライブラリ
import "github.com/gin-gonic/gin" // サードパーティパッケージ

// グループ化されたimport(推奨)
import (
    "fmt"
    "log"
    "os"
)

5. エクスポートルール

GO
package mathutil

var Pi = 3.14159      // 大文字で始まる → エクスポート、外部から参照可能
var version = "1.0"   // 小文字で始まる → アンエクスポート、パッケージ内のみ

func Add(a, b int) int { // 大文字 → エクスポート
    return a + b
}

func subtract(a, b int) int { // 小文字 → アンエクスポート
    return a - b
}
💡 ヒント: エクスポートルールは変数、定数、型、関数、構造体フィールド、メソッド — すべての識別子に適用されます。

6. internal パッケージ

myapp/
├── internal/
│   └── auth/        ← myapp/ とそのサブディレクトリのみインポート可能
│       └── auth.go
└── cmd/
    └── server/
        └── main.go ← ✅ myapp/internal/auth をインポート可能
💡 ヒント: internal はコンパイラが強制するアクセス制御であり、慣例ではありません — 親ディレクトリツリー外のパッケージはインポートできません。

7. サードパーティパッケージのインストール

BASH
# パッケージをダウンロードし、go.mod / go.sum を更新
go get github.com/gin-gonic/gin

# バージョンを指定
go get github.com/gin-gonic/gin@v1.9.1

# 未使用の依存関係を削除
go mod tidy

サンプルコード

例:カスタムパッケージの作成と使用(難易度⭐)

プロジェクト構造:

greet-app/
├── go.mod
├── main.go
└── greeting/
    └── greet.go
▶ 試してみよう

greeting/greet.go:

GO
package greeting

import "fmt"

// Hello は挨拶を返す(大文字、エクスポート)
func Hello(name string) string {
    return fmt.Sprintf("こんにちは、%sさん!Goプログラミングへようこそ。", name)
}

// farewell は別れのメッセージを返す(小文字、アンエクスポート)
func farewell(name string) string {
    return fmt.Sprintf("さようなら、%sさん!", name)
}

// Goodbye は farewell のエクスポートされたラッパー(大文字、エクスポート)
func Goodbye(name string) string {
    return farewell(name)
}

main.go:

GO
package main

import (
    "fmt"
    "greet-app/greeting" // ローカルのサブパッケージをインポート
)

func main() {
    // エクスポートされた関数を使用
    fmt.Println(greeting.Hello("Alice"))
    fmt.Println(greeting.Goodbye("Alice"))

    // ❌ 以下のコードはエラーになります:farewell はアンエクスポート
    // fmt.Println(greeting.farewell("Alice"))
}

実行:

BASH
go run main.go

出力:

TEXT
こんにちは、Aliceさん!Goプログラミングへようこそ。
さようなら、Aliceさん!

例:サードパーティパッケージの使用(難易度⭐⭐)

プロジェクトの初期化:

BASH
mkdir http-demo && cd http-demo
go mod init http-demo
go get github.com/gin-gonic/gin
▶ 試してみよう

main.go:

GO
package main

import (
    "net/http"

    "github.com/gin-gonic/gin" // サードパーティのWebフレームワーク
)

// User はユーザー構造体を定義
type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    r := gin.Default() // デフォルトエンジンを作成

    // GETリクエスト:ウェルカムメッセージを返す
    r.GET("/", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "Goパッケージ管理チュートリアルへようこそ!",
        })
    })

    // GETリクエスト:ユーザー情報を返す
    r.GET("/user", func(c *gin.Context) {
        user := User{
            Name:  "Alice",
            Email: "alice@example.com",
        }
        c.JSON(http.StatusOK, user)
    })

    // サーバーを起動し、ポート8080でリッスン
    r.Run(":8080")
}

実行:

BASH
go run main.go
# サーバー起動後、http://localhost:8080 にアクセス

例:internal パッケージとマルチパッケージの連携(難易度⭐⭐⭐)

プロジェクト構造:

bank-system/
├── go.mod
├── main.go
├── account/
│   └── account.go       ← Account パッケージ(公開)
├── internal/
│   └── validator/
│       └── validator.go ← Validator パッケージ(internal のみ)
└── transaction/
    └── transaction.go   ← Transaction パッケージ(公開)
▶ 試してみよう

go.mod:

TEXT
module bank-system

go 1.24

internal/validator/validator.go:

GO
package validator

import "errors"

// ValidateAmount はトランザクション金額を検証する(internalパッケージのみ)
func ValidateAmount(amount float64) error {
    if amount <= 0 {
        return errors.New("金額はゼロより大きくなければなりません")
    }
    if amount > 1000000 {
        return errors.New("1回のトランザクションは100万を超えることはできません")
    }
    return nil
}

// ValidateAccountID はアカウントIDを検証する
func ValidateAccountID(id string) error {
    if len(id) < 6 {
        return errors.New("アカウントIDは6文字以上でなければなりません")
    }
    return nil
}

account/account.go:

GO
package account

import (
    "fmt"
    "bank-system/internal/validator" // internal パッケージをインポート
)

// Account 構造体
type Account struct {
    ID      string
    Name    string
    Balance float64
}

// NewAccount は新しいアカウントを作成する
func NewAccount(id, name string, balance float64) (*Account, error) {
    // internal パッケージの検証関数を使用
    if err := validator.ValidateAccountID(id); err != nil {
        return nil, fmt.Errorf("アカウント作成失敗: %w", err)
    }
    if balance < 0 {
        return nil, fmt.Errorf("初期残高はマイナスにできません")
    }
    return &Account{
        ID:      id,
        Name:    name,
        Balance: balance,
    }, nil
}

// Deposit は入金する
func (a *Account) Deposit(amount float64) error {
    // internal パッケージの検証関数を使用
    if err := validator.ValidateAmount(amount); err != nil {
        return fmt.Errorf("入金失敗: %w", err)
    }
    a.Balance += amount
    return nil
}

// GetBalance は残高を取得する
func (a *Account) GetBalance() float64 {
    return a.Balance
}

transaction/transaction.go:

GO
package transaction

import (
    "fmt"
    "bank-system/account"
    "bank-system/internal/validator" // internal パッケージもインポート可能
)

// Transfer は振込関数
func Transfer(from, to *account.Account, amount float64) error {
    // internal パッケージで金額を検証
    if err := validator.ValidateAmount(amount); err != nil {
        return fmt.Errorf("振込失敗: %w", err)
    }

    if from.GetBalance() < amount {
        return fmt.Errorf("振込失敗: 残高不足(現在: %.2f)", from.GetBalance())
    }

    // 振込を実行
    if err := from.Deposit(-amount); err != nil {
        return fmt.Errorf("引落し失敗: %w", err)
    }
    if err := to.Deposit(amount); err != nil {
        // ロールバック
        from.Deposit(amount)
        return fmt.Errorf("入金失敗: %w", err)
    }

    return nil
}

main.go:

GO
package main

import (
    "fmt"
    "bank-system/account"
    "bank-system/transaction"
    // ❌ 以下のインポートは失敗します:internal パッケージは外部からインポート不可
    // "bank-system/internal/validator"
)

func main() {
    // 2つのアカウントを作成
    acc1, err := account.NewAccount("ACC001", "Alice", 10000)
    if err != nil {
        fmt.Println("エラー:", err)
        return
    }

    acc2, err := account.NewAccount("ACC002", "Bob", 5000)
    if err != nil {
        fmt.Println("エラー:", err)
        return
    }

    fmt.Printf("振込前 - Alice: %.2f、Bob: %.2f\n", acc1.GetBalance(), acc2.GetBalance())

    // 振込を実行
    err = transaction.Transfer(acc1, acc2, 3000)
    if err != nil {
        fmt.Println("エラー:", err)
        return
    }

    fmt.Printf("振込後 - Alice: %.2f、Bob: %.2f\n", acc1.GetBalance(), acc2.GetBalance())

    // 無効な金額のテスト
    err = acc1.Deposit(-100)
    if err != nil {
        fmt.Println("予想されるエラー:", err)
    }
}

実行:

BASH
go run main.go

出力:

TEXT
振込前 - Alice: 10000.00、Bob: 5000.00
振込後 - Alice: 7000.00、Bob: 8000.00
予想されるエラー: 入金失敗: 金額はゼロより大きくなければなりません

現実世界の使用場面

場面1:再利用可能なロガーパッケージの構築

my-app/
├── go.mod
├── main.go
└── logger/
    └── logger.go

logger/logger.go:

GO
package logger

import (
    "fmt"
    "os"
    "time"
)

// Level はログレベル
type Level int

const (
    LevelDebug Level = iota // 0
    LevelInfo               // 1
    LevelWarn               // 2
    LevelError              // 3
)

// Logger 構造体
type Logger struct {
    prefix string
    level  Level
}

// New はロガーインスタンスを作成する
func New(prefix string, level Level) *Logger {
    return &Logger{
        prefix: prefix,
        level:  level,
    }
}

// log はログを出力する内部メソッド
func (l *Logger) log(level Level, tag, msg string) {
    if level < l.level {
        return
    }
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    line := fmt.Sprintf("[%s] [%s] [%s] %s\n", timestamp, tag, l.prefix, msg)
    os.Stdout.WriteString(line)
}

// Debug はデバッグログ
func (l *Logger) Debug(msg string) {
    l.log(LevelDebug, "DEBUG", msg)
}

// Info は情報ログ
func (l *Logger) Info(msg string) {
    l.log(LevelInfo, "INFO", msg)
}

// Warn は警告ログ
func (l *Logger) Warn(msg string) {
    l.log(LevelWarn, "WARN", msg)
}

// Error はエラーログ
func (l *Logger) Error(msg string) {
    l.log(LevelError, "ERROR", msg)
}

main.go:

GO
package main

import (
    "my-app/logger"
)

func main() {
    log := logger.New("APP", logger.LevelInfo)

    log.Debug("これはInfoレベル以下のため表示されません")
    log.Info("アプリケーションが起動しました")
    log.Warn("ディスク容量が不足しています")
    log.Error("データベース接続に失敗しました")
}

出力:

TEXT
[2026-06-26 10:30:00] [INFO] [APP] アプリケーションが起動しました
[2026-06-26 10:30:00] [WARN] [APP] ディスク容量が不足しています
[2026-06-26 10:30:00] [ERROR] [APP] データベース接続に失敗しました

場面2:レイヤードアーキテクチャでのパッケージ構成

典型的なWebサービスプロジェクトの構造:

web-service/
├── go.mod
├── go.sum
├── main.go
├── config/
│   └── config.go        ← 設定管理
├── internal/
│   ├── model/
│   │   └── user.go      ← データモデル
│   ├── repository/
│   │   └── user_repo.go ← データアクセス層
│   └── service/
│       └── user_service.go ← ビジネスロジック層
└── handler/
    └── user_handler.go  ← HTTPハンドラ層

config/config.go:

GO
package config

// Config はアプリケーション設定
type Config struct {
    Port     string
    DBHost   string
    DBPort   int
    LogLevel string
}

// Load は設定を読み込む(簡略化した例)
func Load() *Config {
    return &Config{
        Port:     "8080",
        DBHost:   "localhost",
        DBPort:   5432,
        LogLevel: "info",
    }
}

internal/model/user.go:

GO
package model

// User モデル
type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

internal/repository/user_repo.go:

GO
package repository

import (
    "fmt"
    "web-service/internal/model"
)

// UserRepository はユーザーデータリポジトリ
type UserRepository struct {
    users  map[int]*model.User
    nextID int
}

// NewUserRepository はリポジトリインスタンスを作成する
func NewUserRepository() *UserRepository {
    return &UserRepository{
        users:  make(map[int]*model.User),
        nextID: 1,
    }
}

// Create はユーザーを作成する
func (r *UserRepository) Create(name, email string) *model.User {
    user := &model.User{
        ID:    r.nextID,
        Name:  name,
        Email: email,
    }
    r.users[r.nextID] = user
    r.nextID++
    return user
}

// FindByID はIDでユーザーを検索する
func (r *UserRepository) FindByID(id int) (*model.User, bool) {
    user, ok := r.users[id]
    return user, ok
}

// FindAll はすべてのユーザーを検索する
func (r *UserRepository) FindAll() []*model.User {
    result := make([]*model.User, 0, len(r.users))
    for _, u := range r.users {
        result = append(result, u)
    }
    return result
}

// String はリポジトリの概要を返す
func (r *UserRepository) String() string {
    return fmt.Sprintf("UserRepository{合計%d人のユーザー}", len(r.users))
}

main.go:

GO
package main

import (
    "fmt"
    "web-service/config"
    "web-service/internal/repository"
)

func main() {
    // 設定を読み込む
    cfg := config.Load()
    fmt.Println("ポート:", cfg.Port)

    // リポジトリを使用
    repo := repository.NewUserRepository()
    repo.Create("Alice", "alice@example.com")
    repo.Create("Bob", "bob@example.com")

    fmt.Println(repo)

    // すべてのユーザーを検索
    for _, u := range repo.FindAll() {
        fmt.Printf("  ID=%d、Name=%s、Email=%s\n", u.ID, u.Name, u.Email)
    }
}

出力:

TEXT
ポート: 8080
UserRepository{合計2人のユーザー}
  ID=1、Name=Alice、Email=alice@example.com
  ID=2、Name=Bob、Email=bob@example.com

❓ よくある質問

質問1:パッケージ名とディレクトリ名は一致させる必要がある?

強制ではありませんが、強く推奨されます。Goの慣例では、パッケージ名はディレクトリ名と一致させるべきです。異なっていても動作しますが、混乱を招きます。

GO
// myutil/calc.go
package calculator // パッケージ名がディレクトリ名と一致しない

// インポートはパスを使用し、使用時はパッケージ名を使用
import "my-module/myutil" // パス
calculator.Add(1, 2)      // パッケージ名を使用

質問2:go mod init のモジュールパスは何でもいい?

はい、ただしリポジトリパスの使用が推奨されます:

BASH
# ✅ 推奨:他の人が参照しやすい
go mod init github.com/yourname/yourproject

# ⚠️ 問題なし:ローカルプロジェクト
go mod init myproject

# ❌ 避けるべき:特殊文字やスペース
go mod init "my project"

質問3:なぜ go.sum ファイルが必要?

go.sum は各依存関係の暗号化ハッシュを記録し、ダウンロードされたコードが改ざんされていないことを保証します。go.mod と一緒にバージョン管理にコミットすべきです:

BASH
# go.mod — 依存関係とバージョンを宣言
# go.sum — 依存関係の完全性を検証
# 両方とも不可欠
git add go.mod go.sum

質問4:同じパッケージ内の複数ファイルはどうやって整理する?

同じディレクトリ内のすべての .go ファイルは同じパッケージを共有し、インポートせずに直接互いを参照できます:

GO
// mathutil/add.go
package mathutil

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

// mathutil/multiply.go — 同じパッケージ、Add を直接使用可能
package mathutil

func Multiply(a, b int) int {
    result := 0
    for i := 0; i < b; i++ {
        result = Add(result, a) // 直接呼び出し、インポート不要
    }
    return result
}

📖 まとめ

重要ポイント 内容
パッケージ宣言 すべての .go ファイルは最初の行にパッケージ名を宣言する必要がある
モジュール初期化 go mod init <module-path>go.mod を作成
依存関係管理 go mod tidy で依存関係を整理、go get でサードパーティパッケージをインストール
エクスポートルール 大文字 = エクスポート、小文字 = パッケージプライベートのみ
internal パッケージ コンパイラが強制するアクセス制御、親ディレクトリツリーからのみインポート可能
ベストプラクティス パッケージ名はディレクトリ名と一致、責任別に整理、循環依存を回避

📝 演習

演習1:文字列ユーティリティパッケージの作成

以下のエクスポートされた関数を持つ stringutil パッケージを作成してください:

そして main.go でインポートして使用してください。

演習2:internal パッケージの練習

設定を読み込む internal/config パッケージ(構造体として)と、その設定を使用する app パッケージを持つプロジェクトを作成してください。検証:app パッケージは internal/config をインポートできますが、外部パッケージはインポートできないことを確認してください。

演習3:マルチパッケージ電卓

以下のパッケージを持つ電卓プロジェクトを設計してください:

要件:scientific パッケージは operation パッケージの関数を呼び出すことができ、すべての検証ロジックは internal/validator に配置します。


次のレッスン:構造体の練習 →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%