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

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

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

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

スポンサーリンク

背景

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

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

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

依存関係の削除

コードがモジュール内のパッケージをもはや使用しなくなった場合、そのモジュールを依存関係として追跡するのを停止できます。

すべての未使用モジュールの追跡を停止するには、go mod tidyコマンドを実行します。このコマンドは、モジュール内のパッケージをビルドするために必要な不足している依存関係も追加する場合があります。

$ go mod tidy

特定の依存関係を削除するには、go getコマンドを使用し、モジュールのモジュールパスを指定して@noneを追加します。以下の例を参照してください:

$ go get example.com/theirmodule@none

go getコマンドは、削除されたモジュールに依存する他の依存関係もダウングレードまたは削除します。


解説

依存関係削除の概念

依存関係の削除は、使わなくなった道具を片付けるようなものです:

道具の整理依存関係の削除
使わない工具を確認未使用パッケージを特定
工具箱から取り出すgo.modから削除
関連部品も整理間接的依存関係も削除
必要なものは残す必要な依存関係は保持
定期的に整理go mod tidyで定期整理

go mod tidyによる自動削除

基本的な使い方

最も推奨される方法:

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

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

実践例: リファクタリング後の整理

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

# ===== リファクタリング前 =====

# go.mod
module example.com/myapp

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/old/logger v1.0.0         # 古いロガー
    github.com/deprecated/helper v2.1.0  # 非推奨ヘルパー
    github.com/new/logger v2.0.0         # 新しいロガー
)

# main.go
package main

import (
    "github.com/gin-gonic/gin"
    "github.com/old/logger"        // 古いロガーを使用
    "github.com/deprecated/helper" // 非推奨ヘルパーを使用
)

func main() {
    logger.Info("Starting app")
    helper.DoSomething()
    
    r := gin.Default()
    r.Run()
}

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

# 新しいロガーに移行
cat > main.go << 'EOF'
package main

import (
    "github.com/gin-gonic/gin"
    "github.com/new/logger"  // 新しいロガーに変更
)

func main() {
    logger.Info("Starting app")
    
    r := gin.Default()
    r.Run()
}
EOF

# ===== go mod tidyで自動整理 =====

$ go mod tidy -v

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

# ===== 整理後のgo.mod =====

$ cat go.mod
# module example.com/myapp
# 
# go 1.21
# 
# require (
#     github.com/gin-gonic/gin v1.9.1
#     github.com/new/logger v2.0.0
# )
# 
# require (
#     github.com/bytedance/sonic v1.9.1 // indirect
#     ... その他の間接的依存関係
# )

# github.com/old/logger と github.com/deprecated/helper は削除された

# ===== 確認 =====

$ go build
# 成功

$ go test ./...
# PASS

go mod tidyの動作詳細

何が起こるか:

ステップ1: ソースコード解析
├─ すべての.goファイルを読み込む
├─ import文を抽出
└─ 実際に使用されているパッケージを特定

ステップ2: 依存関係の比較
├─ go.modに記載されている依存関係
├─ 実際に使用されている依存関係
└─ 差分を計算

ステップ3: 削除と追加
├─ 未使用の依存関係を削除
├─ 不足している依存関係を追加
└─ go.modとgo.sumを更新

ステップ4: 間接的依存関係の整理
├─ 孤立した間接的依存関係を削除
├─ 必要な間接的依存関係を保持
└─ // indirectマーカーを適切に設定

@noneを使った明示的な削除

基本的な使い方

# 特定のモジュールを削除
go get example.com/theirmodule@none

# 複数のモジュールを一度に削除
go get \
    example.com/module1@none \
    example.com/module2@none \
    example.com/module3@none

実践例: 特定パッケージの削除

シナリオ: 特定のパッケージだけを削除したい

# ===== 削除前の状態 =====

$ go list -m all
# example.com/myapp
# github.com/gin-gonic/gin v1.9.1
# github.com/lib/pq v1.10.9          # PostgreSQLドライバ
# github.com/go-sql-driver/mysql v1.7.1  # MySQLドライバ
# github.com/mattn/go-sqlite3 v1.14.18   # SQLiteドライバ

