👻

GoでGitの差分だけFmt,Test,Lintを走らせて高速化する

2022/04/29に公開

Fmt,Test,Lintで使用するパッケージ

種類 パッケージ
Fmt gofmt(標準コマンド), goimports
Test testing(標準ライブラリ)
Lint golangci-lint

この記事を書いたきっかけ

運用が続いてコードが増えてくと、上記の3つがまあまあ遅くなる。
(例えば単体テストのケースが4000~5000になったりとかサービスによっては珍しくない)

純粋に上記の3つをそれぞれ高速化出来たら良いけど、中々開発工数を確保出来なくて、手が出せずにそのまま放置してしまっていた。

  • サクッとやりたい
  • 毎回のPRの差分だけでも、高速化出来れば

と思い、実際に現場で使ってみたらかなり開発しやすくなったので記事を書く事にした。

ローカルでやる場合

Makefileに定義して、他の人が簡単にコマンドを叩けるようにする。
(Windowsだとmakeコマンドが使えないので注意)

.PHONY: help
help: ## 使い方(運用が増えるとmakeコマンド多くなるので、確認出来るように付けておくと便利)
	@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)

.PHONY: dtest
dtest: ## 差分testの実行
	go test -v `$(call diff) | xargs -I{} echo ./{}`

.PHONY: dfmt
dfmt: ## 差分fmtの実行
	gofmt -s -w `$(call diff_files)`
	goimports -w -local "github.com/xx/xxx" `$(call diff)`

.PHONY: dlint
dlint: ## 差分lintの実行
	go mod download
	golangci-lint run -v -c .golangci.yml `$(call diff)`

# 変更差分のあるディレクトリを取得
define diff
git diff --name-only --diff-filter=ACMRT | grep .go$ | xargs -I{} dirname {} | sort | uniq
endef

実行するときはこんな感じ↓

$ make help
$ make dtest
$ make dfmt
$ make dlint

.golanci.yamlはチームによって違うけど、こんな感じ↓

linters-settings:
  goimports:
    local-prefixes: "github.com/xx/xxx"
  gocritic:
    enabled-tags:
      - performance
  gosimple:
    go: "1.17"
  staticcheck:
    go: "1.17"
  stylecheck:
    go: "1.17"
  unused:
    go: "1.17"

linters:
  enable:
    - deadcode
    - typecheck
    - gofmt
    - goimports
    - gocritic
    - gosec
    - govet
    - errcheck
    - staticcheck
    - unused
    - gosimple
    - structcheck
    - varcheck
    - ineffassign

run:
  timeout: 30m
  skip-dirs:
    - pkg/xx
  skip-files:
    - "pkg/proto/.*\\.xx\\.go$"

issues:
  exclude-rules: [ ]

GitHubActionsでやる場合

CIツールでPRごとにやってくれれば、レビューする時にlintやtestが通っているか第三者が確認出来るので、レビューしやすくなる。

test.yaml
name: Pull Request Go Tester
on:
  pull_request:
    branches:
      - '**'
    paths:
      - 'cmd/**'
      - 'pkg/**'
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Set up Go 1.17
        uses: actions/setup-go@v3
        with:
          go-version: 1.17.8
        id: go
​
      - name: Checkout code
        uses: actions/checkout@v3
        with:
          # HEAD^ と HEADの差分をテストするため
          fetch-depth: 2- name: Cache Go Modules
        uses: actions/cache@v2
        id: cache
        with:
          path: ~/go/pkg/mod
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-- name: Download Modules
        if: steps.cache.outputs.cache-hit != 'true'
        run: go mod download
	
​      # masterブランチは通常通り全てのテストを走らせる
      - name: Test for master branch
        if: github.base_ref == 'master'
        run: go test -v `go list ./... | grep -v -e pkg/xxx`
​
      # それ以外のブランチは差分だけのテストを走らせる
      - name: Test for develop branch
        if: github.base_ref != 'master'
        run: go test -v `git diff --name-only --diff-filter=ACMRT ${{ github.sha }}^ ${{ github.sha }} | grep .go$ | xargs -I{} dirname {} | sort | uniq | xargs -I{} echo ./{}`
​
lint.yaml
name: Pull Request Go Linter
on:
  pull_request:
    branches:
      - '**'
    paths:
      - 'cmd/**'
      - 'pkg/**'
jobs:
  lint:
    runs-on: ubuntu-latest
    env:
      GOGC: 50
    steps:
      - name: Set up Go 1.17
        uses: actions/setup-go@v3
        with:
          go-version: 1.17.8
        id: go

      - name: Checkout code
        uses: actions/checkout@v3
        with:
          # HEAD^ と HEADの差分をテストするため
          fetch-depth: 2

      # masterブランチは通常通り全てのテストを走らせる
      - name: golangci-lint for master branch
        if: github.base_ref == 'master'
        uses: golangci/golangci-lint-action@v3
        with:
          version: v1.44.2
          args: -v -c .golangci.yml cmd/... pkg/...
	  
      # それ以外のブランチは差分だけのテストを走らせる
      - name: golangci-lint for develop branch
        if: github.base_ref != 'master'
        uses: golangci/golangci-lint-action@v3
        with:
          version: v1.44.2
          args: -v -c .golangci.yml `git diff --name-only --diff-filter=ACMRT ${{ github.sha }}^ ${{ github.sha }} | grep .go$ | xargs -I{} dirname {} | sort | uniq`

Discussion