Go

Go言語実践編:Go Modules と テスト駆動開発 (TDD) で強固なプロジェクト基盤を作る

Go を触り始めたあなたが次にぶつかる壁――それは 依存関係の管理品質保証 です。Go Modules を使いこなし、テスト駆動開発 (TDD) を取り入れることで、チーム開発でもスケールする堅牢なコードベースを構築できます。本記事では、Modules の基本コマンドからモック生成、CI 自動化まで、実践的なノウハウを詰め込みました。この記事を読み終える頃には、あなたの Go プロジェクトが 再現性・信頼性・拡張性 を兼ね備えたものに生まれ変わります。

この記事で得られること

  • Modules の導入・運用フローが理解できる
  • Go らしいテスト設計パターンを習得できる
  • GitHub Actions によるテスト自動化パイプラインを構築できる
  • 実務で使えるディレクトリ構成・CI ファイル・.gitignore をコピー&ペーストできる

はじめに

前回の記事では "Hello, World!" から軽量 Web サーバーまでを体験し、Go の開発速度と並行処理の楽しさを味わいました。実際の業務で Go を使うには、再現性のある依存管理テスト自動化 が不可欠です。本稿ではその中核である Go Modules と TDD を丁寧に解説します。


Go Modules とは?

歴史と採用背景

Go 1.11 で実験的に導入、1.13 でデフォルト化された 依存管理システム。従来の GOPATH ベースから脱却し、プロジェクト単位で依存ライブラリのバージョンを固定 できます。

旧 (GOPATH)新 (Modules)
$GOPATH/src 配下に強制配置任意ディレクトリで OK
dep など外部ツールが必要標準 go CLI で完結
バージョン固定が煩雑go.mod に宣言的に記述

基本コマンド早見表

シチュエーションコマンド補足
新規プロジェクト開始go mod init example.com/appgo.mod 作成
依存追加 (コード書くだけで可)go get github.com/gin-gonic/gin@v1.9.0@latest 省略可
バージョン整合&不要パッケージ削除go mod tidyCI で実行推奨
依存グラフ確認go mod graph重複確認に便利
キャッシュクリアgo clean -modcache破損時の最終手段

既存プロジェクトを Modules 対応にする

  1. ルートで go mod init <module path> を実行
  2. go build ./... で依存解析 → go.modgo.sum が生成
  3. replace ディレクティブでローカルパッケージを一時的に参照する例: replace example.com/old => ../local/old
  4. CI に go vet, go test, go mod tidy を追加

Tips: GOPATH 時代のコードはインポートパスが github.com/組織/リポジトリ/... になっていることが多い。go fix で自動修正できる場合も。

ディレクトリ構成例とサンプルテスト

Go Modules 初期化直後の最小構成

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

実務で推奨されるレイヤード構成

myapp/
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── go.mod
├── go.sum
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── handler/
│   │   ├── hello.go
│   │   └── hello_test.go
│   └── domain/
│       └── user.go
├── mock/
│   └── mock_user.go
└── Makefile

.gitignore

# Go binaries & test artifacts
*.exe
*.dll
*.so
*.dylib
*.test
*.out

# Build output
bin/
coverage.out

# Dependency directories (if vendored)
vendor/

# Editor / IDE settings
.vscode/
.idea/

.github/workflows/ci.yml

name: Go CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "^1.22"
      - run: go test ./... -v
      - run: go vet ./...
      - run: go mod tidy && git diff --exit-code

cmd/server/main.go

package main

import (
    "log"
    "net/http"

    "example.com/myapp/internal/handler"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/hello", handler.HelloHandler)

    log.Println("start :8080")
    if err := http.ListenAndServe(":8080", mux); err != nil {
        log.Fatal(err)
    }
}

internal/handler/hello.go

package handler

import (
    "encoding/json"
    "fmt"
    "net/http"
)

func HelloHandler(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    if name == "" {
        name = "World"
    }
    resp := map[string]string{"message": fmt.Sprintf("Hello, %s", name)}
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}

internal/handler/hello_test.go

package handler

import (
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
)

func TestHelloHandler(t *testing.T) {
    req := httptest.NewRequest(http.MethodGet, "/hello?name=Go", nil)
    rr := httptest.NewRecorder()

    HelloHandler(rr, req)

    if rr.Code != http.StatusOK {
        t.Fatalf("unexpected status: %d", rr.Code)
    }
    want := `{"message":"Hello, Go"}`
    if strings.TrimSpace(rr.Body.String()) != want {
        t.Fatalf("body = %s, want %s", rr.Body.String(), want)
    }
}

サンプルコードの GitHub リンク


依存関係バージョン管理戦略

SemVer と Minimal Version Selection

Go Modules は SemVer に準拠しつつ、ビルド時に 最小互換バージョン を選択する MVS 方式を採用します。結果として「ビルドは通るがテストが落ちる」トラブルを減らせます。

A@v1.2.0 requires B@v1.1.0
C@v1.3.0 requires B@v1.0.0
→ B の最小バージョン v1.1.0 が選ばれる

go work ワークスペース (Go 1.18+)

複数モジュールを横断した開発に便利。モノレポ構成で go.work を置き、配下のモジュールをまとめてビルド/テストできます。

# Uber-maintained fork (golang/mock レポジトリは 2023-06 にアーカイブ済)
go install go.uber.org/mock/mockgen@latest
mockgen -source=user.go -destination=mock/mock_user.go -package=mock

ポイント: 実装に依存せずテストを高速・安定化できる。


CI/CD でテスト自動化

GitHub Actions サンプル

name: Go CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "^1.22"
      - run: go test ./... -v
      - run: go vet ./...
      - run: go mod tidy && git diff --exit-code

Go vetgo mod tidy を自動チェックに含めることで、レビュー工数を大幅削減。


ベストプラクティスまとめ

  1. go mod tidy保存時フック or CI で強制
  2. 1 パッケージ 1 目的:internal/ を活用し依存方向を制御
  3. テストは テーブル駆動 + サブテスト で可読性と網羅性を両立
  4. モック生成は Makefile / Taskfile に統一コマンドを用意
  5. ベンチとカバレッジを日次 CI でトレンド監視

まとめ

  • Go Modules により依存管理の再現性が向上し、チーム開発の摩擦が激減する。
  • TDD を採用すると、実装より先に仕様が固まり、リファクタリング耐性が高まる。
  • CI/CD でテストとモジュール整合性を自動チェックすることで、安心してデプロイできるパイプライン を構築可能。

次回は Go での並行処理パターンとパフォーマンスチューニング にフォーカスし、context, sync パッケージを使いこなすテクニックを解説します。お楽しみに!


さらなる学習リソース

行動呼びかけ: 手元で go mod init から go test まで一連のコマンドを実行し、記事で紹介した CI 設定も GitHub へ push してみましょう。習うより慣れる、それが Go の醍醐味です!

  • この記事を書いた人

ふくまる

機械設計業をしていたが25歳でエンジニアになると決意して行動開始→ 26歳でエンジニアに転職→ 28歳でフリーランスエンジニアに→ 現在、34歳でフリーランス7年目 Go案件を受注中 Go,GCPが得意分野

-Go