Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
34
Help us understand the problem. What is going on with this article?
@suin

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

期待通り動いた。

34
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
suin
Qiita 4位/TypeScript入門書執筆中/TypeScripterのための座談会「YYTypeScript」主催/『実践ドメイン駆動設計』書籍邦訳レビュア/分報Slack考案/YYPHP主催/CodeIQマガジン執筆/株式会社クラフトマンソフトウェア創設/Web自動テスト「ShouldBee」の開発/TypeScript/DDD/OOP
craftsman_software
「インフラの心配は、もうおしまい」 インフラ運用を自動化し、手作業を限りなくゼロにする会社

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
34
Help us understand the problem. What is going on with this article?