Goの「import cycle not allowed」を修正する:循環依存を解消するパッケージ再構成

intermediate🔷 Go2026-05-13| Go 1.18以降、任意のOS(Linux、macOS、Windows)、任意のプロジェクト規模

Error Message

import cycle not allowed
#go#インポートサイクル#パッケージ#アーキテクチャ

TL;DR

2つ以上のパッケージが直接、またはチェーンを通じて互いにインポートし合うと、Goはコンパイルできません。修正は常に構造的なものです。競合するパッケージが触れない第3のパッケージに共有コードを移動するか、パッケージが密結合すぎる場合はマージします。

エラーの見た目

$ go build ./...
package myapp/service
        imports myapp/repository
        imports myapp/service: import cycle not allowed

Goはインポートチェーン全体を出力するため、どこでサイクルが閉じるかを特定できます。下から上に読んでください。最後に表示されたパッケージが最初のパッケージをインポートすることで循環が完成します。

GoがImportサイクルを禁止する理由

Goは厳格な依存関係の順序でパッケージをコンパイルします。パッケージAがパッケージBを必要とし、BがAを必要とする場合、コンパイラは型チェックの段階でデッドロックに陥ります。有効なコンパイル順序が存在しないためです。

JavaやPythonのような言語はリンク時にこれを回避します。Goはそうしません。言語レベルの回避手段がない、厳格なコンパイル時エラーです。構造を修正するしかありません。

サイクルの検出

エラーメッセージにはすでにチェーンが示されています。依存グラフが複雑な大規模プロジェクトでは、いくつかの追加ツールが役立ちます:

# パッケージのすべての依存関係を表示
go list -f '{{ .Imports }}' ./internal/service

# モジュール全体のすべてのサイクルを検索
go build ./... 2>&1 | grep 'import cycle'

# 視覚的な依存関係グラフを生成(Graphvizが必要)
go install github.com/kisielk/godepgraph@latest
godepgraph -s ./... | dot -Tpng -o deps.png

よくあるシナリオと修正方法

シナリオ1:ServiceとRepositoryが互いにインポートし合う

Goのサイクルエラーの9割はこのパターンです。サービス層がリポジトリを呼び出すのは問題ありません。しかし、リポジトリがサービスパッケージで定義された型を参照するのは問題です。

// BAD: myapp/repository/user.go
import "myapp/service" // cycle!

func (r *UserRepo) Save(u service.User) error { ... }

修正方法: Userのような共有型を専用のmodelsまたはdomainパッケージに移動します。servicerepositoryも互いにインポートせず、どちらもmodelsをインポートします。

// myapp/models/user.go — 末端パッケージ、内部インポートなし
package models

type User struct {
    ID    int
    Email string
}

// myapp/repository/user.go
import "myapp/models" // OK

func (r *UserRepo) Save(u models.User) error { ... }

// myapp/service/user.go
import (
    "myapp/models"     // OK
    "myapp/repository" // OK
)

func CreateUser(repo *repository.UserRepo, email string) error {
    u := models.User{Email: email}
    return repo.Save(u)
}

シナリオ2:アプリケーションロジックに手を伸ばすユーティリティパッケージ

utilsconfigの型を必要とするヘルパーを追加する場合があります。それ自体は問題ありません。しかしconfigが別の理由でutilsをインポートするとサイクルが発生します。

// myapp/utils/helpers.go
import "myapp/config" // configがutilsをインポートするとサイクル発生

修正方法: ユーティリティパッケージは内部インポートをゼロにすべきです。パッケージを参照する代わりに、値をパラメータとして渡します。

// BEFORE — ヘルパーがconfigを自ら取得する
func GetTimeout() time.Duration {
    cfg := config.Load()
    return cfg.Timeout
}

// AFTER — 呼び出し元が値を提供する
func FormatDuration(d time.Duration) string {
    return d.String()
}

シナリオ3:1つにまとめるべき2つのパッケージ

サイクルが問題の根本ではなく症状に過ぎない場合もあります。packageApackageBが常に互いを呼び出していて、きれいな抽出ポイントがない場合、それらは同じパッケージに属しています。

# Before — 互いなしには存在できない2つのパッケージ
myapp/auth/
myapp/session/

# After — 1つの凝集したパッケージにマージ
myapp/auth/          # authとsessionの両方のロジックを含む

シナリオ4:インターフェースを使って依存関係を逆転させる

双方向通信が本当に必要な場合は、下位レベルのパッケージにインターフェースを定義します。上位レベルのパッケージがそれを実装します。共有インポートは不要で、依存関係は一方向に流れます。

// myapp/repository/notifier.go
package repository

// NotifierはServiceレイヤーによって実装されます。
// repositoryはserviceをインポートしません。
type Notifier interface {
    OnUserSaved(id int)
}

type UserRepo struct {
    notifier Notifier
}

func NewUserRepo(n Notifier) *UserRepo {
    return &UserRepo{notifier: n}
}

func (r *UserRepo) Save(u User) error {
    // ... 保存ロジック ...
    r.notifier.OnUserSaved(u.ID)
    return nil
}

// myapp/service/user.go — repository.Notifierを実装
package service

import "myapp/repository"

type UserService struct{}

func (s *UserService) OnUserSaved(id int) {
    // 保存後の通知を処理
}

func SetupRepo(svc *UserService) *repository.UserRepo {
    return repository.NewUserRepo(svc) // serviceをインターフェースとして渡す
}

サイクルを防ぐ構造的なルール

  • 依存関係は内側に流れる: cmdservicerepositorymodels。下位レイヤーは上位レイヤーをインポートしません。開始前にホワイトボードにこの矢印を描いておけば、違反が明らかになります。
  • 共有型は末端パッケージに置く: modelsdomain、またはtypes — 内部インポートがゼロのパッケージです。他のすべてのパッケージはリスクなく安全にインポートできます。
  • インターフェースはコンシューマーに属する: インターフェースは実装するパッケージではなく、使用するパッケージで定義します。これはGoの慣用的なスタイルであり、サイクルが形成される前に自然に破ることができます。
  • internal/commonトラップに注意: 無関係なヘルパーを蓄積するcommonパッケージはサイクルを機械的に解決しますが、メンテナンスの混乱を招きます。そこに本当に属するものを意図的に選択してください。

修正の確認

# クリーンな終了、出力なし = サイクルが解消された
go build ./...

# 何も壊れていないことを確認
go test ./...

# vetで再確認
go vet ./...

# オプション:修正前後を確認するためにグラフを再生成
godepgraph -s ./... | dot -Tpng -o deps-after.png

go build ./...がコード0で終了し、何も出力されないことが確認の証拠です。出力がある場合はまだ作業が残っています。

このエラーに直面したときのクイックチェックリスト

  • サイクルチェーン全体を読む — どのパッケージが問題の原因かを特定する。
  • 1つの型や関数のためだけにインポートが必要な場合は、その型を共有末端パッケージに移動する。
  • 2つのパッケージが常に互いを呼び出している場合は、マージする。
  • インポートをローカルで定義したインターフェースに置き換えられる場合は、そうする。
  • 下位レイヤーのパッケージから上位レイヤーのパッケージをインポートしない — 例外なし。

Related Error Notes