Go言語入門:依存関係の管理 -Vol.12-

スポンサーリンク
Go言語入門:依存関係の管理 -Vol.12- ノウハウ
Go言語入門:依存関係の管理 -Vol.12-
この記事は約16分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語の依存関係の管理ついて解説しています。

スポンサーリンク

背景

Goでアプリケーションを開発していると、必ずと言っていいほど外部パッケージに依存することになります。HTTPルーターやデータベースドライバ、ロギングライブラリなど、車輪の再発明を避けて開発を効率化するために、私たちは日常的にこれらの外部モジュールを利用しています。

しかし、依存関係の管理は「最初に導入したら終わり」というわけにはいきません。セキュリティパッチのリリース、新機能の追加、破壊的変更への対応など、時間の経過とともに依存パッケージのアップグレードや置き換えが必要になってきます。また、複数の開発者が関わるプロジェクトでは、全員が同じバージョンの依存関係を使用できるよう、一貫性を保つことも重要です。

本記事では、Goが提供する依存関係管理ツールを使って、外部依存関係を取り込みながらもアプリケーションの安全性を保つ方法について解説します。公式ドキュメントの内容を日本語で紹介しながら、実際の開発現場で役立つ依存関係管理のベストプラクティスをお伝えしていきます。

コードの依存関係の同期

コードでインポートされているすべてのパッケージの依存関係を管理しながら、もはやインポートしていないパッケージの依存関係を削除できます。

これは、コードと依存関係に変更を加えている場合に便利です。管理されている依存関係とダウンロードされたモジュールのコレクションが、コードでインポートされているパッケージによって特に必要とされるコレクションと一致しなくなった可能性があります。

管理されている依存関係のセットを整理された状態に保つには、go mod tidyコマンドを使用します。コードでインポートされているパッケージのセットを使用して、このコマンドはgo.modファイルを編集し、必要だが欠けているモジュールを追加します。また、関連するパッケージを提供しない未使用のモジュールも削除します。

このコマンドには、削除されたモジュールに関する情報を出力する-vフラグを除いて、引数はありません。

$ go mod tidy

解説

go mod tidyの役割

go mod tidyは、部屋の片付けに似ています:

部屋の片付けgo mod tidy
使っているものを確認import文を解析
必要なものを追加不足している依存関係を追加
使っていないものを捨てる未使用の依存関係を削除
きれいに整理go.modとgo.sumを整理
定期的に実施開発中に頻繁に実行

go mod tidyの基本

コマンドの基本形式

# 基本的な使い方
go mod tidy

# 詳細情報を表示
go mod tidy -v

go mod tidyが行うこと

主な機能:

1. 不足している依存関係の追加
   └─ import文を解析して必要なパッケージを特定
   └─ go.modに追加

2. 未使用の依存関係の削除
   └─ もう使っていないパッケージを特定
   └─ go.modから削除

3. go.sumファイルの更新
   └─ 必要なチェックサムを追加
   └─ 不要なチェックサムを削除

4. 間接的な依存関係の整理
   └─ // indirectマーカーを適切に設定

実践例: go mod tidyの動作

例1: 不足している依存関係の追加

シナリオ: 新しいパッケージをインポートしたがgo.modが更新されていない

// main.go
package main

import (
    "fmt"
    "github.com/gin-gonic/gin"        // すでにgo.modに記載
    "github.com/joho/godotenv"        // 新しく追加(go.modにまだない)
)

func main() {
    godotenv.Load()  // 使用
    
    r := gin.Default()
    r.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "Hello"})
    })
    r.Run()
}

go.mod (tidy実行前):

module example.com/myapp

go 1.21

require github.com/gin-gonic/gin v1.9.1

require (
    github.com/bytedance/sonic v1.9.1 // indirect
    // ... その他の間接的依存関係
)

go mod tidyを実行:

$ go mod tidy

# 実行時の動作:
# 1. main.goを解析
# 2. godotenvがインポートされているが、go.modにない
# 3. godotenvを自動的に追加

