LayerX エンジニアブログ

LayerX の エンジニアブログです。

Go の linter 雰囲気で使っていたから調べ直した #LayerXテックアドカレ

こんにちは。LayerX バクラク事業部 バクラクビジネスカード開発チーム エンジニアの @shota_tech です。 最近運動不足解消のため chocoZAP に通い始めたのですが、胸筋鍛えるマシンだと思って使っていたやつが背筋鍛えるやつでした。

この記事は LayerXテックアドカレ2023 18日目の記事です。前回は @minako-ph が「Notionでスプリントのあれこれをダッシュボードで可視化する」を書いてくれました。次回は弊社 CTO 松本が「ISUCONをLLMで戦った話」を書いてくれる予定なのでご期待ください。

今回は Go でよく使われている linter の特徴についてまとめたのでご紹介します。

背景

LayerX では有志メンバーで「Go言語 100Tips ありがちなミスを把握し、実装を最適化する」の輪読会をやっています。 tech.layerx.co.jp

私もこれに参加しており、毎週新しい発見があり非常に学びが多い会なのですが、新しい発見が多すぎて忘れそう & 業務で実際に使うタイミングは忘れた頃にやってきそうなので、極力 linter で検知出来るようにしておきたいと思ったのがきっかけです。 普段雰囲気で使っている linter について調べ直してまとめたので、何かしらの参考になりましたら幸いです。

go vet

pkg.go.dev

vet は Go に標準搭載されている linter で、コンパイラでは検出されない、バグになりうるコードを検出してくれます (fmt.Printf で引数の数がフォーマットと不一致など)。

Go 公式の Language Server である gopls でも vet が自動実行されているため、VSCode などを使用している場合は特に設定無しでも警告を出してくれます。

またデフォルトでチェックされる項目以外にも、golang.org/x/tools/go/analysis パッケージを使って作成された Analyzer であれば何でも実行出来るため、自作ルールや他の人が作成したルールを適用することも出来ます。詳しくはこちら↓ tech.layerx.co.jp

staticcheck

staticcheck.dev

staticcheck も vet と同様にバグとなりうるコードを検出してくれる linter で、チェック項目が150以上とかなり多いのが特徴です。VSCode でもデフォルトの linter になっており、使っている方も多いのではないかと思います。

各ルールにはコードが採番されており、以下のように TOML 形式の設定ファイルで有効/無効にするルールを選択出来ます。

