Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 15026] ファイルの概要

このコミットは、Go言語の公式ツールである go vet に新たな静的解析ルールを追加するものです。具体的には、sync/atomic パッケージの Add* 系関数(例: atomic.AddUint64)の誤用を検出する機能が導入されました。この誤用とは、アトミック操作の戻り値を、その操作の対象となった変数に再度代入してしまうという、よくある間違いを指します。

コミット

commit 607317651206f32ee11bf97ec0fe8c473bba403d
Author: Rodrigo Rafael Monti Kochenburger <divoxx@gmail.com>
Date:   Wed Jan 30 07:57:11 2013 -0800

    cmd/vet: detect misuse of atomic.Add*
    
    Re-assigning the return value of an atomic operation to the same variable being operated is a common mistake:
    
    x = atomic.AddUint64(&x, 1)
    
    Add this check to go vet.
    
    Fixes #4065.
    
    R=dvyukov, golang-dev, remyoudompheng, rsc
    CC=golang-dev
    https://golang.org/cl/7097048

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/607317651206f32ee11bf97ec0fe8c473bba403d

元コミット内容

このコミットは、go vet ツールに sync/atomic パッケージの Add* 関数群の誤用を検出する機能を追加します。具体的には、x = atomic.AddUint64(&x, 1) のように、アトミック操作の戻り値を、操作対象の変数に再代入するパターンを「よくある間違い」として検出し、警告を発するようにします。この変更は、Go Issue #4065 を修正するものです。

変更の背景

Go言語の sync/atomic パッケージは、マルチスレッド環境下での共有変数の安全な操作を提供します。特に Add* 関数群(例: AddInt32, AddUint64)は、指定された値を変数にアトミックに加算し、その結果の新しい値を返します。しかし、この関数の戻り値の扱いに関して、開発者が誤解しやすい点がありました。

一般的な非アトミックな加算操作では x = x + 1 のように、変数の値を読み取り、加算し、その結果を変数に書き戻すという一連の操作を行います。これに対し、atomic.AddUint64(&x, 1) のようなアトミック操作は、x のアドレスを直接操作し、内部でロックやCPU命令レベルの保証を用いて、他のゴルーチンからの競合なしに x の値を更新します。そして、この関数は更新後の x の値を戻り値として提供します。

ここで発生しがちな誤用は、x = atomic.AddUint64(&x, 1) のように、アトミック操作の戻り値を再び x に代入してしまうことです。この代入は冗長であり、アトミック操作が既に x の値を更新しているため、意味がありません。さらに悪いことに、この冗長な代入は、開発者がアトミック操作のセマンティクスを誤解している可能性を示唆しており、より複雑なコードでは潜在的なバグにつながる可能性があります。例えば、戻り値が別の変数に代入されるべきであったり、そもそも戻り値が不要であったりする場合に、この誤った代入パターンが混乱を招くことがあります。

この問題は Go Issue #4065 として報告され、go vet がこの種の誤用を静的に検出することで、開発者がより安全で意図通りの並行処理コードを書けるようにすることが目的とされました。

前提知識の解説

Go言語の sync/atomic パッケージ

sync/atomic パッケージは、Go言語において低レベルなアトミック操作を提供するパッケージです。アトミック操作とは、複数のゴルーチン(軽量スレッド)が同時に共有データにアクセスする際に、その操作全体が不可分(分割不可能)であることを保証するものです。これにより、データ競合(data race)を防ぎ、並行処理におけるデータの整合性を保つことができます。

主なアトミック操作には以下のようなものがあります。

  • Add (例: AddInt32, AddUint64)*: 指定された値を変数にアトミックに加算し、更新後の値を返します。
  • Load (例: LoadInt32, LoadPointer)*: 変数の値をアトミックに読み取ります。
  • Store (例: StoreInt32, StorePointer)*: 変数に値をアトミックに書き込みます。
  • CompareAndSwap (CAS) (例: CompareAndSwapInt32)*: 変数の現在の値が期待する値と一致する場合にのみ、新しい値にアトミックに更新します。

これらの関数は、ミューテックス(sync.Mutex)のような高レベルな同期プリミティブを使用するよりも、特定の単純な操作においてはより効率的です。

go vet ツール

go vet は、Go言語のソースコードを静的に解析し、潜在的なバグや疑わしい構造を検出するツールです。コンパイルエラーにはならないが、実行時に問題を引き起こす可能性のあるコードパターンを特定するのに役立ちます。例えば、printf フォーマット文字列の誤り、到達不能なコード、構造体タグの誤りなどを検出します。

