containedctx

Table of Contents

  1. 解決する問題
  2. 設定
  3. オプション
  4. サンプル
    1. 検出例
    2. 修正例
  5. 注意点
  6. 参考リンク

containedctx は構造体に context.Context フィールドが含まれていないかをチェックするツールです。

解決する問題

Go の公式ブログ「Contexts and structs」では、context は構造体のフィールドに保持せず、関数やメソッドの第一引数として渡すことが推奨されています。構造体に context を保持すると、リクエストのライフサイクルと構造体のライフサイクルが混同され、キャンセル伝播やタイムアウトが意図通りに動作しなくなります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Before: 構造体に context を保持
type UserService struct {
ctx context.Context
repo UserRepository
}

func NewUserService(ctx context.Context, repo UserRepository) *UserService {
return &UserService{
ctx: ctx, // リクエストの context が構造体のライフサイクルと紐付く
repo: repo,
}
}

func (s *UserService) FindByID(id string) (*User, error) {
return s.repo.FindByID(s.ctx, id) // 古い context が使われ続ける
}
1
2
3
4
5
6
7
8
9
10
11
12
// After: context を引数として渡す
type UserService struct {
repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}

func (s *UserService) FindByID(ctx context.Context, id string) (*User, error) {
return s.repo.FindByID(ctx, id) // 呼び出し元の context が使われる
}

設定

公式ドキュメント

設定オプションはありません。有効化するだけで動作します。

1
2
3
4
5
6
# .golangci.yml
version: "2"

linters:
enable:
- containedctx

オプション

golangci-lint 経由では設定オプションはありません。

サンプル

検出例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// containedctx が警告するパターン

// 1. サービス層に context を保持
type OrderService struct {
ctx context.Context // found a struct that contains a context.Context field
db *sql.DB
logger *slog.Logger
}

// 2. ハンドラーに context を保持
type Handler struct {
ctx context.Context // found a struct that contains a context.Context field
svc *Service
}

// 3. 埋め込みではないが context.Context 型のフィールド
type Worker struct {
parentCtx context.Context // found a struct that contains a context.Context field
tasks chan Task
}

修正例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 1. context をメソッドの引数に変更
type OrderService struct {
db *sql.DB
logger *slog.Logger
}

func (s *OrderService) CreateOrder(ctx context.Context, order Order) error {
return s.db.ExecContext(ctx, "INSERT INTO orders ...", order.ID)
}

// 2. ハンドラーも同様
type Handler struct {
svc *Service
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
result, err := h.svc.Process(ctx, r.Body)
// ...
}

// 3. Worker は起動時に context を受け取る
type Worker struct {
tasks chan Task
}

func (w *Worker) Run(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case task := <-w.tasks:
if err := task.Execute(ctx); err != nil {
slog.Error("execute task", "error", err)
}
}
}
}

注意点

  • Go 公式ブログ「Contexts and structs」が根拠。context はリクエストスコープのデータであり、構造体のフィールドにすべきではない
  • HTTP ハンドラーでは r.Context() から、gRPC では stream.Context() から context を取得するのが正しいパターン
  • 長時間稼働する goroutine(Worker パターン)では、Run(ctx context.Context) のように起動メソッドの引数として渡す
  • サードパーティライブラリが構造体に context を要求する場合は //nolint:containedctx で抑制する

参考リンク