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

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

このコミットは、Go言語のランタイムにおけるnilポインタチェックに関連するテストケースを追加するものです。具体的には、test/nilptr.goファイルに新たなテスト関数が追加され、nilポインタのデリファレンスが適切にパニックを引き起こすことを検証しています。

コミット

commit 1116f74e08a8ccd551830c239c3ee20668ad2c5f
Author: Russ Cox <rsc@golang.org>
Date:   Thu Sep 5 23:06:34 2013 -0400

    test/nilptr: add more tests
    
    These tests were suggested in golang.org/issue/6080.
    They were fixed as part of the new nil pointer checks
    that I added a few weeks ago.
    
    Recording the tests as part of marking the issue closed.
    
    Fixes #6080.
    
    R=golang-dev, r, bradfitz
    CC=golang-dev
    https://golang.org/cl/13255049

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

https://github.com/golang/go/commit/1116f74e08a8ccd551830c239c3ee20668ad2c5f

元コミット内容

このコミットの元の内容は、test/nilptrディレクトリにさらなるテストを追加することです。これらのテストは、golang.org/issue/6080で提案されたものであり、数週間前にRuss Coxによって追加された新しいnilポインタチェックの一部として修正されました。このコミットは、イシューをクローズする一環としてテストを記録するものです。

変更の背景

この変更の背景には、Go言語のランタイムにおけるnilポインタの扱い、特にnilポインタのデリファレンスが引き起こすパニックの挙動に関する厳密な検証の必要性があります。golang.org/issue/6080は、特定のnilポインタ操作が期待通りにパニックを引き起こさない、あるいはその挙動が不明瞭であるという問題提起がなされたイシューであると推測されます。

Go言語では、nilポインタのデリファレンスはランタイムパニックを引き起こすことが保証されています。これは、C/C++のような言語でnilポインタ(またはNULLポインタ)のデリファレンスが未定義動作を引き起こし、セキュリティ脆弱性や予測不能なクラッシュにつながる可能性があるのと対照的です。Goの設計哲学では、このような未定義動作を排除し、プログラムの安全性を高めることが重視されています。

このコミットは、以前に実装された新しいnilポインタチェックが、golang.org/issue/6080で指摘されたようなエッジケースや特定のシナリオにおいても正しく機能することを保証するために、追加のテストカバレッジを提供することを目的としています。テストを追加することで、将来の変更によって既存のnilポインタチェックのロジックが意図せず破壊されることを防ぎ、Goプログラムの堅牢性を維持します。

前提知識の解説

Go言語におけるnilポインタ

Go言語において、ポインタは変数のメモリアドレスを保持する型です。ポインタが何も指していない状態を示すのがnilです。nilは、他の言語におけるNULLnullptrに相当します。

