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

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

このコミットは、Go言語の初期段階におけるI/O操作のAPI設計、特にReadおよびWriteメソッドのエラーハンドリングに関する重要な変更を導入しています。従来のC言語などに見られる、成功時には有効な値を返し、エラー時には特殊な値(例: -1)を返す慣習から脱却し、Go言語が目指す「エラーは明示的なエラー変数で返す」という設計原則を強化しています。

コミット

read および write 関数が、エラー時に -1 を返さなくなり、エラーは error 変数のみを通じて返されるようになりました。

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

https://github.com/golang/go/commit/addd6fa84608a292983f910d8ab1e3dbbfab71a7

元コミット内容

commit addd6fa84608a292983f910d8ab1e3dbbfab71a7
Author: Rob Pike <r@golang.org>
Date:   Tue Nov 18 22:32:01 2008 -0800

    read and write never return -1 now: error return is through the error variable only
    
    R=rsc
    DELTA=13  (9 added, 0 deleted, 4 changed)
    OCL=19538
    CL=19570
---
 src/lib/os/os_file.go | 17 +++++++++++++----\n 1 file changed, 13 insertions(+), 4 deletions(-)\n

変更の背景

この変更は、Go言語の設計哲学、特にエラーハンドリングの原則を確立する初期段階で行われました。C言語などの多くのシステムプログラミング言語では、関数が成功した場合は有効なデータやバイト数を返し、エラーが発生した場合は -1 のような特殊な値を返すことが一般的でした。しかし、このアプローチにはいくつかの問題がありました。

  1. エラーと有効な戻り値の混同: -1 が有効な戻り値として解釈される可能性がある場合、プログラマは常にエラーコードをチェックする必要があり、見落としやすいバグの原因となる可能性がありました。
  2. エラー情報の不足: -1 だけでは、どのような種類のエラーが発生したのかという詳細な情報が得られず、デバッグや適切なエラー回復が困難でした。
  3. Goのエラーハンドリング哲学との不整合: Go言語は、関数が複数の戻り値を返す能力(タプル戻り値)を特徴としており、特に (result, error) のパターンを推奨していました。これにより、成功時の結果とエラー情報を明確に分離し、プログラマにエラーのチェックを強制する設計を目指していました。

このコミットは、ReadWriteといったI/O操作において、C言語的な -1 を返す慣習を完全に排除し、Go言語の標準的なエラーハンドリングパターンである (n int, err error) の形式に統一することを目的としています。これにより、Go言語のAPIの一貫性と堅牢性が向上し、より安全で予測可能なプログラムの記述を促進しました。

前提知識の解説

Go言語のエラーハンドリングの基本

Go言語のエラーハンドリングは、他の多くの言語とは異なる独特のアプローチを採用しています。Goでは例外処理(try-catchなど)のメカニズムは存在せず、代わりに「エラーは戻り値として扱う」という原則に基づいています。

  • 多値戻り値: Goの関数は複数の値を返すことができます。この機能は、操作の結果と、その操作中に発生した可能性のあるエラーを同時に返すために頻繁に利用されます。典型的なパターンは (result, error) です。
  • error インターフェース: Goには組み込みの error インターフェースがあります。これは Error() string という単一のメソッドを持つシンプルなインターフェースです。エラーが発生した場合、関数はこのインターフェースを実装する値を返します。エラーがない場合は nil を返します。
  • 明示的なエラーチェック: Goのプログラマは、関数呼び出しの直後にエラー戻り値を明示的にチェックすることが期待されます。これは通常、if err != nil { ... } という形式で行われます。この明示的なチェックは、エラーの見落としを防ぎ、堅牢なコードを書くことを促します。

C言語におけるI/O関数のエラー慣習