# 状況: PostgreSQLに統一したので、MySQLとSQLiteは不要

# ===== 特定のパッケージを削除 =====

# MySQLドライバを削除
$ go get github.com/go-sql-driver/mysql@none
# go: removed github.com/go-sql-driver/mysql v1.7.1

# SQLiteドライバを削除
$ go get github.com/mattn/go-sqlite3@none
# go: removed github.com/mattn/go-sqlite3 v1.14.18

# ===== 削除後の状態 =====

$ go list -m all
# example.com/myapp
# github.com/gin-gonic/gin v1.9.1
# github.com/lib/pq v1.10.9

# ===== go.modの確認 =====

$ cat go.mod
# module example.com/myapp
# 
# go 1.21
# 
# require (
#     github.com/gin-gonic/gin v1.9.1
#     github.com/lib/pq v1.10.9
# )

# ===== 整理 =====

$ go mod tidy

# ===== 確認 =====

$ go build
# 成功

$ go test ./...
# PASS

@noneの使用ケース

ケース1: 意図的な削除

# パッケージAからパッケージBに移行

# 古いパッケージを明示的に削除
go get github.com/old/package@none

# 新しいパッケージを追加
go get github.com/new/package@latest

# 整理
go mod tidy

ケース2: 開発用パッケージの削除

# 開発中だけ使用していたパッケージ
go get github.com/debug/tool@none
go get github.com/test/helper@none

# 整理
go mod tidy

ケース3: セキュリティ上の理由

# 脆弱性のあるパッケージを削除
go get github.com/vulnerable/package@none

# 代替パッケージを追加
go get github.com/secure/alternative@latest

依存関係削除の完全ワークフロー

ワークフロー1: 計画的なパッケージ削除

# ===== フェーズ1: 現状確認 =====

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

# 依存関係の使用状況を確認
$ go mod why github.com/package/to-remove

# 出力:
# # github.com/package/to-remove
# example.com/myapp/handlers
# github.com/package/to-remove

# どこで使われているか特定
$ grep -r "package/to-remove" --include="*.go"
# handlers/api.go:    "github.com/package/to-remove"

# ===== フェーズ2: コード修正 =====

# import文を削除
$ nano handlers/api.go
# import "github.com/package/to-remove" を削除
# 関連するコードも修正

# ===== フェーズ3: テスト =====

# ビルド確認
$ go build
# 成功

# テスト実行
$ go test ./...
# PASS

# ===== フェーズ4: 依存関係削除 =====

# go mod tidyで自動削除
$ go mod tidy -v
# unused github.com/package/to-remove

# または明示的に削除
$ go get github.com/package/to-remove@none

# ===== フェーズ5: 確認 =====

# 削除されたことを確認
$ go list -m all > dependencies-after.txt
$ diff dependencies-before.txt dependencies-after.txt

# 出力:
# < github.com/package/to-remove v1.0.0
# ←削除された

# ===== フェーズ6: コミット =====

$ git add go.mod go.sum handlers/api.go
$ git commit -m "Remove unused package/to-remove dependency

- Removed import from handlers/api.go
- Replaced functionality with native implementation
- All tests passing

Removed dependencies:
- github.com/package/to-remove v1.0.0"

ワークフロー2: 大規模リファクタリング

# ===== シナリオ =====
# 古いフレームワークから新しいフレームワークへの移行

# ===== フェーズ1: 移行計画 =====

cat > MIGRATION.md << 'EOF'
# Framework Migration Plan

## Old Framework
- github.com/old/framework v1.5.0
- github.com/old/middleware v1.2.0
- github.com/old/router v2.0.0

## New Framework
- github.com/new/framework v2.0.0
- github.com/new/middleware v1.0.0
- github.com/new/router v1.5.0

## Migration Steps
1. Create new handlers with new framework
2. Run both frameworks in parallel
3. Migrate routes one by one
4. Test each migration
5. Remove old framework

