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

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

このコミットは、Go言語の初期のosパッケージにおけるエラーハンドリングの改善を目的としています。具体的には、*os.Error型の値をシステム全体でキャッシュし、再利用することで、エラーオブジェクトの生成コストを削減し、エラー処理の効率と一貫性を向上させています。これにより、同じエラー文字列やerrno(エラー番号)に対応する*os.Errorインスタンスが複数生成されることを防ぎ、メモリ使用量の最適化とオブジェクト比較の簡素化に寄与します。

コミット

  • コミットハッシュ: 289ff7d0e4101c7b69e5794c119ca543bfb34728
  • Author: Rob Pike r@golang.org
  • Date: Wed Jan 7 16:37:43 2009 -0800
  • コミットメッセージ:
    Cache *os.Error values across all users.
    
    R=rsc
    DELTA=27  (23 added, 0 deleted, 4 changed)
    OCL=22245
    CL=22245
    

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

https://github.com/golang/go/commit/289ff7d0e4101c7b69e5794c119ca543bfb34728

元コミット内容

Cache *os.Error values across all users.

R=rsc
DELTA=27  (23 added, 0 deleted, 4 changed)
OCL=22245
CL=22245

変更の背景

Go言語は、その設計当初からエラーを「値」として扱うという哲学を持っています。これは、例外処理を用いる他の多くの言語とは対照的です。Goでは、関数がエラーを返す可能性がある場合、通常は戻り値の最後にerror型の値を返します。エラーが発生しなかった場合はnilが返され、エラーが発生した場合はnilではないerrorインターフェースを実装した値が返されます。

このコミットが行われた2009年当時、Goはまだ開発の初期段階にあり、言語のコアライブラリやランタイムの最適化が活発に行われていました。osパッケージは、ファイルシステムやプロセスなど、オペレーティングシステムとのインタラクションを扱うため、エラーが頻繁に発生する可能性があります。

*os.Errorは、当時のosパッケージにおける具体的なエラー型の一つでした。同じ種類のエラー(例えば「ファイルが見つかりません」や「パーミッションが拒否されました」)がシステム内で何度も発生する可能性があります。もし、これらのエラーが発生するたびに新しい*os.Errorオブジェクトが生成されると、以下のような問題が生じます。

  1. メモリ使用量の増加: 同じ内容のエラーを表すオブジェクトが多数メモリ上に存在することになり、メモリの無駄遣いにつながります。
  2. ガベージコレクションの負荷: 大量の短期的なオブジェクト生成は、ガベージコレクタの動作頻度を上げ、アプリケーションのパフォーマンスに影響を与える可能性があります。
  3. エラー比較の複雑化: エラーの同一性をチェックする際に、ポインタ比較(==)ではなく、値の比較(Error() stringメソッドの結果比較など)が必要になる場合があります。同じエラーを表すオブジェクトが常に同じインスタンスであれば、ポインタ比較で十分となり、コードが簡素化されます。

このコミットは、これらの問題を解決するために、*os.Errorのインスタンスをキャッシュし、再利用するメカニズムを導入しました。これにより、システム全体で同じエラーは同じ*os.Errorオブジェクトとして扱われるようになり、リソースの効率的な利用とエラー処理の一貫性が図られました。

前提知識の解説

Go言語のエラーハンドリング(2009年当時)

Go言語の初期から現在に至るまで、エラーハンドリングの基本的なアプローチは一貫しています。

  • エラーは値: Goではエラーは特別な例外ではなく、通常の戻り値として扱われます。関数は通常、結果とエラーの2つの値を返します。
  • errorインターフェース: Goの組み込みインターフェースであるerrorは、Error() stringという単一のメソッドを持ちます。これにより、任意の値がエラーとして扱われることができます。
    type error interface {
        Error() string
    }
    
  • nilによるエラーの有無の判断: エラーが発生しなかった場合、エラーを返す関数はnilを返します。エラーが発生した場合は、nilではないerrorインターフェースを実装した値が返されます。
  • if err != nilパターン: エラーが発生したかどうかをチェックする最も一般的な方法は、if err != nil { ... }というイディオムです。

os.Errorerrno

このコミットが対象としているos.Errorは、当時のosパッケージで定義されていた具体的なエラー型の一つです。これは、オペレーティングシステムが返すエラー(例えば、ファイル操作の失敗など)をGoプログラム内で表現するために使用されていました。

