golintのソースを読んでGoの書き方を学ぶ
この記事は Go Advent Calendar 2017 16日目の記事です。
Goを使用してまだ日が浅いのですが、書いたソースをgolintに通すと必ず怒られてしまいます…
もう怒られたくないので、golintのソースを読んで勉強してきたいと思います!
はじめに
さて、どこでチェックしているんでしょう?
それは以下の関数内で呼び出している関数でチェックしています。
func (f *file) lint() {
f.lintPackageComment()
f.lintImports()
f.lintBlankImports()
f.lintExported()
f.lintNames()
f.lintVarDecls()
f.lintElses()
f.lintIfError()
f.lintRanges()
f.lintErrorf()
f.lintErrors()
f.lintErrorStrings()
f.lintReceiverNames()
f.lintIncDec()
f.lintErrorReturn()
f.lintUnexportedReturn()
f.lintTimeNames()
f.lintContextKeyTypes()
f.lintContextArgs()
}
https://github.com/morix1500/lint/blob/master/lint.go#L194
これらをひとつひとつ見ていきます。 コードの場所を示していきますが、自分のGitHubにforkしたリポジトリを参照しています。
チェック項目
以下にgolintでどんなチェックをしているのか詳細を見ていきます。
例としてコードを出してますが、あくまで例なので「なにしたいプログラムなの?」と思われるかもですがご容赦を。
- パッケージコメント
- Importのドット指定
- Blank Import
- エクスポート
- 命名規則
- 変数宣言
- elseの使い方
- if文
- range
- Errorf
- エラー変数
- エラー文字列
- レシーバー
- レシーバの名前が統一されているか
- インクリメント
- エラーの返却
- エクスポートされた関数の返却値
- time
- ContextのKeyType
- Contextの引数の位置
パッケージコメント
パッケージにつけるコメントをチェックしています。
https://github.com/morix1500/lint/blob/master/lint.go#L380
パッケージコメントの付け方は以下を参照。
Package Comments
空白行があるか
パッケージコメントと「package」の間には空白を入れてはいけません。
OKパターン
// Package main hoge
package main
import (
"fmt"
)
func main() {
fmt.Println("hoge")
}
NGパターン
// Package main hoge
package main
import (
"fmt"
)
func main() {
fmt.Println("hoge")
}
# エラー内容
hoge.go:2:1: package comment is detached; there should be no blank lines between it and the package statement
不適切なスペースがあるか
パッケージコメント内にインデントが入っているとエラーになります。
// Package main hoge
package main
import (
"fmt"
)
func main() {
fmt.Println("hoge")
}
# エラー内容
hoge.go:1:1: package comment should not have leading space
パッケージコメントであるかどうか
mainパッケージ以外では、「package」より前のコメントはパッケージコメントでなければいけません。
// てすとです
package hoge
import (
"fmt"
)
func main() {
fmt.Println("hoge")
}
# エラー内容
hoge.go:1:1: package comment should be of the form "Package hoge ..."
Importのドット指定
Importのドットの扱いについてチェックしています。
https://github.com/morix1500/lint/blob/master/lint.go#L464
スタイルガイドはこちら
Import Dot
Importでドットを使用していないか
Importの先頭に「.(ドット)」をつけると、使用する際にパッケージ名を省略できますがgolintでは推奨されていません。
package main
import (
. "fmt"
)
func main() {
Println("hoge")
}
hoge.go:4:2: should not use dot imports
Blank Import
importの先頭に「_」をつけると、Blank Importになります。
import対象のパッケージの初期化処理のみ行いたい場合、これを使いますがそのチェックを行います。
Blank Importが存在しているか
mainパッケージ以外、Import Blankは許容されません。 https://github.com/morix1500/lint/blob/master/lint.go#L433
package hoge
import (
"fmt"
_ "strings"
)
func main() {
fmt.Println("hoge")
}
hoge.go:5:2: a blank import should be only in a main or test package, or have a comment justifying it
エクスポート
エクスポートされる関数やtypeや変数に対するルールがあります。
それらのチェックを行います。
https://github.com/morix1500/lint/blob/master/lint.go#L484
命名規則
関数名またはtypeにパッケージ名 + “_“ または パッケージ名 + “大文字” を使うと怒られます。
他のソースから呼び出される場合、hoge.HogeFuga
となり冗長だからではないかと思われます。
Package Names
package hoge
import (
"fmt"
)
func main() {
fmt.Println("hoge")
Hoge()
}
// HogeFuga return boolean
func HogeFuga() bool {
return true
}
hoge.go:12:1: comment on exported function HogeFuga should be of the form "HogeFuga ..."
関数にコメントがついているか
エクスポートされた関数の場合、コメントがついていないといけません。
package hoge
import (
"fmt"
)
func main() {
fmt.Println("hoge")
Hoge()
}
func Hoge() bool {
return true
}
hoge.go:12:1: exported function Hoge should have comment or be unexported
関数のコメントが適正か
エクスポートされた関数に対するコメントが「関数名 コメント」という形式でなければいけません。
OKパターン
package hoge
import (
"fmt"
)
func main() {
fmt.Println("hoge")
Hoge()
}
// Hoge return boolean
func Hoge() bool {
return true
}
NGパターン
package hoge
import (
"fmt"
)
func main() {
fmt.Println("hoge")
Hoge()
}
// hoge
func Hoge() bool {
return true
}
hoge.go:12:1: comment on exported function Hoge should be of the form "Hoge ..."
typeにコメントがついているか
エクスポートされたtypeにコメントがついてるかチェックしています。
package hoge
import (
"fmt"
)
func main() {
fmt.Println("hoge")
Hoge()
}
type Fuga struct {}
// Hoge return boolean
func Hoge() bool {
return true
}
hoge.go:12:6: exported type Fuga should have comment or be unexported
typeのコメントが適正か
エクスポートされたtypeに正しい形式でコメントが付けられているかチェックしています。 これは関数同様、「type名 コメント」という形式でなければいけません。
OKパターン
package hoge
import (
"fmt"
)
func main() {
fmt.Println("hoge")
Hoge()
}
// Fuga is struct
type Fuga struct {}
// Hoge return boolean
func Hoge() bool {
return true
}
NGパターン
package hoge
import (
"fmt"
)
func main() {
fmt.Println("hoge")
Hoge()
}
// ng
type Fuga struct {}
// Hoge return boolean
func Hoge() bool {
return true
}
hoge.go:12:1: comment on exported type Fuga should be of the form "Fuga ..." (with optional leading article)
1行に複数の変数宣言をしているか
Golangでは複数の変数を一行で初期化出来ますが、エクスポートされた変数に関してはそれが出来ません。
これはコメント付与をしなければならない制約によるものでしょう。
package hoge
import (
"fmt"
)
func main() {
fmt.Println("hoge")
Hoge()
}
const Fuga, Piyo = 1
// Hoge return boolean
func Hoge() bool {
return true
}
hoge.go:12:7: exported const Piyo should have its own declaration
変数にコメントがついているか
エクスポートされた変数にはコメントがついていないといけません。
package hoge
import (
"fmt"
)
func main() {
fmt.Println("hoge")
Hoge()
}
var Fuga = 1
// Hoge return boolean
func Hoge() bool {
return true
}
hoge.go:12:5: exported var Fuga should have comment or be unexported
変数のコメントが適正か
上記、関数とtypeと同様で「変数名 コメント」という形式でなければいけません。
package hoge
import (
"fmt"
)
func main() {
fmt.Println("hoge")
Hoge()
}
// ng
var Fuga = 1
// Hoge return boolean
func Hoge() bool {
return true
}
hoge.go:12:1: comment on exported var Fuga should be of the form "Fuga ..."
命名規則
ファイル内の命名に対するチェックを行います。
https://github.com/morix1500/lint/blob/master/lint.go#L542
パッケージ名にアンダースコアが含まれているか
パッケージ名に「_(アンダースコア)」が含まれていてはいけません。ただし、「_test」は除きます。
package hoge_fuga
import (
"fmt"
)
func main() {
fmt.Println("hoge")
}
hoge.go:1:1: don't use an underscore in package name
すべて大文字か
名前がすべて大文字で、アンダースコアが含まれている場合、怒られます。
キャメルケースを推奨しています。
package hoge
import (
"fmt"
)
func main() {
HOGE_FUGA := 1
fmt.Println(HOGE_FUGA)
}
hoge.go:8:2: don't use ALL_CAPS in Go names; use CamelCase
先頭が「k」で始まっていないか
先頭が「k」で始まり、大文字の「A」から「Z」が続く変数名は怒られます。
調べてみましたが、なんで怒られるのかは不明。情報求む。
package hoge
import (
"fmt"
)
func main() {
kHoge := 1
fmt.Println(kHoge)
}
hoge.go:8:2: don't use leading k in Go names; var kHoge should be hoge
名前にアンダースコアが含まれているか
変数名などにアンダースコアが含まれていると怒られます。キャメルケースが推奨されてます。
package hoge
import (
"fmt"
)
func main() {
hoge_fuga := 1
fmt.Println(hoge_fuga)
}
hoge.go:8:2: don't use underscores in Go names; var hoge_fuga should be hogeFuga
initialismsのゆらぎがないか
例えば「Url」は「URL」にしろ!とかそういうのです。
Initialisms
以下に載っているinitialismsが対象です。
https://github.com/morix1500/lint/blob/master/lint.go#L743
package hoge
import (
"fmt"
)
func main() {
Url := "http://example.com"
fmt.Println(Url)
}
hoge.go:8:2: var Url should be URL
変数宣言
変数の宣言方法についてチェックします。
https://github.com/morix1500/lint/blob/master/lint.go#L954
Zeroの代入をしているか
特定の型では、初期値にZeroが入ってるため、Zeroの代入は不要なので、代入してると怒られます。
The zero value
package hoge
import (
"fmt"
)
func main() {
var hoge int = 0
fmt.Println(hoge)
}
hoge.go:8:17: should drop = 0 from declaration of var hoge; it is the zero value
冗長な変数宣言をしてないか
変数は
var hoge int
hoge = 1
fuga := 1
のように宣言しますが、上記を併用するやり方だと冗長だと怒られます。
package hoge
import (
"fmt"
)
func main() {
var hoge int = 1
fmt.Println(hoge)
}
hoge.go:8:11: should omit type int from declaration of var hoge; it will be inferred from the right-hand side
elseの使い方
elseの使い方についてチェックします。 https://github.com/morix1500/lint/blob/master/lint.go#L1031
if内でreturnが完結してないか
関数内でreturnする場合、if文のみでreturnを完結させると怒られます。
文章にすると難しいので以下を見てください。
OKパターン
package main
import (
"fmt"
)
func main() {
fmt.Println(hoge(1))
}
func hoge(fuga int) bool {
if fuga == 1 {
return true
}
return false
}
NGパターン
import (
"fmt"
)
func main() {
fmt.Println(hoge(1))
}
func hoge(fuga int) bool {
if fuga == 1 {
return true
} else {
return false
}
}
hoge.go:14:9: if block ends with a return statement, so drop this else and outdent its block
if文
if文の書き方についてチェックしてます。 https://github.com/morix1500/lint/blob/master/lint.go#L1492
無駄な条件分岐をしていないか
関数の戻り値をそのままreturnすればいいのに、無駄な条件分岐していると怒られます。 こんなところまで見てるんですねぇ…
package main
import (
"fmt"
)
func main() {
fmt.Println(hoge())
}
func hoge() error {
if err := fuga(); err != nil {
return err
}
return nil
}
func fuga() error {
return fmt.Errorf("error")
}
hoge.go:12:2: redundant if ...; err != nil check, just return error instead.
range
rangeについてチェックします。
https://github.com/morix1500/lint/blob/master/lint.go#L1075
不要な記述がないか
以下のようにインデックスのみを取得するrangeは、二つ目の「_」は不要です。
package main
import (
"fmt"
)
func main() {
words := []string{"hoge", "fuga", "foo", "bar"}
for i, _ := range words {
fmt.Println(i)
}
}
hoge.go:9:9: should omit 2nd value from range; this loop is equivalent to `for i := range ...`
Errorf
エラー返却の方法についてチェックしています。
https://github.com/morix1500/lint/blob/master/lint.go#L1101
エラーの文字列フォーマットにErrorfを使っているか
エラーの文字列フォーマットはErrorfを使っていきましょう。
package main
import (
"fmt"
"errors"
)
func main() {
fmt.Println(hoge())
}
func hoge() error {
str := "hoge"
return errors.New(fmt.Sprintf("error: %s", str))
}
hoge.go:14:9: should replace errors.New(fmt.Sprintf(...)) with fmt.Errorf(...)
エラー変数
エラー変数の書き方についてチェックします。
https://github.com/morix1500/lint/blob/master/lint.go#L1139
エラー変数の命名が適正か
「ErrFoo」のような変数名でないと怒られます。
package main
import (
"fmt"
"errors"
)
// HogeErr is error
var HogeErr = errors.New("hoge error")
func main() {
fmt.Println(HogeErr)
}
hoge.go:9:5: error var HogeErr should have name of the form ErrFoo
エラー文字列
エラー文字列についてチェックします。
https://github.com/morix1500/lint/blob/master/lint.go#L1170
エラー文字列の書き方が正しいか
エラー文字列は、大文字で書いたりしてはいけないのと句読点や改行で終わらないようにしないといけないです。
package main
import (
"fmt"
"errors"
)
// ErrHoge is error
var ErrHoge = errors.New("hoge error.")
func main() {
fmt.Println(ErrHoge)
}
hoge.go:9:26: error strings should not be capitalized or end with punctuation or a newline
レシーバー
レシーバーの書き方についてチェックします。
https://github.com/morix1500/lint/blob/master/lint.go#L1227
アンダースコアが指定されていないか
レシーバーにアンダースコアが指定されているのは無駄なのでやめましょう。
↓はだいぶ雑な例になってます。。。
OKパターン
package main
import (
"fmt"
)
// Person is struct
type Person struct{ Name string }
// Greet is function
func (Person) Greet(msg string) {
fmt.Printf("%s %s\n", msg, "hoge")
}
func main() {
p := Person{Name: "Taro"}
p.Greet("Hello")
}
NGパターン
package main
import (
"fmt"
)
// Person is struct
type Person struct{ Name string }
// Greet is function
func (_ Person) Greet(msg string) {
fmt.Printf("%s %s\n", msg, "hoge")
}
func main() {
p := Person{Name: "Taro"}
p.Greet("Hello")
}
hoge.go:11:1: receiver name should not be an underscore, omit the name if it is unused
thisやselfが指定されていないか
以下にも記載されていますが、thisやselfは使うのはやめましょう。
Receiver Names
package main
import (
"fmt"
)
// Person is struct
type Person struct{ Name string}
// Greet is function
func (this Person) Greet(msg string) {
fmt.Printf("%s %s\n", msg, this.Name)
}
func main() {
p := Person{Name: "Taro"}
p.Greet("Hello")
}
hoge.go:11:1: receiver name should be a reflection of its identity; don't use generic names such as "this" or "self"
レシーバの名前が統一されているか
レシーバーの名前は統一しましょう。
package main
import (
"fmt"
)
// Person is struct
type Person struct{ Name string}
// Greet is function
func (p Person) Greet(msg string) {
fmt.Printf("%s %s\n", msg, p.Name)
}
// God is function
func (person Person) God() {
fmt.Printf("%s is god\n", person.Name)
}
func main() {
p := Person{Name: "Taro"}
p.Greet("Hello")
p.God()
}
hoge.go:16:1: receiver name person should be consistent with previous receiver name p for Person
インクリメント
インクリメントの書き方についてチェックします。
https://github.com/morix1500/lint/blob/master/lint.go#L1260
正しいインクリメントの記述をしているか
インクリメントは「++」や「–」が推奨されています。
package main
import (
"fmt"
)
func main() {
num := 0
num += 1
fmt.Println(num)
}
hoge.go:9:2: should replace num += 1 with num++
エラーの返却
エラーの返却の仕方についてチェックします。
https://github.com/morix1500/lint/blob/master/lint.go#L1288
多値返却時のエラーの位置が正しいか
多値返却時、エラーの位置は最後に書かないと怒られます。
package main
import (
"fmt"
)
func main() {
err, num := hoge()
if err != nil {
fmt.Println(err)
}
fmt.Println(num)
}
func hoge() (error, int) {
return nil, 1
}
hoge.go:15:1: error should be the last type when returning multiple items
エクスポートされた関数の返却値
エクスポートされた関数の返却値をチェックします。
https://github.com/morix1500/lint/blob/master/lint.go#L1312
エクスポートされていない値を返却していないか
エクスポートされた関数で、エクスポートされていない値を返却すると怒られます。
package main
import (
"fmt"
)
type person struct{ Name string }
func main() {
p := Hoge()
fmt.Println(p.Name)
}
// Hoge is function
func Hoge() person {
p := person{Name: "Taro"}
return p
}
hoge.go:15:13: exported func Hoge returns unexported type main.person, which can be annoying to use
time
timeに関するチェックです。
https://github.com/morix1500/lint/blob/master/lint.go#L1376
time.Duration型の変数名は適正か
time.Duration型の変数名のSuffixに時間単位の表記ゆれがないかチェックしています。
以下の単位文字列を使ってると怒られます。
var timeSuffixes = []string{
"Sec", "Secs", "Seconds",
"Msec", "Msecs",
"Milli", "Millis", "Milliseconds",
"Usec", "Usecs", "Microseconds",
"MS", "Ms",
}
package main
import (
"fmt"
"time"
)
func main() {
var oneSeconds = time.Second * 1
fmt.Println(oneSeconds)
}
hoge.go:10:6: var oneSeconds is of type time.Duration; don't use unit-specific suffix "Seconds"
ContextのKeyType
Contextのキーの型をチェックしています。
https://github.com/morix1500/lint/blob/master/lint.go#L1412
context.WithValueのキーの型が基本型でないか
context.WithValueで指定するキーに基本型(intやstring)を指定していないかチェックしています。
この理由ですが、ドキュメントにこう書かれていました。
The provided key must be comparable and should not be of type string or any other built-in type to avoid collisions between packages using context. Users of WithValue should define their own types for keys. To avoid allocating when assigning to an interface{}, context keys often have concrete type struct{}. Alternatively, exported context key variables’ static type should be a pointer or interface.
基本型じゃなくて独自の型を指定しろとあります。 実際使用していないので、あまりぴんと来ていませんが、
func WithValue(parent Context, key, val interface{}) Context
キーになんでも設定できコンパイルチェックできないから、いろんなものを入れることができないようにしておこう。みたいなことでしょうか?
package main
import (
"fmt"
"context"
)
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "Hoge", 1)
fmt.Println(ctx.Value("Hoge").(int))
}
hoge.go:10:8: should not use basic type string as key in context.WithValue
Contextの引数の位置
Contextを引数に使う場合の位置についてチェックしてます。
https://github.com/morix1500/lint/blob/master/lint.go#L1452
contextの引数の位置は正しいか
contextを関数の引数に含める場合は、先頭じゃないと怒られます。
package main
import (
"fmt"
"context"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 2 * time.Second)
defer cancel()
go loop("Hoge", ctx)
select {
case <- ctx.Done():
fmt.Println(ctx.Err())
}
}
func loop(msg string, ctx context.Context) {
for {
fmt.Println(msg)
}
}
hoge.go:23:1: context.Context should be the first parameter of a function
最後に
というわけで、現時点(2017/12/16)でgolintでチェックしているものをひとつひとつ見てきました。
体裁を整えるというよりも、Golangを使って開発するエンジニアが困らないようにするためのチェックがほとんどで、golintはチーム開発時には必須のツールだと改めて思いました。
個人的にですが、lintにひっかかるコードを書くのはパズルみたいで楽しかったです!
また「なぜ制限しているのか?」まで踏み込んで調べることができたのでGolangの理解を深めることが出来ました!
では!