## Rollback Plan
Keep old framework code in separate branch
EOF

# ===== フェーズ2: 段階的移行 =====

# 週1: 新フレームワークを追加
go get github.com/new/framework@latest
go get github.com/new/middleware@latest
go get github.com/new/router@latest

# 週2-4: ルートを順次移行
# 新旧両方のフレームワークが共存

# 週5: 最後のルートを移行
# 古いフレームワークのコードを削除

# ===== フェーズ3: 古いフレームワーク削除 =====

# 古いimportをすべて削除
find . -name "*.go" -exec sed -i '/old\/framework/d' {} \;
find . -name "*.go" -exec sed -i '/old\/middleware/d' {} \;
find . -name "*.go" -exec sed -i '/old\/router/d' {} \;

# テスト
go test ./...

# 依存関係削除
go get github.com/old/framework@none
go get github.com/old/middleware@none
go get github.com/old/router@none

# 整理
go mod tidy

# ===== フェーズ4: 確認とコミット =====

# ビルド確認
go build

# すべてのテスト
go test ./...

# ベンチマーク比較
go test -bench=. ./... > new-benchmark.txt
# 旧フレームワークと比較

# コミット
git add .
git commit -m "Complete migration from old framework to new framework

Migration summary:
- Migrated 50 routes
- Removed old framework dependencies
- All tests passing
- Performance improved by 30%

Removed dependencies:
- github.com/old/framework v1.5.0
- github.com/old/middleware v1.2.0
- github.com/old/router v2.0.0

Added dependencies:
- github.com/new/framework v2.0.0
- github.com/new/middleware v1.0.0
- github.com/new/router v1.5.0"

間接的依存関係の削除

孤立した依存関係の削除

シナリオ: 直接的依存関係を削除したが、間接的依存関係が残っている

# ===== 削除前 =====

$ cat go.mod
# module example.com/myapp
# 
# go 1.21
# 
# require github.com/package/parent v1.0.0
# 
# require (
#     github.com/package/child1 v1.0.0 // indirect
#     github.com/package/child2 v1.0.0 // indirect
#     github.com/package/child3 v1.0.0 // indirect
# )

# ===== parentを削除 =====

$ go get github.com/package/parent@none

# ===== go mod tidyで孤立依存関係を削除 =====

$ go mod tidy -v
# unused github.com/package/child1
# unused github.com/package/child2
# unused github.com/package/child3

# ===== 削除後 =====

$ cat go.mod
# module example.com/myapp
# 
# go 1.21
# 
# # すべての関連依存関係が削除された

依存関係ツリーの確認

# 依存関係ツリーを確認
$ go mod graph | grep github.com/package/parent

# 出力:
# example.com/myapp github.com/package/parent@v1.0.0
# github.com/package/parent@v1.0.0 github.com/package/child1@v1.0.0
# github.com/package/parent@v1.0.0 github.com/package/child2@v1.0.0
# github.com/package/parent@v1.0.0 github.com/package/child3@v1.0.0

# parentを削除すると、child1, child2, child3も不要になる

トラブルシューティング

問題1: go mod tidyで削除されない

# 症状: パッケージを使っていないのに削除されない

# 原因1: ビルドタグで使用されている
# ファイル例: database_postgres.go
// +build postgres

import _ "github.com/lib/pq"

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

# 原因2: テストコードで使用
# ファイル例: test_helper_test.go
import "github.com/test/helper"

# 対処: 本当に不要なら削除
rm test_helper_test.go
go mod tidy

# 原因3: コメントアウトされているだけ
# // import "github.com/old/package"  ← コメント

# 対処: 完全に削除
# コメントも含めて削除

# 確認
grep -r "old/package" --include="*.go"

問題2: @noneで削除できない

# エラー
$ go get github.com/package/name@none
go: github.com/package/name@none: invalid version: unknown revision none

# 原因: typo

# 正しい構文:
$ go get github.com/package/name@none
#                                ↑
#                                小文字のnone

問題3: 他のパッケージが依存している

# エラー
$ go get github.com/package/a@none
go: github.com/package/b requires github.com/package/a

