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

[インデックス 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型にラップされることがありました。

既存のisExistos.IsExistの内部実装)関数は、PathError型のエラーはアンラップしてその内部のエラーをチェックしていましたが、LinkError型のエラーは考慮されていませんでした。このため、os.RenameLinkErrorを返した場合、os.IsExistは常にfalseを返してしまい、アプリケーションが「ファイルが既に存在する」という状況を適切にハンドリングできないという問題が発生していました。

このコミットは、この問題を解決し、Windows上でのファイル操作におけるエラーハンドリングの一貫性と正確性を向上させることを目的としています。

前提知識の解説

os.IsExist関数

os.IsExist(err error)は、Go言語のosパッケージが提供するエラー判別関数の一つです。この関数は、引数として渡されたエラーerrが、ファイルやディレクトリが既に存在するために操作が失敗したことを示す場合にtrueを返します。例えば、os.OpenFile関数でos.O_EXCLフラグ(排他的作成)を指定してファイルを作成しようとした際に、そのファイルが既に存在する場合に返されるエラーに対してos.IsExisttrueを返します。

重要なのは、os.IsExistが「ファイルが存在するかどうか」をチェックする関数ではないという点です。ファイルが存在するかどうかを確認するには、通常os.Stat関数を使用し、返されたエラーがnilであるか、またはos.IsNotExist(err)trueであるかをチェックします。

os.PathErroros.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
}

このコードでは、もしerrPathError型であれば、その内部のpe.Errを取り出して、それがsyscall.ERROR_ALREADY_EXISTSsyscall.ERROR_FILE_EXISTS、またはErrExistのいずれかと一致するかをチェックしていました。

しかし、os.Renameのような操作は、Windows上でLinkErrorを返すことがあり、このLinkErrorの内部にsyscall.ERROR_ALREADY_EXISTSなどの「既に存在する」ことを示すエラーが含まれている可能性がありました。変更前のisExistLinkErrorをアンラップしないため、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に、成功した場合はoktrueを代入します。
  • if ok: 型アサーションが成功した場合(つまり、err*os.LinkError型であった場合)にブロック内のコードが実行されます。
  • err = pe.Err: LinkError構造体の内部にラップされている実際のシステムコールエラー(Errフィールド)を取り出し、それをerr変数に再代入します。

この処理により、LinkErrorisExist関数に渡された場合でも、その内部の具体的なエラーコード(例: syscall.ERROR_ALREADY_EXISTS)が次の比較処理に渡されるようになります。これにより、os.Renameのような操作がLinkErrorを返した場合でも、os.IsExistが正しく「ファイルが既に存在する」という状態を判別できるようになります。

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

このテストファイルは、TestErrIsExistAfterRenameというテスト関数を含んでいます。

  1. 一時ディレクトリの作成: ioutil.TempDirを使用して一時的なディレクトリを作成します。これはテストのクリーンな実行環境を保証するためです。
  2. ソースファイルとターゲットファイルのパス設定: srcdestという2つのファイルパスを一時ディレクトリ内に定義します。
  3. 最初のファイル作成とリネーム:
    • os.Create(src)srcファイルを作成します。
    • os.Rename(src, dest)srcdestにリネームします。この操作は成功するはずです。
  4. 二度目のファイル作成と意図的なリネーム失敗:
    • 再度os.Create(src)srcファイルを作成します。
    • 重要な部分: os.Rename(src, dest)を再度実行します。この時、destファイルは既に存在するため、このos.Rename呼び出しはエラーを返すことが期待されます。
    • if err == nil { t.Fatal("Rename should have failed") }: エラーが返されなかった場合はテストを失敗させます。
  5. os.IsExistによるエラーの検証:
    • if s := checkErrorPredicate("os.IsExist", os.IsExist, err); s != "" { t.Fatal(s) }: ここで、os.IsExist関数が、os.Renameが返したエラーに対してtrueを返すことを検証しています。checkErrorPredicateはテストヘルパー関数で、指定されたエラー述語(この場合はos.IsExist)が期待通りに動作するかをチェックします。もしos.IsExisttrueを返さなければ、テストは失敗します。

このテストは、LinkErroros.Renameによって返され、そのエラーがos.IsExistによって正しく「既に存在する」エラーとして認識されることを具体的に示しています。

関連リンク

参考にした情報源リンク