go.mod (tidy実行後):

module example.com/myapp

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/joho/godotenv v1.5.1  // ← 自動追加
)

require (
    github.com/bytedance/sonic v1.9.1 // indirect
    // ... その他の間接的依存関係
)

例2: 未使用の依存関係の削除

シナリオ: 古いコードを削除して、パッケージが不要になった

// main.go (リファクタリング後)
package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
    // "github.com/old/package" ← もう使っていない
)

func main() {
    r := gin.Default()
    r.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "Hello"})
    })
    r.Run()
}

go.mod (tidy実行前):

module example.com/myapp

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/old/package v1.0.0      // ← もう使っていない
    github.com/unused/lib v2.1.0       // ← もう使っていない
)

go mod tidyを実行:

$ go mod tidy -v

# 出力:
# unused github.com/old/package
# unused github.com/unused/lib

go.mod (tidy実行後):

module example.com/myapp

go 1.21

require github.com/gin-gonic/gin v1.9.1

require (
    github.com/bytedance/sonic v1.9.1 // indirect
    // ... その他の間接的依存関係
)

// github.com/old/package と github.com/unused/lib は削除された

例3: 間接的依存関係の整理

シナリオ: 直接使っていたパッケージを削除したが、間接的には必要

// 変更前: ginとechoの両方を直接使用
import (
    "github.com/gin-gonic/gin"
    "github.com/labstack/echo/v4"
)

// 変更後: ginのみ使用(echoは削除)
import (
    "github.com/gin-gonic/gin"
)

go.mod (tidy実行前):

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/labstack/echo/v4 v4.11.0  // 直接的な依存関係として記載
)

go mod tidyを実行:

$ go mod tidy

go.mod (tidy実行後):

require github.com/gin-gonic/gin v1.9.1

// echoは完全に削除(ginの間接的依存関係でもないため)

go mod tidy の詳細な動作

解析プロセス

go mod tidyの内部動作:

ステップ1: ソースコード解析
├─ すべての.goファイルを読み込む
├─ import文を抽出
└─ 必要なパッケージリストを作成

ステップ2: 依存関係ツリーの構築
├─ 各パッケージの依存関係を解析
├─ 直接的依存関係と間接的依存関係を分類
└─ 最小バージョン選択(MVS)を適用

ステップ3: go.modの更新
├─ 不足しているrequireを追加
├─ 不要なrequireを削除
└─ indirectマーカーを適切に設定

ステップ4: go.sumの更新
├─ 新しいモジュールのチェックサムを追加
├─ 削除されたモジュールのチェックサムを削除
└─ ファイルを最適化

-vフラグの使用

詳細情報を表示:

$ go mod tidy -v

# 出力例:
# unused github.com/old/package
# unused github.com/deprecated/lib
# unused github.com/test/helper

出力の意味:

unused [モジュールパス]

意味:
- このモジュールはgo.modに記載されていた
- しかし、コードでは使用されていない
- go mod tidyによって削除された

実践的な使用シナリオ

シナリオ1: 開発中の定期的な整理

日常的な開発ワークフロー:

# 朝: 作業開始
$ git pull
$ go mod tidy  # チームメンバーの変更に同期

# 開発中: 新機能を追加
# - 新しいパッケージをインポート
# - 古いコードを削除

# コミット前: 整理
$ go mod tidy
$ git add go.mod go.sum
$ git commit -m "Add new feature"

# 夜: 作業終了前
$ go mod tidy  # 最終確認
$ go test ./...
$ git push

シナリオ2: 大規模なリファクタリング後

リファクタリングの完全なワークフロー:

# ===== リファクタリング開始 =====

# 現状を記録
$ go list -m all > deps-before-refactor.txt
$ git add .
$ git commit -m "Checkpoint before refactoring"

# ===== リファクタリング実行 =====

# 古いコードを削除
rm old_handler.go
rm deprecated_service.go

# 新しいコードを追加
cat > new_handler.go << 'EOF'
package handlers