# 原因: パッケージbがパッケージaに依存

# 確認
$ go mod why github.com/package/a
# # github.com/package/a
# example.com/myapp
# github.com/package/b
# github.com/package/a

# 対処方法1: パッケージbも削除
$ go get github.com/package/b@none
$ go get github.com/package/a@none

# 対処方法2: パッケージbを更新
# (パッケージaへの依存がない新バージョン)
$ go get github.com/package/b@latest
$ go get github.com/package/a@none

ベストプラクティス

定期的な依存関係の整理

月次メンテナンスの例:

#!/bin/bash
# monthly-cleanup.sh

echo "=== Monthly Dependency Cleanup ==="
echo ""

# 1. 現状を記録
echo "1. Recording current state..."
go list -m all > deps-before-cleanup.txt

# 2. 未使用依存関係を削除
echo "2. Cleaning up unused dependencies..."
go mod tidy -v > cleanup-report.txt

# 3. 削除された依存関係を表示
echo "3. Removed dependencies:"
cat cleanup-report.txt

# 4. 新しい状態を記録
go list -m all > deps-after-cleanup.txt

# 5. 差分を表示
echo "4. Changes:"
diff deps-before-cleanup.txt deps-after-cleanup.txt

# 6. テスト
echo "5. Running tests..."
go test ./...

# 7. ビルド
echo "6. Building..."
go build

echo ""
echo "=== Cleanup Complete ==="

チーム開発でのルール

# Dependency Management Guidelines

## Regular Cleanup
- **Weekly**: Review new dependencies
- **Monthly**: Run `go mod tidy`
- **Quarterly**: Audit all dependencies

## Removal Process
1. Identify unused package
2. Remove import statements
3. Run tests
4. Execute `go mod tidy`
5. Commit with explanation

## Commit Message Format

deps: Remove unused package XYZ

  • Removed import from module A
  • Replaced with native implementation
  • All tests passing

Removed:

  • github.com/package/xyz v1.0.0

Reason: No longer needed after refactoring


## Review Checklist
- [ ] All imports removed
- [ ] Tests passing
- [ ] Build successful
- [ ] go.mod cleaned
- [ ] Commit message clear

ドキュメント化

DEPENDENCIES.mdの更新:

# Dependencies Log

## 2023-12-15: Cleanup

### Removed
- github.com/old/logger v1.0.0
  - Reason: Migrated to new logger
  - Replaced by: github.com/new/logger v2.0.0
  - Removed by: @developer1

- github.com/deprecated/helper v2.1.0
  - Reason: Functionality moved to native implementation
  - No replacement needed
  - Removed by: @developer2

### Impact
- Build size reduced by 15MB
- Dependency count reduced from 45 to 38
- No breaking changes

### Testing
- All unit tests passing
- Integration tests passing
- Manual testing completed

まとめ

依存関係削除の2つの方法

方法1: go mod tidy(推奨)

go mod tidy
  • ✅ すべての未使用依存関係を自動削除
  • ✅ 安全で確実
  • ✅ 最も一般的

方法2: @noneで明示的削除

go get package@none
  • ✅ 特定のパッケージのみ削除
  • ✅ 意図が明確
  • ✅ 間接的依存関係も削除

削除の流れ

1. import文を削除
   ↓
2. テスト実行
   ↓
3. go mod tidy 実行
   ↓
4. ビルド確認
   ↓
5. コミット

チェックリスト

削除前の確認:
□ パッケージが本当に不要か確認
□ 代替実装の準備完了
□ テストの更新完了

削除後の確認:
□ go build が成功
□ go test ./... がパス
□ go.modから削除されている
□ go.sumから削除されている

重要なポイント

  • ✅ 定期的に go mod tidy を実行
  • ✅ 削除理由をドキュメント化
  • ✅ テストを必ず実行
  • ✅ チームで情報共有
  • ✅ コミットメッセージに詳細記載

これで、依存関係を適切に削除・管理できます!

おわりに 

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

よっしー
よっしー

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

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

コメント

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