errnoは、Unix系システムで広く使われているエラー番号の概念です。システムコールが失敗した場合、グローバル変数errnoに特定のエラーを示す整数値が設定されます。例えば、ENOENTは「No such file or directory」(ファイルまたはディレクトリが存在しない)を意味するerrnoです。Goのsyscallパッケージは、これらのシステムコールやerrnoにアクセスするための機能を提供していました。

syscall.errstr(errno)は、与えられたerrnoに対応するエラーメッセージ文字列を返す関数です。この文字列は、*os.Errorオブジェクトの内部でエラーメッセージとして保持されます。

技術的詳細

このコミットの核心は、*os.Errorオブジェクトのキャッシュメカニズムの導入です。これは主に2つのmap(ハッシュマップ)と、*os.Errorを生成する2つの関数NewErrorおよびErrnoToErrorの変更によって実現されています。

  1. ErrorTab (errnoによるキャッシュ):

    • var ErrorTab = make(map[int64] *Error): このmapは、errnoint64型のエラー番号)をキーとして、対応する*os.Errorオブジェクトを値として保持します。
    • オペレーティングシステムのエラー番号に基づいてエラーオブジェクトを取得する際に使用されます。
  2. ErrorStringTab (エラー文字列によるキャッシュ):

    • var ErrorStringTab = make(map[string] *Error): このmapは、エラーメッセージ文字列(string型)をキーとして、対応する*os.Errorオブジェクトを値として保持します。
    • 任意のエラーメッセージ文字列からエラーオブジェクトを生成する際に使用されます。
  3. NewError(s string) *Error 関数の変更:

    • この関数は、与えられたエラーメッセージ文字列sに基づいて*os.Errorオブジェクトを生成または取得します。
    • 変更前: 単純に新しい*Error{s}を返していました。
    • 変更後:
      1. まず、sが空文字列の場合はnilを返します。これは、エラーがない状態を示す慣習的なGoの動作に合致します。
      2. 次に、ErrorStringTabマップを検索し、同じ文字列sに対応する*os.Errorオブジェクトが既に存在するかどうかを確認します。
      3. もし存在すれば、その既存のオブジェクトを返します。
      4. 存在しない場合は、新しく*Error{s}オブジェクトを生成し、それをErrorStringTabsをキーとして格納してから返します。
    • これにより、同じエラーメッセージ文字列からは常に同じ*os.Errorインスタンスが返されるようになります。
  4. ErrnoToError(errno int64) *Error 関数の変更:

    • この関数は、与えられたerrnoに基づいて*os.Errorオブジェクトを生成または取得します。
    • 変更前: NewError(syscall.errstr(errno))を呼び出して新しいエラーを生成し、それをErrorTabに格納してから返していました。
    • 変更後:
      1. まず、errno0の場合はnilを返します。errno0は通常、エラーがないことを意味します。
      2. 次に、ErrorTabマップを検索し、同じerrnoに対応する*os.Errorオブジェクトが既に存在するかどうかを確認します。
      3. もし存在すれば、その既存のオブジェクトを返します。
      4. 存在しない場合は、NewError(syscall.errstr(errno))を呼び出して新しい*os.Errorオブジェクトを生成します。このNewErrorの呼び出し自体が、エラー文字列によるキャッシュメカニズムを利用します。
      5. 生成されたオブジェクトをErrorTaberrnoをキーとして格納してから返します。
    • これにより、同じerrnoからは常に同じ*os.Errorインスタンスが返されるようになります。

競合状態に関するコメント

コミットされたコードには以下のコメントが含まれています。 // These functions contain a race if two goroutines add identical // errors simultaneously but the consequences are unimportant. これは、「もし2つのゴルーチンが同時に同じエラーを追加しようとした場合、これらの関数には競合状態が含まれるが、その結果は重要ではない」という意味です。

このコメントが指しているのは、mapへの書き込みが複数のゴルーチンから同時に行われた場合の挙動です。Goのmapは、複数のゴルーチンからの同時書き込みに対して安全ではありません(パニックを引き起こす可能性があります)。しかし、この文脈では、ErrorTabErrorStringTabに同じキーで同じ値(または等価な値)を複数回書き込もうとする競合状態を指していると考えられます。

なぜ「結果は重要ではない」とされているかというと、たとえ複数のゴルーチンが同時に新しいエラーオブジェクトを生成し、マップに格納しようとしたとしても、最終的にマップに格納されるのはそのエラーに対応する単一のオブジェクトであり、プログラムの論理的な振る舞いに大きな影響を与えないためです。最悪の場合、一時的に余分なオブジェクトが生成されるか、マップへの書き込みが少し遅れる程度で、プログラムのクラッシュや不正な状態には繋がらないと判断されたのでしょう。これは、パフォーマンスが最優先されるが、厳密な排他制御が複雑さを増す場合に許容される設計判断の一つです。

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

