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

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

このコミットは、Go言語のランタイムにおける配列スライス操作時のエラーメッセージを改善するものです。具体的には、src/runtime/array.c ファイルが変更され、スライスが境界外であった場合に表示されるエラーメッセージに、問題となったスライスの範囲と元の配列の容量が含まれるようになりました。これにより、デバッグ時の情報が格段に増え、開発者が問題の原因を特定しやすくなります。

コミット

commit 9b8a6dc7da6e0ef2a3afe9094fcda4f645442702
Author: Russ Cox <rsc@golang.org>
Date:   Wed Nov 19 09:35:36 2008 -0800

    change array slice error to include bounds
    
    $ 6.out
    slice[5:12] of [10] array
    throw: array slice
    SIGSEGV: segmentation violation
    
    R=r
    DELTA=15  (13 added, 0 deleted, 2 changed)
    OCL=19540
    CL=19580

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

https://github.com/golang/go/commit/9b8a6dc7da6e0ef2a3afe9094fcda4f645442702

元コミット内容

change array slice error to include bounds

$ 6.out
slice[5:12] of [10] array
throw: array slice
SIGSEGV: segmentation violation

R=r
DELTA=15  (13 added, 0 deleted, 2 changed)
OCL=19540
CL=19580

変更の背景

このコミットは、Go言語がまだ初期開発段階にあった2008年11月に行われました。当時のGo言語では、配列のスライス操作が不正な境界で行われた場合、詳細なエラー情報が提供されず、単に「array slice」というメッセージと共にSIGSEGV(セグメンテーション違反)のような低レベルなシグナルによってプログラムが異常終了していました。

例としてコミットメッセージに示されている $ 6.out の出力は、slice[5:12] of [10] array というスライス操作が、容量が10の配列に対して行われ、それが原因でSIGSEGVが発生していることを示しています。このようなエラーメッセージでは、開発者はどのスライス操作が問題を引き起こしたのか、そしてそのスライスがなぜ不正なのかを特定するために、コードを詳細に追跡する必要がありました。

この変更の目的は、このようなデバッグの困難さを解消し、より開発者フレンドリーなエラーメッセージを提供することにありました。具体的には、スライス操作が不正であった際に、要求されたスライスの開始インデックスと終了インデックス、そして元の配列の実際の容量をエラーメッセージに含めることで、問題の根本原因を即座に把握できるようにすることが目指されました。これは、Go言語が実用的な言語として成長していく上で、エラーハンドリングの品質を向上させるための重要な一歩でした。

前提知識の解説

Go言語の配列とスライス

  • 配列 (Array): Go言語における配列は、同じ型の要素を固定長で連続して格納するデータ構造です。配列の長さは宣言時に決定され、実行中に変更することはできません。例: var a [10]int は10個の整数を格納できる配列を宣言します。
  • スライス (Slice): スライスはGo言語の強力な機能であり、配列の上に構築された動的なビューです。スライスは、基となる配列の一部を参照し、その長さは実行時に変更できます。スライスは、[low:high] の形式で作成され、low から high-1 までの要素を含みます。low が省略された場合は0、high が省略された場合は基となる配列またはスライスの長さがデフォルト値となります。スライスは、長さ (length) と容量 (capacity) を持ちます。長さはスライスに含まれる要素の数、容量はスライスの開始位置から基となる配列の末尾までの要素の数です。

Goランタイム

Goランタイムは、Goプログラムの実行を管理する低レベルなシステムです。これには、ガベージコレクタ、スケジューラ、メモリ管理、そして組み込み関数の実装などが含まれます。Go言語の初期段階では、ランタイムの多くの部分がC言語(またはCに似た言語)で書かれていました。src/runtime/array.c のようなファイルは、当時のランタイムがC言語で実装されていたことの名残です。これらのファイルは、Goプログラムが直接呼び出すことはありませんが、Go言語の組み込み機能(例えば、スライス操作)の背後で動作します。

エラーハンドリングの重要性

ソフトウェア開発において、エラーハンドリングは非常に重要です。適切に設計されたエラーメッセージは、開発者がプログラムの異常動作の原因を迅速に特定し、修正するのに役立ちます。情報が不足しているエラーメッセージは、デバッグプロセスを著しく遅らせ、開発者の生産性を低下させます。このコミットは、まさにこの「情報不足のエラーメッセージ」の問題に対処しています。

