環境情報
この記事は以下の環境で稼働確認を実施しました。
- Windows
- Microsoft Windows [Version 10.0.19041.84]
- go version go1.13.5 windows/amd64
- Linux
- Ubuntu 18.04 LTS (Bionic Beaver)
- go version go1.13.8 linux/amd64
事象
Open状態のファイルに対して、os.Rename
を呼び出すと、LinuxとWindowsでは結果が異なります。具体的にはLinuxではos.Rename
に成功しますが、Windowsではエラーになります。
以下はOpen状態のファイルに対して、os.Rename
を利用するコマンドです。
package main
import "os"
func main() {
fp, err := os.Open("old.txt")
if err != nil {
panic(err)
}
defer fp.Close()
err = os.Rename("old.txt", "new.txt")
if err != nil {
panic(err)
}
}
これをLinux(Ubuntu)環境でこのコードを実行すると、os.Rename
が想定通り動作することが分かります。
$ ls -ltr
total 0
-rwxrwxrwx 1 developer developer 214 Feb 23 22:01 main.go
-rwxrwxrwx 1 developer developer 0 Feb 23 22:05 old.txt
$ go run main.go
$ ls -ltr
total 0
-rwxrwxrwx 1 developer developer 214 Feb 23 22:01 main.go
-rwxrwxrwx 1 developer developer 0 Feb 23 22:05 new.txt
一方、Windows環境でこのプログラムを実行すると、panicが発生しました。
C:\qiita>go run main.go
panic: rename old.txt new.txt: The process cannot access the file because it is being used by another process.
goroutine 1 [running]:
main.main()
C:/qiita/main.go:14 +0x131
exit status 2
原因
Linux
Linuxの場合、os.Rename
の実体はrenameat
になります。これは以下のようにstrace
を利用すすことで確かめることができます。
$ go build -o main
$ strace -o strace.log ./main
renameat
は相対パスでファイル名の変更を行うAPIで、そのman
ページを見ると以下の通りに記述されており、Open状態のファイルでも名称変更ができることがわかります。_
Open file descriptors for oldpath are also unaffected.
Windows
「Goならわかるシステムプログラミング 第10回 - ファイルシステムと、その上のGo言語の関数たち(1)」 によると、Windowsでのos.Rename
の実体はMoveFileEx
というWin32 apiとのことです。MoveFileEx
のドキュメントを見たところ、このAPIは対象ファイルの削除権限を要するとのことでした。
To delete or rename a file, you must have either delete permission on the file or delete child permission in the parent directory.
ここでGo言語本体のソースコードを見たところ、ファイルのオープンにはCreateFile
というWin32 APIを利用しているようです。CreateFile
は開いたファイルの共有状態を指定するのですが、Go言語ではFILE_SHARE_DELETE
を指定していません (該当のソースコードはこのあたり)
つまり削除権限なしの状態でオープンされているファイルに対して、削除権限を要するリネーム処理を行っているわけで、エラーになるのは当然といえます。
対策
作成したプログラムがさまざまなプラットフォーム・OSで利用されることが分かっている場合は、os.Rename
の前には必ずファイルを閉じておくことが大事です。
余談1
CreateFile
を呼び出す際にFILE_SHARE_DELETE
を付与して、WindowsでもLinuxと同じ挙動になるようにしてほしいという要望は実際あるようです(該当のIssueはこれ)。ただこの記事を読むと、単にFILE_SHARE_DELETE
を付与すればよいだけでもないらしく、なかなか難しい問題をはらんでいることがが分かります。
余談2
とあるライブラリを利用しているときに、ドキュメントの記述と実際の挙動が一致しなかったことが、今回の問題に行き着いた個人的なきっかけになります。ちなみにそのライブラリのコードは次のようになっていました。
fp, err := os.Open("old.txt")
DoSomething(fp)
os.Rename("old.txt", "new.txt")
Windowsではos.Rename
がエラーを返すのですが、そのエラーをハンドリングせずに捨てており、結果として奇妙な動作になっていたのでした。APIが返してくるエラーを無視しないというのも大事な教訓ですね。