Why the question "How to check if a file exists in Go?" went viral
\n\nIf you've ever written a Go program that works with files, you've likely encountered this question. On Stack Overflow, it has gathered hundreds of thousands of views, with dozens of comments featuring heated debates. It seems simple, right? Python has os.path.exists(), PHP has file_exists(), but what about Go?
The problem is that Go does not have a built-in FileExists() function. And this is no accident. The language developers deliberately avoided this approach because checking file existence is a pure race condition. While you are checking, the file could be deleted or created by another goroutine. Therefore, in Go, it is common to simply try to open the file and handle the error.
But how do you do it correctly? How do you avoid losing performance and running into hidden bugs? Today, we will break down checking file existence in Go from start to finish: from basic examples to production-ready solutions.
\n\nFirst approach: os.Stat and os.IsNotExist (Stack Overflow classic)
\n\nThe most popular answer on Stack Overflow looks like this:
\n\npackage main\n\nimport (\n "fmt"\n "os"\n)\n\nfunc fileExists(filename string) bool {\n info, err := os.Stat(filename)\n if os.IsNotExist(err) {\n return false\n }\n return err == nil && !info.IsDir()\n}\n\nfunc main() {\n if fileExists("example.txt") {\n fmt.Println("File exists")\n } else {\n fmt.Println("File not found")\n }\n}\n\n\nThis code uses os.Stat(), which returns file information. If the file does not exist, the function returns an error, and we check it using os.IsNotExist(). Additionally, we verify that it is not a directory — since os.Stat() will also work on a folder.
Pros: simplicity, readability, minimal dependencies.
\n\nCons: potential race condition between checking and opening the file. If you check existence and then open the file, the file may disappear between these operations. In Go, it is common to immediately try to open the file and handle the error.
\n\nPerformance of os.Stat
\n\nThe os.Stat() call is a system call. On Linux, it is stat() or lstat(). It is fast, but not instantaneous. On average, one call takes 1-5 microseconds on an SSD. If you are checking hundreds of thousands of files, this can become a bottleneck.
Optimization: if you need to check the existence of many files, consider caching results or using filepath.Glob() with a pattern.
Second approach: opening the file and handling errors (Go Way)
\n\nA more idiomatic way is to immediately try to open the file. This solves the race condition problem:
\n\npackage main\n\nimport (\n "fmt"\n "os"\n)\n\nfunc openFile(filename string) (bool, error) {\n file, err := os.Open(filename)\n if err != nil {\n if os.IsNotExist(err) {\n return false, nil\n }\n return false, err // Other error: no permissions, broken symlink, etc.\n }\n defer file.Close()\n return true, nil\n}\n\nfunc main() {\n exists, err := openFile("example.txt")\n if err != nil {\n fmt.Printf("Error: %v\\n", err)\n return\n }\n if exists {\n fmt.Println("File exists and is accessible")\n }\n}\n\n\nThis code immediately opens the file. If an error occurs, we check whether it is specifically the "file not found" error. Any other error (e.g., insufficient permissions) is returned upward — this gives more control.
\n\nPros: atomic operation, proper error handling, no race condition.
\n\nCons: we open the file even if we only need to check its existence. For large files, this can be costly