fatcontext

Table of Contents

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

fatcontext はループや関数リテラル内で context が肥大化するパターンを検出するツールです。

解決する問題

ループ内で ctx = context.WithValue(ctx, ...) のように context を再代入すると、ループのたびに前の context をラップした新しい context が作られ、チェーンが際限なく伸びていきます。これにより context が「肥大化(fat)」し、メモリ使用量の増大と値の検索パフォーマンス低下を引き起こします。

1
2
3
4
5
6
7
8
9
10
// Before: ループ内で context を再代入 → 肥大化
func ProcessItems(ctx context.Context, items []Item) error {
for _, item := range items {
ctx = context.WithValue(ctx, "item_id", item.ID)
if err := process(ctx, item); err != nil {
return fmt.Errorf("process item %s: %w", item.ID, err)
}
}
return nil
}
1
2
3
4
5
6
7
8
9
10
// After: ループ内で新しい変数にシャドーイング
func ProcessItems(ctx context.Context, items []Item) error {
for _, item := range items {
ctx := context.WithValue(ctx, "item_id", item.ID) // := で新しい変数
if err := process(ctx, item); err != nil {
return fmt.Errorf("process item %s: %w", item.ID, err)
}
}
return nil
}

設定

公式ドキュメント

1
2
3
4
5
6
7
8
9
# .golangci.yml 設定例
version: "2"

linters:
enable:
- fatcontext
settings:
fatcontext:
check-struct-pointers: false

オプション

オプション デフォルト 説明
check-struct-pointers bool false 構造体のポインタフィールドの context もチェック対象にする。誤検知が増える可能性がある

サンプル

検出例

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
// fatcontext が警告するパターン

// 1. for ループ内での context 再代入
func handleBatch(ctx context.Context, ids []string) {
for _, id := range ids {
ctx = context.WithValue(ctx, "id", id) // nested context in loop
doWork(ctx)
}
}

// 2. context.WithCancel / WithTimeout も同様
func pollItems(ctx context.Context, items []Item) {
for _, item := range items {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second) // nested context in loop
process(ctx, item)
cancel()
}
}

// 3. 関数リテラル内での context 再代入
func setup(ctx context.Context) {
fn := func() {
ctx = context.WithValue(ctx, "key", "value") // nested context in function literal
use(ctx)
}
fn()
}

修正例

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
// 1. := でシャドーイング
func handleBatch(ctx context.Context, ids []string) {
for _, id := range ids {
ctx := context.WithValue(ctx, "id", id) // 新しいスコープの変数
doWork(ctx)
}
}

// 2. WithTimeout も同様にシャドーイング
func pollItems(ctx context.Context, items []Item) {
for _, item := range items {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second) // := なので OK
process(ctx, item)
cancel()
}
}

// 3. 関数リテラル内もシャドーイング
func setup(ctx context.Context) {
fn := func() {
ctx := context.WithValue(ctx, "key", "value")
use(ctx)
}
fn()
}

注意点

  • =(代入)と :=(宣言)の違いが重要。ループ内では := を使ってシャドーイングすることで、各イテレーションで独立した context を作成できる
  • context.WithValuecontext.WithCancelcontext.WithTimeoutcontext.WithDeadline のすべてが対象
  • check-struct-pointers: true にすると、構造体フィールドに保持された context のチェックも行うが、誤検知が増える可能性がある
  • context の肥大化はメモリリークに似た症状を引き起こすが、GC では回収されないため、長時間稼働するサーバーで特に問題になる

参考リンク