はじめに
この記事は私の頭の中の整理を目的として書かれています。
記事中で紹介する書き方について、私自身絶対の自信があるわけではないので、問題のある場合はご指摘いただけると幸いです。
ディレクトリが存在しないときだけ作る
Go でディレクトリを作るときは os.Mkdir()
関数を使います。
プログラム中でディレクトリを作る必要があり、かつそのディレクトリは既に存在するかもしれないから、まだ作成されていないときだけ作成するようにする必要があるとします。
これをそのままプログラムに表現すると、だいたい以下のようになるでしょうか。
stat, err := os.Stat(dir)
if err != nil {
// ディレクトリの存在をチェック
if os.IsNotExist(err) {
// 存在しないので作る
err = os.Mkdir(dir, 0755)
if err != nil {
// ディレクトリの作成に失敗
panic(err)
}
}
}
これでもほとんどの場合は問題にならないでしょう。
「ほとんどの場合」というのは、例えば自分だけが手元で動かすちょっとしたコマンドラインツールなど、複数のプロセスが同時に起動することがまず無いような場合です。
ですが、プログラムが並列に実行されると問題になるでしょう。
例えば以下のような状況です。
- プロセス A でディレクトリ DIR の存在チェックをし、「存在しない」という結果を得る
- プロセス B でディレクトリ DIR の存在チェックをし、「存在しない」という結果を得る
- プロセス A で DIR は「存在しない」ので作成しようとする (成功する)
- プロセス B で DIR は「存在しない」ことになっているので作成しようとする (実際はプロセス A が既に作っているのでエラーになる)
ここで「プロセス」と言っているのは UNIX プロセスかもしれませんし、Goroutine からもしれません。
上記の例でいうと、存在しないはずのディレクトリを作成しようとしてエラーになったので panic()
されてしまいます。
並列性に気をつけて書き直す
以下のようにすれば、この問題を避けることができます。
// いきなりディレクトリの作成を試みる
err := os.Mkdir(dir, 0755)
if err != nil {
if !os.IsExist(err) {
// 「ディレクトリが既に存在する」以外のエラー
panic(err)
}
}
とりあえずエラーを気にせずやってみて、エラーが起こったときはそのエラーが想定通りのものであるかをチェックします。
上記の実装でいうと、それ以外の場合は回復を諦めて panic()
しています。
これならほぼ同時に複数のプロセスが同一のディレクトリを作成しようとしても、後者はディレクトリの作成に失敗し、エラー内容のチェックをした結果「想定通りのエラーしか起きていないから問題ない」として、何事もなくプログラムの実行を継続できます。
os.MkdirAll() の場合
os.Mkdir()
は一度に一階層のディレクトリしか作ることしかできません。
つまり、foo/bar
というディレクトリを作るとき、foo
がなければその時点でエラーになります。
foo
を作ってその次に foo/bar
を作る、ということをする必要があります。
os.MkdirAll()
であれば、存在しない階層をすべて作ってくれるので、こちらを使ったほうが楽にできる場合がほとんどでしょう。
ただし os.MkdirAll()
は、対象のディレクトリが既に存在してもエラーを返しません。
なので以下のようにすればよいでしょう。
err := os.MkdirAll(dir)
if err != nil {
panic(err)
}
os.Mkdir()
のときのように os.IsExist()
を使ってエラー状況をチェックする必要はないでしょう。
問題は os.MkdirAll()
が並列実行しても問題無い実装になっているかですが、そこは後日あらためてソースコードを読み込んで記事にできればと思ってます。