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

More than 1 year has 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

期待通り動いた。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.