[インデックス 10886] ファイルの概要
コミット
コミットハッシュ: 796a2c19ea0f8be23022b234667b06abbab20030 作成者: Alex Brainman alex.brainman@gmail.com 日付: 2011年12月20日 11:52:20 +1100 メッセージ: "os: make sure Remove returns correct error on windows"
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/796a2c19ea0f8be23022b234667b06abbab20030
元コミット内容
このコミットは、Go言語のosパッケージにおけるRemove
関数の実装をリファクタリングし、Windows環境でのエラーハンドリングを改善したものです。主な変更点は以下の通りです:
- file_posix.goから
Remove
関数を削除(33行削除) - file_unix.goに
Remove
関数を追加(30行追加) - file_windows.goに
Remove
関数を追加(29行追加)
変更統計:
- 3ファイルの変更
- 59行の追加
- 33行の削除
- 正味26行の追加
変更の背景
このコミットは、Go言語の初期開発段階(2011年)において、クロスプラットフォームでのファイル削除機能の実装において発生していた問題を解決するために行われました。
当時の実装では、POSIX(Unix系)とWindowsで異なる動作をするファイル削除操作を、単一のfile_posix.goファイルで処理しようとしていました。しかし、WindowsとUnix系システムでは、以下の根本的な違いがあります:
Unix系システムでの動作
syscall.Unlink
(ファイル削除)とsyscall.Rmdir
(ディレクトリ削除)を使用- 両方のシステムコールが失敗した場合、
ENOTDIR
エラーを使用して適切なエラーを決定 - OS XとLinuxでは
unlink(dir)
がEISDIR
を返すかどうかが異なる
Windows環境での問題
syscall.DeleteFile
(ファイル削除)とsyscall.RemoveDirectory
(ディレクトリ削除)を使用- エラーハンドリングが不適切で、正確なエラーメッセージが返されない
- Windows特有の
FILE_ATTRIBUTE_DIRECTORY
フラグの確認が必要
前提知識の解説
ファイルシステムの基本概念
ファイル削除の複雑性 プログラムからファイルを削除する際、システムはファイルなのかディレクトリなのかを知る必要があります。これは、多くのオペレーティングシステムでファイルとディレクトリに対して異なるシステムコールを使用するためです。
POSIXとWindowsの違い
- POSIX(Unix系):
unlink()
でファイルを削除、rmdir()
でディレクトリを削除 - Windows:
DeleteFile()
でファイルを削除、RemoveDirectory()
でディレクトリを削除
システムコールとエラーハンドリング
ENOTDIRエラー
- "Not a directory"を意味するエラーコード
- ディレクトリが期待される場所にファイルが存在する場合に発生
- Unix系システムでは
rmdir(file)
がENOTDIR
を返すことが保証されている
EISDIRエラー
- "Is a directory"を意味するエラーコード
- ファイルが期待される場所にディレクトリが存在する場合に発生
- OS XとLinuxで
unlink(dir)
の動作が異なる
Go言語のクロスプラットフォーム設計
Go言語は、単一のソースコードで複数のプラットフォームで動作するアプリケーションを作成できるように設計されています。これを実現するために、プラットフォーム固有の実装を別々のファイルに分離する手法が採用されています。
技術的詳細
実装の分離戦略
ファイル分離の理由
- プラットフォーム固有の最適化:各OSに最適化された実装を提供
- エラーハンドリングの改善:各プラットフォームで適切なエラーメッセージを返す
- 保守性の向上:プラットフォーム固有のバグを分離して修正可能
ビルドタグの活用 Goのビルドシステムは、ファイル名に基づいて適切なファイルを選択します:
file_unix.go
:Unix系システム(Linux、macOS等)でビルドfile_windows.go
:Windowsシステムでビルド
エラーハンドリングの改善
Unix系システムでの改善
// 両方のシステムコールが失敗した場合のエラー判定
if e1 != syscall.ENOTDIR {
e = e1
}
return &PathError{"remove", name, e}
Windows環境での改善
// ファイル属性を確認してエラーを決定
if e1 != e {
a, e2 := syscall.GetFileAttributes(p)
if e2 != nil {
e = e2
} else {
if a&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
e = e1
}
}
}
コアとなるコードの変更箇所
削除された共通実装(file_posix.go)
// Remove removes the named file or directory.
func Remove(name string) error {
// System call interface forces us to know
// whether name is a file or directory.
// Try both: it is cheaper on average than
// doing a Stat plus the right one.
e := syscall.Unlink(name)
if e == nil {
return nil
}
e1 := syscall.Rmdir(name)
if e1 == nil {
return nil
}
// Both failed: figure out which error to return.
// OS X and Linux differ on whether unlink(dir)
// returns EISDIR, so can't use that. However,
// both agree that rmdir(file) returns ENOTDIR,
// so we can use that to decide which error is real.
// Rmdir might also return ENOTDIR if given a bad
// file path, like /etc/passwd/foo, but in that case,
// both errors will be ENOTDIR, so it's okay to
// use the error from unlink.
// For windows syscall.ENOTDIR is set
// to syscall.ERROR_PATH_NOT_FOUND, hopefully it should
// do the trick.
if e1 != syscall.ENOTDIR {
e = e1
}
return &PathError{"remove", name, e}
}
追加されたUnix系実装(file_unix.go)
// Remove removes the named file or directory.
func Remove(name string) error {
// System call interface forces us to know
// whether name is a file or directory.
// Try both: it is cheaper on average than
// doing a Stat plus the right one.
e := syscall.Unlink(name)
if e == nil {
return nil
}
e1 := syscall.Rmdir(name)
if e1 == nil {
return nil
}
// Both failed: figure out which error to return.
// OS X and Linux differ on whether unlink(dir)
// returns EISDIR, so can't use that. However,
// both agree that rmdir(file) returns ENOTDIR,
// so we can use that to decide which error is real.
// Rmdir might also return ENOTDIR if given a bad
// file path, like /etc/passwd/foo, but in that case,
// both errors will be ENOTDIR, so it's okay to
// use the error from unlink.
if e1 != syscall.ENOTDIR {
e = e1
}
return &PathError{"remove", name, e}
}
追加されたWindows実装(file_windows.go)
// Remove removes the named file or directory.
func Remove(name string) error {
p := &syscall.StringToUTF16(name)[0]
// Go file interface forces us to know whether
// name is a file or directory. Try both.
e := syscall.DeleteFile(p)
if e == nil {
return nil
}
e1 := syscall.RemoveDirectory(p)
if e1 == nil {
return nil
}
// Both failed: figure out which error to return.
if e1 != e {
a, e2 := syscall.GetFileAttributes(p)
if e2 != nil {
e = e2
} else {
if a&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
e = e1
}
}
}
return &PathError{"remove", name, e}
}
コアとなるコードの解説
Unix系実装の解説
基本的な削除戦略
syscall.Unlink(name)
を試行(ファイル削除)- 成功した場合は
nil
を返す - 失敗した場合は
syscall.Rmdir(name)
を試行(ディレクトリ削除) - 成功した場合は
nil
を返す
エラー判定ロジック
- 両方のシステムコールが失敗した場合、どちらのエラーを返すかを決定
rmdir(file)
はENOTDIR
を返すことが保証されているe1 != syscall.ENOTDIR
の場合、ディレクトリ削除のエラーを採用
プラットフォーム間の差異への対応
- OS XとLinuxでは
unlink(dir)
がEISDIR
を返すかどうかが異なる ENOTDIR
を基準にすることで、この差異を回避
Windows実装の解説
文字列エンコーディングの処理
p := &syscall.StringToUTF16(name)[0]
- Windows APIはUTF-16エンコーディングを使用
- Go文字列(UTF-8)をUTF-16に変換してからシステムコール実行
削除戦略
syscall.DeleteFile(p)
を試行(ファイル削除)- 成功した場合は
nil
を返す - 失敗した場合は
syscall.RemoveDirectory(p)
を試行(ディレクトリ削除) - 成功した場合は
nil
を返す
高度なエラー判定
if e1 != e {
a, e2 := syscall.GetFileAttributes(p)
if e2 != nil {
e = e2
} else {
if a&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
e = e1
}
}
}
- 両方のエラーが異なる場合、
GetFileAttributes
でファイル属性を確認 FILE_ATTRIBUTE_DIRECTORY
フラグを確認してディレクトリかどうか判定- ディレクトリの場合は
RemoveDirectory
のエラーを採用
PathErrorの統一
両実装とも最終的に&PathError{"remove", name, e}
を返し、一貫したエラーインターフェースを提供しています。
関連リンク
- os package - Go Packages
- syscall package - Go Packages
- unlink(2) - Linux manual page
- rmdir(2) - Linux manual page
- Go Issue #9606: os: Remove/RemoveAll should remove read-only files on Windows
- Go Issue #18974: os: IsNotExist returns false for syscall.ENOTDIR