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

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

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

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

スポンサーリンク

背景

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

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

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

未公開モジュールコードに対する開発とテスト

コードが、公開されていない可能性のある依存関係モジュールを使用するように指定できます。これらのモジュールのコードは、それぞれのリポジトリ、それらのリポジトリのフォーク、または、それらを消費する現在のモジュールと同じドライブ上にあるかもしれません。

以下のような場合に、これを行いたいと思うかもしれません:

  • 外部モジュールのコードをフォークおよび/またはクローンした後など、独自の変更を加えたい場合。たとえば、モジュールの修正を準備してから、モジュールの開発者にプルリクエストとして送信したい場合があります。
  • 新しいモジュールを構築していて、まだ公開していないため、go getコマンドが到達できるリポジトリで利用できない場合。

解説

未公開モジュールの開発シナリオ

未公開モジュールの開発は、料理本の試作に似ています:

料理本の試作未公開モジュール開発
レシピを作成中新しいモジュールを開発中
まだ出版していないまだリポジトリに公開していない
家族に試食してもらうローカルでテスト
フィードバックを反映コードを修正
完成したら出版リポジトリにプッシュ

未公開モジュールを使用する方法

3つの主要なアプローチ

1. replace ディレクティブ (ローカルパス)
   └─ ローカルディスク上のモジュールを使用

2. replace ディレクティブ (フォーク)
   └─ フォークしたリポジトリを使用

3. ワークスペース (go.work)
   └─ 複数のモジュールを同時開発

アプローチ1: ローカルパスでのreplace

基本的な使い方

ディレクトリ構造:

myproject/
├── myapp/
│   ├── go.mod          # メインアプリケーション
│   ├── main.go
│   └── handlers/
│       └── api.go
│
└── mylib/              # 開発中のライブラリ
    ├── go.mod
    ├── lib.go
    └── utils.go

myapp/go.mod の設定:

module example.com/myapp

go 1.21

// mylibはまだ公開されていない
// ローカルパスで参照
replace example.com/mylib => ../mylib

require example.com/mylib v0.0.0

myapp/main.go:

package main

import (
    "fmt"
    "example.com/mylib"  // ローカルのmylibを使用
)

func main() {
    result := mylib.DoSomething()
    fmt.Println(result)
}

実行とテスト:

# myappディレクトリで
cd myapp

# ビルド(ローカルのmylibを使用)
$ go build
# 成功

# 実行
$ go run main.go
# mylibのコードが実行される

# テスト
$ go test ./...
# mylibを含めてテスト

実践例: 新しいライブラリの開発

完全なワークフロー:

# ===== ステップ1: ディレクトリ構造を作成 =====

mkdir -p ~/projects/mywork
cd ~/projects/mywork

# ライブラリを作成
mkdir mylib
cd mylib
go mod init github.com/username/mylib

# ライブラリのコードを書く
cat > lib.go << 'EOF'
package mylib

func Greet(name string) string {
    return "Hello, " + name + "!"
}
EOF

cd ..

# ===== ステップ2: アプリケーションを作成 =====

mkdir myapp
cd myapp
go mod init example.com/myapp

# アプリケーションのコードを書く
cat > main.go << 'EOF'
package main

import (
    "fmt"
    "github.com/username/mylib"
)

func main() {
    message := mylib.Greet("World")
    fmt.Println(message)
}
EOF

# ===== ステップ3: replaceディレクティブを追加 =====

# go.modを編集
cat > go.mod << 'EOF'
module example.com/myapp

go 1.21

replace github.com/username/mylib => ../mylib

require github.com/username/mylib v0.0.0
EOF

# ===== ステップ4: 開発とテスト =====

# ビルド
$ go build
# 成功

# 実行
$ go run main.go
# Hello, World!

# ライブラリを修正
cd ../mylib
cat >> lib.go << 'EOF'

func Goodbye(name string) string {
    return "Goodbye, " + name + "!"
}
EOF

# アプリケーションで新機能を使用
cd ../myapp
cat >> main.go << 'EOF'
    goodbye := mylib.Goodbye("World")
    fmt.Println(goodbye)
EOF

# 再実行
$ go run main.go
# Hello, World!
# Goodbye, World!

# ===== ステップ5: 公開準備 =====

# ライブラリを公開
cd ../mylib
git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/username/mylib.git
git push -u origin main
git tag v0.1.0
git push origin v0.1.0