C言語の標準ライブラリにおけるI/O関数(例: read(), write())では、以下のようなエラーハンドリングの慣習が広く用いられています。

  • 成功時の戻り値: 成功した場合、関数は実際に読み書きされたバイト数など、有効な結果を返します。
  • エラー時の戻り値: エラーが発生した場合、関数は通常 -1 を返します。
  • errno 変数: エラーの種類に関する詳細な情報は、グローバル変数 errno に設定されます。プログラマは -1 が返された後に errno の値をチェックして、具体的なエラー原因(例: EAGAIN, EIO, EINVAL など)を特定します。

このC言語的なアプローチは、Go言語が目指すエラーハンドリングの明確さとは対照的であり、Goの設計者はこの慣習から脱却しようとしました。

技術的詳細

このコミットの技術的な核心は、Go言語のI/O操作におけるエラー表現の一貫性を確立することにあります。具体的には、osパッケージ内のファイルディスクリプタ (FD) に関連する ReadWriteWriteString メソッドの挙動が変更されました。

変更前は、これらのメソッドはエラーが発生した場合に、読み書きされたバイト数を示す ret 変数に -1 を設定し、同時に err 変数にもエラー情報を設定していました。これはC言語の read(2)write(2) システムコールがエラー時に -1 を返す挙動を模倣したものでした。

しかし、Go言語の設計思想では、成功時の戻り値とエラー情報は明確に分離されるべきであり、ret(読み書きされたバイト数)は常に有効なバイト数(0以上)を表現すべきであるという考え方があります。エラーが発生した場合は、err 変数のみが非nilとなり、そのエラーオブジェクトが詳細な情報を持つべきです。

このコミットでは、以下の2つの主要な変更が導入されました。

  1. fd == nil の場合の戻り値の変更:

    • 変更前: return -1, EINVAL
    • 変更後: return 0, EINVAL fdnil の場合(無効なファイルディスクリプタ)、これは不正な引数エラー (EINVAL) です。この場合、読み書きされたバイト数は 0 であるべきであり、-1 という特殊な値は使用されなくなりました。
  2. syscall 呼び出し後の -1 の変換:

    • syscall.readsyscall.write といった低レベルのシステムコールは、依然としてC言語の慣習に従い、エラー時に -1 を返す可能性があります。
    • このコミットでは、これらのシステムコールから返された r (バイト数) が -1 であった場合、それを 0 に変換するロジックが追加されました (if r < 0 { r = 0 })。
    • これにより、ReadWrite メソッドの最終的な戻り値である ret は、常に 0 以上の値(実際に読み書きされたバイト数)を保証するようになりました。エラーの詳細は ErrnoToError(e) によって変換された err 変数を通じてのみ伝達されます。

この変更により、Go言語のI/O APIは、その設計原則に完全に合致し、プログラマは ret の値が常に有効なバイト数を表すものとして信頼できるようになりました。エラーの有無は err 変数をチェックするだけで判断でき、コードの可読性と堅牢性が向上しました。

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

変更は src/lib/os/os_file.go ファイル内の FD 型の ReadWriteWriteString メソッドに集中しています。

