| Tested on | RHEL 10.2 with Go 1.27.0 |
|---|---|
| Package | go 1.27.0 |
| Applies to | Any host with Go 1.20+ installed |
| Privilege | Normal user |
| Scope | os.Stat and os.Lstat for file metadata, existence checks, regular file vs directory, and error handling with errors.Is. Does not cover directory walking, chmod, file watchers, or decoding FileInfo.Sys(). |
| Related guides | Golang os package Read a file in Go Read and update the same file Get current directory in Go Getting started with Go |
os.Stat is the usual entry point when you need file metadata or want to know whether a path exists without opening it for reads or writes:
info, err := os.Stat("file.txt")On success, info is an fs.FileInfo describing that filesystem path. os.Stat follows symbolic links, so when the path is a symlink the metadata normally describes the target, not the link node.
Get file metadata with os.Stat
func Stat(name string) (fs.FileInfo, error) stats the named path. FileInfo in the os package is a type alias for fs.FileInfo:
type FileInfo = fs.FileInfoThe methods you use most often:
| Method | Meaning |
|---|---|
Name() |
Base name of the file |
Size() |
Length in bytes for regular files; system-dependent for other file types |
Mode() |
File type and permission bits |
Mode().Perm() |
Permission bits only (rwx for owner, group, other) |
ModTime() |
Last modification time |
IsDir() |
Whether the path is a directory |
Sys() returns platform-specific stat data (inode numbers, device IDs). Skip it unless you are writing low-level tooling; the portable fields above cover everyday checks.
This program prints a few FileInfo fields from a file in the current directory:
package main
import (
"fmt"
"log"
"os"
"time"
)
func main() {
fi, err := os.Stat("file.txt")
if err != nil {
log.Fatal(err)
}
fmt.Printf("name=%s size=%d mode=%s mod=%s isDir=%v\n",
fi.Name(), fi.Size(), fi.Mode(), fi.ModTime().UTC().Format(time.RFC3339), fi.IsDir())
}name=file.txt size=6 mode=-rw-r--r-- mod=2026-08-23T08:00:00Z isDir=falseCreate file.txt before you run; your size, mode, and timestamp will differ.
Check whether a path exists
The common pattern is to call os.Stat and branch on the error. Use errors.Is with os.ErrNotExist so wrapped errors still match:
package main
import (
"errors"
"fmt"
"os"
)
func main() {
path := "file.txt"
_, err := os.Stat(path)
switch {
case err == nil:
fmt.Println(path, "exists")
case errors.Is(err, os.ErrNotExist):
fmt.Println(path, "does not exist")
default:
fmt.Println(path, "stat failed:", err)
}
}file.txt existsPermission denied is not the same as “does not exist.” If you map every error to false, you hide real problems—a path you cannot read might still be there. Return or propagate non-ErrNotExist errors instead of treating them as “missing.”
os.IsNotExist still works for older code, but errors.Is(err, os.ErrNotExist) is the idiomatic choice in new programs.
Check whether the path is a regular file or directory
After os.Stat succeeds, use the type bits on FileInfo:
if info.IsDir() {
// directory
}
if info.Mode().IsRegular() {
// ordinary file
}!info.IsDir() only tells you the path is not a directory. Named pipes, devices, sockets, and other special files also satisfy that test. When you specifically need a regular file, call info.Mode().IsRegular().
A directory path still “exists” in the existence sense: os.Stat returns nil and info.IsDir() is true.
os.Stat vs (*os.File).Stat
os.Stat(path) works from a path string alone. When you already have an open *os.File, call f.Stat() to get metadata for that open file without doing another path-based os.Stat lookup:
f, err := os.Open("file.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
info, err := f.Stat()That is handy right after os.Open or os.Create when you want size or mode without a second path lookup. For “does this path exist?” or symlink behavior, start with os.Stat on the path.
os.Stat vs os.Lstat for symbolic links
| Path | os.Stat |
os.Lstat |
|---|---|---|
| Normal file | Describes the file | Describes the file |
| Symlink to existing file | Describes the target | Describes the symlink |
| Broken symlink | Error (target missing) | Describes the symlink |
os.Stat follows the final link and reports the target’s metadata. os.Lstat stops at the link itself. On a broken symlink, os.Stat fails with an error (typically os.ErrNotExist after unwrapping) because the target path is gone; os.Lstat still succeeds and shows that the path is a symlink.
Pick Lstat when symlink identity matters—for example, a tool that removes or rewrites the link rather than the target.
After os.Lstat succeeds, test the symlink bit on Mode:
info, err := os.Lstat(path)
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink != 0 {
fmt.Println("symbolic link")
}Handle os.Stat errors correctly
os.Stat errors are returned as *os.PathError, which is an alias of fs.PathError. The error string includes the operation, path, and underlying cause.
_, err := os.Stat(path)
if errors.Is(err, os.ErrNotExist) {
// path missing
}
if errors.Is(err, os.ErrPermission) {
// cannot stat (permission denied)
}To read the path from the error value:
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("failed on", pathErr.Path)
}Do not match substrings in err.Error() such as "no such file". Wording varies by platform and wrapped layers; errors.Is and errors.As stay stable.
Summary
os.Stat returns portable fs.FileInfo metadata—name, size, mode, modification time, and whether the path is a directory—without opening the file. For existence checks, treat errors.Is(err, os.ErrNotExist) as “missing” and handle every other error on its own; permission denied does not mean the path is absent.
When you need a regular file, use info.Mode().IsRegular(), not !info.IsDir(). For symlinks, remember that os.Stat follows links and fails on broken targets, while os.Lstat describes the link node itself. For broader file I/O beyond metadata, see the Golang os package overview.