import (
    "github.com/new/framework"  // 新しい依存関係
)

func NewHandler() {
    // 新しい実装
}
EOF

# ===== 依存関係の整理 =====

# tidyを実行
$ go mod tidy -v

# 出力:
# unused github.com/old/framework
# unused github.com/deprecated/helper

# 確認
$ go list -m all > deps-after-refactor.txt
$ diff deps-before-refactor.txt deps-after-refactor.txt

# ===== テスト =====

$ go test ./...
# すべてパス

$ go build
# 成功

# ===== コミット =====

$ git add .
$ git commit -m "Refactor: Replace old framework with new framework

Removed dependencies:
- github.com/old/framework
- github.com/deprecated/helper

Added dependencies:
- github.com/new/framework

All tests passing."

シナリオ3: マージ後の競合解決

プルリクエストマージ後:

# メインブランチから更新を取得
$ git checkout main
$ git pull

# go.modに競合がある可能性
$ cat go.mod
# <<<<<<< HEAD
# require github.com/package/a v1.0.0
# =======
# require github.com/package/a v1.1.0
# >>>>>>> feature-branch

# 競合を手動で解決
# エディタで修正

# tidyで整理
$ go mod tidy

# これで:
# - 正しいバージョンが選択される
# - 重複が削除される
# - go.sumが更新される

# 確認
$ go mod verify
$ go test ./...

シナリオ4: CI/CDパイプラインでの使用

GitHub Actions の例:

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      
      # go.modが最新であることを確認
      - name: Verify dependencies
        run: |
          go mod tidy
          git diff --exit-code go.mod go.sum
        # go mod tidyの後に変更があればビルド失敗
      
      - name: Build
        run: go build -v ./...
      
      - name: Test
        run: go test -v ./...

このチェックの意味:

CI/CDでgo mod tidyを実行する理由:

1. 開発者がtidyを忘れていないか確認
   └─ コミット前にtidyすることを強制

2. go.modとgo.sumの一貫性を保証
   └─ チーム全員が同じ依存関係を使用

3. 不要な依存関係の混入を防止
   └─ セキュリティリスクの低減

4. ビルドの再現性を確保
   └─ どの環境でも同じビルド結果

go mod tidyのベストプラクティス

実行タイミング

✅ go mod tidyを実行すべき時:

日常的:
□ 新しいパッケージをインポートした後
□ 古いコードを削除した後
□ git pullの後(他の人の変更を取り込んだ時)
□ コミット前(習慣として)

特別な場合:
□ 依存関係を手動で変更した後
□ go.modをマージした後
□ リファクタリング完了後
□ 依存関係の問題が発生した時

実行前の確認

# tidyを実行する前に、現状を確認するのが安全

# 1. 現在の依存関係を記録
$ go list -m all > current-deps.txt

# 2. 変更予定の確認(dry-run的な使い方)
$ go mod tidy
$ git diff go.mod go.sum

# 3. 問題なければ確定
$ git add go.mod go.sum

# 4. 問題があれば元に戻す
$ git checkout go.mod go.sum

チーム開発での運用

プロジェクトのルール例:

# 開発ガイドライン

## 依存関係管理

### 必須事項
1. コミット前に必ず `go mod tidy` を実行
2. `go.mod` と `go.sum` を必ずコミット
3. 依存関係の変更は別コミットにする

### 手順
```bash
# 新機能の開発
git checkout -b feature/new-feature
# コードを書く
go mod tidy
go test ./...

# コミット
git add go.mod go.sum
git commit -m "deps: Update dependencies for new feature"

git add .
git commit -m "feat: Implement new feature"

レビュー時の確認

  • [ ] go mod tidy が実行されているか
  • [ ] 不要な依存関係が追加されていないか
  • [ ] セキュリティ上の問題がある依存関係はないか

---

## トラブルシューティング

### 問題1: tidyを実行してもgo.modが変わらない

```bash
# 症状
$ go mod tidy
# 何も出力されない
$ git diff go.mod
# 変更なし

