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

[インデックス 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環境でのエラーハンドリングを改善したものです。主な変更点は以下の通りです:

  1. file_posix.goからRemove関数を削除(33行削除)
  2. file_unix.goRemove関数を追加(30行追加)
  3. file_windows.goRemove関数を追加(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()でディレクトリを削除
  • WindowsDeleteFile()でファイルを削除、RemoveDirectory()でディレクトリを削除

システムコールとエラーハンドリング

ENOTDIRエラー

  • "Not a directory"を意味するエラーコード
  • ディレクトリが期待される場所にファイルが存在する場合に発生
  • Unix系システムではrmdir(file)ENOTDIRを返すことが保証されている

EISDIRエラー

  • "Is a directory"を意味するエラーコード
  • ファイルが期待される場所にディレクトリが存在する場合に発生
  • OS XとLinuxでunlink(dir)の動作が異なる

Go言語のクロスプラットフォーム設計

Go言語は、単一のソースコードで複数のプラットフォームで動作するアプリケーションを作成できるように設計されています。これを実現するために、プラットフォーム固有の実装を別々のファイルに分離する手法が採用されています。

技術的詳細

実装の分離戦略

ファイル分離の理由

  1. プラットフォーム固有の最適化:各OSに最適化された実装を提供
  2. エラーハンドリングの改善:各プラットフォームで適切なエラーメッセージを返す
  3. 保守性の向上:プラットフォーム固有のバグを分離して修正可能

ビルドタグの活用 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系実装の解説

基本的な削除戦略

  1. syscall.Unlink(name)を試行(ファイル削除)
  2. 成功した場合はnilを返す
  3. 失敗した場合はsyscall.Rmdir(name)を試行(ディレクトリ削除)
  4. 成功した場合は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に変換してからシステムコール実行

削除戦略

  1. syscall.DeleteFile(p)を試行(ファイル削除)
  2. 成功した場合はnilを返す
  3. 失敗した場合はsyscall.RemoveDirectory(p)を試行(ディレクトリ削除)
  4. 成功した場合は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}を返し、一貫したエラーインターフェースを提供しています。

関連リンク

参考にした情報源リンク