🕺

Goのlinter、nilassignの紹介とgolangci-lintにマージされた話

2021/07/29に公開

はじめまして、しぶちゃりです。
今回はGoの静的解析ツール nilassign の紹介をさせていただきます。

nilに代入することで発生するpanic

Cをはじめとしたポインタの概念が存在する言語ではnullにデータを渡すことで言語ごとにエラーが発生します。これはデータを代入するアドレスが存在しない(null)にも関わらず利用するためです。
その例に漏れず、Goのnull型に相当するnilも、データを渡したり、参照することでエラーが発生します。以下がその例です。

package main

import (
	"fmt"
)

func main() {
	var i *int
	*i = 1
	fmt.Println(i)
}

The Go Playground

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x49777f]

goroutine 1 [running]:
main.main()
	/tmp/sandbox000987391/prog.go:9 +0x1f

またポインタ型のフィールドを持つ構造体でもruntime panicが起きます。

package main

import (
	"fmt"
)

func main() {
	n := Node{}
	n.Value = 1           // OK
	n.ChildNode.Value = 1 // runtime panic
	fmt.Println(n)
}

type Node struct {
	Value     int
	ChildNode *Node
}

The Go Playground

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x497783]

goroutine 1 [running]:
main.main()
	/tmp/sandbox811268421/prog.go:10 +0x23

このようにruntime panicでプログラムが異常終了するにも関わらず、代入自体は言語仕様的に代入可能性を満たすため、できてしまいます。
また、go vetコマンドを用いてもこのエラーは検出されないためgo buildコマンドでコンパイル自体は成功します。そのため、複雑なデータ構造の操作などが発生した際に思わぬバグを埋め込んでしまう可能性があります。

nilassignでnil参照を検出する

上記のようなエラーを事前に検出し防ぐために、nilassignというlinterを作成しました。
このlinterを用いることで、コンパイルよりも前にruntime panicを防ぐことが可能です。
以下がその例です。

package main

func main() {
	{
		var i *int
		*i = 2 // ng
	}

	{
		n := &Node{}
		n.Val = 1 // ok

		*n.Pval = 1                 // NG
		n.Node.Val = 1              // NG
		n.Node.Node.Val = 1         // NG
		n.ChildNode = &Node{Val: 1} // OK
		n.PVal = &num               // OK

	}
}

type Node struct {
	Val  int
	Pval *int
	Node *Node
}

fish

go vet -vettool=(which nilassign) ./...

./main.go:6:3: this assignment occurs invalid memory address or nil pointer dereference
./main.go:13:3: this assignment occurs invalid memory address or nil pointer dereference
./main.go:14:3: this assignment occurs invalid memory address or nil pointer dereference
./main.go:15:3: this assignment occurs invalid memory address or nil pointer dereference

bash

$ go vet -vettool=`which nilassign` ./...

./main.go:6:3: this assignment occurs invalid memory address or nil pointer dereference
./main.go:13:3: this assignment occurs invalid memory address or nil pointer dereference
./main.go:14:3: this assignment occurs invalid memory address or nil pointer dereference
./main.go:15:3: this assignment occurs invalid memory address or nil pointer dereference

このツールはCIにも組み込むことが可能です。

CircleCI

- run:
    name: Install nilassign
    command: go get github.com/sivchari/nilassign

- run:
    name: Run nilassign
    command: go vet -vettool=`which nilassign` ./...

GitHub Actions

- name: Install nilassign
  run: go get github.com/sivchari/nilassign

- name: Run nilassign
  run: go vet -vettool=`which nilassign` ./...

golangci-lint経由でも利用可能

上記のlinterはgolangci-lintにも組み込まれているため、golangci-lint経由で利用することも可能です。
golangci-lintはよく利用しており、お世話になっているため、自分が作成したlinterがマージされたのはとても嬉しかったです。

まとめ

今回ご紹介したlinterはGitHubで公開しています。もしいいなと思ったらスターをいただけるとモチベーションにつながるのでとても嬉しいです。
最後までお読みいただきありがとうございました。

https://github.com/sivchari/nilassign

Discussion