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

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

このコミットは、Go言語の標準ライブラリ os パッケージにおける os.IsExist() 関数のWindows環境での挙動を修正し、より正確にファイルやディレクトリの存在を示すエラーを判定できるようにするためのものです。特に、WindowsがPOSIX標準に完全に準拠していないことに起因する問題を解決し、os.IsExist()syscall.ERROR_ALREADY_EXISTSsyscall.ERROR_FILE_EXISTS といったWindows固有のエラーコードも適切に処理するように拡張しています。また、この変更に伴い、Windows固有の実装を分離するためのビルドタグの調整と、新しいテストケースの追加が行われています。

コミット

commit 0238cec02144991036dadb7ee58e8c9a2de2b0de
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Tue Mar 13 12:50:04 2012 +1100

    os, syscall: windows really isn't posix compliant, fix os.IsExist()
    
    R=golang-dev, rsc, bradfitz, alex.brainman
    CC=golang-dev
    https://golang.org/cl/5754083

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

https://github.com/golang/go/commit/0238cec02144991036dadb7ee58e8c9a2de2b0de

元コミット内容

os, syscall: windows really isn't posix compliant, fix os.IsExist()

R=golang-dev, rsc, bradfitz, alex.brainman
CC=golang-dev
https://golang.org/cl/5754083

変更の背景

Go言語の os パッケージには、ファイル操作で発生したエラーが特定の意味を持つかどうかを判定するためのヘルパー関数群(os.IsExist, os.IsNotExist, os.IsPermission など)が提供されています。これらの関数は、異なるオペレーティングシステム(OS)間で一貫したエラーハンドリングを提供することを目的としています。

しかし、Windowsオペレーティングシステムは、UNIX系のOSが準拠するPOSIX(Portable Operating System Interface)標準に完全に準拠していません。特に、ファイルやディレクトリが既に存在する場合に返されるエラーコードが、POSIX準拠のシステム(例: Linux, macOS)とWindowsでは異なることが問題となっていました。

従来の os.IsExist() は、主にPOSIXシステムで使われる syscall.EEXIST エラーコードに基づいて存在エラーを判定していました。しかし、Windowsではファイルが既に存在する場合に syscall.EEXIST ではなく、ERROR_ALREADY_EXISTSERROR_FILE_EXISTS といった異なるエラーコードを返すことがありました。この不一致により、Windows環境で os.OpenFileos.O_EXCL フラグ(ファイルが既に存在する場合はエラーを返す)などを使用した場合に、os.IsExist() が期待通りに true を返さないというバグが発生していました。

このコミットは、このWindows固有のエラーコードを os.IsExist() が認識できるように拡張し、クロスプラットフォームでの os.IsExist() の信頼性を向上させることを目的としています。

前提知識の解説

POSIX (Portable Operating System Interface)

POSIXは、UNIX系オペレーティングシステムのAPIに関する標準規格群です。これにより、異なるUNIX系OS間でソフトウェアの互換性を高めることができます。ファイルシステム操作、プロセス管理、スレッド、ネットワークなど、多岐にわたるAPIが定義されています。多くのUNIX系OS(Linux、macOS、FreeBSDなど)はPOSIXに準拠していますが、Windowsは歴史的に独自のAPIセット(Win32 API)を持っており、POSIXに完全には準拠していません。このため、同じ操作でもOSによって異なるエラーコードや挙動を示すことがあります。

os.IsExist() 関数

Go言語の os パッケージで提供される func IsExist(err error) bool は、与えられたエラーが「ファイルまたはディレクトリが既に存在すること」を示すものである場合に true を返します。これは、ファイル作成時に os.O_EXCL フラグを指定して、ファイルが既に存在する場合にエラーを検出する際などに利用されます。この関数は、OS固有のエラーコードを抽象化し、開発者がプラットフォームに依存しないエラーハンドリングを記述できるようにするためのものです。

syscall パッケージ

Go言語の syscall パッケージは、オペレーティングシステムの低レベルなシステムコールへのインターフェースを提供します。これにより、GoプログラムからOSのカーネル機能に直接アクセスできます。ファイル操作、プロセス管理、ネットワーク通信など、OSの基本的な機能はシステムコールを通じて行われます。syscall パッケージはOSごとに異なる実装を持ち、各OS固有のエラーコードや定数を定義しています。

PathError

