CUBE SUGAR CONTAINER

技術系のこと書きます。

Golang: ソースコードを静的に解析する

2016/4/5 追記: こっちを使った方が便利そうです。

blog.amedama.jp


今回は Golang でイケてるソースコードを書くのに使うと良さそうなツールについて調べた。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.11.4
BuildVersion:   15E65
$ go version
go version go1.6 darwin/amd64

標準のコンパイラ

まずは大前提として Golang は他の言語に比べると標準のコンパイラも結構細かくお作法について口出ししてくれる。

例えば、次のようにして使われない変数 n を残したソースコードを用意する。

$ cat << 'EOF' > helloworld.go
package main

import (
  "fmt"
)

func main() {
  n := 1 // 使われない変数
  fmt.Println("Hello, World!")
}
EOF

これを go run コマンドでコンパイル&実行してみるとコンパイラがちゃんと注意してくれる。 オプションとかは特につけることなく厳しくチェックしてくれるのはとても心強い。

$ go run helloworld.go
# command-line-arguments
./helloworld.go:8: n declared and not used

親切だね。

go vet

とはいえコンパイラでは拾ってくれないものもある。

例えば、次のようにして到達しないコードが含まれるソースコードを用意してみよう。

$ cat << 'EOF' > helloworld.go
package main

import (
  "fmt"
)

func main() {
  return
  fmt.Println("Hello, World!") // 到達しないコード
}
EOF

今度も go run コマンドで実行してみる。 するとエラーなどは特に出ることがなく実行できてしまった。

$ go run helloworld.go

こんなときは標準のツールとして用意されている go vet コマンドにかけてみよう。 すると、先ほどの問題の行を特定して教えてくれた。

$ go vet helloworld.go
helloworld.go:9: unreachable code
exit status 1

gofmt

また Golang は標準ツールとしてコーディングスタイルのチェッカが同梱されている。

例えば、次のようにインデントにスペースを使ったソースコードを用意しておこう。

$ cat << 'EOF' > helloworld.go
package main

import (
  "fmt"
)

func main() {
  fmt.Println("Hello, World!")
}
EOF

これを gofmt コマンドにかけるとインデントがタブに変換されたものが標準出力として得られる。 Golang のコーディングスタイルではインデントが 8 タブに決まっているため。

$ gofmt helloworld.go
package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello, World!")
}

直接ファイルを上書きしてしまいたいときは -w オプションを付ける。 あるいは go fmt コマンドを使っても良さそう。

$ gofmt -w helloworld.go

golint

次に紹介するのは Golang 版の lint の golint だ。

これは標準で同梱されているツールではないので go get コマンドでインストールする。

$ go get -u github.com/golang/lint/golint

次は、一見すると何も問題がないようなソースコードを用意しておこう。

$ cat << 'EOF' > helloworld.go
package main

import (
  "fmt"
)

func Greet() {
  fmt.Println("Hello, World!")
}

func main() {
  Greet()
}
EOF

これを golint にかけると公開されている関数 (先頭が大文字) にも関わらず godoc のコメントがないことで怒られる。

$ golint helloworld.go
helloworld.go:7:1: exported function Greet should have comment or be unexported

次はコメントをつけてみよう。

$ cat << 'EOF' > helloworld.go
package main

import (
  "fmt"
)

// Greet は挨拶する関数です
func Greet() {
  fmt.Println("Hello, World!")
}

func main() {
  Greet()
}
EOF

今度は怒られない。

$ golint helloworld.go

goimports

Golang のコンパイラは使われていないインポートがあるときも激おこになる。

使われていないパッケージ os をインポートしたソースコードを用意しておこう。

$ cat << 'EOF' > helloworld.go
package main

import (
  "fmt"
  "os" // os パッケージは使われていない
)

// Greet は挨拶する関数です
func Greet() {
  fmt.Println("Hello, World!")
}

func main() {
  Greet()
}
EOF

もちろん、これは go run コマンドすると怒られる。

$ go run helloworld.go
# command-line-arguments
./helloworld.go:5: imported and not used: "os"

まあ、これは手動で削除すれば良いだけなんだけど、それをアシストしてくれる goimports というツールもある。 これも標準で同梱されていないのでインストールしよう。

$ go get golang.org/x/tools/cmd/goimports

先ほどのソースコードにかけると os が削除された内容が標準出力で得られる。 とはいえ、同じ行にあったコメントなんかは残るみたいだ。

$ goimports helloworld.go
package main

import "fmt"

// os パッケージは使われていない

// Greet は挨拶する関数です
func Greet() {
    fmt.Println("Hello, World!")
}

func main() {
    Greet()
}

ファイルを直接上書きしたいときは gofmt コマンドと同じように -w オプションをつける。

$ goimports -w helloworld.go

ちなみに goimports は不要なパッケージを削除するのとは反対に必要なパッケージを追加することもできる。

次のようにして fmt パッケージが足りていないソースコードを用意しておこう。

$ cat << 'EOF' > helloworld.go 
package main

// Greet は挨拶する関数です
func Greet() {
  fmt.Println("Hello, World!")
}

func main() {
  Greet()
}
EOF

これを goimports にかけると、今度は fmt パッケージが追加されている。

$ goimports helloworld.go 
package main

import "fmt"

// Greet は挨拶する関数です
func Greet() {
    fmt.Println("Hello, World!")
}

func main() {
    Greet()
}

べんり。

まとめ

今回は質の高い Golang のソースコードを書く助けになるツールをいくつか使ってみた。 ここではすべてコマンドラインでそれぞれのツールを実行したけど、実際にはエディタや CI と連携させて使うことになるはず。