[インデックス 13550] ファイルの概要
このコミットは、Go言語のos
パッケージにおけるWindows環境でのエラーハンドリングを改善するものです。具体的には、os.IsExist
関数がLinkError
型のエラーを正しく処理できるように修正し、関連するテストケースを追加しています。これにより、ファイル操作(特にリネーム操作)でファイルが既に存在する場合に、os.IsExist
が期待通りにtrue
を返すようになります。
コミット
commit f2b8f6b451667a2c8d1b3553ac7fb06de28e8181
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Wed Aug 1 12:55:04 2012 +1000
os: Rename error to fit IsExist
Fixes #3828.
R=golang-dev, iant, rsc
CC=golang-dev
https://golang.org/cl/6420056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f2b8f6b451667a2c8d1b3553ac7fb06de28e8181
元コミット内容
os: Rename error to fit IsExist
このコミットは、os
パッケージ内のエラー処理を改善し、特にos.IsExist
関数がリネーム操作によって発生するエラーを適切に認識できるようにすることを目的としています。これはGoのIssue #3828を修正するものです。
変更の背景
Go言語のos
パッケージには、ファイルシステム操作に関連するエラーを判別するためのヘルパー関数がいくつか提供されています。その一つがos.IsExist(err error)
です。この関数は、与えられたエラーerr
が「ファイルやディレクトリが既に存在するために操作が失敗した」ことを示している場合にtrue
を返します。しかし、その名前から誤解されがちですが、単にファイルやディレクトリが存在するかどうかを確認するものではありません。
GoのIssue #3828では、Windows環境においてos.Rename
関数が、移動先に同名のファイルが既に存在する場合に返すエラーが、os.IsExist
によって正しく認識されないという問題が報告されていました。os.Rename
は内部的にsyscall.MoveFileEx
などのWindows APIを呼び出しますが、このAPIが返すエラーコードが、Goのos
パッケージで定義されているLinkError
型にラップされることがありました。
既存のisExist
(os.IsExist
の内部実装)関数は、PathError
型のエラーはアンラップしてその内部のエラーをチェックしていましたが、LinkError
型のエラーは考慮されていませんでした。このため、os.Rename
がLinkError
を返した場合、os.IsExist
は常にfalse
を返してしまい、アプリケーションが「ファイルが既に存在する」という状況を適切にハンドリングできないという問題が発生していました。
このコミットは、この問題を解決し、Windows上でのファイル操作におけるエラーハンドリングの一貫性と正確性を向上させることを目的としています。
前提知識の解説
os.IsExist
関数
os.IsExist(err error)
は、Go言語のos
パッケージが提供するエラー判別関数の一つです。この関数は、引数として渡されたエラーerr
が、ファイルやディレクトリが既に存在するために操作が失敗したことを示す場合にtrue
を返します。例えば、os.OpenFile
関数でos.O_EXCL
フラグ(排他的作成)を指定してファイルを作成しようとした際に、そのファイルが既に存在する場合に返されるエラーに対してos.IsExist
はtrue
を返します。
重要なのは、os.IsExist
が「ファイルが存在するかどうか」をチェックする関数ではないという点です。ファイルが存在するかどうかを確認するには、通常os.Stat
関数を使用し、返されたエラーがnil
であるか、またはos.IsNotExist(err)
がtrue
であるかをチェックします。
os.PathError
とos.LinkError
Goのos
パッケージでは、ファイルシステム操作中に発生するエラーをより詳細に表現するために、特定のエラー型を定義しています。
os.PathError
: パスに関連する操作(例:os.Open
,os.Stat
,os.Remove
)でエラーが発生した場合に返されるエラー型です。この構造体は、操作の種類(Op)、関連するパス(Path)、そして基となるシステムコールエラー(Err)を含みます。os.LinkError
: リンク操作(例:os.Link
,os.Symlink
,os.Rename
)でエラーが発生した場合に返されるエラー型です。この構造体は、操作の種類(Op)、古いパス(Old)、新しいパス(New)、そして基となるシステムコールエラー(Err)を含みます。
これらのエラー型は、内部に実際のシステムコールエラー(例: syscall.Errno
)をラップしています。os.IsExist
のようなヘルパー関数は、これらのラッパーエラーから基となるシステムコールエラーを取り出して判別する必要があります。
Windowsのシステムコールエラー
Windowsオペレーティングシステムでは、ファイルシステム操作に関するエラーは特定の数値コードで表現されます。このコミットに関連する主要なエラーコードは以下の通りです。
syscall.ERROR_ALREADY_EXISTS
(0xB7 / 183): 指定されたファイルが既に存在します。syscall.ERROR_FILE_EXISTS
(0x50 / 80): ファイルが存在します。
これらのエラーコードは、Goのsyscall
パッケージを通じてアクセスできます。os.IsExist
は、これらのシステムコールエラーが基となる場合にtrue
を返すように設計されています。また、Go独自の内部エラーとしてos.ErrExist
も存在します。
技術的詳細
このコミットの技術的な核心は、os
パッケージのWindows固有の実装であるsrc/pkg/os/error_windows.go
内のisExist
関数に、LinkError
型のアンラップ処理を追加した点です。
isExist
関数は、os.IsExist
の内部で呼び出され、実際のエラー判別ロジックを担っています。変更前は、この関数は以下のようにPathError
のみをアンラップしていました。
func isExist(err error) bool {
if pe, ok := err.(*PathError); ok {
err = pe.Err
}
return err == syscall.ERROR_ALREADY_EXISTS ||
err == syscall.ERROR_FILE_EXISTS || err == ErrExist
}
このコードでは、もしerr
がPathError
型であれば、その内部のpe.Err
を取り出して、それがsyscall.ERROR_ALREADY_EXISTS
、syscall.ERROR_FILE_EXISTS
、またはErrExist
のいずれかと一致するかをチェックしていました。
しかし、os.Rename
のような操作は、Windows上でLinkError
を返すことがあり、このLinkError
の内部にsyscall.ERROR_ALREADY_EXISTS
などの「既に存在する」ことを示すエラーが含まれている可能性がありました。変更前のisExist
はLinkError
をアンラップしないため、LinkError
が直接これらのシステムコールエラーと一致することはなく、結果としてos.IsExist
は常にfalse
を返していました。
このコミットでは、PathError
のチェックに加えて、LinkError
のチェックも追加されました。
if pe, ok := err.(*LinkError); ok {
err = pe.Err
}
この変更により、isExist
関数は、LinkError
が渡された場合でも、その内部の基となるシステムコールエラーを取り出し、それが「既に存在する」ことを示すエラーコードと一致するかどうかを正確に判別できるようになりました。これにより、os.Rename
が返すLinkError
に対してもos.IsExist
が期待通りに機能するようになり、Windows環境でのファイル操作におけるエラーハンドリングの信頼性が向上しました。
また、この変更を検証するために、src/pkg/os/error_windows_test.go
という新しいテストファイルが追加されました。このテストは、一時ディレクトリとファイルを作成し、os.Rename
を意図的に失敗させる(移動先にファイルが既に存在する状況を作り出す)ことで、os.IsExist
が正しくtrue
を返すことを確認しています。
コアとなるコードの変更箇所
src/pkg/os/error_windows.go
--- a/src/pkg/os/error_windows.go
+++ b/src/pkg/os/error_windows.go
@@ -10,6 +10,9 @@ func isExist(err error) bool {
if pe, ok := err.(*PathError); ok {
err = pe.Err
}
+ if pe, ok := err.(*LinkError); ok {
+ err = pe.Err
+ }
return err == syscall.ERROR_ALREADY_EXISTS ||
err == syscall.ERROR_FILE_EXISTS || err == ErrExist
}
src/pkg/os/error_windows_test.go
(新規ファイル)
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package os_test
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func TestErrIsExistAfterRename(t *testing.T) {
dir, err := ioutil.TempDir("", "go-build")
if err != nil {
t.Fatalf("Create temp directory: %v", err)
}
defer os.RemoveAll(dir)
src := filepath.Join(dir, "src")
dest := filepath.Join(dir, "dest")
f, err := os.Create(src)
if err != nil {
t.Fatalf("Create file %v: %v", src, err)
}
f.Close()
err = os.Rename(src, dest)
if err != nil {
t.Fatalf("Rename %v to %v: %v", src, dest, err)
}
f, err = os.Create(src)
if err != nil {
t.Fatalf("Create file %v: %v", src, err)
}
f.Close()
err = os.Rename(src, dest)
if err == nil {
t.Fatal("Rename should have failed")
}
if s := checkErrorPredicate("os.IsExist", os.IsExist, err); s != "" {
t.Fatal(s)
return
}
}
コアとなるコードの解説
src/pkg/os/error_windows.go
の変更
追加された以下の3行がこのコミットの主要な変更点です。
if pe, ok := err.(*LinkError); ok {
err = pe.Err
}
これは、isExist
関数に渡されたエラーerr
が*os.LinkError
型であるかどうかをチェックしています。
pe, ok := err.(*LinkError)
: 型アサーションを行い、err
が*os.LinkError
型であれば、その値をpe
に、成功した場合はok
にtrue
を代入します。if ok
: 型アサーションが成功した場合(つまり、err
が*os.LinkError
型であった場合)にブロック内のコードが実行されます。err = pe.Err
:LinkError
構造体の内部にラップされている実際のシステムコールエラー(Err
フィールド)を取り出し、それをerr
変数に再代入します。
この処理により、LinkError
がisExist
関数に渡された場合でも、その内部の具体的なエラーコード(例: syscall.ERROR_ALREADY_EXISTS
)が次の比較処理に渡されるようになります。これにより、os.Rename
のような操作がLinkError
を返した場合でも、os.IsExist
が正しく「ファイルが既に存在する」という状態を判別できるようになります。
src/pkg/os/error_windows_test.go
の新規追加
このテストファイルは、TestErrIsExistAfterRename
というテスト関数を含んでいます。
- 一時ディレクトリの作成:
ioutil.TempDir
を使用して一時的なディレクトリを作成します。これはテストのクリーンな実行環境を保証するためです。 - ソースファイルとターゲットファイルのパス設定:
src
とdest
という2つのファイルパスを一時ディレクトリ内に定義します。 - 最初のファイル作成とリネーム:
os.Create(src)
でsrc
ファイルを作成します。os.Rename(src, dest)
でsrc
をdest
にリネームします。この操作は成功するはずです。
- 二度目のファイル作成と意図的なリネーム失敗:
- 再度
os.Create(src)
でsrc
ファイルを作成します。 - 重要な部分:
os.Rename(src, dest)
を再度実行します。この時、dest
ファイルは既に存在するため、このos.Rename
呼び出しはエラーを返すことが期待されます。 if err == nil { t.Fatal("Rename should have failed") }
: エラーが返されなかった場合はテストを失敗させます。
- 再度
os.IsExist
によるエラーの検証:if s := checkErrorPredicate("os.IsExist", os.IsExist, err); s != "" { t.Fatal(s) }
: ここで、os.IsExist
関数が、os.Rename
が返したエラーに対してtrue
を返すことを検証しています。checkErrorPredicate
はテストヘルパー関数で、指定されたエラー述語(この場合はos.IsExist
)が期待通りに動作するかをチェックします。もしos.IsExist
がtrue
を返さなければ、テストは失敗します。
このテストは、LinkError
がos.Rename
によって返され、そのエラーがos.IsExist
によって正しく「既に存在する」エラーとして認識されることを具体的に示しています。
関連リンク
- Go Issue #3828: https://github.com/golang/go/issues/3828 (このコミットが修正した問題のトラッキング)
- Go CL 6420056: https://golang.org/cl/6420056 (このコミットの変更セット)
参考にした情報源リンク
os.IsExist
のドキュメントと一般的な誤解に関する情報:- Go言語のエラーハンドリングに関する一般的な情報:
- Windowsシステムエラーコードに関する情報:
- Microsoft Learn: System Error Codes (0-499) -
ERROR_ALREADY_EXISTS
,ERROR_FILE_EXISTS
など (具体的なURLは変動する可能性があるため、一般的な検索クエリで参照) 例: "Windows System Error Codes ERROR_ALREADY_EXISTS" で検索可能。
- Microsoft Learn: System Error Codes (0-499) -
- Go言語の
os
パッケージのソースコード (特にerror.go
,error_windows.go
): - Go言語の
syscall
パッケージのソースコード (特にWindows関連):