Goでアーキテクチャ違反とデッドコードを機械的に検出する

AIエージェント × knipで無駄コードを簡単に掃除等で紹介されているが、 TypeScriptであればknipのような便利ツールがある。Goだとdeadcodeがそれかなーと思いつつ、AI時代にデッドコードを削除したり、アーキテクチャの制約を機械的に検知したくなるのでメモ書き。

deadcodeでコード削除

皆さんお馴染みdeadcode は Go公式ツールの一部。コールグラフ解析で到達不可能な関数を検出する。

go run golang.org/x/tools/cmd/deadcode@latest -test ./...

-test つけるとテストから呼ばれてる関数は除外してくれる。

go-arch-lintでアーキテクチャを保つ

go-arch-lint は Go のパッケージ間依存を YAML で定義して違反を検出するツール。

存在はこれで知った。

developers.cyberagent.co.jp

.go-arch-lint.yml をリポジトリルートに置く:

(例)

version: 3
workdir: .

components:
  # Domain 層
  domain_user:
    in: internal/domain/user
  domain_order:
    in: internal/domain/order

  # UseCase 層
  usecase:
    in: internal/usecase/**

  # Infrastructure 層
  infra_repository:
    in: internal/infra/repository
  infra_repository_impl:
    in: internal/infra/repository/postgres
  infra_external:
    in: internal/infra/payment

  # 共通パッケージ
  pkg:
    in: pkg/**

commonComponents:
  - pkg

deps:
  domain_order:
    mayDependOn:
      - domain_user

  usecase:
    mayDependOn:
      - domain_user
      - domain_order
      - infra_repository
      - infra_external

  infra_repository_impl:
    mayDependOn:
      - domain_user
      - domain_order
      - infra_repository

mayDependOn はホワイトリスト方式。なので書いてないやつは全部違反になる。

golangci-lintで未使用コード検出

unused(未使用の宣言)、unparam(未使用の引数)、ineffassignwastedassign あたりを有効化し、いらないものを検知できるようにする。

linters:
  enable:
    - unused
    - unparam
    - ineffassign
    - wastedassign

カスタムリンター

go-arch-lint で足りなければ go/ast でカスタムリンター書ける。例えば UseCase の公開メソッドにGoDocを強制するリンター

file, _ := parser.ParseFile(fset, path, nil, parser.ParseComments)

for _, decl := range file.Decls {
    fn, ok := decl.(*ast.FuncDecl)
    if !ok || fn.Recv == nil || !ast.IsExported(fn.Name.Name) {
        continue
    }

    // レシーバの型名を取得(*OrderUseCase → "OrderUseCase")
    recvType := fn.Recv.List[0].Type
    if star, ok := recvType.(*ast.StarExpr); ok {
        recvType = star.X
    }
    ident, ok := recvType.(*ast.Ident)
    if !ok || !strings.HasSuffix(ident.Name, "UseCase") {
        continue
    }

    // GoDoc に必要な行があるかチェック
    if fn.Doc == nil {
        // GoDoc 自体がない → 違反
    } else {
        found := false
        for _, c := range fn.Doc.List {
            if strings.Contains(c.Text, "....") {
                found = true
            }
        }
        if !found {
            // ... 行がない → 違反
        }
    }
}

parser.ParseFile して file.Decls をループ、条件に合う関数の fn.Doc を覗くだけ。100〜200行で書けるのでプロジェクト固有ルールの強制におすすめ。

試してないやつ

DatadogのHow we reduced the size of our Agent Go binaries by up to 77%が面白かった。 Datadogはdeadcodeでリンカーの最適化が無効化を特定し、reflect の呼び出しサイトをパッチ。さらに goda で依存グラフを可視化して不要なパッケージ依存を丸ごと削除。結果、バイナリサイズ最大77%削減したらしい。

自分のプロジェクトはそこまで巨大じゃないのでソースコードレベルで十分だけど、大規模になったらこのアプローチも試してみたいなーと思う。