Go言語の os パッケージでは、ファイルパスに関連する操作でエラーが発生した場合に、*os.PathError 型のエラーが返されることがあります。PathError は、エラーが発生した操作(Op)、関連するファイルパス(Path)、そして元のシステムコールエラー(Err)を含む構造体です。os.IsExist() などのヘルパー関数は、この PathError の内部にある Err フィールドを調べて、実際のエラーコードを判定します。

Windows固有のエラーコード

Windows APIでは、操作が失敗した場合に特定の数値エラーコードを返します。このコミットで特に重要となるのは以下のエラーコードです。

  • ERROR_ALREADY_EXISTS (183): オブジェクト(ファイル、ディレクトリ、ミューテックスなど)が既に存在する場合に返されるエラーコードです。例えば、CreateDirectory 関数で既に存在するディレクトリを作成しようとした場合などに発生します。
  • ERROR_FILE_EXISTS (80): ファイルが既に存在する場合に返されるエラーコードです。例えば、CreateFile 関数で CREATE_NEW フラグを指定して、既に存在するファイルを作成しようとした場合などに発生します。

これらのエラーコードは、POSIXの EEXIST に相当するWindowsのエラーです。

ビルドタグ (Build Tags)

Go言語では、ソースファイルの先頭に // +build <tag> の形式でビルドタグを記述することで、特定の環境でのみそのファイルをコンパイルするように制御できます。これにより、OS固有のコードやアーキテクチャ固有のコードを分離し、クロスプラットフォーム対応のアプリケーションを容易に開発できます。例えば、// +build windows と書かれたファイルはWindows環境でのみコンパイルされ、// +build linux と書かれたファイルはLinux環境でのみコンパイルされます。

技術的詳細

このコミットの核心は、Windows環境における os.IsExist() の挙動を、POSIX準拠のシステムと整合させることです。

従来のGoの os.IsExist() は、内部的に syscall.EEXIST をチェックしていました。これはUNIX系システムでは適切ですが、Windowsではファイルやディレクトリが既に存在する場合に syscall.EEXIST 以外のエラーコード(ERROR_ALREADY_EXISTSERROR_FILE_EXISTS)が返されることがありました。このため、Windowsで os.OpenFileos.O_EXCL フラグを使ってファイル作成を試み、ファイルが既に存在した際に返されるエラーが os.IsExist() で正しく判定されないという問題がありました。

この修正では、以下の変更が行われました。

  1. os/error_posix.go からWindowsを除外: src/pkg/os/error_posix.go は、POSIX準拠のシステム(darwin, freebsd, linux, netbsd, openbsd)向けに os.IsExist() などのヘルパー関数を定義していました。このファイルから windows ビルドタグが削除され、Windows固有の実装が分離されることになりました。

  2. os/error_windows.go の新規追加: Windows固有の os.IsExist(), os.IsNotExist(), os.IsPermission() の実装を含む src/pkg/os/error_windows.go が新しく追加されました。このファイルは +build windows タグを持つため、Windows環境でのみコンパイルされます。 この新しい os.IsExist() 実装では、syscall.EEXIST に加えて、Windows固有のエラーコードである syscall.ERROR_ALREADY_EXISTSsyscall.ERROR_FILE_EXISTS も存在エラーとして認識するように拡張されました。これにより、Windows上でのファイル存在チェックの信頼性が向上します。

  3. syscall/ztypes_windows.go へのWindowsエラーコードの追加: src/pkg/syscall/ztypes_windows.go は、Windowsのシステムコールで使用される定数や型を定義するファイルです。このファイルに、ERROR_FILE_EXISTS (80) と ERROR_ALREADY_EXISTS (183) の定数が追加されました。これにより、GoプログラムからこれらのWindows固有のエラーコードを syscall パッケージを通じて参照できるようになります。

  4. os/error_test.go の新規追加: os.IsExist() の挙動を検証するための新しいテストファイル src/pkg/os/error_test.go が追加されました。このテストは、ioutil.TempFile を使って一時ファイルを作成し、そのファイルが既に存在する状態で os.OpenFileos.O_RDWR|os.O_CREATE|os.O_EXCL フラグで開こうとすることでエラーを意図的に発生させます。そして、そのエラーが os.IsExist() によって正しく true と判定されることを確認します。このテストは、Windows環境での修正が正しく機能するかを検証する上で非常に重要です。