# アプリケーションのgo.modを更新
cd ../myapp
# replaceディレクティブを削除
go mod edit -dropreplace=github.com/username/mylib

# 公開されたバージョンを使用
go get github.com/username/mylib@v0.1.0

# 確認
$ cat go.mod
# module example.com/myapp
# 
# go 1.21
# 
# require github.com/username/mylib v0.1.0

アプローチ2: フォークしたリポジトリの使用

シナリオ: 外部ライブラリにバグ修正を追加

状況:

問題:
- github.com/third-party/library にバグを発見
- 本家にプルリクエストを送りたい
- それまでの間、自分のフォークを使用したい

手順:

# ===== ステップ1: リポジトリをフォーク =====

# GitHubでフォークボタンをクリック
# フォーク先: github.com/myusername/library

# ===== ステップ2: フォークをクローン =====

cd ~/projects
git clone https://github.com/myusername/library.git
cd library

# ===== ステップ3: バグを修正 =====

# ブランチを作成
git checkout -b fix-critical-bug

# コードを修正
nano lib.go
# バグを修正

# コミット
git add .
git commit -m "Fix critical bug in processing function"

# プッシュ
git push origin fix-critical-bug

# ===== ステップ4: 自分のアプリでフォークを使用 =====

cd ~/projects/myapp

# go.modを編集
cat >> go.mod << 'EOF'

// 自分のフォークを使用(バグ修正版)
replace github.com/third-party/library => github.com/myusername/library v0.0.0-20231215123456-abcdef123456
EOF

# または、コマンドで追加
go mod edit -replace=github.com/third-party/library=github.com/myusername/library@fix-critical-bug

# 依存関係を更新
go mod tidy

# ===== ステップ5: テスト =====

go test ./...
# バグ修正が機能することを確認

# ===== ステップ6: プルリクエスト =====

# GitHubでプルリクエストを作成
# タイトル: "Fix critical bug in processing function"
# 説明: 詳細な説明を記載

# ===== ステップ7: マージされた後 =====

# 本家がプルリクエストをマージして新バージョンをリリース
# 例: v1.2.5

# replaceを削除
go mod edit -dropreplace=github.com/third-party/library

# 最新版を取得
go get github.com/third-party/library@v1.2.5

# 確認
cat go.mod
# require github.com/third-party/library v1.2.5
# (replaceディレクティブは削除されている)

実践例: 複数のフォークを管理

// go.mod
module example.com/myapp

go 1.21

// 複数のライブラリをフォーク
replace (
    github.com/lib1/package => github.com/myuser/package v0.0.0-20231215-abc123
    github.com/lib2/tool => github.com/myuser/tool v0.0.0-20231216-def456
    github.com/lib3/helper => ../local-forks/helper
)

require (
    github.com/lib1/package v1.0.0
    github.com/lib2/tool v2.0.0
    github.com/lib3/helper v1.5.0
)

アプローチ3: Go Workspaces (go.work)

Workspaceの概念

Go 1.18以降の新機能:

Workspaceとは:
- 複数のモジュールを同時に開発できる環境
- go.workファイルで設定
- replaceディレクティブより柔軟
- チームで共有しない(個人の開発環境)

基本的な使い方

ディレクトリ構造:

mywork/
├── go.work          # Workspace設定
├── myapp/
│   ├── go.mod
│   └── main.go
├── lib1/
│   ├── go.mod
│   └── lib.go
└── lib2/
    ├── go.mod
    └── helper.go

Workspaceの作成:

# ===== ステップ1: Workspaceを初期化 =====

cd ~/projects/mywork

# workspaceを作成
$ go work init

# 生成されたgo.work
cat go.work
# go 1.21

# ===== ステップ2: モジュールを追加 =====

# モジュールを作成
mkdir myapp lib1 lib2

# 各モジュールを初期化
cd myapp && go mod init example.com/myapp && cd ..
cd lib1 && go mod init example.com/lib1 && cd ..
cd lib2 && go mod init example.com/lib2 && cd ..

# Workspaceにモジュールを追加
$ go work use ./myapp
$ go work use ./lib1
$ go work use ./lib2

# または一度に追加
$ go work use ./myapp ./lib1 ./lib2

# 確認
$ cat go.work
# go 1.21
# 
# use (
#     ./myapp
#     ./lib1
#     ./lib2
# )

コードの作成:

// lib1/lib.go
package lib1