src/lib/os/os_error.go ファイルの変更点です。

--- a/src/lib/os/os_error.go
+++ b/src/lib/os/os_error.go
@@ -12,24 +12,47 @@ export type Error struct {
 	s string
 }
 
+// Indexed by errno.
+// If we worry about syscall speed (only relevant on failure), we could
+// make it an array, but it's probably not important.
 var ErrorTab = make(map[int64] *Error);
 
+// Table of all known errors in system.  Use the same error string twice,
+// get the same *os.Error.
+var ErrorStringTab = make(map[string] *Error);
+
+// These functions contain a race if two goroutines add identical
+// errors simultaneously but the consequences are unimportant.
+
+// Allocate an Error objecct, but if it's been seen before, share that one.
 export func NewError(s string) *Error {
-	return &Error{s}
+	if s == "" {
+		return nil
+	}
+	err, ok := ErrorStringTab[s];
+	if ok {
+		return err
+	}
+	err = &Error{s};
+	ErrorStringTab[s] = err;
+	return err;
 }
 
+// Allocate an Error objecct, but if it's been seen before, share that one.
 export func ErrnoToError(errno int64) *Error {
 	if errno == 0 {
 		return nil
 	}
+\t// Quick lookup by errno.
 	err, ok := ErrorTab[errno];
 	if ok {
 		return err
 	}
-\te := NewError(syscall.errstr(errno));
-\tErrorTab[errno] = e;\n-\treturn e;\n+\terr = NewError(syscall.errstr(errno));
+\tErrorTab[errno] = err;
+\treturn err;
 }
+\n export var (
  	ENONE = ErrnoToError(syscall.ENONE);
  	EPERM = ErrnoToError(syscall.EPERM);

コアとなるコードの解説

src/lib/os/os_error.go

このファイルは、Goのosパッケージにおけるエラー型Errorの定義と、エラーオブジェクトを生成・管理する関数を含んでいます。

  1. var ErrorTab = make(map[int64] *Error);:

    • errno(エラー番号)をキーとする*os.Errorのマップを宣言し、初期化しています。
    • このマップは、ErrnoToError関数内で、特定のerrnoに対応する*os.Errorオブジェクトをキャッシュするために使用されます。
  2. var ErrorStringTab = make(map[string] *Error);:

    • エラーメッセージ文字列をキーとする*os.Errorのマップを宣言し、初期化しています。
    • このマップは、NewError関数内で、特定のエラーメッセージ文字列に対応する*os.Errorオブジェクトをキャッシュするために使用されます。
  3. export func NewError(s string) *Error { ... } の変更:

    • if s == "" { return nil }: エラー文字列が空の場合、nilを返すように変更されました。これはGoのエラーハンドリングの慣習に沿ったものです。
    • err, ok := ErrorStringTab[s]; if ok { return err }: ErrorStringTabマップを検索し、既に同じエラー文字列のエラーオブジェクトが存在すれば、それを返します。これにより、重複するオブジェクトの生成を防ぎます。
    • err = &Error{s}; ErrorStringTab[s] = err; return err;: 既存のオブジェクトが見つからなかった場合、新しい*Errorオブジェクトを生成し、それをErrorStringTabに格納してから返します。
  4. export func ErrnoToError(errno int64) *Error { ... } の変更:

    • if errno == 0 { return nil }: errno0の場合(エラーなしを示す)はnilを返すように変更されました。
    • err, ok := ErrorTab[errno]; if ok { return err }: ErrorTabマップを検索し、既に同じerrnoのエラーオブジェクトが存在すれば、それを返します。
    • err = NewError(syscall.errstr(errno)); ErrorTab[errno] = err; return err;: 既存のオブジェクトが見つからなかった場合、syscall.errstr(errno)でエラー文字列を取得し、その文字列を使ってNewErrorを呼び出します。NewErrorは文字列ベースのキャッシュメカニズムを利用するため、ここでも重複が避けられます。生成されたオブジェクトはErrorTabにも格納され、errnoによるキャッシュも行われます。

これらの変更により、Goのosパッケージは、エラーオブジェクトの生成と管理において、より効率的で一貫性のある振る舞いをするようになりました。

関連リンク

参考にした情報源リンク