Goのバッドプラクティスを防ぐcontainedctx
はじめまして、しぶちゃりです。
今回はGoの静的解析ツール containedctx
の紹介をさせていただきます。
Go頻出のcontextパッケージ
Goを書いている方は一度はcontextに触れたことがあるのではないでしょうか?この記事ではcontextの具体的な使い方については触れませんが、contextの説明についてこのように書かれています。
Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
翻訳するとだいたいこのような感じです。
contextパッケージの中で定義されているContext型は、デッドライン、キャンセルシグナル、その他のリクエストに応じた値をAPI境界やプロセス間で伝達します。
より詳しい説明はGoDocや、さきさんのこちらの記事を見ていただくことをお勧めします。
contextのバッドプラクティス
contextにはいくつかバッドプラクティスが存在します。その中の一つが
Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx:
Contextは構造体の中に保存せずに、Contextを必要としている関数に明示的に渡してください。コンテキストは変数名ctxとして第1引数に渡すべきです。
というものです。
例えばこのようなコードを考えてみましょう
package main
import (
"context"
"fmt"
)
type S struct {
ctx context.Context
}
func (s *S) A() {
B(s.ctx)
}
func B(ctx context.Context) {
go C()
select {
case <-ctx.Done():
stop()
}
}
func C() {
fmt.Println("from C")
}
func stop() {
// stop...
}
func root(ctx context.Context, s *S) {
s.A()
}
簡略化のためデッドロックが起きるなど少々雑なコードですが、このコードを実行した場合、rootメソッドをコールした側がcontextをキャンセルしたとしてもs.A()
以下のコードは実際にはキャンセルできていないためgoroutine leakが発生してしまいます。
また、http.Requestのcontextをライフサイクルとして考えた際に、structにcontextを含めてしまうことでライフサイクル以上にcontextが生き続けてしまいcontextに詰めた値が漏洩する可能性があるなどセキュリティ的にもよろしくないです。
加えてGoの公式ブログでもstructに入れることをバッドプラクティスとしており、Exception to the rule: preserving backwards compatibility
としてnet/httpのRequestが後方互換性のため許されています。
containedctx
今回紹介するcontainedctxはここまでで紹介したcontextをstructに入れないというバッドプラクティスを禁止するためのlinterです。
package main
import "context"
type ok struct {
i int
s string
}
type ng struct {
ctx context.Context
}
type empty struct{}
このコードに対して実際に実行すると以下の結果を得ることができます。
go vet -vettool=(which containedctx) ./...
# a
./main.go:11:2: found a struct that contains a context.Context field
このツールはCIにも組み込むことが可能です。
CircleCI
- run:
name: install containedctx
command: go get github.com/sivchari/containedctx
- run:
name: run containedctx
command: go vet -vettool=(which containedctx ) ./...
GitHub Actions
- name: install containedctx
run: go get github.com/sivchari/containedctx
- name: run tenv
run: go vet -vettool=(which containedctx ) ./...
golangci-lint経由でも利用可能
上記のlinterはgolangci-lintにも組み込まれているため、golangci-lint経由で利用することも可能です。
golangci-lintで実際にMergeされたPRが以下になります。
自分の作成したlinterを今後PRで出したい!と思う方の例にもなると思います。
まとめ
今回ご紹介したlinterはGitHubで公開しています。
もしいいと思ったらスターをいただけるとモチベーションにつながるのでとても嬉しいです。
最後までお読みいただきありがとうございました。
Discussion