SODA Engineering Blog
🔌

golangci-lintのModule Plugin Systemが良さそうなので使ってみた

2024/06/12に公開

はじめに

この記事では、golangci-lintのv1.57.0でリリースされたModule Plugin Systemについて既存のPlugin機構を交えて解説します。

弊社サービスSNKRDUNKのバックエンドはGoで実装されておりLinterはgolangci-lintを使っています。
Pluginで動かしているLinterもあり、その影響でローカル環境でgolangci-lintを実行するのがやや手間になっており、何か良い方法がないかと調べていたらModule Plugin Systemというものがリリースされていたので、自分自身の理解のためにも今回の記事を書くに至りました。

golangci-lintのPlugin Systemについて

まずはじめに、golangci-lintにlinterを追加する方法は以下の2通りあります。

1. public linterとしてgolangci-lint本体に実装する
2. private linterとして実装して、golangci-lintのPluginとして動かす

1のケースではgolangci-lintの公式ドキュメントに沿って実装してPRを出すフローです。
label: newのラベルでフィルタリングするとlinterに関するPR一覧を確認できます。
declinedされてるlinterも結構ありますが、この辺のPRを読むのも勉強になりそうですね。

Plugin Systemは、2のprivate linterをgolangci-lintの一部として動作することを可能にする仕組みです。
Plugin SystemにはGo Plugin SystemModule Plugin Systemの2つが存在します。

Go Plugin System

https://golangci-lint.run/plugins/go-plugins/

Go Plugin SystemはModule Plugin Systemがリリースされる前から存在しているPlugin機構で、Go標準パッケージで用意されているPlugin機構を利用して実現されています。
仕組としては任意のパスに配置したPluginの実行ファイル(*.so)をgolanci-lintが実行時に標準Pluginパッケージの仕組みを使ってロードして実行するという感じです。

追加手順

1. Linterを実装する

Linterではgolang.org/x/tools/go/analysisAnalyzerを提供する必要があります。
https://github.com/golangci/example-plugin-linter/blob/master/example.go#L10-L14

さらにPluginとして動かすためにはplugin/example.goのようファイルを作成して、([]*analysis.Analyzer, error)を返すNew関数を実装する必要があります。
↓が公式ドキュメントに載っている例です。
https://github.com/golangci/example-plugin-linter/blob/master/plugin/example.go

2. Linterをビルドして*.so実行ファイルを生成する

実行ファイルを生成するには以下のようにgo buildコマンドでオプション-buildmode=pluginを指定して、手順1で追加したplugin/exmaple.goファイルパスを指定します。

go build -buildmode=plugin plugin/example.go

3. .golanci.ymllinters-settings.custom.{linter_name}.pathでビルドした*.soファイルへのパスを設定して、linters.enableで有効かする

以下が具体的な指定方法です

linters-settings:
  custom:
    example:
      path: /example.so
      description: The description of the linter
linters:
  disable-all: true
  enable:
    - example

Go Plugin Systemのつらみ

1. golangci-lintをCGO_ENABLED=1でビルドする必要がある

これはGo標準のPluginでCGO_ENABLED=1じゃないと動かないのが影響してると思われます。
https://github.com/golang/go/issues/19569

2. golangci-lintと同じスタックでPluginをビルドする必要がある

Goのバージョン, 依存するライブラリのバージョンなどは全て揃えないと動かないです。つらい。
以下のようにgolangci-lintをblank importすることで、明示的にgolangci-lintに依存させてライブラリのバージョンを揃えるということをしていました。
https://github.com/toshiki-otaka/argument_checker/blob/main/tools/tools.go

3. ビルドしたPluginの実行ファイル(*.so)を任意のディレクトリに配置する必要がある

1人でローカル開発する分にはあまり気にする問題ではないかも知れませんが、チームで開発する場合にそれぞれの手元でPluginをビルドしてもらうのは結構大変です。
そしてローカルのgolangci-lintのバージョンがそれぞれ異なることが原因で、ローカルでPluginが動かないこともあります。

Module Plugin System

