Edited at

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

More than 5 years have passed since last update.

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
}

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

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

テストコード:


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
}


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

#!/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
}
}


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さんのコメントで、ファイルの存在チェックは os.Stat でチェックするだけでいいとのことなので試してみた。


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

期待通り動いた。