checks = ["all", "-SA9003", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-ST1023"]

ちなみにコードの接頭辞には以下のような意味があります。

接頭辞 名前 内容
SA staticcheck コードの正しさに関するチェック
S simple コードの簡潔さに関するチェック
ST stylecheck コーディングスタイルに関するチェック
QF quickfix gopls の自動リファクタリングとして表示されるチェック

revive

github.com

revive も staticcheck と並び有名な linter の1つで、以前 Go 公式 organization に存在していた golint (今はアーカイブ済み) と互換性があります。staticcheck と比べると、コーディングスタイルに関するチェック項目が多いのが特徴です。

設定ファイルは TOML 形式で記述出来、ルールによっては Arguments を指定することで挙動をカスタマイズすることも出来ます (例えば rule.argument-limit では引数の数の上限を指定出来るなど)。

severity = "warning"
confidence = 0.8
errorCode = 0
warningCode = 0

# Enable all available rules
enableAllRules = true

# Disabled rules
[rule.blank-imports]
    Disabled = true
...

# Rule tuning
[rule.argument-limit]
    Arguments = [5]
...

golangci-lint

golangci-lint.run

golangci-lint は staticcheck や revive とはジャンルが異なり、様々な linter を一元管理・実行することが出来るツールです。 複数の linter を組み合わせて使いたい場合に、個別の linter をインストールすることなく利用出来、設定ファイルも1つに集約出来るので便利です。

対応している linter は vet や staticcheck、revive の他にも、特定のユースケースに特化した linter など多数あり、「XXX を検知出来る linter ないかな」と思ったらまずここから探してみると良さそうです。

golangci-lint.run

設定ファイルは YAML 形式で、有効にする linter と各 linter の設定を記述します。

linters:
  disable-all: true
  enable:
    - govet
    - staticcheck
    - revive
    ...

linters-settings:
  staticcheck:
    checks: ["all"]
  revive:
    ...

go-ruleguard

github.com

go-ruleguard はカスタムルールを定義することが出来るツールです。社内で独自のルールを定めたい場合などに便利です。 静的解析の知識があまり無くても直感的にルールを記述出来、また Go に特化しているので型情報の参照なども簡単です。

例えば以下のようなコードがあったとします。

res, err := query()
if err != nil {
    if err == sql.ErrNoRows {
        // sql.ErrNoRows の処理
    } else {
        // その他のエラーの処理
    }
}

Go言語 100Tips ありがちなミスを把握し、実装を最適化する」の No.51 でも説明されていますが、エラーが特定のエラーと一致するかどうかの識別は == よりも errors.Is を使う方が、エラーがラップされている場合も正常に検知出来るため好ましいです (詳細はこちら)。これを go-ruleguard を使って検知したい場合、以下のように書けます。

import (
    "github.com/quasilyte/go-ruleguard/dsl"
)

func errorsIsNotUsed(m dsl.Matcher) {
    m.Match(`err == $target`).
        Where(m["target"].Type.Is("error")).
        Report("err should be checked using errors.Is").
        Suggest("errors.Is(err, $target)")
}

Match で対象となる文字列を指定、WhereMatch 内で定義した変数に関する条件を追加、Report でエラーメッセージを定義しています。 ちなみに最後の Suggest を設定すると、go-ruleguard 実行時に fix オプションをつけた場合に自動修正してくれます。

実行結果は以下のような感じで出力されます。

$ ruleguard -rules rules.go main.go
path/to/main.go:10:6: errorsIsNotUsed: err should be checked using errors.Is (rules.go:8)

go-critic という linter を経由することで golangci-lint でも実行出来ます。

www.quasilyte.dev

semgrep

semgrep.dev

semgrep も go-ruleguard と同様にカスタムルールを定義出来るツールですが、こちらは Go に限らず様々な言語のコードを検出出来ます。

設定ファイルは YAML で記述します。go-ruleguard で挙げた例と同じことを検出しようとすると以下のように書けます。

rules:
    - id: errors-is-not-used
    patterns:
        - pattern: err == $TARGET
        - metavariable-regex:
            metavariable: $TARGET
            regex: (^Err.*|.*\.Err.*)
    message: "err should be checked using errors.Is"
    languages: [go]
    severity: WARNING

pattern で対象となる文字列を指定、metavariable-regexpattern 内で定義した変数に関する条件を追加、message でエラーメッセージを定義しています。 ($TARGETerror 型に限定する方法が分からなかったので Err から始まる文字列で妥協してますmm)

実行結果は以下のような感じで出力されます。

$ semgrep -f rules.yml main.go
               
               
┌─────────────┐
│ Scan Status │
└─────────────┘
  Scanning 1 file (only git-tracked) with 1 Code rule:
            
  CODE RULES
  Scanning 1 file.
                    
  SUPPLY CHAIN RULES
                  
  No rules to run.
                  
          
  PROGRESS
   
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:00                                                                                                                        
                  
                  
┌────────────────┐
│ 1 Code Finding │
└────────────────┘
                           
    main.go 
       errors-is-not-used            
          err should be checked using errors.Is
                                               
           10if err == sql.ErrNoRows {

                
                
┌──────────────┐
│ Scan Summary │
└──────────────┘

Ran 1 rule on 1 file: 1 finding.

個人的には go-ruleguard の方が型情報なども簡単に参照出来て使いやすいなと感じましたが、semgrep はプリセットされているルールが豊富だったり Playground も用意されていたりなど、エコシステムが充実している印象です。 この辺りの比較の深掘りや、もっと実践的な使い方などはまたの機会にご紹介出来ればなと思います。


終わりに

今回は Go の主要な linter についてまとめました。 弊社内でも様々な linter が採用されていますが、どのようなルールが設定されているか普段あまり意識していなかったため、これを機に linter についての知見を深めていこうと思います。 また今後は輪読会で得られた知見を lint ルールに落とし込むなど、組織全体としてより安心安全なプロダクト開発を出来るようにしていければなと思います。

最後に、LayerX では一緒に働いてくれる仲間を大募集中です! この記事を読んでくださって、「輪読会面白そう」とか「こんな便利な linter あるよ」という方がいらっしゃいましたら、LayerX Casual Night というお酒を飲みながら技術の話をゆる~く行うイベントも開催しておりますので、是非一度カジュアルにお話させてください!

jobs.layerx.co.jp

採用情報はこちら↓ jobs.layerx.co.jp