🐀

Goのバッドプラクティスを防ぐcontainedctx

2022/02/26に公開

はじめまして、しぶちゃりです。
今回は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や、さきさんのこちらの記事を見ていただくことをお勧めします。

https://zenn.dev/hsaki/books/golang-context

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で出したい!と思う方の例にもなると思います。

https://github.com/golangci/golangci-lint/pull/2382

まとめ

今回ご紹介したlinterはGitHubで公開しています。

もしいいと思ったらスターをいただけるとモチベーションにつながるのでとても嬉しいです。

最後までお読みいただきありがとうございました。

https://github.com/sivchari/containedctx

Discussion