GMOメイクショップでエンジニアをしている黒木です。
22年に新卒で入社し、Goでバックエンドの開発をしています。
入社後にGoを学び始めて、学びたての頃は、よく循環参照のエラーを起こしていました。
この経験を踏まえて、Goを学びたての頃に、これを知っていれば、循環参照は怖くないという思いを元に、循環参照が起きた場合、どこが原因で循環参照が起きているかの特定方法と、解決法をご紹介いたします。
ゴール
Goを使っている方なら一度は、import cycle not allowed
というエラーに出会ったことはないでしょうか。
このエラーは、パッケージが互いに参照し合っているため発生します。
とは言っても、Goを学びたての頃は、どこが原因なの? ということが多々あると思います。今回は、パッケージ間の依存関係をグラフ化することで、どこが原因かを特定して、循環参照を解決してみたいと思います。
どうやるか
godepgraphというツールを使って、プロジェクト内の依存関係をグラフ化します。その後、グラフから循環参照の原因を特定し、循環参照を解消するところまでやってみます。
まず始めに
循環参照とは、複数のパッケージが互いに参照し合うことです。パッケージ間の依存関係を有向グラフで表した際に、始点と終点が同じになる経路、いわゆる閉路が存在すると、循環参照が発生します。
循環参照が起こる例を挙げると、以下のような状態になります。packageA を始点として、packageB を経由し、packageA が終点となります。始点と終点が同じ状態です。
このような状態でも、循環参照が発生します。packageA を始点として、 packageB、packageC を経由し、packageA が終点となります。
反対に、循環参照が起こらない例は、以下のような状態になります。packageAを始点、packageBが終点となっているような、始点と終点が異なる状態です。
循環参照が起こるコードを書いてみる
今回は、packageA・packageB・packageCを定義し、packageA → packageB → packageC → packageAのような依存関係があるコードを実装します。
import-cycle-example
というモジュールを作り、進めていきます。
$ go mod init import-cycle-example
まずは、packageA/packageA.go
を定義します。
package packageA import ( "import-cycle-example/packageB" // この後packageBを定義します ) func Called() { packageB.Called() }
続いてpackageB/packageB.go
、packageC/packageC.go
を定義します。
package packageB import ( "import-cycle-example/packageB" ) func Called() { packageC.Called() }
package packageC import ( "import-cycle-example/packageA" ) func Called() { packageA.Called() }
そして最後にmain.goを定義し、まずはpackageAのCalled()をコールします。
package main import ( "import-cycle-example/packageA" ) func main() { packageA.Called() }
プログラムを実行すると、循環参照が起こりました。
$ go run main.go package command-line-arguments imports import-cycle-example/packageA imports import-cycle-example/packageB imports import-cycle-example/packageC imports import-cycle-example/packageA: import cycle not allowed imports import-cycle-example/packageA: import cycle not allowed
依存関係をグラフ化して解決する
小規模なプロジェクトであれば、依存関係をグラフ化して、解消するのがおすすめです。 まずは、依存関係のグラフ化のためにgodepgraphをインストールします。
$ go install github.com/kisielk/godepgraph@latest
インストールが完了したら、プロジェクト配下で依存関係をグラフ化します。godepgraph モジュール名 | dot -Tpng -o グラフを出力するファイル名
に沿って、コマンドを実行します。
$ godepgraph import-cycle-example | dot -Tpng -o import-cycle-graph.png
出力されたグラフは以下のようになります。
このグラフから、packageA → packageB → packageC → packageAのような依存関係になっていることが分かります。
packageAが始点となり、packageB、packageCを経由して、packageAが終点になる経路になっていることから、循環参照していることがわかります。
今回は、以下のようにpackageCからpackageAの参照を削除して、閉路にならないようにします。
packageC/packageC.go
を以下のように修正します。
package packageC func Called() { //packageA.Called() }
プログラムを実行すると、エラーが消えて、循環参照が解消されました。
$ go run main.go $
再度依存関係を、グラフ化してみましょう。
$ godepgraph import-cycle-example | dot -Tpng -o import-cycle-fixed-graph.png
packageAが始点となり、packageCが終点になっているので、循環参照が解消されたことがわかります。
一方で、大規模なプロジェクトでこの方法を使用すると、どうなるでしょうか。弊社のmakeshopというプロダクトで、依存関係をグラフ化してみます。
パッケージ数が多いため、グラフから依存関係を把握するのは、得策ではないように感じます。
循環参照が起こないようにするには
ルールに沿ってコーディングすれば、循環参照が起きることはなくなります。その一例として参考までに、クリーンアーキテクチャという設計思想があります。
弊社のmakeshopというサービスでは、クリーンアーキテクチャの思想に習って、ディレクトリが組まれています。
クリーンアーキテクチャは、プロダクトをいくつかのレイヤーに分類します。外側のレイヤーから内側のレイヤーへの依存だけが許可されているので、双方向に依存し合うことはありません。
出典:https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
したがって、このルールにしたがってコーディングできれば、大規模なプロジェクトでも循環参照が起きることはなくなります。
まとめ
循環参照は、Goを学びたての頃にはよくつまづくところですが、原因がわかれば怖くありません。
今回紹介したこの方法以外にも、循環参照を解決する方法はたくさんありますが、少しでもご参考になれば幸いです。