func Process(data string) string {
    return "Processed: " + data
}
// lib2/helper.go
package lib2

import "example.com/lib1"

func Help(input string) string {
    return lib1.Process(input) + " (with help)"
}
// myapp/main.go
package main

import (
    "fmt"
    "example.com/lib1"
    "example.com/lib2"
)

func main() {
    result1 := lib1.Process("test")
    fmt.Println(result1)
    
    result2 := lib2.Help("test")
    fmt.Println(result2)
}

実行とテスト:

# Workspaceのルートディレクトリで
cd ~/projects/mywork

# アプリケーションを実行
$ cd myapp && go run main.go
# Processed: test
# Processed: test (with help)

# すべてのモジュールをテスト
$ go work sync  # go.modファイルを同期
$ cd myapp && go test ./...
$ cd ../lib1 && go test ./...
$ cd ../lib2 && go test ./...

Workspaceの利点

従来の方法 (replace) との比較:

replaceディレクティブ:
❌ 各モジュールのgo.modを編集必要
❌ コミットすると他の人に影響
❌ 公開前に削除を忘れる可能性

Workspace (go.work):
✅ go.workだけで管理
✅ ファイルをコミットしない(.gitignore)
✅ 個人の開発環境のみに影響
✅ 複数モジュールの同時開発が容易

実践例: マイクロサービス開発

# ===== プロジェクト構造 =====

microservices/
├── go.work
├── api-gateway/
│   ├── go.mod
│   └── main.go
├── auth-service/
│   ├── go.mod
│   └── main.go
├── user-service/
│   ├── go.mod
│   └── main.go
└── shared-lib/
    ├── go.mod
    └── common.go

# ===== Workspace設定 =====

cd microservices

# Workspaceを作成
go work init

# すべてのサービスを追加
go work use ./api-gateway
go work use ./auth-service
go work use ./user-service
go work use ./shared-lib

# ===== 開発 =====

# shared-libを修正
cd shared-lib
nano common.go
# 変更を保存

# 即座にすべてのサービスに反映
cd ../api-gateway
go run main.go  # 最新のshared-libを使用

cd ../auth-service
go run main.go  # 最新のshared-libを使用

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

# Workspaceルートで全サービスをテスト
cd ~/projects/microservices
for dir in api-gateway auth-service user-service shared-lib; do
    echo "Testing $dir"
    (cd $dir && go test ./...)
done

replaceディレクティブの詳細

構文と使用方法

基本構文:

replace original-module => replacement-module version

// 例:
replace github.com/old/package => github.com/new/package v1.2.3
replace github.com/package => ../local/path
replace github.com/package v1.2.3 => github.com/fork v1.2.4

複数のreplaceディレクティブ:

replace (
    github.com/pkg1 => ../local/pkg1
    github.com/pkg2 => github.com/myfork/pkg2 v0.0.0-20231215-abc123
    github.com/pkg3 v1.0.0 => github.com/pkg3 v1.0.1
)

replaceの種類

1. ローカルパスへのreplace:

replace github.com/user/package => ../local-package

// 相対パス
replace github.com/user/package => ../package
replace github.com/user/package => ../../other/package

// 絶対パス
replace github.com/user/package => /home/user/dev/package

2. 別のリポジトリへのreplace:

// フォークしたリポジトリ
replace github.com/original/pkg => github.com/myfork/pkg v1.0.0

// 別のモジュール
replace github.com/old/pkg => github.com/new/pkg v2.0.0

3. 特定バージョンのreplace:

// v1.2.3のみを置き換え
replace github.com/pkg v1.2.3 => github.com/fork v1.2.4

// すべてのバージョンを置き換え
replace github.com/pkg => github.com/fork v1.2.4

go mod editでのreplace操作

# replaceを追加
$ go mod edit -replace=github.com/pkg=../local-pkg

# 特定バージョンのみreplace
$ go mod edit -replace=github.com/pkg@v1.0.0=../local-pkg

# replaceを削除
$ go mod edit -dropreplace=github.com/pkg

# 確認
$ cat go.mod

ベストプラクティス

開発フェーズ別の推奨アプローチ

フェーズ1: プロトタイプ開発

# ローカルディレクトリで素早く開発
replace github.com/myuser/newlib => ../newlib

推奨理由:
✅ 素早い反復開発
✅ セットアップが簡単
✅ コミットやプッシュ不要

