yyh-gl's icon

yyh-gl's Tech Blog

技術ネタ中心のブログです。主な扱いはバックエンド技術と設計です。

yyh-gl

4 分で読めます

featured

DMM Advent Calendar 2019

本記事は DMM Advent Calendar 2019 の 9日目 の記事です。


私は現在、DMM.com の CDS というチームに所属し、
主にユーザレビュー基盤 のバックエンドを開発しています。


今回は、Go用Linterである GolangCI-Lint を軽く紹介した後に、
GolangCI-Lint のハマリポイントとその解決策である設定周りの話をします。

Linter 導入していますか?

突然ですが、みなさんの開発環境には Linter が導入されているでしょうか?

私の所属するチームでは、
コーディング規約違反 および コンパイラでは見つけられないエラー を検知するために、
ローカルと CI において Linter を回すようにしています。

GoにおけるLinter

Goの場合、Linterがデフォルトで用意されているうえに、
ライブラリとして公開されているものも多く存在します。

なかでも有名なものに以下のようなものがあります。

  • govet:GoデフォルトのLinter
  • errcheck:ちゃんとエラーハンドリングしているかチェックしてくれる
  • unused:未使用の定義をチェックしてくれる
  • goimports:未使用のimportを消してくれたり、フォーマット修正してくれる
  • gosimple:コードをシンプルにしてくれる

しかしながら、多すぎるがゆえに どれを選択すればいいのか分からなくなりがちです
加えて、導入する Linter が増えれば、その分だけ 導入・管理コストが増加 します。

この問題を解決してくれるツールが GolangCI-Lint です。

GolangCI-Lint

勉強会でもよく耳にするようになってきている+多くの紹介記事があるので、
ここで詳しく説明する必要もないかもしれませんが、いちおう少しだけ触れておきます。


GolangCI-Lint とは、 GoのLinterを一元管理するためのツールです。
開発者は GolangCI-Lint を導入するだけで様々な Linter を実行することができます。

したがって、Linter の導入・管理コストが一気に下がりますし、
運用していく過程で不要だと感じた Linter は、簡単に無効化することもできるので、
気軽に Linter を試用することができます。

対応 Linter はこちら に一覧が載っています。

似たようなツールに gometalinter というのがあったのですが、
こちらの議論 の結果、なくなることが決定しました。
今後の主流は GolangCI-Lint です


(…ロゴいいですよね👍)

使ってみる

導入

こちら に導入方法が書いてあります。

Binary のインストール方法を紹介しておくと、下記のようになります。

# $(go env GOPATH)/bin ディレクトリ配下にインストールする方法
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.21.0

# ./bin ディレクトリ配下にインストールする方法
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.21.0

# alpine linux 用のインストール方法
wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.21.0

もちろん go get でもインストールできますし、
他にも brew や Docker イメージとしても提供されています。

IDEやエディタ上で実行する方法も紹介 されており、サポートが手厚いです。

弊チームでは、ローカル用コンテナイメージのビルド時に go get してインストールしています。

実行

$ golangci-lint run コマンドで実行できます。
テストファイルにも Lint をかけたい場合は、--tests オプションを付与します。

何も設定しない状態では、こちら に記載のある Linter が実行されます。

では、実際に動かしてみます。

$ docker-compose exec -T app golangci-lint run --tests ./...
handler/rest/blog.go:82:27: Error return value of `(*encoding/json.Encoder).Encode` is not checked (errcheck)
	json.NewEncoder(w).Encode(res)
	                         ^
domain/model/task/task.go:9:2: structtag: struct field tag `json:"title","hoge"` not compatible with reflect.StructTag.Get: key:"value" pairs not separated by spaces (govet)
	Title         string      `json:"title","hoge"`
	^

エラーが出ました。
2行目と5行目の最後に括弧書きでエラーを発見した Linter の名前が書いてあります。
(厳密には Lint で出力された内容はエラーではありませんが、CIがこけるという意味で「エラー」と呼ぶことにします)
今回の場合だと、errcheck と govet がエラーを発見したようですね。

GolangCI-Lint には検知できないエラーがある…?🧐

では、ここから本記事の主題に入っていきたいと思います。
実際に GolangCI-Lint を導入しようとしてハマったポイントです。

といっても、GolangCI-Lint の README はとても詳細に書かれているので、
なにかあっても README を見ればすぐ解決できます👍




そんなこんなでいきなりですが、同じソースコードに対して、
GolangCI-Lint を使わずに golint を単体で走らせてみます。

$ golint ./...
domain/model/task/task.go:7:1: comment on exported type Task should be of the form "Task ..." (with optional leading article)

!?
さきほどの GolangCI-Lint にはなかったエラーが出力されました。

なんとなく分かってきた方もおられると思いますが、
GolangCI-Lint はデフォルト設定だと、いくつかのエラーを無視するようになっています。

例えば、今回の例だと、コメントの記述形式についてのエラーですが、
そこまで厳密に守らなくてもいい内容ですね。(僕は守りたい派ですが。。。)
したがって、GolangCI-Lint が気を利かせて無視するようにしてくれています。

デフォルトで無視されるルール

デフォルト設定だと無視されるルールは
こちら--exclude-use-defaultオプションの説明のところに記載があります。
抜粋してくると以下のとおりです。

Linter名無視されるエラー(Linterが出力する内容)
1errcheckError return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked
2golint(comment on exported (method|function|type|const)|should have( a package)? comment|comment should be of the form)
3golinttest系パッケージにおける func name will be used as test\.Test.* by other packages, and that stutters; consider calling this
4govet(possible misuse of unsafe.Pointer|should have signature)
5staticcheckineffective break statement. Did you mean to break out of the outer loop
6gosecUse of unsafe calls should be audited
7gosecSubprocess launch(ed with variable|ing should be audited)
8gosecerrcheckと重複するエラーチェック G104
9gosec(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)
10gosecPotential file inclusion via variable

