Go言語でファイルの存在をチェックする関数を実装していて、予期しない振る舞いがあったのでメモ。
よく見かける実装
How to check if a file exists in Go? - Stack Overflowなどで見かける実装で下記のようなコードがある。
func fileExists(filename string) bool {
_, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return true
}
結論から言うと、このスニペットではちゃんと判定できない場合がある。
テストコードを見てみよう。
テストコード:
package main
import (
"os"
)
func main() {
testCases := map[string]bool{
"dir": true,
"file.txt": true,
".dotfile": true,
"link-to-file.txt": true,
"unknown-dir": false,
"unknown-file": false,
".unknown-dotfile": false,
"unknown-link-to-file.txt": false,
"dir/unknown-dir": false,
"file.txt/unknown-dir": false,
".dotfile/unknown-dir": false,
"link-to-file.txt/unknown-dir": false,
}
for filename, expects := range testCases {
if fileExists("/tmp/golang-test/"+filename) == expects {
println("PASS: " + filename)
} else {
println("FAIL: " + filename)
}
}
}
func fileExists(filename string) bool {
_, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return true
}
テスト用のファイルを作るスクリプト
#!/bin/bash
mkdir -p /tmp/golang-test
cd /tmp/golang-test
touch file.txt
mkdir dir
touch .dotfile
ln -s file.txt link-to-file.txt
実行結果
ご覧のとおり、存在するファイルのあとに /
でファイル名を続けると、ファイルがないのに true
が返ってきて結果的に FAIL になる。
go1.2 darwin/amd64 の実行結果
PASS: file.txt
PASS: .dotfile
PASS: link-to-file.txt
PASS: dir
PASS: unknown-dir
PASS: unknown-file
PASS: .unknown-dotfile
PASS: unknown-link-to-file.txt
PASS: dir/unknown-dir
FAIL: file.txt/unknown-dir
FAIL: .dotfile/unknown-dir
FAIL: link-to-file.txt/unknown-dir
go1.2 linux/amd64 の実行結果
PASS: file.txt
PASS: .dotfile
PASS: link-to-file.txt
PASS: unknown-dir
PASS: dir/unknown-dir
FAIL: .dotfile/unknown-dir
PASS: dir
PASS: unknown-file
PASS: .unknown-dotfile
PASS: unknown-link-to-file.txt
FAIL: file.txt/unknown-dir
FAIL: link-to-file.txt/unknown-dir
いろいろ調べてみたが、file.txt/unknown-dir
のようなパスでは syscall.ENOTDIR
が返ってくる場合があり、os.IsNotExists
ではこれをハンドリングしていないようである。もしかしたら、syscall.ENOTDIR
がOS依存なのかもしれず、それでハンドリングしていないのかもしれない。知ってる人がいたら教えてほしい。
ちゃんとした実装?
上の file-exists1.go
に下記のエラーハンドリングを追加したのが file-exists2.go
である。
if pathError, ok := err.(*os.PathError); ok {
if pathError.Err == syscall.ENOTDIR {
return false
}
}
package main
import (
"fmt"
"os"
"syscall"
)
func main() {
testCases := map[string]bool{
"dir" : true,
"file.txt": true,
".dotfile" : true,
"link-to-file.txt" : true,
"unknown-dir" : false,
"unknown-file" : false,
".unknown-dotfile" : false,
"unknown-link-to-file.txt" : false,
"dir/unknown-dir" : false,
"file.txt/unknown-dir" : false,
".dotfile/unknown-dir" : false,
"link-to-file.txt/unknown-dir" : false,
}
for filename, expects := range testCases {
if fileExists("/tmp/golang-test/" + filename) == expects {
fmt.Printf("PASS: %s\n", filename)
} else {
fmt.Printf("FAIL: %s\n", filename)
}
}
}
func fileExists(filename string) bool {
_, err := os.Stat(filename)
if pathError, ok := err.(*os.PathError); ok {
if pathError.Err == syscall.ENOTDIR {
return false
}
}
if os.IsNotExist(err) {
return false
}
return true
}
実行結果
go1.2 darwin/amd64 の実行結果
PASS: file.txt
PASS: .dotfile
PASS: unknown-dir
PASS: .unknown-dotfile
PASS: unknown-link-to-file.txt
PASS: dir/unknown-dir
PASS: file.txt/unknown-dir
PASS: .dotfile/unknown-dir
PASS: dir
PASS: link-to-file.txt
PASS: unknown-file
PASS: link-to-file.txt/unknown-dir
go1.2 linux/amd64 の実行結果
PASS: dir
PASS: file.txt
PASS: unknown-dir
PASS: unknown-file
PASS: .unknown-dotfile
PASS: dir/unknown-dir
PASS: file.txt/unknown-dir
PASS: link-to-file.txt/unknown-dir
PASS: .dotfile
PASS: link-to-file.txt
PASS: unknown-link-to-file.txt
PASS: .dotfile/unknown-dir
これで期待するどおりの結果となった。
もっとちゃんとした実装(追記 2014/02/19)
kaz8さんのコメントで、ファイルの存在チェックは os.Stat
でチェックするだけでいいとのことなので試してみた。
package main
import (
"fmt"
"os"
)
func main() {
testCases := map[string]bool{
"dir" : true,
"file.txt": true,
".dotfile" : true,
"link-to-file.txt" : true,
"unknown-dir" : false,
"unknown-file" : false,
".unknown-dotfile" : false,
"unknown-link-to-file.txt" : false,
"dir/unknown-dir" : false,
"file.txt/unknown-dir" : false,
".dotfile/unknown-dir" : false,
"link-to-file.txt/unknown-dir" : false,
}
for filename, expects := range testCases {
if fileExists("/tmp/golang-test/" + filename) == expects {
fmt.Printf("PASS: %s\n", filename)
} else {
fmt.Printf("FAIL: %s\n", filename)
}
}
}
func fileExists(filename string) bool {
_, err := os.Stat(filename)
return err == nil
}
実行結果
go1.2 darwin/amd64 の実行結果
PASS: dir
PASS: file.txt
PASS: .dotfile
PASS: unknown-dir
PASS: unknown-file
PASS: unknown-link-to-file.txt
PASS: .dotfile/unknown-dir
PASS: link-to-file.txt/unknown-dir
PASS: link-to-file.txt
PASS: .unknown-dotfile
PASS: dir/unknown-dir
PASS: file.txt/unknown-dir
go1.2 linux/amd64 の実行結果
PASS: dir
PASS: .dotfile
PASS: unknown-file
PASS: unknown-link-to-file.txt
PASS: file.txt
PASS: link-to-file.txt
PASS: unknown-dir
PASS: .unknown-dotfile
PASS: dir/unknown-dir
PASS: file.txt/unknown-dir
PASS: .dotfile/unknown-dir
PASS: link-to-file.txt/unknown-dir
期待通り動いた。