# 原因と対処:

# 原因1: すでに整理されている
# → 問題なし

# 原因2: ビルドタグでコードが無視されている
# → すべてのビルドタグを含める
$ go mod tidy -tags=integration,e2e,debug

# 原因3: テストコードの依存関係
# → テストを含めて確認
$ go test -i ./...
$ go mod tidy

問題2: tidyで予期しないモジュールが削除される

# 症状
$ go mod tidy -v
unused github.com/important/package

# しかし、このパッケージは必要!

# 原因と対処:

# 原因1: コンパイル条件で使用
# ファイル例: db_postgres.go
// +build postgres

import "github.com/lib/pq"

# 対処: ビルドタグを指定
$ go mod tidy -tags=postgres

# 原因2: テストコードでのみ使用
# 対処: コード内で明示的に参照を保持
import (
    _ "github.com/important/package" // 明示的な保持
)

# または、toolsファイルを作成
// tools.go
// +build tools

package tools

import _ "github.com/important/package"

問題3: go.sumが肥大化

# 症状
$ wc -l go.sum
5000 go.sum  # 大きすぎる!

# 原因:
# 古い依存関係のチェックサムが残っている

# 対処:
# go.sumを削除して再生成
$ rm go.sum
$ go mod tidy
$ go test ./...  # チェックサムを再生成

# 確認
$ wc -l go.sum
800 go.sum  # 大幅に減少

問題4: tidyの後にビルドが失敗

# 症状
$ go mod tidy
$ go build
# エラー: package not found

# 原因と対処:

# 原因1: キャッシュの問題
$ go clean -modcache
$ go mod download
$ go build

# 原因2: go.sumの破損
$ rm go.sum
$ go mod tidy
$ go build

# 原因3: バージョン競合
$ go mod graph | grep problem-package
# 依存関係ツリーを確認
$ go get problem-package@latest

高度な使用例

ベンダリングとの組み合わせ

# vendorディレクトリを使用する場合

# 1. tidyで整理
$ go mod tidy

# 2. vendorディレクトリに依存関係をコピー
$ go mod vendor

# 3. vendorを使ってビルド
$ go build -mod=vendor

# 4. vendorも含めてコミット
$ git add go.mod go.sum vendor/
$ git commit -m "Update dependencies and vendor"

複数モジュールプロジェクト

# モノレポ構造の例
myproject/
├── service-a/
│   ├── go.mod
│   └── main.go
├── service-b/
│   ├── go.mod
│   └── main.go
└── shared/
    ├── go.mod
    └── common.go

# 各モジュールでtidyを実行
$ cd service-a && go mod tidy
$ cd ../service-b && go mod tidy
$ cd ../shared && go mod tidy

# または、スクリプトで一括実行
#!/bin/bash
for dir in service-a service-b shared; do
    echo "Tidying $dir"
    (cd $dir && go mod tidy)
done

まとめ

go mod tidyの役割

go mod tidyは:

✅ 必要な依存関係を追加
✅ 不要な依存関係を削除
✅ go.modを整理
✅ go.sumを更新
✅ 依存関係の一貫性を保証

基本的な使い方

# シンプルな実行
go mod tidy

# 詳細情報付き
go mod tidy -v

# 特定のビルドタグを考慮
go mod tidy -tags=integration

実行タイミング

定期的に実行:
□ コミット前
□ git pull後
□ パッケージ追加/削除後
□ リファクタリング後

重要なポイント

  • ✅ 開発中は頻繁に実行
  • ✅ go.modとgo.sumは必ずコミット
  • ✅ CI/CDで整合性を確認
  • ✅ チームで実行ルールを統一
  • ✅ 変更内容をレビュー

ベストプラクティス:

# 習慣化すべきワークフロー
git pull
go mod tidy
go test ./...
# コードを書く
go mod tidy
git add go.mod go.sum
git commit

これで、依存関係を常に整理された状態に保てます!

おわりに 

本日は、Go言語の依存関係の管理について解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました