ハラミTech

技術系ブログです

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でどんなチェックをしているのか詳細を見ていきます。
例としてコードを出してますが、あくまで例なので「なにしたいプログラムなの?」と思われるかもですがご容赦を。

パッケージコメント

パッケージにつけるコメントをチェックしています。
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

Indent Error Flow

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

Error String

エラー文字列の書き方が正しいか

エラー文字列は、大文字で書いたりしてはいけないのと句読点や改行で終わらないようにしないといけないです。

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の理解を深めることが出来ました!

では!