[インデックス 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
オブジェクトが生成されると、以下のような問題が生じます。
- メモリ使用量の増加: 同じ内容のエラーを表すオブジェクトが多数メモリ上に存在することになり、メモリの無駄遣いにつながります。
- ガベージコレクションの負荷: 大量の短期的なオブジェクト生成は、ガベージコレクタの動作頻度を上げ、アプリケーションのパフォーマンスに影響を与える可能性があります。
- エラー比較の複雑化: エラーの同一性をチェックする際に、ポインタ比較(
==
)ではなく、値の比較(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.Error
とerrno
このコミットが対象としている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
の変更によって実現されています。
-
ErrorTab
(errnoによるキャッシュ):var ErrorTab = make(map[int64] *Error)
: このmap
は、errno
(int64
型のエラー番号)をキーとして、対応する*os.Error
オブジェクトを値として保持します。- オペレーティングシステムのエラー番号に基づいてエラーオブジェクトを取得する際に使用されます。
-
ErrorStringTab
(エラー文字列によるキャッシュ):var ErrorStringTab = make(map[string] *Error)
: このmap
は、エラーメッセージ文字列(string
型)をキーとして、対応する*os.Error
オブジェクトを値として保持します。- 任意のエラーメッセージ文字列からエラーオブジェクトを生成する際に使用されます。
-
NewError(s string) *Error
関数の変更:- この関数は、与えられたエラーメッセージ文字列
s
に基づいて*os.Error
オブジェクトを生成または取得します。 - 変更前: 単純に新しい
*Error{s}
を返していました。 - 変更後:
- まず、
s
が空文字列の場合はnil
を返します。これは、エラーがない状態を示す慣習的なGoの動作に合致します。 - 次に、
ErrorStringTab
マップを検索し、同じ文字列s
に対応する*os.Error
オブジェクトが既に存在するかどうかを確認します。 - もし存在すれば、その既存のオブジェクトを返します。
- 存在しない場合は、新しく
*Error{s}
オブジェクトを生成し、それをErrorStringTab
にs
をキーとして格納してから返します。
- まず、
- これにより、同じエラーメッセージ文字列からは常に同じ
*os.Error
インスタンスが返されるようになります。
- この関数は、与えられたエラーメッセージ文字列
-
ErrnoToError(errno int64) *Error
関数の変更:- この関数は、与えられた
errno
に基づいて*os.Error
オブジェクトを生成または取得します。 - 変更前:
NewError(syscall.errstr(errno))
を呼び出して新しいエラーを生成し、それをErrorTab
に格納してから返していました。 - 変更後:
- まず、
errno
が0
の場合はnil
を返します。errno
が0
は通常、エラーがないことを意味します。 - 次に、
ErrorTab
マップを検索し、同じerrno
に対応する*os.Error
オブジェクトが既に存在するかどうかを確認します。 - もし存在すれば、その既存のオブジェクトを返します。
- 存在しない場合は、
NewError(syscall.errstr(errno))
を呼び出して新しい*os.Error
オブジェクトを生成します。このNewError
の呼び出し自体が、エラー文字列によるキャッシュメカニズムを利用します。 - 生成されたオブジェクトを
ErrorTab
にerrno
をキーとして格納してから返します。
- まず、
- これにより、同じ
errno
からは常に同じ*os.Error
インスタンスが返されるようになります。
- この関数は、与えられた
競合状態に関するコメント
コミットされたコードには以下のコメントが含まれています。
// These functions contain a race if two goroutines add identical // errors simultaneously but the consequences are unimportant.
これは、「もし2つのゴルーチンが同時に同じエラーを追加しようとした場合、これらの関数には競合状態が含まれるが、その結果は重要ではない」という意味です。
このコメントが指しているのは、map
への書き込みが複数のゴルーチンから同時に行われた場合の挙動です。Goのmap
は、複数のゴルーチンからの同時書き込みに対して安全ではありません(パニックを引き起こす可能性があります)。しかし、この文脈では、ErrorTab
やErrorStringTab
に同じキーで同じ値(または等価な値)を複数回書き込もうとする競合状態を指していると考えられます。
なぜ「結果は重要ではない」とされているかというと、たとえ複数のゴルーチンが同時に新しいエラーオブジェクトを生成し、マップに格納しようとしたとしても、最終的にマップに格納されるのはそのエラーに対応する単一のオブジェクトであり、プログラムの論理的な振る舞いに大きな影響を与えないためです。最悪の場合、一時的に余分なオブジェクトが生成されるか、マップへの書き込みが少し遅れる程度で、プログラムのクラッシュや不正な状態には繋がらないと判断されたのでしょう。これは、パフォーマンスが最優先されるが、厳密な排他制御が複雑さを増す場合に許容される設計判断の一つです。
コアとなるコードの変更箇所
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
の定義と、エラーオブジェクトを生成・管理する関数を含んでいます。
-
var ErrorTab = make(map[int64] *Error);
:errno
(エラー番号)をキーとする*os.Error
のマップを宣言し、初期化しています。- このマップは、
ErrnoToError
関数内で、特定のerrno
に対応する*os.Error
オブジェクトをキャッシュするために使用されます。
-
var ErrorStringTab = make(map[string] *Error);
:- エラーメッセージ文字列をキーとする
*os.Error
のマップを宣言し、初期化しています。 - このマップは、
NewError
関数内で、特定のエラーメッセージ文字列に対応する*os.Error
オブジェクトをキャッシュするために使用されます。
- エラーメッセージ文字列をキーとする
-
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
に格納してから返します。
-
export func ErrnoToError(errno int64) *Error { ... }
の変更:if errno == 0 { return nil }
:errno
が0
の場合(エラーなしを示す)は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
パッケージは、エラーオブジェクトの生成と管理において、より効率的で一貫性のある振る舞いをするようになりました。
関連リンク
参考にした情報源リンク
- Goのエラーハンドリングの歴史と哲学に関する情報源(Web検索結果より)
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHiFtd2HMGbITpo4Cl-F-cedlj2FdIDvDKKPW5rq_tE28SoYGExUhL6zLW84F0U2VY1yuicyz1oVZ4aKuTpt_WUIHLOTF-YEP1a41C5SKGY4jgB__n-3HePAvVMKJYsoaAJMazTUJhBy1mZmuaMUiWPJOk=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGdlQHfQK9hYGax7lMvlqBxpAF5YQ9oChs0bJ6B6tmltJTPipl8K648HUCPg1ZIlRHbWuJpGtO6aK812TVk-386zUmJ9vB6m84JUSAmb9jZtuDAKKJ6nAoQheNUWEwuUMh5q-A_npZT6LQjEDRD8kDj9OHF7EEU96ugjprz3g==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHOzt8OeS_7pQ0BVy1tjOV370wiLvdV3S_FiRLsCc9cRnkqEBhLIsabMwJUSzmt-ujEZX-Mzo5juDtnhbPaBRSnywNiaFud6zMp_3o10Yubx78etsQHCtq1PMPfcv059qjUJ0SuRwIVQuzby3WoSKLbHxzfXFjZpE1DkNInBYSnnc2x8Dgk9KDyGlzidb-m
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHJp-Epbc2X-5nzNluX0HGNO0h3M37ocVT8FpYnmQ0OBglAMxEORoyEexjIYy1JHgd1swnSNPgYqDuME6Vdzl9xIJRkQ7WJECBpG2Q2XXXJAo4Gq_9d80gz9zV3a_31O5-4eT5bxER033QQGRyG9MhczdPdsVIyudKcKSTg5qveY68eAvA4YykbfSXJz6L8s5gGCdsOmzzY4SCBJJRaMBeWZV5jDWDehWgSChT5m6lWbadXDTYL4PLv
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFNUYplc4eLepMDMYRzPildVQnYlFa_IxTcu3f7wmSQ8WRF5brwJTsPfDhfzEhShdtU1wsFHAMFkdir3csNcAQ_FMwq46-8Sl8fnwM6bQ26TpLrOHPH-YPRdggci9JMKM1nKELT
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGLH4WxscbzXAl5c4UEvlQikZNpkJ6wxeYj69krYN4jYeMuPvem7BTN0_h-JzzSIBP2JnVe_JJTDaQ5wzUa_L6UU7KuSFsyrgYcUjDYtbINhnPH5OnLA_mU-EVxB03E6E6YCcLQG4H7hIBM7l28sBWnbiuknqB0OXxKmjJGjt4RC4Bg8nd0kZ3IBlAaC5YK
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGHr_vKMUWU646_JBXkkMXiK0KhAsz67aD25NsCsdZmMdnTE7X5DlZv_wgOQ06z24Y4KYCf2MAU355KqctlP6aSXIp-_yXFU20sKjgr1yzQq6vePKfSaRNMIVg3KGfoNF9Y6Keim3km9CpTJa7QskxgdGDl64munWSW9ksmsDHGuP2sGfF5dpsnE73j_rIh6lm-xzre67m2RU_JSa4OFYRC6VjR
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHT5Nj0T730c4Q3YOMu4lL1kvoyTElryes-AQZ66sUSKupNUGPVfnITucG_Qr2GnkiASjV_SZ5wLONC-P2IBuzwvGNa0gAw6M7DgIJExaeWfwwxkwXZpkPzKqL6