go vet は、Go開発ワークフローの重要な一部であり、コードの品質と信頼性を向上させるためにCI/CDパイプラインなどで頻繁に利用されます。

go/ast パッケージ

go/ast パッケージは、Go言語のソースコードの抽象構文木(AST: Abstract Syntax Tree)を表現するためのデータ構造を提供します。Goのコンパイラやツール(go vetgo fmt など)は、このASTを解析してコードの構造を理解し、様々な処理を行います。

ASTは、ソースコードの各要素(変数宣言、関数呼び出し、式、ステートメントなど)をノードとして表現し、それらの間の階層的な関係を示します。go vet はこのASTを走査(walk)することで、特定のコードパターンを識別し、問題のある箇所を報告します。

go/token パッケージ

go/token パッケージは、Go言語のソースコードにおけるトークン(キーワード、識別子、演算子など)やファイル内の位置情報(行番号、列番号など)を定義します。ASTノードは、このパッケージの型を使用して、ソースコード内の対応する位置を参照します。

go/printer パッケージ

go/printer パッケージは、go/ast パッケージで表現された抽象構文木(AST)をGo言語のソースコードとして整形して出力する機能を提供します。このコミットでは、gofmt 関数内で printer.Fprint を使用してASTノードを文字列に変換し、比較のために利用しています。これにより、ASTノードが表現するコードスニペットの正規化された文字列表現を得ることができます。

技術的詳細

このコミットの主要な変更は、go vet ツールに atomic.go という新しいファイルを追加し、main.go を修正してこの新しいチェックを統合することです。

src/cmd/vet/atomic.go の追加

このファイルには、sync/atomic パッケージの誤用を検出するためのロジックが実装されています。

  1. checkAtomicAssignment(n *ast.AssignStmt) 関数:

    • この関数は、代入ステートメント (ast.AssignStmt) を受け取ります。
    • 代入の右辺 (n.Rhs) をイテレートし、各右辺が関数呼び出し (ast.CallExpr) であるかどうかをチェックします。
    • 関数呼び出しの場合、それが atomic パッケージの関数であるかどうか、特に AddInt32, AddInt64, AddUint32, AddUint64, AddUintptr のいずれかであるかを識別します。
    • 該当する場合、checkAtomicAddAssignment 関数を呼び出して、具体的な誤用パターンをチェックします。
  2. checkAtomicAddAssignment(left ast.Expr, call *ast.CallExpr) 関数:

    • この関数は、代入の左辺 (left) と、atomic.Add* 関数の呼び出し (call) を受け取ります。
    • atomic.Add* 関数の最初の引数(通常はアトミック操作の対象となる変数のアドレス)を取得します。
    • 以下の2つのケースで誤用を検出します。
      • ケース1: x = atomic.AddUint64(&x, 1) の形式:
        • atomic.AddUint64 の最初の引数が &x のようにアドレス演算子 (token.AND) を伴う場合、そのオペランド (uarg.X) と代入の左辺 (left) が同じ変数 (x) を参照しているかを f.gofmt を使って文字列比較で確認します。
      • ケース2: *y = atomic.AddUint64(y, 1) の形式(ポインタ経由):
        • 代入の左辺がポインタのデリファレンス (*ast.StarExpr) である場合、そのデリファレンスされた式 (star.X) と atomic.AddUint64 の最初の引数 (arg) が同じ変数を参照しているかを f.gofmt を使って文字列比較で確認します。
    • いずれかの条件が満たされた場合、f.Warn を呼び出して「direct assignment to atomic value」(アトミック値への直接代入)という警告を発します。
  3. gofmt(x ast.Expr) string 関数:

    • go/printer パッケージを使用して、与えられたAST式 (ast.Expr) をGo言語のコード文字列に変換します。これにより、ASTノードの構造的な比較ではなく、そのコード表現の文字列比較が可能になり、変数が同じであるかどうかの判断を容易にします。

src/cmd/vet/main.go の変更

  • go/printer パッケージがインポートに追加されました。
  • 新しいフラグ vetAtomic が追加され、go vet -atomic でこのチェックを有効にできるようになりました。また、vetAll フラグが設定されている場合も自動的に有効になります。
  • File 構造体に b bytes.Buffer が追加され、gofmt 関数で使用されるようになりました。
  • Visit メソッドに ast.AssignStmt のケースが追加され、代入ステートメントが検出された際に f.walkAssignStmt(n) が呼び出されるようになりました。
  • walkAssignStmt 関数が追加され、f.checkAtomicAssignment(stmt) を呼び出すことで、アトミック操作の誤用チェックが実行されるようになりました。

