技術備忘記

日々調べたこととかとりとめなく

Go言語でのimport cycle not allowedに対処する

はじめに

golang でのWeb開発では、RubyにおけるRailsのようなデファクトスタンダードフレームワークがないので、 パッケージ構成に関して、開発者に大きな裁量があります。

通常、Web開発では多くのフレームワークが採用しているMVC + Service的な形のパッケージ構成にすることが 無難ではないかと思います。

しかし、標準パッケージやOSSのコードでは結構フリーダムな構成(に見える)が多く、 一時期私も「こういう感じがGoっぽいぜ!」と若干厨二的な発想に陥り、 あえてMVC + Service的な構成を取らずに開発をすることがありました。

結果、表題の import cycle not allowed に悩まされることが増え、 最近またMVC + Serviceの形に戻すようになりました。 (この戻す作業が本当に大変でした...)

パッケージ構成についてはそれはそれで面白いテーマですが、 今回はどうしても避けられないimport cycleが発生した場合、どう対処するか?というお話です。

なぜimport cycleが禁止されているのか?

公式なドキュメントが見つけられなかったのですが、コンパイラ(Go Compiler)の実装をシンプルにするためでしょう。

Go Compilerは、複雑な実装を避けるためCやC++のような高度なオプティマイザも無ければ、 インライン関数もサポートしていません。 同じ理由でこのimport cycleもサポートしていないのだと思います。

Javaなんかは意外とOKなんですね。 昔苦しんだ記憶があるのですが、相互参照による予期せぬ動作、とかだったのかもしれません。

個人的には、やはりGoやC言語のようにコンパイルでエラーにするのが良いと思います。

対処方法

intarfaceを用いるのが一般的なようです。

Before

package foo

import (
    "fmt"
    "bar"
)

type Foo struct{}

func (f Foo) P() {
    fmt.Println("foo")
}

func CallBar() {
    bar.Bar{}.P()
}
package bar

import (
    "fmt"
    "foo"
)


type Bar struct{}

func (b Bar) P() {
    fmt.Println("bar")
}

func CallFoo() {
    foo.Foo{}.P()
}
import cycle not allowed
package foo
  imports bar
  imports foo

After

package foo

import (
    "buz"
    "fmt"
)

type Foo struct{}

func (f Foo) P() {
    fmt.Println("foo")
}

func CallBar(p buz.Printer) {
    p.P()
}
package bar

import (
    "buz"
    "fmt"
)

type Bar struct{}

func (b Bar) P() {
    fmt.Println("bar")
}

func CallFoo(p buz.Printer) {
    p.P()
}
package buz

type Printer interface {
    P()
}
package hoge

import (
    "bar"
    "foo"
)

func Call() {
    foo.CallBar(bar.Bar{})
    bar.CallFoo(foo.Foo{})
}

依存関係は以下のようになります

foo => buz
bar => buz
hoge => foo, bar

最後に

一通り苦しんだ後標準パッケージの構成を改めてみると、 相当な検討、議論があって今の形になっているということがわかった気がします。 外面だけ真似ても駄目ですね(^^;)