- 投稿日:2020-07-05T23:48:34+09:00
もうgolangci-lintなんていらねいさ 歌おう別れの歌を
tl;dr
- go vet (go/analysis/passes) がとても充実している
- 惰性でgolangci-lintに頼っているかもしれない
- 何が必要で何がいらないのか見極めが必要そう
- go vetをannotationsに飛ばすGitHub Action作ったよ
背景
go の静的解析と群雄割拠のlinter群
過去のgo界隈では、様々な開発者が
go/types
やgo/ast
を駆使して思い思いのI/Fのlinterを作り、今でもそれらの多くは生き残っています。
それらのI/F差を吸収・統合する目的でgometalinter
などが作られてきました。
今はgometalinter
は後継のgolangci-lint
にその座を譲り、golangci-lint
のほぼ一強となっています。
go/analysis
の登場ですがそれも昔の話。
@tenntennさんなどがもうさんざんいろいろな記事を書いておられるのでいまさら語るまでもないことですが、goにおけるlinterの作成はgolang.org/x/tools/go/analysis
の登場により、構文解析処理のラップ(Pass
/inspect
)、解析間の依存I/Fの定義(Fact
)、ユーザーI/Fの共通定義(xxxchecker
)といった部分が整理されています。
goの公式から提供されている lintergo vet
も今ではgo/analysis
の共通I/Fに則っています。
go vet
とgolangci-lint
ところでかの
golangci-lint
ですが、利用していると 同じ lint を違うlinterから同時に指摘されるケースが目につく 気がしています。
golangci-lint
自体が、go vet
を利用しており、どうもgo vet
と他のlinterとで同じ lint を同時に検出するケースがあるようです。
go vet
の中身を見てみると、なかなか面白い構成です。$ go tool vet help asmdecl report mismatches between assembly files and Go declarations assign check for useless assignments atomic check for common mistakes using the sync/atomic package bools check for common mistakes involving boolean operators buildtag check that +build tags are well-formed and correctly located cgocall detect some violations of the cgo pointer passing rules composites check for unkeyed composite literals copylocks check for locks erroneously passed by value httpresponse check for mistakes using HTTP responses loopclosure check references to loop variables from within nested functions lostcancel check cancel func returned by context.WithCancel is called nilfunc check for useless comparisons between functions and nil printf check consistency of Printf format strings and arguments shift check for shifts that equal or exceed the width of the integer stdmethods check signature of methods of well-known interfaces structtag check that struct field tags conform to reflect.StructTag.Get tests check for common mistaken usages of tests and examples unmarshal report passing non-pointer or non-interface values to unmarshal unreachable check for unreachable code unsafeptr check for invalid conversions of uintptr to unsafe.Pointer unusedresult check for unused results of calls to some functions久々にそのラインナップを確認して、そんなにチェックしてたのか、と思うくらいに多いので、少し面食らいました。
一方の golangci-lintを見てみます。govet - Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string errcheck - Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases staticcheck - Staticcheck is a go vet on steroids, applying a ton of static analysis checks unused - Checks Go code for unused constants, variables, functions and types gosimple - Linter for Go source code that specializes in simplifying a code structcheck - Finds unused struct fields varcheck - Finds unused global variables and constants ineffassign - Detects when assignments to existing variables are not used deadcode - Finds unused code typecheck - Like the front-end of a Go compiler, parses and type-checks Go code bodyclose - checks whether HTTP response body is closed successfully noctx - noctx finds sending http request without context.Context golint - Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes rowserrcheck - checks whether Err of rows is checked successfully stylecheck - Stylecheck is a replacement for golint gosec - Inspects source code for security problems interfacer - Linter that suggests narrower interface types unconvert - Remove unnecessary type conversions dupl - Tool for code clone detection goconst - Finds repeated strings that could be replaced by a constant gocyclo - Computes and checks the cyclomatic complexity of functions gocognit - Computes and checks the cognitive complexity of functions asciicheck - Simple linter to check that your code does not contain non-ASCII identifiers gofmt - Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification gofumpt - Gofumpt checks whether code was gofumpt-ed. goimports - Goimports does everything that gofmt does. Additionally it checks unused imports goheader - Checks is file header matches to pattern maligned - Tool to detect Go structs that would take less memory if their fields were sorted depguard - Go linter that checks if package imports are in a list of acceptable packages misspell - Finds commonly misspelled English words in comments lll - Reports long lines unparam - Reports unused function parameters dogsled - Checks assignments with too many blank identifiers (e.g. x, , , _, := f()) nakedret - Finds naked returns in functions greater than a specified function length prealloc - Finds slice declarations that could potentially be preallocated scopelint - Scopelint checks for unpinned variables in go programs gocritic - The most opinionated Go source code linter gochecknoinits - Checks that no init functions are present in Go code gochecknoglobals - Checks that no globals are present in Go code godox - Tool for detection of FIXME, TODO and other comment keywords funlen - Tool for detection of long functions whitespace - Tool for detection of leading and trailing whitespace wsl - Whitespace Linter - Forces you to use empty lines! goprintffuncname - Checks that printf-like functions are named with f at the end gomnd - An analyzer to detect magic numbers. goerr113 - Golang linter to check the errors handling expressions gomodguard - Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. godot - Check if comments end in a period testpackage - linter that makes you use a separate _test package nestif - Reports deeply nested if statements exportloopref - An analyzer that finds exporting pointers for loop variables. exhaustive - check exhaustiveness of enum switch statements sqlclosecheck - Checks that sql.Rows and sql.Stmt are closed. nolintlint - Reports ill-formed or insufficient nolint directives数はやたらめったら多いですが、いくつか「これは実は要らないのでは?」と思えるものもちらほら見受けられます。
deadcode
:go vet
のunreachable
で代えられないか?ineffassign
:go vet
のassign
ってこれ相当なのでは?- などなど
また、いくつかの linter は
go/analysis
に準拠したI/Fを提供しているため、go/analysis/xxxchecker
で呼び出すことが可能です。
(これを調べるのにひどく苦労しました)
- govet
- staticcheck
- unused
- gosimple
- bodyclose
- noctx
- rowserrcheck
- stylecheck
- asciicheck
- goprintffuncname
- gomnd
- goerr113
- testpackage
- exportloopref
- exhaustive
golangci-lint
からの脱却これらの背景を踏まえ、
golangci-lint
から脱却できる可能性を模索しています。
go vet
only最低限のlinterは
go vet
で揃っているのだから、実はgolangci-lint
を使わなくても良いかもしれません。その他の linterを組み合わせる
必要な(
go/analysis
に準拠した)linterがあれば、go/analysis/xxxchecker
で効率よく実行可能です。例えば
cmd/lint/main.go// +build lint package main import ( // ... ) func main() { multichecker.Main( assign.Analyzer, unreachable.Analyzer, simple.Analayzer, exportloopref.Analyzer, ) }というような lint 専用の entrypoint をリポジトリにおいて、
github/workflows/lint.ymlname: Lint on: [push] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 - name: run linter run: go run ./cmd/lint/main.go ./...とでもすれば、golangci-lintよろしくカスタムのlintを実行できます。
脱却のpros./cons.
別に今あるものが動いてるんだから良いじゃないか、という話は大いにあるので、pros./cons.をまとめてみました。
golangci-lintの優位性
- 無思考で利用できる
- 公式のGitHub Actionがある
- なんとなくアップデートすればそれなりに新しいlinterに追随できる
- GitHub/super-linters を利用すれば勝手についてくる
// nolint
コメントによる細やかな調整が可能- GitHub Annotation が見やすい
go/analysis
に準拠していない linter を統合できる脱却の優位性
- Contributorに
golangci-lint
のinstallを求めない
- installしなくてもできるけど、CI待つことにはなる
- .golangci.yml のような設定ファイルを覚える必要がない
- 独自に
go.mod
でバージョン指定ができる
- Third-party linterも(
go/analysis
準拠ならば)自分で取り込める- 新しいバージョンのlinterにバグがあったら巻き戻すことも可能
- プロジェクトローカルのlinterを手軽に追加できる
脱却へのマイルストーン
とはいえ脱却にあたっては、まだまだ不透明な部分が多くあります。
そこで脱却に向けて何をすれば良いのかも検討してみました。
(順不同)GitHub Action の作成
golangci-lint
はその名の通り、CIでのサポートを充実させています。
- GitHub Actionで実行する
- GitHubのAnnotationに投稿する
といった機能があります。
先走って後者だけは作ってしまいました。https://github.com/kyoh86/go-check-action
step: - name: go vet run: go vet -json ./... 2> diagnostics.json - name: annotate diagnostics uses: kyoh86/go-check-action/annotate@v1 with: level: error exit-code: 1のように、
go vet
など(go/analysis/xxxchecker
準拠の)linterであれば、
その-json
フラグの出力をParseしてGitHub Annotationに投稿してくれます。前者も用意したいですね。(まだ作ってないよ)
step: - name: run analyzers uses: kyoh86/go-check-action/check@v1 with: analyzer-packages: | golang.org/x/tools/go/analysis/passes/assign golang.org/x/tools/go/analysis/passes/unreachable github.com/dominikh/go-tools/simple github.com/kyoh86/exportlooprefこんなふうにできたら最高なんですが。多分やれる。
何が検出されなくなるのか見極める
golangci-lintで見つけられたlintのうち、いくつかは
go/analysis
に準拠していないので、使えなくなります。
その時
-deadcode
:go vet
のunreachable
で代えられないか?
-ineffassign
:go vet
のassign
ってこれ相当なのでは?
というように、相当する準拠したlinterと置換したとき(あるいは置換できなかったとき)、何が検出されなくなるのかはきちんと見極める必要があるでしょう。古い linter を
go/analysis
準拠のものに作り変える漏れるlintのうち、どうしても困るものは新しい linter を作る必要が生じる可能性,もあります。
きっつい。まとめ
go/analysisの充実で、golangci-lintのような辛いラッパーを撲滅できる可能性が出てきました。
gophersのみんなで新しいlinterをどんどん作って、いっそgolangci-lint
に別れを告げられるようにしていきましょう。マイルストンに書いた調査その他は…頑張ります…つらい
- 投稿日:2020-07-05T17:04:52+09:00
Azure botで質問をするGoのサンプルコード
公式
ソース
package main import ( "bytes" "fmt" "io/ioutil" "net/http" "strconv" ) func main() { // Represents the various elements used to create HTTP request URIs // for QnA Maker operations. // From Publish Page: HOST // Example: https://YOUR-RESOURCE-NAME.azurewebsites.net/qnamaker var host string = "https://qa-bot-service.azurewebsites.net/qnamaker" // Authorization endpoint key // From Publish Page var endpoint_key string = "ffdbcf11-cxxxxxxxxxxxxxxxxxxxxxxxa" // Management APIs postpend the version to the route // From Publish Page, value after POST // Example: /knowledgebases/xxxxxxxxxxxxxxx/generateAnswer var route string = "/knowledgebases/xxxxxxxxxxxxxxxxxxxx/generateAnswer" // JSON format for passing question to service var question string = "{'question': 'パスワード','top': 3}" req, _ := http.NewRequest("POST", host+route, bytes.NewBuffer([]byte(question))) req.Header.Add("Authorization", "EndpointKey "+endpoint_key) req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Length", strconv.Itoa(len(question))) client := &http.Client{} response, err := client.Do(req) if err != nil { panic(err) } defer response.Body.Close() body, _ := ioutil.ReadAll(response.Body) fmt.Printf(string(body) + "\n") }実行
go run main.go
- 投稿日:2020-07-05T16:55:29+09:00
go修行16日目 time関数など
time関数
- RFC3339がよく使われる Postgresqlなどで
package main import ( "fmt" "time" ) func main() { t := time.Now() fmt.Println(t) fmt.Println(t.Format(time.RFC3339)) fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) }2020-07-05 13:33:12.5673789 +0900 JST m=+0.002000201 2020-07-05T13:33:12+09:00 2020 July 5 13 33 12正規表現
- WebサーバのバックエンドでURLによって処理を判定する場合などに使う
package main import ( "fmt" "regexp" ) func main() { // プログラム内で1回だけとかならまとめて書く // aaaeなどがヒットする match, _ := regexp.MatchString("a([a-z]+)e", "apple") fmt.Println(match) // 何度も使いまわす場合、MustCompileで正規表現のみ宣言しておく r := regexp.MustCompile("a([a-z]+)e") ms := r.MatchString("apple") fmt.Println(ms) // s := "/view/test" というようなURLをひっかけたい r2 := regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$") fs := r2.FindString("/view/test") fmt.Println(fs) // 一部の文字だけ取り出したい場合 fss := r2.FindStringSubmatch("/view/test") fmt.Println(fss, fss[0], fss[1], fss[2]) }true true /view/test [/view/test view test] /view/test view testsort
package main import ( "fmt" "sort" ) func main() { i := []int{5, 3, 2, 8, 7} s := []string{"d", "a", "f"} p := []struct { Name string Age int }{ {"Nancy", 20}, {"Vera", 30}, {"Mike", 40}, {"Bob", 50}, } fmt.Println(i, s, p) sort.Ints(i) sort.Strings(s) // 名前順 sort.Slice(p, func(i, j int) bool { return p[i].Name < p[j].Name }) // 年齢順 sort.Slice(p, func(i, j int) bool { return p[i].Age < p[j].Age }) fmt.Println(i, s, p) }iota
package main import "fmt" // 自動的に連番を振ってくれる iotaを入れてあげなくても自動的にインクリメントされる const ( c1 = iota c2 c3 ) const ( // 0を使わない場合「_」 _ = iota KB int = 1 << (10 * iota) MB GB ) func main() { fmt.Println(c1, c2, c3) fmt.Println(KB, MB, GB) }0 1 2 1024 1048576 1073741824context
- Webアプリなどでユーザーリクエストの処理が長い場合のタイムアウト設定などに使う
package main import ( "context" "fmt" "time" ) func longProcess(ctx context.Context, ch chan string) { fmt.Println("run") // 2秒 time.Sleep(2 * time.Second) fmt.Println("finish") ch <- "result" } func main() { ch := make(chan string) // context timeoutを付け加えられる ctx := context.Background() // 1秒 ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() go longProcess(ctx, ch) for { select { case <-ctx.Done(): fmt.Println(ctx.Err()) return case <-ch: fmt.Println("success") return } } }run context deadline exceededgioutil
package main import ( "fmt" "io/ioutil" "log" ) func main() { // パケットやファイルを読み込む場合ioutilを使う // osとは役割が違う content, err := ioutil.ReadFile("main.go") if err != nil { log.Fatal(err) } fmt.Println(string(content)) if err := ioutil.WriteFile("ioutil_temp.go", content, 0666); err != nil { log.Fatalln(err) } }API server listening at: 127.0.0.1:17064 package main import ( "fmt" "io/ioutil" "log" ) func main() { // パケットやファイルを読み込む場合ioutilを使う // osとは役割が違う content, err := ioutil.ReadFile("main.go") if err != nil { log.Fatal(err) } fmt.Println(string(content)) if err := ioutil.WriteFile("ioutil_temp.go", content, 0666); err != nil { log.Fatalln(err) } } Process exiting with code: 0package main import ( "bytes" "fmt" "io/ioutil" ) func main() { r := bytes.NewBuffer([]byte("abc")) // バッファはReadAllで読み込む content, _ := ioutil.ReadAll(r) fmt.Println(string(content)) }abcネットワーク系パッケージ
- URLを読み込む
package main import ( "fmt" "io/ioutil" "net/http" ) func main() { resp, _ := http.Get("http://example.com") defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) fmt.Println(string(body)) }API server listening at: 127.0.0.1:37622 <!doctype html> <html> <head> <title>Example Domain</title> <meta charset="utf-8" /> <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <style type="text/css"> body { background-color: #f0f0f2; margin: 0; padding: 0; font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; } div { width: 600px; margin: 5em auto; padding: 2em; background-color: #fdfdff; border-radius: 0.5em; box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02); } a:link, a:visited { color: #38488f; text-decoration: none; } @media (max-width: 700px) { div { margin: 0 auto; width: auto; } } </style> </head> <body> <div> <h1>Example Domain</h1> <p>This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.</p> <p><a href="https://www.iana.org/domains/example">More information...</a></p> </div> </body> </html> Process exiting with code: 0package main import ( "fmt" "io/ioutil" "net/http" "net/url" ) func main() { // URLが正しいものかチェック base, _ := url.Parse("http://example.com") // example.comのあとのURLをつけることができる reference, _ := url.Parse("/test?a=1&n=2") endpoint := base.ResolveReference(reference).String() fmt.Println(endpoint) req, _ := http.NewRequest("GET", endpoint, nil) // ヘッダをつけることができる req.Header.Add("If-None-Match", "W/xxxx") q := req.URL.Query() q.Add("c", "3&%") fmt.Println(q) fmt.Println(q.Encode()) req.URL.RawQuery = q.Encode() var client *http.Client = &http.Client{} resp, _ := client.Do(req) body, _ := ioutil.ReadAll(resp.Body) fmt.Println(string(body)) }API server listening at: 127.0.0.1:36071 http://example.com/test?a=1&n=2 map[a:[1] c:[3&%] n:[2]] a=1&c=3%26%25&n=2 <!doctype html> <html> <head>unmarhal
- jsonデータをネットワークから受け取る場合などに利用
package main import ( "encoding/json" "fmt" ) type Person struct { // ``で囲ってあげると変換してくれる Name string `json:"name"` Age int `json:"age"` NickNames []string `json:"nicknames"` } func main() { b := []byte(`{"name":"mike","age":20,"nicknames":["a","b","c"]}`) var p Person // ネットワークから入ってきたjsonをStructに自動的に入れてくれる if err := json.Unmarshal(b, &p); err != nil { fmt.Println(err) } fmt.Println(p.Name, p.Age, p.NickNames) // 再度変換をかける v, _ := json.Marshal(p) fmt.Println(string(v)) }mike 20 [a b c] {"name":"mike","age":20,"nicknames":["a","b","c"]}
- 投稿日:2020-07-05T12:10:51+09:00
Gormのエンティティのiotaの扱い
gormでiotaな型をモデルのフィールド値にしたい時
iotaのままモデルに突っ込めると定数で分岐できたり、変化球が突っ込まれることがなくなるのでiotaされている型をフィールドに定義したかった。
結論
iotaな型はゼロ値がnilとして扱われるので、gormのタグオプションの
not null
を指定してあげて、ゼロ値をnilでなく、0とさせる。またはiotaのスタートを1などにしてあげる。↓1はじめ
type Gender int const ( // Male is man Male Gender = 1 + iota // 1 // Female is woman Female // 2 )gormエンティティ
user_info.go// Gender is sex of human type Gender int const ( // Male is man Male Gender = iota // Female is woman Female ) type ( // Info is detailed user information. UserInfo struct { common.BaseEntity // 独自定義。各エンティティの共通項を抜き出したもの // gormのタグオプション、not nullを指定すればok. Gender Gender `gorm:"type:tinyint(1);not null"` Birthday *time.Time `gorm:"type:datetime;default:null;"` LeaveDate *time.Time `gorm:"type:datetime;default:null;index:idx_leaveDate"` } )テーブル定義
user_infos.sql-- mysql CREATE TABLE `user_infos` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `deleted_at` datetime DEFAULT NULL, `created_at` datetime DEFAULT NULL, `updated_at` datetime DEFAULT NULL, `gender` tinyint(1) NOT NULL, `birthday` datetime DEFAULT NULL, `leave_date` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_user_infos_deleted_at` (`deleted_at`) ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4;