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

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

このコミットは、Go言語のコード修正ツールである gofixnetudpgroup サブコマンドにおけるパニック(プログラムの異常終了)を回避するための修正です。具体的には、関数本体(body)を持たない関数が gofix の処理対象となった際に発生する問題を解決します。

コミット

commit c29cd8abb9e17c468843b097cb0d944ace6b2625
Author: David Symonds <dsymonds@golang.org>
Date:   Sat Nov 5 11:28:23 2011 +1100

    gofix: avoid panic on body-less functions in netudpgroup.
    
    R=rsc, r
    CC=golang-dev
    https://golang.org/cl/5347041
---
 src/cmd/gofix/netudpgroup.go      |  2 +-\
 src/cmd/gofix/netudpgroup_test.go | 20 ++++++++++++++++++++\
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/src/cmd/gofix/netudpgroup.go b/src/cmd/gofix/netudpgroup.go
index 12a2efa287..b54beb0de3 100644
--- a/src/cmd/gofix/netudpgroup.go
+++ b/src/cmd/gofix/netudpgroup.go
@@ -30,7 +30,7 @@ func netudpgroup(f *ast.File) bool {
 	fixed := false
 	for _, d := range f.Decls {
 		fd, ok := d.(*ast.FuncDecl)
-		if !ok {
+		if !ok || fd.Body == nil {
 			continue
 		}
 		walk(fd.Body, func(n interface{}) {
diff --git a/src/cmd/gofix/netudpgroup_test.go b/src/cmd/gofix/netudpgroup_test.go
index 24f4abc167..88c0e093fc 100644
--- a/src/cmd/gofix/netudpgroup_test.go
+++ b/src/cmd/gofix/netudpgroup_test.go
@@ -28,6 +28,26 @@ func f() {
 	err := x.JoinGroup(nil, gaddr)
 	err = y.LeaveGroup(nil, gaddr)
 }
+`,\n+\t},\n+\t// Innocent function with no body.\n+\t{\n+\t\tName: "netudpgroup.1",\n+\t\tIn: `package main\n+\n+import "net"\n+\n+func f()\n+\n+var _ net.IP\n+`,\n+\t\tOut: `package main\n+\n+import "net"\n+\n+func f()\n+\n+var _ net.IP\n `,\n \t},\n }\n```

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

[https://github.com/golang/go/commit/c29cd8abb9e17c468843b097cb0d944ace6b2625](https://github.com/golang/go/commit/c29cd8abb9e17c468843b097cb0d944ace6b2625)

## 元コミット内容

gofix: avoid panic on body-less functions in netudpgroup.

R=rsc, r CC=golang-dev https://golang.org/cl/5347041


## 変更の背景

このコミットは、Go言語の `gofix` ツールが、関数本体を持たない(宣言のみで実装がない)関数を処理する際に発生するパニックを修正するために行われました。

`gofix` は、Go言語のバージョンアップに伴うAPIの変更や慣習の変更に対応するために、既存のGoコードを自動的に修正するツールです。`netudpgroup` は `gofix` のサブコマンドの一つで、`net` パッケージにおけるUDPマルチキャストグループ関連のAPI変更に対応するためのものです。

Go言語では、関数の宣言のみを行い、その実装を後で提供する(例えば、外部のC言語ライブラリにリンクする場合など)ことがあります。このような「本体を持たない関数」は、Goの抽象構文木(AST)上では `FuncDecl` ノードとして表現されますが、その `Body` フィールドは `nil` となります。

`gofix` の `netudpgroup` サブコマンドは、GoコードのASTを走査して特定のパターンを見つけ、修正を適用します。しかし、この走査処理が `fd.Body` が `nil` である可能性を考慮していなかったため、`nil` ポインタに対するアクセスが発生し、パニックを引き起こしていました。このコミットは、この `nil` チェックを追加することで、ツールの堅牢性を向上させています。

## 前提知識の解説

### gofix

`gofix` は、Go言語のソースコードを自動的に修正するためのコマンドラインツールです。Go言語は後方互換性を重視していますが、それでも言語仕様や標準ライブラリのAPIが変更されることがあります。`gofix` は、このような変更に対応するために、古いコードを新しい慣習やAPIに自動的に変換する役割を担います。例えば、Go 1.0リリース前のAPI変更や、その後のGoのバージョンアップに伴う修正に利用されてきました。

### 抽象構文木 (AST: Abstract Syntax Tree)

ASTは、プログラミング言語のソースコードを抽象的な階層構造で表現したものです。コンパイラやリンタ、コード分析ツールなどは、ソースコードを直接扱うのではなく、まずASTに変換してから処理を行います。Go言語の `go/ast` パッケージは、GoソースコードのASTを表現するためのデータ構造と、それを操作するための機能を提供します。

*   **`ast.File`**: Goのソースファイル全体を表すASTのルートノードです。
*   **`ast.Decl`**: 宣言(変数宣言、型宣言、関数宣言など)を表すインターフェースです。
*   **`ast.FuncDecl`**: 関数宣言を表す構造体です。これには関数の名前、型、そして関数本体(`Body`)などの情報が含まれます。
*   **`ast.BlockStmt`**: 関数本体やif文、for文などのブロックを表す構造体です。これは複数のステートメント(`Stmt`)のリストを含みます。

### netudpgroup

`netudpgroup` は `gofix` のサブコマンドの一つで、Goの `net` パッケージにおけるUDPマルチキャストグループ関連のAPI変更に対応するためのものです。Go言語の初期のバージョンでは、UDPマルチキャストグループへの参加や脱退に関するAPIが現在とは異なっていました。`netudpgroup` は、古いAPIを使用しているコードを新しいAPIに自動的に修正することを目的としていました。

### 本体を持たない関数 (Body-less functions)

Go言語では、関数の宣言のみを行い、その実装(関数本体)を持たない関数を定義することが可能です。これは主に、C言語など他の言語で実装された関数をGoから呼び出す際に使用される `//go:linkname` や `//go:cgo_export_static` などの特殊なコメント(Goディレクティブ)と組み合わせて利用されます。このような関数は、GoのAST上では `ast.FuncDecl` として表現されますが、その `Body` フィールドは `nil` となります。

例:
```go
package main

import "net"

// func f() // このように宣言のみで本体がない関数

技術的詳細

gofix は、GoソースコードをASTとして読み込み、そのASTを走査(traverse)して特定のパターンを見つけ、修正を適用します。このコミットの対象となった netudpgroup サブコマンドも同様にASTを走査していました。

問題は、netudpgroup の処理ロジックが、ast.FuncDeclBody フィールドが nil である可能性を考慮していなかった点にありました。通常、Goの関数は必ず本体を持ちますが、前述の通り、特殊なケースでは本体を持たない関数も存在します。

元のコードでは、f.Decls(ファイル内のすべての宣言のリスト)をイテレートし、各宣言が ast.FuncDecl であるかどうかを ok 変数でチェックしていました。しかし、oktrue であったとしても、fd.Bodynil である可能性がありました。

// 修正前
for _, d := range f.Decls {
    fd, ok := d.(*ast.FuncDecl)
    if !ok { // ここではast.FuncDeclでない場合のみスキップ
        continue
    }
    // fd.Bodyがnilの場合、ここでパニックが発生する可能性があった
    walk(fd.Body, func(n interface{}) { ... })
}

walk 関数は、ASTノードを再帰的に走査するためのユーティリティ関数であり、その引数には有効なASTノードが期待されます。fd.Bodynil の場合、walk 関数に nil が渡され、内部で nil ポインタのデリファレンスが発生し、パニックを引き起こしていました。

このコミットでは、if !ok の条件に || fd.Body == nil を追加することで、ast.FuncDecl でない場合だけでなく、ast.FuncDecl であっても Bodynil の場合には処理をスキップするように変更しました。これにより、walk 関数に nil が渡されることを防ぎ、パニックを回避しています。

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

src/cmd/gofix/netudpgroup.go ファイルの以下の行が変更されました。

--- a/src/cmd/gofix/netudpgroup.go
+++ b/src/cmd/gofix/netudpgroup.go
@@ -30,7 +30,7 @@ func netudpgroup(f *ast.File) bool {
 	fixed := false
 	for _, d := range f.Decls {
 		fd, ok := d.(*ast.FuncDecl)
-		if !ok {
+		if !ok || fd.Body == nil {
 			continue
 		}
 		walk(fd.Body, func(n interface{}) {

また、src/cmd/gofix/netudpgroup_test.go には、本体を持たない関数をテストケースとして追加し、この修正が正しく機能することを確認しています。

--- a/src/cmd/gofix/netudpgroup_test.go
+++ b/src/cmd/gofix/netudpgroup_test.go
@@ -28,6 +28,26 @@ func f() {
 	err := x.JoinGroup(nil, gaddr)
 	err = y.LeaveGroup(nil, gaddr)
 }
+`,\n+\t},\n+\t// Innocent function with no body.\n+\t{\n+\t\tName: "netudpgroup.1",\n+\t\tIn: `package main\n+\n+import "net"\n+\n+func f()\n+\n+var _ net.IP\n+`,\n+\t\tOut: `package main\n+\n+import "net"\n+\n+func f()\n+\n+var _ net.IP\n `,\n \t},\n }\n```

## コアとなるコードの解説

変更された行は `src/cmd/gofix/netudpgroup.go` の `if !ok` の部分です。

```go
		fd, ok := d.(*ast.FuncDecl)
		if !ok || fd.Body == nil {
			continue
		}
  • fd, ok := d.(*ast.FuncDecl): これは型アサーションです。d(現在の宣言)が *ast.FuncDecl 型に変換可能であれば、その値が fd に代入され、oktrue になります。変換不可能であれば fd はゼロ値(nil)となり、okfalse になります。
  • if !ok: これは、現在の宣言 d が関数宣言(*ast.FuncDecl)ではない場合に true となります。この場合、netudpgroup の処理対象ではないため、continue で次の宣言に移ります。
  • || fd.Body == nil: ここが追加された部分です。!okfalse(つまり d*ast.FuncDecl であった)場合でも、fd.Bodynil である可能性をチェックします。fd.Bodynil であるということは、その関数が本体を持たないことを意味します。このような関数は netudpgroup の修正対象ではないため、同様に continue でスキップされます。

この修正により、walk(fd.Body, ...) が呼び出される前に fd.Bodynil でないことが保証され、nil ポインタデリファレンスによるパニックが回避されます。

関連リンク

参考にした情報源リンク