🐈

Goで静的解析ツールを作ってみた話

2021/02/14に公開

はじめまして、しぶちゃりです。
今回は以前から欲しいなと思っていたGo言語の静的解析ツールを作成したのでご紹介します。

gocapture

Go言語で開発をしている方なら馴染みのある問題の1つにスライスの容量(cap)問題があります。
この問題を説明する前にGo言語の配列とスライスの違いについて説明します。

配列は固定長、スライスは可変長

Go言語には配列とスライスという2種類の似たようなデータ構造があります。
細かな仕様についてはそれだけで記事になると思いますので今回は扱いません。

今回この2つのデータ構造を使う上で重要なことは、スライスが配列の参照型で長さと容量を持っているという点です。

スライスで発生する容量問題

スライスの特徴について前述しましたが、具体的にスライスを使う上でどのような問題が生じるかについて説明します。以下サンプルコードです。

package main

import "fmt"

func main() {
	const MAX = 10000
	var checkCap int
	sampleSlice := make([]int, 0)
	fmt.Println(len(sampleSlice), cap(sampleSlice))
	for i := 0; i < MAX; i++ {
		sampleSlice = append(sampleSlice, i)
		if checkCap < cap(sampleSlice) {
			fmt.Printf("%d slice: len %d -> cap %d \n", i, len(sampleSlice), cap(sampleSlice))
		}
		checkCap = cap(sampleSlice)
	}
}
0 0
0 slice: len 1 -> cap 1 
1 slice: len 2 -> cap 2 
2 slice: len 3 -> cap 4 
4 slice: len 5 -> cap 8 
8 slice: len 9 -> cap 16 
16 slice: len 17 -> cap 32 
32 slice: len 33 -> cap 64 
64 slice: len 65 -> cap 128 
128 slice: len 129 -> cap 256 
256 slice: len 257 -> cap 512 
512 slice: len 513 -> cap 1024 
1024 slice: len 1025 -> cap 1280 
1280 slice: len 1281 -> cap 1696 
1696 slice: len 1697 -> cap 2304 
2304 slice: len 2305 -> cap 3072 
3072 slice: len 3073 -> cap 4096 
4096 slice: len 4097 -> cap 5120 
5120 slice: len 5121 -> cap 7168 
7168 slice: len 7169 -> cap 9216 
9216 slice: len 9217 -> cap 12288 

このようにGo言語では事前に定義していたスライスの容量(cap)を超えた場合に1024までは値を2倍で確保し直します。

その他の特徴やより詳細な説明はtentenさんが書いている以下の記事がおすすめです。
実装して理解するスライス #golang

適切な容量を確保していないと新たなメモリを都度確保することになり、非常に効率が悪いです。
そして、解決方法としてはmakeを利用し容量を確保するということになります。

ただし場合によってはcapを指定できない場合もあるからだと思いますが、Go言語の標準チェックツールであるgo vetでは容量を確保していない部分に関して指摘をすることはありません。

意図的に容量を確保していない場合はいいですが、ヒューマンエラーはつきものです。仮にこの知識を持っていないエンジニアが実装した場合思わぬことに繋がる場合もあります。

そこで今回作成した静的解析ツールがgocaptureという容量を確保していないスライスや、ここでは提示していませんが、長さを確保していないmap型をチェックしてくれる静的解析のツールです。

使用例

package main

func main() {
	// sufficient capacity
	c := make([]int, 0, 5)
	print(c)

	// zero capacity
	a := make([]int, 0)
	print(a)
}

fish

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

bash

go vet -vettool=`which gocapture` ./...

output

./main.go:9:19: captured the not capacity

このようにgo vetと合わせて利用することで上記の容量の問題に気づくことが可能です。
現状念のためということもあり、意図的に容量を確保していない部分もチェックするようにしていますが、アップデートにより明示的にチェックから外す場所を指定できるようにしようとも考えています。

最後に

長くなりましたが、ここまで読んでいただきありがとうございました。
もし要望や、実装の改善などがあればぜひissueやPRよろしくお願い致します!
とても励みになります!

https://github.com/sivchari/gocapture

Discussion