フェーズ2: 機能開発

# Workspaceを使用
go work init
go work use ./app ./lib1 ./lib2

推奨理由:
✅ 複数モジュールの同時開発
✅ go.modを汚さない
✅ チームメンバーに影響なし

フェーズ3: バグ修正(外部ライブラリ)

# フォークを使用
replace github.com/third-party/lib => github.com/myuser/lib v0.0.0-20231215-abc

推奨理由:
✅ 修正をチームで共有
✅ プルリクエスト準備
✅ バージョン管理

フェーズ4: 本番リリース

# すべてのreplaceを削除
go mod edit -dropreplace=github.com/myuser/lib

# 公開バージョンを使用
go get github.com/myuser/lib@v1.0.0

推奨理由:
✅ 再現可能なビルド
✅ 依存関係が明確
✅ セキュリティと安定性

.gitignoreの設定

# go.workは個人の開発環境設定なのでコミットしない
go.work
go.work.sum

# ビルド成果物
*.exe
*.test
/bin/
/dist/

# エディタ設定
.vscode/
.idea/

チーム開発でのルール

# 開発ガイドライン

## 未公開モジュールの開発

### ローカル開発
- `go.work`を使用(コミット禁止)
- `replace`は一時的な使用のみ
- 必ずコミット前に削除

### フォークの使用
- バグ修正の場合のみ
- コミットメッセージに理由を記載
- プルリクエストのリンクを追加

### 禁止事項
- ❌ replaceディレクティブをmainブランチにマージ
- ❌ go.workファイルをコミット
- ❌ 絶対パスの使用

### 例外
- ✅ ドキュメントにreplaceの使用例を記載
- ✅ 開発環境セットアップガイドでgo.work説明

トラブルシューティング

問題1: replaceが効かない

# 症状
$ go build
# ローカルパスではなくリモートから取得しようとする

# 原因と対処:

# 原因1: typo
replace github.com/user/pkg => ../pckg  # ❌ typo

# 対処: パスを確認
$ ls ../pkg
$ go mod edit -replace=github.com/user/pkg=../pkg

# 原因2: モジュールパスの不一致
# ローカルのgo.modを確認
$ cat ../pkg/go.mod
# module github.com/user/package  # ❌ 異なる!

# 対処: 正しいモジュールパスを使用
replace github.com/user/package => ../pkg

問題2: Workspaceでモジュールが見つからない

# 症状
$ go build
# package not found

# 確認
$ cat go.work
# go 1.21
# 
# use ./app

# 問題: libが追加されていない

# 対処
$ go work use ./lib
$ go work sync
$ go build

問題3: replaceを削除し忘れてプッシュ

# 問題: ローカルパスのreplaceをコミットしてしまった

# 対処
# 1. replaceを削除
$ go mod edit -dropreplace=github.com/user/pkg

# 2. 公開バージョンを使用
$ go get github.com/user/pkg@v1.0.0

# 3. 修正をコミット
$ git add go.mod go.sum
$ git commit --amend
# または
$ git commit -m "Remove local replace directive"

# 4. 強制プッシュ(注意!)
$ git push --force-with-lease

まとめ

未公開モジュール開発の3つのアプローチ

1. ローカルパスreplace

replace github.com/user/lib => ../lib
  • ✅ シンプル
  • ✅ 素早い開発
  • ❌ go.modを編集必要

2. フォークreplace

replace github.com/original/lib => github.com/fork/lib v0.0.0-date-hash
  • ✅ バグ修正に最適
  • ✅ チームで共有可能
  • ❌ プルリクエストまでの一時的措置

3. Workspace (go.work)

go work use ./app ./lib1 ./lib2
  • ✅ 複数モジュール同時開発
  • ✅ go.modを汚さない
  • ✅ 個人の開発環境

使い分けガイド

新しいライブラリ開発:
→ Workspace (go.work)

外部ライブラリのバグ修正:
→ Fork + replace

クイックプロトタイプ:
→ ローカルパスreplace

本番リリース:
→ すべてのreplaceを削除

重要なポイント

  • ✅ go.workはコミットしない
  • ✅ replaceは一時的な使用
  • ✅ 公開前にすべて削除
  • ✅ チームで開発ルールを統一
  • ✅ ドキュメントに記載

これで、未公開モジュールを効率的に開発できます!

おわりに 

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

よっしー
よっしー

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

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

コメント

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