SIGSEGV (Segmentation Fault)

SIGSEGVは、Unix系オペレーティングシステムで発生するシグナルの一つで、「セグメンテーション違反」を意味します。これは、プログラムがアクセスを許可されていないメモリ領域にアクセスしようとしたときに発生します。例えば、ヌルポインタの逆参照、解放済みメモリへのアクセス、または配列の境界外アクセスなどが原因で発生します。SIGSEGVが発生すると、通常、プログラムは強制終了します。このコミットの例では、不正なスライス操作がメモリの不正アクセスを引き起こし、SIGSEGVに至っていたことが示唆されています。

技術的詳細

このコミットの技術的詳細は、GoランタイムのC言語で書かれた部分、特に配列とスライスの操作を扱うsrc/runtime/array.cファイルに焦点を当てています。

  1. src/runtime/array.cの役割: このファイルは、Go言語の配列とスライスに関する低レベルな操作、例えば新しい配列の割り当て (sys·newarray) や、既存の配列からスライスを作成する (sys·arraysliced, sys·arrayslices) などのランタイム関数を実装しています。これらの関数は、Goコンパイラによって生成されたコードから内部的に呼び出されます。

  2. 新しいヘルパー関数 throwslice の導入: このコミットの主要な変更点は、throwsliceという新しい静的関数が追加されたことです。

    static void
    throwslice(uint32 lb, uint32 hb, uint32 n)
    {
        prints("slice[");
        sys·printint(lb);
        prints(":");
        sys·printint(hb);
        prints("] of [");
        sys·printint(n);
        prints("] array\n");
        throw("array slice");
    }
    

    この関数は、以下の3つの引数を受け取ります。

    • lb (lower bound): スライスの開始インデックス。
    • hb (higher bound): スライスの終了インデックス(排他的)。
    • n (capacity/length): 元の配列またはスライスの容量(または長さ)。 throwsliceは、これらの情報を使用して、slice[lb:hb] of [n] arrayという形式の人間が読みやすいエラーメッセージを標準出力(またはランタイムのログ)に出力します。その後、throw("array slice")を呼び出して、ランタイムレベルでのエラー(パニック)を発生させます。printssys·printintは、Goランタイム内部で使用される低レベルなI/O関数であり、C言語のprintfに似た機能を提供します。
  3. sys·arrayslicedsys·arrayslices関数における変更: sys·arrayslicedsys·arrayslicesは、Goのスライス操作の背後にあるランタイム関数です。これらは、スライスが元の配列の境界を超えてアクセスしようとした場合にエラーを検出します。 変更前は、これらの関数内で境界チェックに失敗した場合、単にthrow("sys·arraysliced: new size exceeds old size")throw("sys·arrayslices: new size exceeds cap")のような汎用的なエラーメッセージを伴うthrowが呼び出されていました。 このコミットでは、これらのthrow呼び出しが、新しく追加されたthrowslice関数への呼び出しに置き換えられました。これにより、エラー発生時にlbhbnといった具体的なスライス情報がthrowsliceに渡され、より詳細なエラーメッセージが出力されるようになりました。

    • sys·arrayslicedでは、old->cap(元の配列の容量)がnとして渡されます。
    • sys·arrayslicesでは、nel(元の配列の要素数)がnとして渡されます。

この変更により、Go言語のユーザーは、スライス操作でエラーが発生した場合に、どのスライスが、どの範囲で、どの容量の配列に対して行われたのかという具体的な情報を得られるようになり、デバッグの効率が大幅に向上しました。

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

