Go言語: ファイルの存在をちゃんとチェックする実装?


Go言語でファイルの存在をチェックする関数を実装していて、予期しない振る舞いがあったのでメモ。

# よく見かける実装

[How to check if a file exists in Go? - Stack Overflow](http://stackoverflow.com/questions/12518876/how-to-check-if-a-file-exists-in-go)などで見かける実装で下記のようなコードがある。

```go
func fileExists(filename string) bool {
	_, err := os.Stat(filename)

	if os.IsNotExist(err) {
		return false
	}

	return true
}
```

結論から言うと、このスニペットではちゃんと判定できない場合がある。

テストコードを見てみよう。

テストコード:

```go:file-exists1.go
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
}
```

テスト用のファイルを作るスクリプト

```bash
#!/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` である。

```go
	if pathError, ok := err.(*os.PathError); ok {
		if pathError.Err == syscall.ENOTDIR {
			return false
		}
	}
```

```go:file-exists2.go
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さんのコメント](http://qiita.com/suin/items/b9c0f92851454dc6d461#comment-70c188ae8ad783fe57f9)で、ファイルの存在チェックは `os.Stat` でチェックするだけでいいとのことなので試してみた。

```go:file-exists3.go
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
```

期待通り動いた。