Module Plugin Systemはv1.57.0でリリースされた新しいPlugin機構です。
https://golangci-lint.run/product/changelog/#v1570

この新しく追加されたPlugin機構では、Go Plugin Systemのつらみを解消するアプローチをいくつかとっています。

  • PluginのLinterパッケージをシンプルなGoの依存関係として、golangci-lintのcmd/golanci-lint/plugins.goにブランクインポートする

  • PluginのLinterパッケージのinit関数でgithub.com/golangci/plugin-module-register/registerパッケージのPlugin関数を使って、レジスタに自身を登録する

  • golangci-lint内部では、レジスタからプラグインをロードする

上記のようなアプローチをすることで、GoのPlugin機構を使うことがなくなるのでCGO_ENABLED=1でgolangci-lintをビルドする必要がなくなります。
さらに、シンプルなGoの依存関係としてPluginをブランクインポートするので、golanci-lintビルド時にPluginが依存パッケージとして同梱されてる状態なので、Plugin側でバージョン差分などを気にする必要がなくなります。
そして、Pluginはgolangci-lintのバイナリに含まれているので、別途Pluginの実行ファイルを任意のディレクトリに配置する必要もありません。

Go Plugin Systemのつらみを解消していてすごい!!

さらに、この変更に伴ってgolanci-lint customコマンドもリリースされ、設定ファイルを追加することで、このコマンド一発でPluginを同梱したgolanci-lintバイナリをビルドしてくれます。便利すぎる

追加手順

1. .custom-gcl.ymlに設定を記述する

version: v1.59.0 # golangci-lintのバージョン
name: custom-golangci-lint # バイナリの名前。デフォルトはcustom-gcl
destination: ./.plugins/ # バイナリが格納されるディクトりを指定
plugins:
  - module: 'github.com/snkrdunk/empty_err_checker' # Pluginのモジュール名
    path: ../empty_err_checker/ # Pluginへのパス

2. golangci-lint customでカスタムバイナリをビルドする

-vオプションをつけるとログを確認できます。ビルドが成功すると以下のような表示になります。
ログからcmd/golangci-lint/plugins.goにPluginパッケージをブランクインポートしていることが分かります。

$ golangci-lint custom -v
INFO Cloning golangci-lint repository
INFO Adding plugin imports
INFO generated imports info /var/folders/d_/j7r2nk5x74dblhw7p5v_rgc80000gn/T/custom-gcl649156304/golangci-lint/cmd/golangci-lint/plugins.go:
package main

import (
        _ "github.com/snkrdunk/empty_err_checker"
)

INFO Adding replace directives
INFO run: go mod edit -replace github.com/snkrdunk/empty_err_checker=/Users/otaka.toshiki/go/src/github.com/snkrdunk/empty_err_checker
INFO Running go mod tidy
INFO Building golangci-lint binary
INFO Moving golangci-lint binary

3. .golangci.ymlに設定を記述

以下が設定例です。
Go Plugin Systemと違うのは、typeで"module"を指定していることです。

linters-settings:
  custom:
    custom_linters:
      type: "module"
      description: custom_linters is checking terms of snkrdunk.com.
linters:
  disable-all: true
  enable:
    - custom_linters

4. カスタムビルドしたgolangci-lintを実行

今回の例だと、.pluginsディレクトリにcustom-golangci-lintという名前でカスタムビルドしたバイナリを格納したので以下のように実行すると、Pluginを含んだ状態でgolangci-lintが走ります。

$./.plugins/custom-golangci-lint run

最後に

Module Plugin Systemが追加されたPRでGo Plugin Systemが廃止されるか、今後も共存するのかという会話があり、2つのPlugin Systemは共存できメンテナンスもそこまで必要ないが、これが将来必ずGo Plugin Systemを廃止しないということではないとコメントされています。


https://github.com/golangci/golangci-lint/pull/4437#issuecomment-1979723752

なので、すぐにGo Plugin Systemが廃止されることはない思いますが、今後廃止される可能性はあるので早めにModule Plugin Systemを使うようにした方が良いかもしれません。

SODA Engineering Blog
SODA Engineering Blog

Discussion