Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Goでファイルの存在確認

Goでファイルの存在確認について、インターネットではos.Statの戻り値がエラーかどうかを判定する方法が紹介されていますけれど、os.Statはファイルが存在する場合でもエラーを返すことがあるため、この方法では正しく判定できないケースが存在します。また、複数プロセスが同じファイルにアクセスする場合は、os.Statの直後で、別のプロセスによってファイルが作成されたり削除されたりするかもしれません。正確に存在確認する場合は、存在確認した後に行う処理によっていくつかパターンがありますが、基本は、事前に存在を確認するのではなく、意図しない場合にエラーとなるようなフラグを立てておいて、OSのシステムコールが返したエラーを判定することになります。

Cなどでは、Unix系OSと異なり、Windowsは別の関数とフラグを、Plan 9は別のフラグを使いますが、Go標準パッケージのsyscallはその辺りの違いを吸収してくれているので、以下の内容はそのまま使えます。*1

目的別に紹介

ファイルの存在確認をする目的ごとに、対応方法は異なります。以下ではos.OpenFileを使いますが、Goにおいてはos.Openまたはos.Createは以下の呼び出しと同じなので、同じフラグになるのであればどの関数を使っても構いません。

// os.Open(name)は以下と同じ
os.OpenFile(name, os.O_RDONLY, 0)

// os.Create(name)は以下と同じ
os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)

ファイルが存在すれば読む

例えば設定ファイルがあれば読む場合などです。事前の確認を行わず、ファイルを読み込みフラグで開いて、エラーなら、それを使って原因を調べるといいでしょう。

f, err := os.OpenFile(file, os.O_RDONLY, 0)
if err != nil {
    if os.IsNotExist(err) {
        return nil // ファイルが存在しない
    }
    return err // それ以外のエラー(例えばパーミッションがない)
}
// ファイルが正しく読み込める
return nil

ioutil.ReadFileなどを使う場合も、とりあえず読み込んでみて、エラーなら上と同じように判定すればいいです。

ファイルを作成するが存在した場合は何もしない

ファイルがなければデフォルトの値でファイルを作成する場合などで使います。os.O_CREATEと同時にos.O_EXCLをセットすることで、作成できなかった場合にエラーとなるため、存在していたことを判定したい場合はos.IsExistで確認する必要があります。

f, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
    if os.IsExist(err) {
        return nil // ファイルが既に存在していた
    }
    return err // それ以外のエラー(例えばパーミッションがない)
}
// ファイルに書き込み可能
return nil

ファイルが存在すれば読み込むが、なければ新規作成する

ログなどをファイルへ書き込む場合に使うと良さそうです。ファイルの有無によりエラーとなることがないため、エラーの判定は特にありません。

f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
    return err // エラー(例えばパーミッションがない)
}
// ファイルを読み書き可能
return nil

ファイルが存在すれば削除して新規作成する

設定ファイルの更新などで使います。実際はファイルを削除するわけではなく、os.O_TRUNCフラグによってファイルの内容を消去しています。

f, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
    return err // エラー(例えばパーミッションがない)
}
// ファイルに書き込み可能
return nil

ioutil.WriteFileはこのフラグと同等です。

ファイルが存在すれば削除する

削除の場合はos.OpenFileの代わりにos.Removeを使いますが、基本はos.OpenFileと同様に、実行してからのエラーを判定します。

err := os.Remove(file)
if err != nil {
    if os.IsNotExist(err) {
        return nil // 存在していないので何もしなくていい
    }
    return err // エラー(例えばパーミッションがない)
}
// ファイルに書き込み可能
return nil

参考情報

アトミックなファイル更新

ファイルの書き込みについて補足です。

上記では、ファイルの内容を更新するパターンを紹介しましたが、ファイルの内容を更新途中で電源が落ちたなどの原因によって、中途半端な状態が発生することがあります。これを避けるために、POSIXでは同一ファイルシステムにおいてrenameがアトミックであることを利用して、新しいファイルの内容を別のファイルとして保存して、全て終わったあとで本来のファイル名にリネームする手法が使われます。これを自分で書くのは意外と大変なので、Goならnatefinch/atomicを使えば良いでしょう。

また、最近のOSにはファイルの書き込みバッファが存在するため、バッファを使わない一部の例外を除いて、bufio.Writer.Flushなどでフラッシュしてもファイルには書き込まれていない状態が起こり得ます。これがどういった原理なのかは以下の記事が分かりやすいと思います。

*1:Windowssyscall_windows.goPlan 9const_plan9.go辺り