これらの変更により、Goの os.IsExist() 関数は、Windowsを含むすべてのサポート対象OSで、ファイルやディレクトリの存在を示すエラーをより正確に判定できるようになりました。

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

このコミットによる主要なコード変更は以下の4つのファイルにわたります。

  1. src/pkg/os/error_posix.go

    --- a/src/pkg/os/error_posix.go
    +++ b/src/pkg/os/error_posix.go
    @@ -2,7 +2,7 @@
     // Use of this source code is governed by a BSD-style
     // license that can be found in the LICENSE file.
     
    -// +build darwin freebsd linux netbsd openbsd windows
    +// +build darwin freebsd linux netbsd openbsd
     
     package os
     
    
    • ビルドタグから windows が削除されました。これにより、このファイルで定義されている IsExist などの関数はWindowsではコンパイルされなくなります。
  2. src/pkg/os/error_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"
    	"testing"
    )
    
    func TestErrIsExist(t *testing.T) {
    	f, err := ioutil.TempFile("", "_Go_ErrIsExist")
    	if err != nil {
    		t.Fatalf("open ErrIsExist tempfile: %s", err)
    		return
    	}
    	defer os.Remove(f.Name())
    	defer f.Close()
    	f2, err := os.OpenFile(f.Name(), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
    	if err == nil {
    		f2.Close()
    		t.Fatal("Open should have failed")
    		return
    	}
    	if !os.IsExist(err) {
    		t.Fatalf("os.IsExist does not work as expected for %#v", err)
    		return
    	}
    }
    
    • os.IsExist() の動作を検証するための新しいテストケースが追加されました。
  3. src/pkg/os/error_windows.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
    
    import "syscall"
    
    // IsExist returns whether the error is known to report that a file already exists.
    // It is satisfied by ErrExist as well as some syscall errors.
    func IsExist(err error) bool {
    	if pe, ok := err.(*PathError); ok {
    		err = pe.Err
    	}
    	return err == syscall.EEXIST || err == syscall.ERROR_ALREADY_EXISTS ||
    		err == syscall.ERROR_FILE_EXISTS || err == ErrExist
    }
    
    // IsNotExist returns whether the error is known to report that a file does not exist.
    // It is satisfied by ErrNotExist as well as some syscall errors.
    func IsNotExist(err error) bool {
    	if pe, ok := err.(*PathError); ok {
    		err = pe.Err
    	}
    	return err == syscall.ENOENT || err == ErrNotExist
    }
    
    // IsPermission returns whether the error is known to report that permission is denied.
    // It is satisfied by ErrPermission as well as some syscall errors.
    func IsPermission(err error) bool {
    	if pe, ok := err.(*PathError); ok {
    		err = pe.Err
    	}
    	return err == syscall.EACCES || err == syscall.EPERM || err == ErrPermission
    }
    
    • Windows環境でのみコンパイルされる os.IsExist, os.IsNotExist, os.IsPermission の実装が追加されました。
    • IsExist 関数が syscall.ERROR_ALREADY_EXISTSsyscall.ERROR_FILE_EXISTS をチェックするようになりました。
  4. src/pkg/syscall/ztypes_windows.go

    --- a/src/pkg/syscall/ztypes_windows.go
    +++ b/src/pkg/syscall/ztypes_windows.go
    @@ -10,11 +10,13 @@ const (
      	ERROR_PATH_NOT_FOUND      Errno = 3
      	ERROR_ACCESS_DENIED       Errno = 5
      	ERROR_NO_MORE_FILES       Errno = 18
    +	ERROR_FILE_EXISTS         Errno = 80
      	ERROR_BROKEN_PIPE         Errno = 109
      	ERROR_BUFFER_OVERFLOW     Errno = 111
      	ERROR_INSUFFICIENT_BUFFER Errno = 122
      	ERROR_MOD_NOT_FOUND       Errno = 126
      	ERROR_PROC_NOT_FOUND      Errno = 127
    +	ERROR_ALREADY_EXISTS      Errno = 183
      	ERROR_ENVVAR_NOT_FOUND    Errno = 203
      	ERROR_OPERATION_ABORTED   Errno = 995
      	ERROR_IO_PENDING          Errno = 997
    
    • Windows固有のエラーコード ERROR_FILE_EXISTS (80) と ERROR_ALREADY_EXISTS (183) が syscall.Errno 型の定数として追加されました。

コアとなるコードの解説

src/pkg/os/error_posix.go の変更

このファイルのビルドタグから windows が削除されたことで、error_posix.go で定義されていた IsExist などの関数は、Windows環境ではコンパイルされなくなりました。これは、WindowsがPOSIXに完全に準拠していないため、Windows固有のエラーハンドリングロジックを別途用意する必要があるという設計判断に基づいています。これにより、各OSに最適化されたエラー判定ロジックを提供するための基盤が整いました。

src/pkg/os/error_test.go の新規追加

TestErrIsExist 関数は、os.IsExist() の動作を検証するための統合テストです。

  1. ioutil.TempFile("", "_Go_ErrIsExist") で一時ファイルを新規作成し、そのファイルハンドル f とエラー err を取得します。これにより、テスト対象のファイルが確実に存在するようにします。
  2. defer os.Remove(f.Name())defer f.Close() で、テスト終了後に一時ファイルをクリーンアップし、ファイルハンドルを閉じます。
  3. os.OpenFile(f.Name(), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) を呼び出します。
    • f.Name(): 既に存在する一時ファイルのパスを指定します。
    • os.O_RDWR: 読み書きモードで開きます。
    • os.O_CREATE: ファイルが存在しない場合に作成します。
    • os.O_EXCL: os.O_CREATE と同時に使用された場合、ファイルが既に存在するとエラーを返します。このフラグがこのテストの肝であり、意図的に「ファイルが存在する」というエラーを発生させます。
    • 0600: ファイルのパーミッションを設定します。
  4. if err == nil で、os.OpenFile がエラーを返さなかった場合にテストを失敗させます。これは、os.O_EXCL フラグにより、ファイルが既に存在するためエラーが返されるべきだからです。
  5. if !os.IsExist(err) で、発生したエラー erros.IsExist() によって「ファイルが存在する」エラーとして正しく判定されるかを検証します。もし os.IsExist(err)false を返した場合、テストは失敗し、os.IsExist が期待通りに動作していないことを示します。

このテストは、特にWindows環境において、os.OpenFileERROR_ALREADY_EXISTSERROR_FILE_EXISTS などのエラーを返した場合でも、os.IsExist() がそれらを正しく処理できることを保証します。

src/pkg/os/error_windows.go の新規追加

このファイルは、Windows環境でのみコンパイルされる os パッケージのエラーヘルパー関数の実装を提供します。

  • IsExist(err error) bool:

    • まず、エラーが *PathError 型であるかをチェックし、もしそうであれば内部の pe.Err を取り出します。これは、os パッケージの多くの関数が PathError を返すため、元のシステムコールエラーを抽出するためです。
    • 次に、抽出されたエラーが syscall.EEXISTsyscall.ERROR_ALREADY_EXISTSsyscall.ERROR_FILE_EXISTS、またはGoの内部エラー定数 ErrExist のいずれかと一致するかをチェックします。
    • この変更により、Windowsが返す可能性のある複数の「既に存在する」エラーコードを os.IsExist() が適切に処理できるようになり、クロスプラットフォームでの一貫性が向上しました。
  • IsNotExist(err error) boolIsPermission(err error) bool:

    • これらの関数も同様に、PathError から元のエラーを抽出し、Windows固有の syscall エラーコード(syscall.ENOENTsyscall.EACCES, syscall.EPERM)とGoの内部エラー定数(ErrNotExist, ErrPermission)を組み合わせてチェックするように実装されています。これにより、Windows環境での「ファイルが存在しない」エラーや「パーミッション拒否」エラーの判定も正確に行えるようになります。

src/pkg/syscall/ztypes_windows.go の変更

このファイルは、Windowsのシステムコールで使用される定数を定義しています。

  • const ( ブロック内に、ERROR_FILE_EXISTS (値: 80) と ERROR_ALREADY_EXISTS (値: 183) の2つの Errno 型の定数が追加されました。
  • これらの定数が追加されたことで、Goの syscall パッケージを通じてWindows固有のエラーコードを直接参照できるようになり、os/error_windows.goIsExist 関数でこれらのエラーコードを比較することが可能になりました。これは、Windows APIが返す具体的なエラーコードをGoの型システムにマッピングする重要なステップです。

これらの変更全体として、Go言語がWindows環境でファイル操作のエラーをより正確かつ堅牢に処理できるようになり、開発者がプラットフォームの違いを意識することなく、一貫したエラーハンドリングロジックを記述できる基盤が強化されました。

関連リンク

参考にした情報源リンク