Files and paths¶
Two packages cover almost everything: os for the filesystem, and
path/filepath for manipulating the names. The whole-file calls are the
ones you reach for most.
if err := os.WriteFile("notes.txt", []byte("hello\n"), 0o644); err != nil {
return err
}
b, err := os.ReadFile("notes.txt")
if err != nil {
return err
}
fmt.Printf("%q\n", string(b)) // output: "hello\n"
ReadFile returns []byte, not a string, and it opens, reads and closes
for you. Use it whenever the file fits comfortably in memory; stream it
otherwise, which the next article covers.
Permissions are octal¶
That 0o644 is a Unix permission bitmask — owner read/write, everyone
else read. The 0o prefix is Go's octal literal syntax. It applies only
when the call creates the file; an existing file keeps its own mode.
0o644 for data, 0o755 for directories and executables, 0o600 when
the contents are sensitive.
Checking what something is¶
fi, err := os.Stat("notes.txt")
if err != nil {
return err
}
fmt.Println(fi.Name(), fi.Size(), fi.IsDir(), fi.Mode().Perm())
// output: notes.txt 6 false -rw-r--r--
Perm() prints the symbolic form of the same bits: three groups of
rwx for owner, group and others, with - where a permission is
absent. -rw-r--r-- is 0o644 written the other way round.
Missing files: test the error, not the path¶
Every filesystem call returns a *fs.PathError wrapping a specific
cause, so errors.Is answers the "does it exist" question:
_, err := os.ReadFile("nope.txt")
fmt.Println(errors.Is(err, fs.ErrNotExist)) // output: true
var pe *fs.PathError
fmt.Println(errors.As(err, &pe), pe.Op) // output: true open
os.ErrNotExist and fs.ErrNotExist are the same value, so either
works. There is an os.IsNotExist(err) helper in older code; prefer
errors.Is, which sees through wrapping.
Resist the urge to call os.Stat first and then open. Between the two
calls the answer can change, and you have to handle the failure anyway —
just open it and check the error.
Opening for control¶
os.Create truncates or creates. os.OpenFile is the general form, and
appending is the common reason to reach for it:
f, _ := os.Create("log.txt")
fmt.Fprintln(f, "line1")
f.Close()
af, _ := os.OpenFile("log.txt", os.O_APPEND|os.O_WRONLY, 0o644)
fmt.Fprintln(af, "line2")
af.Close()
An *os.File is an io.Writer, which is why fmt.Fprintln works on it.
In real code close with defer:
For a file you wrote, a deferred Close whose error you discard is a
real risk. The write calls can succeed while the filesystem only reports
a failure — a full disk, a network mount dropping — when the descriptor
is closed. Close it explicitly and check the error before reporting
success.
Directories¶
os.MkdirAll("a/b/c", 0o755) // creates parents, no error if it exists
os.Remove(path) // one file or one empty directory
os.RemoveAll(dir) // recursive; no error if absent
MkdirAll is the one to reach for — plain Mkdir fails if the parent is
missing, and errors when the directory already exists. Removing a file
that is not there is an error:
fmt.Println(os.Remove("notes.txt")) // output: <nil>
fmt.Println(os.Remove("notes.txt") != nil) // output: true
For scratch space, let the library pick the location and clean up after yourself:
Building paths with filepath¶
Never join paths with + or /. filepath.Join uses the right
separator for the platform, and it cleans the result:
fmt.Println(filepath.Join("a", "b", "..", "c")) // output: a/c
fmt.Println(filepath.Join("a", "", "b")) // output: a/b
Empty segments vanish, which makes it safe to join a variable that might be blank.
fmt.Println(filepath.Base("/x/y/z.txt")) // output: z.txt
fmt.Println(filepath.Dir("/x/y/z.txt")) // output: /x/y
fmt.Println(filepath.Ext("/x/y/z.txt")) // output: .txt
Ext includes the dot. Stripping it is a strings job:
filepath.Rel gives the path from one place to another, which is how you
turn absolute walk results back into readable names:
There is also a path package. It is for slash-separated things that are
not filesystem paths — URLs, embedded FS names. For files, use
filepath.
Listing and walking¶
os.ReadDir lists one directory. It returns fs.DirEntry values, which
know the name and whether it is a directory without an extra syscall:
ents, err := os.ReadDir(dir)
if err != nil {
return err
}
for _, e := range ents {
fmt.Println(e.Name(), e.IsDir())
}
filepath.WalkDir recurses. The callback receives an error argument,
and the first thing to do is check it — otherwise an unreadable
subdirectory silently truncates your walk:
var found []string
walkErr := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && filepath.Ext(path) == ".go" {
found = append(found, path)
}
return nil
})
if walkErr != nil {
return walkErr
}
Returning a non-nil error stops the walk and WalkDir returns it.
Returning fs.SkipDir from a directory skips its whole subtree instead:
filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if d.IsDir() && d.Name() == "vendor" {
return fs.SkipDir
}
// ...
return nil
})
That is the idiom for ignoring vendor, node_modules or .git.
Renaming and copying¶
os.Rename moves a file, atomically when both paths are on the same
filesystem — which is how you write a file safely: write to a temporary
name, then rename over the target, so a reader never sees a half-written
file.
There is no os.Copy. Open both and use io.Copy, in the next article.
From Python:
os.ReadFile/WriteFilearepathlib.read_bytes/write_bytes,filepath.Joinisos.path.join,WalkDirisos.walkwith a callback instead of a generator. The habit to drop isif os.path.exists(...): here you attempt the operation and inspect the error witherrors.Is.
Quick reference¶
| Task | Call |
|---|---|
| read a whole file | os.ReadFile(p) → []byte |
| write a whole file | os.WriteFile(p, b, 0o644) |
| open for reading / writing | os.Open / os.Create / os.OpenFile |
| append | os.OpenFile(p, os.O_APPEND\|os.O_WRONLY, 0o644) |
| metadata | os.Stat(p) → fs.FileInfo |
| does it exist | errors.Is(err, fs.ErrNotExist) |
| make directories | os.MkdirAll(p, 0o755) |
| delete | os.Remove / os.RemoveAll |
| temp directory | os.MkdirTemp("", "prefix") + defer os.RemoveAll |
| build a path | filepath.Join(...) |
| split a path | Base, Dir, Ext, Rel |
| list one directory | os.ReadDir(p) |
| walk a tree | filepath.WalkDir(root, fn), fs.SkipDir to prune |
| move / replace atomically | os.Rename(tmp, target) |