49
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-02-18

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

期待通り動いた。

49
39
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
49
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?