src/cmd/vet/Makefile の変更

  • atomic.go がテストの対象ファイルリストに追加され、新しいチェックがテストスイートに含まれるようになりました。

コアとなるコードの変更箇所

src/cmd/vet/atomic.go (新規ファイル)

// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"go/ast"
	"go/token"
	"sync/atomic"
)

// checkAtomicAssignment walks the assignment statement checking for comomon
// mistaken usage of atomic package, such as: x = atomic.AddUint64(&x, 1)
func (f *File) checkAtomicAssignment(n *ast.AssignStmt) {
	if !*vetAtomic && !*vetAll {
		return
	}

	if len(n.Lhs) != len(n.Rhs) {
		return
	}

	for i, right := range n.Rhs {
		call, ok := right.(*ast.CallExpr)
		if !ok {
			continue
		}
		sel, ok := call.Fun.(*ast.SelectorExpr)
		if !ok {
			continue
		}
		pkg, ok := sel.X.(*ast.Ident)
		if !ok || pkg.Name != "atomic" {
			continue
		}

		switch sel.Sel.Name {
		case "AddInt32", "AddInt64", "AddUint32", "AddUint64", "AddUintptr":
			f.checkAtomicAddAssignment(n.Lhs[i], call)
		}
	}
}

// checkAtomicAddAssignment walks the atomic.Add* method calls checking for assigning the return value
// to the same variable being used in the operation
func (f *File) checkAtomicAddAssignment(left ast.Expr, call *ast.CallExpr) {
	arg := call.Args[0]
	broken := false

	if uarg, ok := arg.(*ast.UnaryExpr); ok && uarg.Op == token.AND {
		broken = f.gofmt(left) == f.gofmt(uarg.X)
	} else if star, ok := left.(*ast.StarExpr); ok {
		broken = f.gofmt(star.X) == f.gofmt(arg)
	}

	if broken {
		f.Warn(left.Pos(), "direct assignment to atomic value")
	}
}

// BadAtomicAssignmentUsedInTests はテストで使用される誤ったアトミック代入の例です。
// この関数は、go vet が検出するべきパターンを網羅しています。
func BadAtomicAssignmentUsedInTests() {
	x := uint64(1)
	x = atomic.AddUint64(&x, 1)        // ERROR "direct assignment to atomic value"
	_, x = 10, atomic.AddUint64(&x, 1) // ERROR "direct assignment to atomic value"
	x, _ = atomic.AddUint64(&x, 1), 10 // ERROR "direct assignment to atomic value"

	y := &x
	*y = atomic.AddUint64(y, 1) // ERROR "direct assignment to atomic value"

	var su struct{ Counter uint64 }
	su.Counter = atomic.AddUint64(&su.Counter, 1) // ERROR "direct assignment to atomic value"
	z1 := atomic.AddUint64(&su.Counter, 1)
	_ = z1 // Avoid err "z declared and not used"

	var sp struct{ Counter *uint64 }
	*sp.Counter = atomic.AddUint64(sp.Counter, 1) // ERROR "direct assignment to atomic value"
	z2 := atomic.AddUint64(sp.Counter, 1)
	_ = z2 // Avoid err "z declared and not used"

	au := []uint64{10, 20}
	au[0] = atomic.AddUint64(&au[0], 1) // ERROR "direct assignment to atomic value"
	au[1] = atomic.AddUint64(&au[0], 1) // これは警告されない(au[1]にau[0]のアトミック操作結果を代入しているため)

	ap := []*uint64{&au[0], &au[1]}
	*ap[0] = atomic.AddUint64(ap[0], 1) // ERROR "direct assignment to atomic value"
	*ap[1] = atomic.AddUint64(ap[0], 1) // これは警告されない
}

src/cmd/vet/main.go (変更箇所抜粋)

// ... (import文の追加)
import (
	// ...
	"go/printer" // 追加
	// ...
)

// ... (vetAtomic フラグの追加)
var (
	// ...
	vetAtomic          = flag.Bool("atomic", false, "check for common mistaken usages of the sync/atomic package") // 追加
)

// ... (File 構造体へのバッファ追加)
type File struct {
	// ...
	b bytes.Buffer // 追加
}

// ... (Visit メソッドの変更)
func (f *File) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.AssignStmt: // 追加
		f.walkAssignStmt(n) // 追加
	// ...
	}
	return f
}