さきほど例に挙げていた、golint のコメント記述形式に関するエラーは、表中2番のエラーです。
だから、GolangCI-Lint では検知されなかったんですね。

このルール、人によっては「これ無視しちゃだめだろ」と思われるものもあると思いますが、
投稿日時点ではこのようなルールがデフォルトで無視されるようになっています。

設定ファイル .golangci.yml

気を利かせてくれているのは分かりますが、無視しないで欲しいときもありますよね。
逆にこのエラーは無視してほしいっていうニーズもあると思います。

そこで登場するのが .golangci.yml です。
.golangci.yml により、GolangCI-Lint の細かな設定が可能になります。

CLIのオプションでも指定できますが、チームで共有するなら設定ファイルの方がいいでしょう。
また、後述しますが、CLIのオプションでは指定できない設定もあるので注意が必要です。

設定方法

設定ファイルとして .golangci.yml を紹介しましたが、他にも下記の拡張子が使用できます。

  • .golangci.toml
  • .golangci.json

今回は.golangci.ymlを使用します。

設定ファイルのサンプルがGitHub上に公開 されています。

使えるオプションはCLIと同じです。
ただし、CLI では、Linter ごとの設定(linters-settings)ができないため、
Linter ごとに細かく設定をしたい場合は設定ファイルを書く必要があります。

設置場所

次に、.golangci.ymlをどこに置くのかという話ですが、
PC のルートディレクトリからプロジェクトのルートディレクトリ内のどこか であればOKです。

例えば、$GOPATH が /go で、プロジェクトルートが /go/src/github.com/yyh-gl/hoge-project だった場合、
以下のディレクトリ内を見に行ってくれます。

  • ./
  • /go/src/github.com/yyh-gl/hoge-project
  • /go/src/github.com/yyh-gl
  • /go/src/github.com
  • /go/src
  • /go
  • /

上にいくほど優先順位が高いです。(PCのルートディレクトリが一番低い)
基本的には各プロジェクトのルートに置いておけばいいでしょう。

実際に読み込まれている設定ファイルは-vオプションで確認可能です。

$ golangci-lint run --tests -v ./...
level=info msg="[config_reader] Config search paths: [./ /go/src/github.com/yyh-gl/hoge-project /go/src/github.com/yyh-gl /go/src/github.com /go/src /go /]"
level=info msg="[config_reader] Used config file .golangci.yml" ← ここ

<省略>

では、実際に設定ファイルを変更し、
さきほどの golint が検知していたコメント記述形式に関するエラーを、
GolangCI-Lint でも検知できるようにしてみます。

“デフォルトで無視されるルール"を無視する

golint が検知していたコメント記述形式に関するエラーを検知するには、
“デフォルトで無視されるルール"を無視する必要があります。

設定自体はすごく簡単です。

# .golangci.yml

issues:
  exclude-use-default: false

以上です。

テストしてみましょう。

$ docker-compose exec -T app golangci-lint run --tests ./...
handler/rest/blog.go:82:27: Error return value of `(*encoding/json.Encoder).Encode` is not checked (errcheck)
	json.NewEncoder(w).Encode(res)
	                         ^
domain/model/task/task.go:7:1: comment on exported type Task should be of the form "Task ..." (with optional leading article) (golint)
// Taskhoge : タスクを表現するドメインモデル
^
domain/model/task/task.go:9:2: structtag: struct field tag `json:"title","hoge"` not compatible with reflect.StructTag.Get: key:"value" pairs not separated by spaces (govet)
	Title         string      `json:"title","hoge"`
	^

golint のエラーが増えましたね👍

このように簡単に GolangCI-Lint の設定を変更することができます。

細かな設定も可能

さきほど少し触れましたが、各 Linter ごとの細かな設定も可能です。

linters-settings

各 Linter ごとの設定は linters-settings によって定義できます。

# .golangci.yml

linters-settings:
  errcheck:
    check-type-assertions: false
    check-blank: false
    ignore: fmt:.*,io/ioutil:^Read.*
    exclude: /path/to/file.txt
  govet:
    check-shadowing: true
    settings:
      printf:
        funcs:
          - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
          - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
          - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
          - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
    enable:
      - atomicalign
    enable-all: false
    disable:
      - shadow
    disable-all: false
  golint:
    min-confidence: 0.8

例えば、golint の min-confidence は Lint の厳しさを設定するもので、
数値が低いほど厳しいルールが適用されます。
(ちなみに、デフォルトは 0.8で、1.1 にすると何も検知しなくなります😇)

他の設定たち

GolangCI-Lint で使用できる設定を探したい場合は、
設定ファイルのサンプルを参考にすればOKです。

このファイルの中に利用可能な全ての設定とデフォルト値が記載されています👍 最高ですね

まとめ

GolangCI-Lint により、様々な Linter が一元管理でき、
Linter の導入・管理コストがとても低くなったと感じています。
また、いろいろな Linter を気軽に試せるようになりました。

ちょっとしたコーディング規約違反を毎回人力で指摘している方や
コンパイラでは発見できないエラーを潰すのに疲弊している方などは、
ぜひ、GolangCI-Lint の導入を検討しみてはいかかでしょうか?

最高の DX です🎁


DMM Advent Calendar 2019、明日は mimickn さんです!

最近の投稿

About

東京で働くソフトウェアエンジニアです。バックエンドがメインですが、フロントエンドやインフラもさわっています。