--- a/src/lib/os/os_file.go
+++ b/src/lib/os/os_file.go
@@ -55,35 +55,44 @@ func (fd *FD) Close() *Error {
 
 func (fd *FD) Read(b *[]byte) (ret int, err *Error) {
 	if fd == nil {
-		return -1, EINVAL
+		return 0, EINVAL
 	}
 	var r, e int64;
 	if len(b) > 0 {  // because we access b[0]
 		r, e = syscall.read(fd.fd, &b[0], int64(len(b)));
+		if r < 0 {
+			r = 0
+		}
 	}
 	return int(r), ErrnoToError(e)
 }
 
 func (fd *FD) Write(b *[]byte) (ret int, err *Error) {
 	if fd == nil {
-		return -1, EINVAL
+		return 0, EINVAL
 	}
 	var r, e int64;
 	if len(b) > 0 {  // because we access b[0]
 		r, e = syscall.write(fd.fd, &b[0], int64(len(b)));
+		if r < 0 {
+			r = 0
+		}
 	}
 	return int(r), ErrnoToError(e)
 }
 
 func (fd *FD) WriteString(s string) (ret int, err *Error) {
 	if fd == nil {
-		return -1, EINVAL
+		return 0, EINVAL
 	}
 	b := new([]byte, len(s)+1);
 	if !syscall.StringToBytes(b, s) {
-		return -1, EINVAL
+		return 0, EINVAL
 	}
 	r, e := syscall.write(fd.fd, &b[0], int64(len(s)));
+	if r < 0 {
+		r = 0
+	}
 	return int(r), ErrnoToError(e)
 }
 

コアとなるコードの解説

Read メソッドの変更

  • if fd == nil ブロック:
    • 変更前: return -1, EINVAL
    • 変更後: return 0, EINVAL ファイルディスクリプタ fd が無効な場合、読み込みは行われないため、読み込まれたバイト数は 0 であるべきです。エラーは EINVAL (Invalid argument) として err 変数を通じて返されます。
  • syscall.read 呼び出し後:
    • r, e = syscall.read(fd.fd, &b[0], int64(len(b))) の行の直後に if r < 0 { r = 0 } が追加されました。
    • これは、低レベルの syscall.read がエラー時に -1 を返す可能性があるため、その値をGoの Read メソッドの戻り値として適切に変換するためのものです。r が負の値(エラーを示す)であれば、読み込まれたバイト数は 0 として扱われます。実際のシステムエラーは e 変数に格納され、ErrnoToError(e) によってGoのエラー型に変換されて返されます。

Write メソッドの変更

  • if fd == nil ブロック:
    • Read メソッドと同様に、return -1, EINVAL から return 0, EINVAL に変更されました。無効なファイルディスクリプタへの書き込みは、書き込まれたバイト数が 0 であるべきです。
  • syscall.write 呼び出し後:
    • r, e = syscall.write(fd.fd, &b[0], int64(len(b))) の行の直後に if r < 0 { r = 0 } が追加されました。
    • syscall.write がエラー時に -1 を返す場合、書き込まれたバイト数は 0 として扱われます。エラーの詳細は err 変数を通じて返されます。

WriteString メソッドの変更

  • if fd == nil ブロック:
    • Read および Write メソッドと同様に、return -1, EINVAL から return 0, EINVAL に変更されました。
  • if !syscall.StringToBytes(b, s) ブロック:
    • 文字列をバイトスライスに変換する際にエラーが発生した場合も、return -1, EINVAL から return 0, EINVAL に変更されました。
  • syscall.write 呼び出し後:
    • r, e := syscall.write(fd.fd, &b[0], int64(len(s))) の行の直後に if r < 0 { r = 0 } が追加されました。
    • syscall.write がエラー時に -1 を返す場合、書き込まれたバイト数は 0 として扱われます。エラーの詳細は err 変数を通じて返されます。

これらの変更により、Go言語のI/O操作は、成功時には常に 0 以上のバイト数を返し、エラー時には nil ではない error オブジェクトを返すという、Goらしい一貫したエラーハンドリングパターンに統一されました。

関連リンク

  • Go言語の公式ドキュメント: https://go.dev/
  • Go言語のエラーハンドリングに関するブログ記事 (Go公式ブログなど):

参考にした情報源リンク

  • Go言語のソースコード (特に初期のコミット履歴): https://github.com/golang/go
  • Unix/Linux man pages (e.g., read(2), write(2)): C言語のシステムコールの挙動を理解するため。
  • Go言語のエラーハンドリングに関する議論や設計ドキュメント (Goの初期設計に関する情報源):
    • Goの初期設計に関するメーリングリストのアーカイブやデザインドキュメントは、このコミットが行われた2008年当時の情報を見つけるのが難しい場合がありますが、Goの進化を追う上で重要です。