【Golang】fe3dback/go-arch-lintでアーキテクチャの破壊を防ぐ

前書き:アーキテクチャは容易に壊される

アーキテクチャリンターであるfe3dback/go-arch-lintnao1215/sqlyに導入したので、使用方法のメモを記事として残します。結論としては、初期設定が面倒ですが、期待通りの効果が得られました。なお、既存コードがカオスなアーキテクチャの場合、go-arch-lintを採用できないと思われます。

 

まず、アーキテクチャをリンターでチェックする発想に至った理由から、説明します。以前、ペアプロ中にドライバ側(実装する人)がアーキテクチャルールに反しているのを偶然目撃しました。違反内容は、「外部サービス操作用パッケージ内でのみ使用できる構造体をユースケースレイヤーから呼び出した」というものです。構造体の定義場所が悪いと思いつつも、ルール違反してしまう理由はアーキテクチャを理解していないからだと考え、対策が必要と考えました(ちなみに、ドライバの方に「その使い方、ダメですよ」と声をかけたら、「そうなんですか?」と返答がありました)

  

前提条件ですが、当時は以下のような状況で開発していました。

  • アーキテクチャに関するドキュメントが存在
  • Pull Request(以降PR)単位のレビューで、アーキテクチャルール違反をレビューアが検知
  • 必ずしもアーキテクチャルールが遵守されていたわけではない(グレーゾーンや暗黙の了解があった)

 

実装者のスキルレベルに合わせて、PRレビューの確認観点を意識的に変えるのは、それなりの難しさがあります。レビュー時間もかかります。レビューが長引くと、疲労によって余計な一言をコメントしてしまうリスクも高まります。となると、機械(リンター)ができることは機械にやらせよう、という発想に辿り着きます。機械から指摘された方が、イラッとしませんしね。

    

リンター候補

fe3dback/go-arch-lintarch-go/arch-goが候補でした。どちらもインポート対象パッケージ(依存パッケージ)をチェックする機能があります。

   

これらのリンターの差分は何でしょうか。go-arch-lintは、依存関係をグラフ化する機能があります。しかし、それ以外はarch-goの方が多機能です。例えば、依存パッケージ内に含められる定義(例:インターフェースのみ)を制御できたり、パラメータや返り値の数、ファイル単位のパブリック関数の数や関数の行数、命名規則のチェックなどができます。また、アーキテクチャを go test でき、コンプライアンス(リンター設定)遵守レベルのしきい値チェック機能があります。

 

しかし、go-arch-lintを採用しました。その理由は、「機能が少ない分、相対的に設定が楽そう」「既存プロジェクトは、arch-goの厳しい設定をパスできない」と考えたからです。プロジェクト特性に合わせて、好きなリンターを選択すれば良いかなといったレベル感です。

           

fe3dback/go-arch-lintのインストール方法

           

go-arch-lintの設定

.go-arch-lint.yml ファイルに設定を書き、プロジェクトのルートディレクトリに配置します。設定読み込みは、go-arch-lint checkを実行すれば、自動的に設定値が反映された状態でリンターが動作します。

 

.go-arch-lint.yml に記載する設定項目は、GitHubに詳細説明が書かれています。下表に、2025年2月13日時点の各設定項目を示します(GitHubに書かれている説明を訳したもの)

パス 必須 説明
version 必須 int スキーマバージョン(最新: 3)
workdir 任意 str 解析対象の相対ディレクトリ
allow 任意 map グローバルルール
. depOnAnyVendor 任意 bool プロジェクト内のすべてのファイルが任意のベンダーコードをインポートできるか
. deepScan 任意 bool 高度なASTコード解析を使用(v3以降デフォルト true
exclude 任意 []str 解析対象から除外するディレクトリのリスト(相対パス)
excludeFiles 任意 []str ファイル名の正規表現ルール。該当ファイルとそのパッケージを解析対象から除外
components 必須 map Goパッケージの抽象化。1つのコンポーネント = 1つ以上のGoパッケージ
. %name% 必須 str コンポーネントの名前
. . in 必須 str, []str 1つ以上の相対ディレクトリ名。グロブパターン対応(例: src/*/engine/**)
vendors 任意 map ベンダーライブラリ(go.mod)
. %name% 必須 str ベンダーコンポーネントの名前
. . in 必須 str, []str 1つ以上のベンダーライブラリのインポートパス(例: github.com/abc/*/engine/**)
deps 必須 map 依存関係のルール
. %name% 必須 str コンポーネントの名前(”components” セクションで定義したものと同一)
. . mayDependOn 任意 []str このコンポーネントがインポート可能なコンポーネントのリスト
. . canUse 任意 []str このコンポーネントがインポート可能なベンダーのリスト

   

基本的な設定は、以下のような流れで行います。

  1. excludeFilesに、除外対象ファイルを設定
  2. vendorsに、サードパーティライブラリのエイリアス名を設定
  3. componentsに、開発対象パッケージのエイリアス名を設定
  4. commonVendorsに、どのパッケージからも呼び出せるサードパーティライブラリ名を設定
  5. commonComponentsに、どのパッケージからも呼び出せるパッケージ名(componentsで定義したパッケージ)を設定
  6. depsに、各パッケージ(componentsで定義したパッケージ)の依存関係および利用するサードパーティライブラリを設定

        

設定例:公式の例、nao1215/sqlyの例

公式の設定例

        

nao1215/sqlyでの設定例

           

GitHub Actionsによるアーキテクチャルール違反の検知

.github/workflows/arch-lint.ymlに以下の設定を書くと、PR作成時にアーキテクチャが期待通りに実装されているかをチェックできます。

 

                 

pre-commitによるコミット前チェック

公式のREADMEでは、pre-commitを利用して、コミット前にアーキテクチャルール違反チェックおよびグラフ更新チェックを行う方法が提示されています。私は、python環境を構築するのがそれなりに手間だと考えているので、この方法を採用していません。

 

以下、公式のREADMEに書かれた手順の転載です。

  1. pre-commitをhttps://pre-commit.com/#installからインストール
  2. .pre-commit-config.yamlファイルをプロジェクトルートディレクトリに配置し、後述のコードブロックの内容を記載
  3. 必要であれば、argsにフラグを設定
  4. 設定を最新バージョンに更新するには、pre-commit autoupdateを実行
  5. pre-commitを有効にするには、pre-commit installを実行

             

最後に

OSS開発ではクリーンアーキテクチャを採用しないので、リンターでチェックする必要性がないかなと感じています。その一方で、業務の場合は、メンバのスキルやアーキテクチャに対する理解度がマチマチなので、リンターの出番があるかなと。

           

おすすめ