このコミットにおけるコアとなるコードの変更箇所は、主にsrc/runtime/array.cファイルに集中しています。

  1. 新しいヘルパー関数 throwslice の追加:

    --- a/src/runtime/array.c
    +++ b/src/runtime/array.c
    @@ -38,6 +38,19 @@ sys·newarray(uint32 nel, uint32 cap, uint32 width, Array* ret)
     	}
     }
     
    +static void
    +throwslice(uint32 lb, uint32 hb, uint32 n)
    +{
    +\tprints("slice[");
    +\tsys·printint(lb);
    +\tprints(":");
    +\tsys·printint(hb);
    +\tprints("] of [");
    +\tsys·printint(n);
    +\tprints("] array\n");
    +\tthrow("array slice");
    +}
    +\n
     // arraysliced(old *[]any, lb uint32, hb uint32, width uint32) (ary *[]any);\
     void
     sys·arraysliced(Array* old, uint32 lb, uint32 hb, uint32 width, Array* ret)
    
  2. sys·arraysliced 内のエラーハンドリングの変更:

    --- a/src/runtime/array.c
    +++ b/src/runtime/array.c
    @@ -62,7 +75,7 @@ sys·arraysliced(Array* old, uint32 lb, uint32 hb, uint32 width, Array* ret)
     		\tsys·printint(old->cap);
     		\tprints("\n");
     		}
    -\t\tthrow("sys·arraysliced: new size exceeds old size");
    +\t\tthrowslice(lb, hb, old->cap);
     	}
     
     	// new array is inside old array
    
  3. sys·arrayslices 内のエラーハンドリングの変更:

    --- a/src/runtime/array.c
    +++ b/src/runtime/array.c
    @@ -109,7 +122,7 @@ sys·arrayslices(byte* old, uint32 nel, uint32 lb, uint32 hb, uint32 width, Arra\
     		\tsys·printint(width);
     		\tprints("\n");
     		}
    -\t\tthrow("sys·arrayslices: new size exceeds cap");
    +\t\tthrowslice(lb, hb, nel);
     	}
     
     	// new array is inside old array
    

コアとなるコードの解説

throwslice 関数の追加

この関数は、スライス操作が不正な境界で行われた際に、統一された形式で詳細なエラーメッセージを出力するためのユーティリティとして導入されました。

  • lb (lower bound) と hb (higher bound) は、ユーザーが指定したスライスの開始インデックスと終了インデックスです。
  • n は、スライスが適用される元の配列またはスライスの実際の容量(または要素数)です。 この関数は、これらの数値情報をprints(文字列出力)とsys·printint(整数出力)を組み合わせて、slice[開始インデックス:終了インデックス] of [容量] arrayという形式の文字列を生成し、出力します。この出力は、デバッグ時にどのスライス操作が問題を引き起こしたのか、そしてそのスライスがなぜ不正なのか(例えば、要求された終了インデックスが容量を超えているなど)を即座に理解するのに役立ちます。メッセージ出力後、最終的にthrow("array slice")を呼び出し、ランタイムレベルでのパニックを発生させます。

sys·arrayslicedsys·arrayslices の変更

これらの関数は、Go言語のスライス構文(例: arr[a:b])が内部的に呼び出すランタイム関数です。 変更前は、スライスの境界チェックに失敗した場合、これらの関数は直接throwを呼び出し、"sys·arraysliced: new size exceeds old size""sys·arrayslices: new size exceeds cap"といった、具体的な情報に乏しいエラーメッセージを出力していました。

このコミットでは、これらの直接的なthrow呼び出しが、新しく追加されたthrowslice関数への呼び出しに置き換えられました。

  • sys·arrayslicedでは、throwslice(lb, hb, old->cap)が呼び出されます。ここでold->capは、元の配列の実際の容量を表します。
  • sys·arrayslicesでは、throwslice(lb, hb, nel)が呼び出されます。ここでnelは、元の配列の要素数を表します。

この変更により、スライス操作が境界外であった場合、単なる汎用的なエラーメッセージではなく、問題のスライスの具体的な範囲と、そのスライスが適用された元の配列の容量がエラーメッセージに含まれるようになりました。これにより、開発者はエラーメッセージを見ただけで、例えば「容量10の配列に対して、インデックス5から12までのスライスを要求したが、これは範囲外である」といった具体的な状況を把握できるようになり、デバッグの労力が大幅に削減されます。

関連リンク

参考にした情報源リンク

  • この解説は、主に提供されたコミット情報(コミットメッセージ、変更されたファイルの内容)と、Go言語の配列、スライス、ランタイムに関する一般的な知識に基づいて作成されました。特定の外部記事やドキュメントを直接参照したものではありませんが、Go言語の公式ドキュメントや関連ブログ記事が、Goの概念を理解する上で常に参照されています。