// walkAssignStmt walks an assignment statement
func (f *File) walkAssignStmt(stmt *ast.AssignStmt) { // 追加
	f.checkAtomicAssignment(stmt) // 追加
}

// ... (gofmt 関数の追加)
// goFmt returns a string representation of the expression
func (f *File) gofmt(x ast.Expr) string { // 追加
	f.b.Reset() // 追加
	printer.Fprint(&f.b, f.fset, x) // 追加
	return f.b.String() // 追加
}

src/cmd/vet/Makefile (変更箇所抜粋)

--- a/src/cmd/vet/Makefile
+++ b/src/cmd/vet/Makefile
@@ -4,4 +4,4 @@
 
 test testshort:
 	go build
-	../../../test/errchk ./vet -printfuncs='Warn:1,Warnf:1' print.go rangeloop.go
+	../../../test/errchk ./vet -printfuncs='Warn:1,Warnf:1' print.go rangeloop.go atomic.go

コアとなるコードの解説

このコミットの核となるロジックは、src/cmd/vet/atomic.go に実装されています。

  1. checkAtomicAssignment 関数:

    • この関数は、GoのASTを走査する際に、代入文 (ast.AssignStmt) に遭遇すると呼び出されます。
    • まず、vetAtomic または vetAll フラグが有効になっているかを確認し、チェックが不要な場合はすぐにリターンします。
    • 代入の左辺と右辺の要素数が一致しない場合は、多重代入などの複雑なケースであるため、このチェックの対象外としてスキップします。
    • 代入の右辺をループで処理し、各右辺が関数呼び出しであるか、その関数が atomic パッケージの Add* 関数であるかを特定します。
    • atomic.Add* 関数であることが確認された場合、その代入の左辺と関数呼び出しを checkAtomicAddAssignment 関数に渡して、具体的な誤用パターンをチェックさせます。
  2. checkAtomicAddAssignment 関数:

    • この関数は、atomic.Add* 関数の誤用パターンを具体的に検出します。
    • atomic.Add* 関数の最初の引数(arg)は、アトミック操作の対象となる変数のアドレスです。
    • ケース1 (x = atomic.AddUint64(&x, 1)) の検出:
      • arg&x のように単項演算子 &(アドレス取得)を伴う式 (ast.UnaryExpr) であるかをチェックします。
      • もしそうであれば、f.gofmt(left)(代入の左辺 x の文字列表現)と f.gofmt(uarg.X)&xx の文字列表現)が一致するかを比較します。一致すれば、同じ変数への冗長な代入が行われていると判断します。
    • ケース2 (*y = atomic.AddUint64(y, 1)) の検出:
      • left*y のようにポインタのデリファレンス (ast.StarExpr) であるかをチェックします。
      • もしそうであれば、f.gofmt(star.X)*yy の文字列表現)と f.gofmt(arg)atomic.AddUint64 の引数 y の文字列表現)が一致するかを比較します。一致すれば、ポインタ経由で同じ変数への冗長な代入が行われていると判断します。
    • いずれかの誤用パターンが検出された場合、f.Warn を呼び出して、該当するコードの位置 (left.Pos()) と共に「direct assignment to atomic value」という警告メッセージを出力します。
  3. gofmt 関数:

    • このヘルパー関数は、go/printer パッケージを利用して、与えられたASTノード(式)を正規化された文字列形式に変換します。これにより、ASTノードの構造的な比較ではなく、そのコード表現の文字列比較が可能になり、変数が同じであるかどうかの判断を容易にしています。例えば、x(x) のようなASTは異なるかもしれませんが、gofmt を通せばどちらも "x" となり、比較が正確に行えます。

BadAtomicAssignmentUsedInTests 関数は、この新しい go vet チェックが正しく機能するかを検証するためのテストケースとして含まれています。様々な誤用パターン(単一変数、多重代入、ポインタ経由、構造体フィールド、配列要素など)が網羅されており、それぞれに対して ERROR "direct assignment to atomic value" というコメントが付加されています。これは、go vet がこれらの行で警告を発することを期待していることを示しています。

関連リンク

参考にした情報源リンク

  • 上記のGitHub IssueおよびGo CLのページ
  • Go言語の公式ドキュメント (sync/atomic, cmd/vet)
  • Go言語のASTに関する一般的な情報源 (例: go/ast パッケージのドキュメント)
  • Go言語の静的解析に関する記事やチュートリアル (一般的な知識として)