Golang Project Structure

Tips and tricks for writing and structuring Go code

How to Check Whether a File Exists or Not With Go

Language

  • unknown

by

There is no single function in the Go standard library that will return a boolean value set to true if a given file or directory exists, based on a given path, or false if it doesn’t.

A large archive of paper files.
Modern computer systems store so many files that it can often be hard to remember what actually exists on our hard drives, or whether a file that once existed has been deleted.

Some people may argue that such a function should have been provided, especially since other popular programming languages have equivalents.

For example, Node.js, Java, Rust, Ruby, Bash, C++, C#, Objective-C, D, Kotlin, Dart, R, Perl, PHP and Python are just some of the many languages that have this functionality built into their standard libraries.

However, we will see that it is very easy to recreate this functionality ourselves in Go with no more than a few lines of code.

Introducing the Stat Function

Despite the seeming inadequacy of Go’s standard library, it is very easy to check if a file exists or not by using the os.Stat function.

The name of the function is a shortened version of the word “statistics”, since it provides some basic metadata about a file.

It is based on the stat function that is available in the C programming language on UNIX-based systems.

In Go, you call os.Stat with a single argument: the path to the file that you are interested in getting some information about.

(If you are looking up a symbolic link, then os.Stat will follow the link and provide information about the file at its target location. On the other hand, you can use the related os.Lstat function if you want information about the symbolic link itself, without actually following it.)

The Return Values of os.Stat

This function returns a value that fulfils the os.FileInfo interface: this has methods that can be called to learn more information about the file, such as its size in bytes or the time that it was last modified (using the Size and ModTime methods respectively).

The os.Stat function also returns an error value, which will be set to equal a specific constant if the file does not exist.

In order to determine whether the file exists at the given path or not, we can simply compare the error value with os.ErrNotExist.

(Note that os.ErrNotExist is equivalent to fs.ErrNotExist — and the value of the latter is actually assigned to the former in the source code for the "os" package.)

Looking at Some Example Code

The example below shows exactly how to work out whether a file exists at a given path or not using our trusty os.Stat function:

package main

import (
	"fmt"
	"os"
)

func main() {
	const myFilePath = "/tmp/myFile.txt"

	if _, err := os.Stat(myFilePath); err != nil {
		if os.IsNotExist(err) {
			fmt.Println("THE FILE DOES NOT EXIST")
		} else {
			panic(err)
		}
	} else {
		fmt.Println("THE FILE EXISTS")
	}
}

We haven’t even assigned the first os.FileInfo return value to a variable here, because it’s not necessary for our purposes. After all, if a file doesn’t exist, we can hardly expect to find information about it!

The os.IsNotExist function above simply returns true if the error passed to it equals os.ErrNotExist. We could have written the comparison ourselves, but calling that function is more idiomatic.

We could also have used the errors.Is function, which compares any two errors to one another, like so:

errors.Is(err, fs.ErrNotExist)

(The function above also checks the entire chain of errors obtained by repeatedly calling errors.Unwrap on the first argument — although that shouldn’t matter here, since the error returned by the os.Stat function will not be wrapped.)

Creating a Function to Determine Whether a File Exists

Finally, in the example below, we have placed the file-checking code into its own function, so that it can more easily be reused, if we need to check the existence of multiple files at different times in the operation of our program:

package main

import (
	"fmt"
	"os"
)

func doesFileExist(path string) (found bool, err error) {
	if _, err = os.Stat(path); err != nil {
		if os.IsNotExist(err) {
			err = nil
		}
	} else {
		found = true
	}

	return
}

func main() {
	const myFilePath = "/tmp/myFile.txt"

	fileExists, err := doesFileExist(myFilePath)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Does the file exist? (%t)\n", fileExists)
}

We make use of named return values in the doesFileExist function.

We begin by assigning the error value from os.Stat function to the err parameter. However, if this value is simply signifying that the file does not exist, then we reset err to nil, because we are using the found parameter to provide this information.

In other words, we do not want to return an error simply because the file cannot be found, because that is a reasonably expected outcome of running the function.

Likewise, we only set found to true if the os.Stat function returns no error, because, in any other case, it will already be set to false by default. So if the function encounters an error or the file is not found, then found will always return false.

Modifying Our Function to Check Whether a Directory Exists

The example below defines a function that checks not only whether a path exists in the file system but also whether that path points to a directory (rather than an ordinary file):

package main

import (
	"fmt"
	"os"
)

func doesDirectoryExist(path string) (found bool, err error) {
	if fileInfo, localErr := os.Stat(path); localErr != nil {
		if !os.IsNotExist(localErr) {
			err = localErr
		}
	} else if fileInfo.IsDir() {
		found = true
	}

	return
}

func main() {
	const myDirectoryPath = "/usr/local"

	fileExists, err := doesDirectoryExist(myDirectoryPath)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Does the directory exist? (%t)\n", fileExists)
}

Handily, the IsDir method on the os.FileInfo interface provides exactly the information that we need, so we only set found to true if the return value of this method is also true.

This means that if a file does exist at the path but it is not a directory, the doesDiretoryExist function will return false for found (and nil for err), since that isn’t good enough to satisfy our condition.

Notice also how we’ve flipped around the error-handling logic from our previous example. We use a locally scoped variable named localErr to store the error from os.Stat, and only assign this to the return parameter if the error does not relate to file-existence.

This approach to handling errors is no better or worse than the one we used in the doesFileExist function: it’s just a different way of achieving the same basic result.

Leave a Reply

Your email address will not be published. Required fields are marked *