Goでは、以下のような場合にnilポインタが発生します。

  • 宣言されたが初期化されていないポインタ(ゼロ値がnil
  • スライス、マップ、チャネル、インターフェース、関数などの参照型で、初期化されていない場合
  • 明示的にnilが代入された場合

nilポインタをデリファレンス(ポインタが指す値にアクセスしようとすること)しようとすると、Goランタイムはpanic(パニック)を発生させます。これはプログラムの異常終了を意味し、通常はリカバリーメカニズム(deferrecover)によって捕捉されない限り、プログラム全体がクラッシュします。

パニック (Panic) とリカバリー (Recover)

Go言語には、エラーハンドリングのメカニズムとしてerrorインターフェースと、より深刻な例外的な状況を扱うためのpanic/recoverメカニズムがあります。

  • Panic: プログラムが回復不能な状態に陥ったことを示すために使用されます。例えば、nilポインタのデリファレンス、配列の範囲外アクセス、ゼロ除算などがパニックを引き起こします。パニックが発生すると、現在の関数の実行が停止し、遅延関数(defer文で登録された関数)が実行され、呼び出しスタックを遡ってパニックが伝播します。
  • Recover: defer関数内でrecover()を呼び出すことで、パニックを捕捉し、パニック状態から回復することができます。これにより、プログラムのクラッシュを防ぎ、エラーを適切に処理する機会を得ることができます。しかし、panic/recoverは通常の制御フローのエラーハンドリングには推奨されず、本当に例外的な状況でのみ使用されるべきです。

スライス (Slices)

Goのスライスは、配列のセグメントを参照するデータ構造です。スライスは、内部的にポインタ、長さ(len)、容量(cap)の3つの要素から構成されます。

  • ポインタ: スライスが参照する基底配列の先頭要素へのポインタ。
  • 長さ: スライスに含まれる要素の数。
  • 容量: スライスの基底配列の先頭から、基底配列の終わりまでの要素の数。

スライスは、配列全体またはその一部を「ビュー」として提供します。x[:]のようなスライス式は、配列x全体を参照するスライスを作成します。nilポインタの配列やnilポインタのスライスに対してスライス操作を行う場合、その挙動はnilポインタデリファレンスと同様にパニックを引き起こすことが期待されます。

rangeループ

Goのfor...rangeループは、スライス、配列、文字列、マップ、チャネルなどのコレクションをイテレートするために使用されます。スライスや配列に対してrangeを使用すると、各要素のインデックスと値が返されます。

for i := range collection の形式ではインデックスのみが返され、for i, v := range collection の形式ではインデックスと値の両方が返されます。nilスライスに対してrangeループを使用してもパニックは発生しませんが、nilポインタの配列をスライスしてからrangeループを使用するような複雑なケースでは、nilポインタデリファレンスが発生する可能性があります。

技術的詳細

このコミットで追加されたテストケースは、Goランタイムがnilポインタのデリファレンスを検出する能力を、より複雑なシナリオで検証することに焦点を当てています。特に、配列ポインタのnil値に対するスライス操作や、その結果として得られるスライスに対するrangeループの挙動がテストされています。

追加されたテスト関数p13p14p15p16は、それぞれ異なるnilポインタ関連の操作を試み、shouldPanicヘルパー関数を通じて、それらが期待通りにパニックを引き起こすことを確認します。

  • p13():

    func p13() {
        var x *[10]int // nilの配列ポインタ
        y := x[:]      // nilの配列ポインタをスライス
        _ = y
    }
    

    このテストは、nilの配列ポインタxに対してスライス操作x[:]を行った場合にパニックが発生するかを検証します。Goの仕様では、配列ポインタのデリファレンスは、そのポインタがnilであればパニックを引き起こします。スライス操作は、内部的に基底配列へのアクセスを伴うため、nilポインタのデリファレンスと同様の挙動が期待されます。

  • p14():

    func p14() {
        println((*[1]int)(nil)[:]) // nilの配列ポインタを型変換し、スライス
    }
    

    このテストは、明示的にnilを配列ポインタ型に型変換し、その結果に対してスライス操作を行った場合にパニックが発生するかを検証します。これはp13と似ていますが、より直接的にnilポインタの型変換とスライス操作の組み合わせをテストしています。

  • p15():

    func p15() {
        for i := range (*[1]int)(nil)[:] { // nilの配列ポインタをスライスし、rangeループでインデックスのみをイテレート
            _ = i
        }
    }
    

    このテストは、p14と同様のnilポインタのスライスに対して、rangeループでインデックスのみをイテレートした場合にパニックが発生するかを検証します。rangeループは通常、nilスライスに対してはパニックを起こしませんが、基底となるデータがnilポインタのデリファレンスを必要とする場合はパニックが発生する可能性があります。

  • p16():

    func p16() {
        for i, v := range (*[1]int)(nil)[:] { // nilの配列ポインタをスライスし、rangeループでインデックスと値の両方をイテレート
            _ = i + v
        }
    }
    

    このテストは、p15と同様のnilポインタのスライスに対して、rangeループでインデックスと値の両方をイテレートした場合にパニックが発生するかを検証します。値vへのアクセスは、nilポインタのデリファレンスを伴うため、パニックが期待されます。

これらのテストは、Goコンパイラとランタイムが、nilポインタのデリファレンスを伴う可能性のある複雑な式や操作を正しく識別し、適切なタイミングでパニックを発生させることを保証するために重要です。特に、スライス操作やrangeループのような高レベルの抽象化の背後で、基底となるポインタ操作が正しく処理されているかを確認します。

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

変更はtest/nilptr.goファイルのみです。

--- a/test/nilptr.go
+++ b/test/nilptr.go
@@ -40,6 +40,10 @@ func main() {
 	shouldPanic(p10)
 	shouldPanic(p11)
 	shouldPanic(p12)
+	shouldPanic(p13)
+	shouldPanic(p14)
+	shouldPanic(p15)
+	shouldPanic(p16)
 }
 
 func shouldPanic(f func()) {
@@ -152,3 +156,27 @@ func p12() {
 	var p *T = nil
 	println(*(&((*p).i)))\n
 }\n+\n+// Tests suggested in golang.org/issue/6080.\n+\n+func p13() {\n+\tvar x *[10]int\n+\ty := x[:]\n+\t_ = y\n+}\n+\n+func p14() {\n+\tprintln((*[1]int)(nil)[:])\n+}\n+\n+func p15() {\n+\tfor i := range (*[1]int)(nil)[:] {\n+\t\t_ = i\n+\t}\n+}\n+\n+func p16() {\n+\tfor i, v := range (*[1]int)(nil)[:] {\n+\t\t_ = i + v\n+\t}\n+}\n```

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

このコミットのコアとなる変更は、`main`関数内で`shouldPanic`関数を呼び出す行の追加と、`p13`、`p14`、`p15`、`p16`という新しいテスト関数の定義です。

*   **`main`関数内の変更**:
    `shouldPanic(p13)`、`shouldPanic(p14)`、`shouldPanic(p15)`、`shouldPanic(p16)`の4行が追加されています。これは、新しく定義されたテスト関数が、それぞれ実行時にパニックを引き起こすことを期待してテストフレームワークに登録されることを意味します。`shouldPanic`関数は、引数として渡された関数を実行し、その実行がパニックを引き起こすことを検証するヘルパー関数です。パニックが発生しない場合はテストが失敗します。

*   **`p13()`関数**:
    `var x *[10]int`で、要素数10の`int`型配列へのポインタ`x`を宣言しています。初期化されていないポインタは`nil`値を持つため、`x`は`nil`ポインタです。
    `y := x[:]`で、このnilポインタの配列`x`に対してスライス操作を行っています。Goのランタイムは、nilポインタの配列をスライスしようとすると、基底配列へのアクセスが必要となるため、nilポインタデリファレンスとして検出し、パニックを引き起こします。
    `_ = y`は、コンパイラが`y`が未使用であると警告するのを避けるための慣用的な記述です。

*   **`p14()`関数**:
    `(*[1]int)(nil)`は、`nil`値を要素数1の`int`型配列へのポインタ型に型変換しています。これにより、nilの配列ポインタが明示的に作成されます。
    `println((*[1]int)(nil)[:])`は、このnilの配列ポインタに対してスライス操作を行い、その結果を`println`で出力しようとしています。このスライス操作がnilポインタデリファレンスを引き起こし、パニックとなります。

*   **`p15()`関数**:
    `for i := range (*[1]int)(nil)[:]`は、`p14`と同様に作成されたnilの配列ポインタのスライスに対して`range`ループを実行しています。この`range`ループは、スライスの要素にアクセスしようとする際にnilポインタデリファレンスを引き起こし、パニックとなります。`_ = i`は、インデックス`i`が未使用であるというコンパイラの警告を避けるためのものです。

*   **`p16()`関数**:
    `for i, v := range (*[1]int)(nil)[:]`も`p15`と同様にnilの配列ポインタのスライスに対して`range`ループを実行していますが、ここではインデックス`i`と値`v`の両方を取得しようとしています。値`v`へのアクセスは、nilポインタのデリファレンスを伴うため、パニックとなります。`_ = i + v`は、`i`と`v`が未使用であるというコンパイラの警告を避けるためのものです。

これらのテスト関数は、Goコンパイラとランタイムが、nilポインタの配列に対するスライス操作や、その結果に対する`range`ループといった、一見すると直接的なnilポインタデリファレンスではないように見えるが、実際には内部的にnilポインタデリファレンスを伴う操作を正しく検出し、パニックを発生させることを保証します。これにより、Goプログラムの実行時安全性と堅牢性が向上します。

## 関連リンク

*   [Go issue 6080: runtime: nil pointer dereference on slice of nil array pointer](https://github.com/golang/go/issues/6080)
*   [Go CL 13255049: test/nilptr: add more tests](https://go.dev/cl/13255049)

## 参考にした情報源リンク

*   [Go Programming Language Specification - Pointers](https://go.dev/ref/spec#Pointers)
*   [Go Programming Language Specification - Panics](https://go.dev/ref/spec#Panics)
*   [Go Programming Language Specification - Slices](https://go.dev/ref/spec#Slice_types)
*   [Go Programming Language Specification - For statements (range clause)](https://go.dev/ref/spec#For_statements)
*   [A Tour of Go - Pointers](https://go.dev/tour/moretypes/1)
*   [A Tour of Go - Slices](https://go.dev/tour/moretypes/7)
*   [Effective Go - Errors](https://go.dev/doc/effective_go#errors)
*   [The Go Blog - Defer, Panic, and Recover](https://go.dev/blog/defer-panic-and-recover)
*   [Go issue 6080: runtime: nil pointer dereference on slice of nil array pointer](https://github.com/golang/go/issues/6080) (Web検索で取得)
*   [Go CL 13255049: test/nilptr: add more tests](https://go.dev/cl/13255049) (Web検索で取得)