| 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 | Task-oriented overview of the Go os package: open and read files, create and write, Stat, rename, remove, directories, working directory, and basic environment access. Does not cover signals, process spawning, pipes, or full permissions theory. |
| Related guides | Read a file in Go os.Stat in Go Environment variables in Go Get current directory in Go Getting started with Go |
Go's os package provides portable access to operating-system features such as files, directories, environment variables, process arguments, and the working directory. You do not need every function on the page to get productive work done. This guide maps common tasks to the right API, shows short runnable examples, and points to dedicated articles when a topic deserves a full walkthrough.
The examples below use notes.txt, out.txt, and a few temporary directories in your current working directory. Each example imports only the Go standard library, so you can save the program files and run go run without creating a module.
| Task | Function |
|---|---|
| Read whole file | os.ReadFile |
| Open file stream | os.Open |
| Create/truncate file | os.Create |
| Write whole file | os.WriteFile |
| Custom open flags | os.OpenFile |
| File metadata | os.Stat |
| Create directory tree | os.MkdirAll |
| List directory | os.ReadDir |
| Current directory | os.Getwd |
| Read environment variable | os.Getenv / os.LookupEnv |
For line-by-line reads, large files, and timeout patterns, see ways to read a file in Go. For metadata fields and permission bits in depth, see os.Stat in Go.
Open and read files with os.Open and os.ReadFile
Pick the API by how much of the file you need in memory and whether you need streaming.
os.ReadFile reads the entire file into a []byte and closes the path internally. It fits small config files, generated blobs, and one-shot reads where holding the whole content in memory is acceptable.
os.Open returns *os.File, which implements io.Reader and related interfaces. Use it when you scan lines, read chunks, seek, or pass a stream to another package. After a successful open you must close the file yourself.
Create notes.txt with two lines before running the open example:
printf 'line one\nline two\n' > notes.txtprintf exits silently on success; notes.txt now holds two lines for the reader.
Stream notes.txt line by line with os.Open and bufio.Scanner:
cat > read_open.go << 'EOF'
package main
import (
"bufio"
"fmt"
"log"
"os"
)
func main() {
f, err := os.Open("notes.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
sc := bufio.NewScanner(f)
for sc.Scan() {
fmt.Println(sc.Text())
}
if err := sc.Err(); err != nil {
log.Fatal(err)
}
}
EOFRun the scanner example against notes.txt:
go run read_open.goline one
line twoEach printed line confirms the scanner walked the open file without loading it through a separate ReadFile call first.
For a whole-file read on the same path, ReadFile is shorter because it handles open and close for you:
b, err := os.ReadFile("notes.txt")
if err != nil {
log.Fatal(err)
}
fmt.Print(string(b))That pattern prints the same two lines. Reserve ReadFile for files whose size you trust; streaming stays on Open plus bufio or io helpers.
Create and write files
The file-creation APIs below require the parent directory to already exist. Permission literals such as 0o644 set the mode bits you request; on Unix the process umask still shapes the effective mode on disk.
| Need | API |
|---|---|
| Create/truncate + incremental writes | os.Create |
| Write all bytes in one call | os.WriteFile |
| Append | os.OpenFile + O_APPEND |
| Custom flags | os.OpenFile |
os.Create truncates an existing file or creates a new one, then opens it read-write. os.WriteFile also truncates or creates, writes the full []byte, and closes the file inside the function.
Write a one-line file with WriteFile, then create a second file incrementally with Create:
cat > create_write.go << 'EOF'
package main
import (
"fmt"
"log"
"os"
)
func main() {
if err := os.WriteFile("out.txt", []byte("first\n"), 0o644); err != nil {
log.Fatal(err)
}
f, err := os.Create("demo.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
if _, err := f.WriteString("hello\n"); err != nil {
log.Fatal(err)
}
fmt.Println("created and wrote")
}
EOFExecute both write patterns and confirm the program finishes cleanly:
go run create_write.gocreated and wroteout.txt holds first and demo.txt holds hello on separate lines.
Append with os.OpenFile
os.WriteFile always replaces existing content. To append, open with append flags and write through *os.File:
cat > append.go << 'EOF'
package main
import (
"log"
"os"
)
func main() {
f, err := os.OpenFile("out.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
log.Fatal(err)
}
if _, err := f.WriteString("second\n"); err != nil {
f.Close()
log.Fatal(err)
}
if err := f.Close(); err != nil {
log.Fatal(err)
}
}
EOFAppend a second line, then print out.txt to verify both lines survived:
go run append.go && cat out.txtfirst
secondThe second line landed after first because O_APPEND preserved prior bytes.
Work with file information, rename, and remove
os.Stat returns fs.FileInfo with size, modification time, and whether the path is a directory. Use it for metadata checks; the dedicated os.Stat guide walks through mode bits and directory detection in more detail.
Prefer errors.Is(err, os.ErrNotExist) over parsing error text. Platforms phrase missing-path errors differently, so string matching breaks when you move between Linux, macOS, or Windows.
Create notes.txt for the Stat demo:
printf 'line one\nline two\n' > notes.txtSave and run a small program that checks a missing path and inspects notes.txt:
cat > stat_demo.go << 'EOF'
package main
import (
"errors"
"fmt"
"os"
"time"
)
func main() {
if _, err := os.Stat("missing.txt"); err != nil {
fmt.Println("missing:", errors.Is(err, os.ErrNotExist))
}
fi, err := os.Stat("notes.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("size=%d mod=%s\n", fi.Size(), fi.ModTime().UTC().Format(time.RFC3339))
}
EOFRun the Stat snippet:
go run stat_demo.goSample output (your modification time will differ):
missing: true
size=18 mod=2026-08-23T07:15:42Zmissing: true shows ErrNotExist detection working. size=18 matches the two lines in notes.txt; the mod timestamp reflects when the file was written on your machine.
os.Rename changes the path of an existing file or directory. On success it returns nil; your program produces no output unless you print a confirmation. Renaming across different filesystems can fail.
os.Remove removes a file or an empty directory. Use os.RemoveAll when you intentionally need to remove a directory and everything below it. A missing path returns an error whose message depends on the OS, which is another reason to use errors.Is instead of substring checks.
Create and list directories
Directory helpers mirror the file APIs: one level versus a full tree, list versus delete.
os.Mkdir creates a single directory and fails when parent directories are missing. os.MkdirAll creates the full path, which is what you want for cache or data directories under a new tree. os.ReadDir returns a directory's immediate entries sorted by filename, without opening each path as a file.
cat > dirs.go << 'EOF'
package main
import (
"fmt"
"log"
"os"
)
func main() {
if err := os.MkdirAll("data/cache", 0o755); err != nil {
log.Fatal(err)
}
entries, err := os.ReadDir("data")
if err != nil {
log.Fatal(err)
}
for _, e := range entries {
fmt.Println(e.Name())
}
}
EOFList what MkdirAll created under data:
go run dirs.gocachecache is the only child under data, which matches the MkdirAll("data/cache", …) call. When you need to delete that tree, os.RemoveAll("data") removes the directory and everything under it; treat it as destructive.
Working directory and environment variables
os.Getwd reports the process working directory. os.Getenv returns the value of a variable or an empty string when it is unset. os.LookupEnv adds a boolean so you can tell unset variables from variables explicitly set to an empty string.
These snippets are intentionally short; see environment variables in Go and get current directory in Go for configuration patterns and change-directory workflows.
cat > env_wd.go << 'EOF'
package main
import (
"fmt"
"os"
)
func main() {
wd, err := os.Getwd()
if err != nil {
fmt.Println("getwd:", err)
return
}
fmt.Println("wd:", wd)
val, ok := os.LookupEnv("GOLANG_OS_DEMO")
fmt.Printf("unset ok=%v val=%q\n", ok, val)
if err := os.Setenv("GOLANG_OS_DEMO", ""); err != nil {
fmt.Println("setenv:", err)
return
}
val2, ok2 := os.LookupEnv("GOLANG_OS_DEMO")
fmt.Printf("empty ok=%v val=%q\n", ok2, val2)
}
EOFRun the working-directory and LookupEnv demo:
go run env_wd.goSample output (your working-directory path will differ):
wd: /home/user/golang-os-example
unset ok=false val=""
empty ok=true val=""The wd line reflects wherever you ran the program. The first lookup shows the variable was not set (ok=false). After Setenv with an empty value, ok=true with val="" proves LookupEnv distinguishes unset from empty.
os.File and when files must be closed
*os.File is the concrete type returned by os.Open, os.Create, and os.OpenFile. It implements io.Reader, io.Writer, io.Closer, and related interfaces, so helpers that accept those interfaces work with an open file handle.
Call Close on every successful open or create. The safe pattern checks the error first, then defers close on the non-nil file:
f, err := os.Open("notes.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()Do not defer f.Close() before you know err is nil. Closing a nil *os.File panics.
Convenience functions such as os.ReadFile, os.WriteFile, and os.ReadDir open paths internally and close them before they return. You still close handles you obtain from Open, Create, and OpenFile.
Common os package mistakes
| Mistake | Result |
|---|---|
Assuming os.Create preserves contents |
Existing file is truncated |
Using os.WriteFile to append |
Existing contents are replaced |
Deferring Close() before checking err |
Possible nil dereference |
| Parsing error strings | Platform-dependent behavior |
| Assuming permission argument overrides umask | Unix effective mode may differ |
Using os to join path components |
Use path/filepath |
The os package operates on path strings you pass in. To build paths portably, use path/filepath helpers such as filepath.Join and filepath.WalkDir instead of manual slash concatenation.
Summary
The golang os package is the standard entry point for files, directories, and basic environment access in Go. You opened and read paths with os.Open and os.ReadFile, created and wrote bytes with os.Create and os.WriteFile, appended through os.OpenFile, and inspected metadata with os.Stat while checking errors.Is(err, os.ErrNotExist) instead of fragile string matches.
Directory work boils down to MkdirAll for trees and ReadDir for listings; Getwd, Getenv, and LookupEnv cover the small slice of process context most programs need from os itself. The recurring lifecycle pattern is open or create, check errors, defer Close on *os.File, and prefer whole-file helpers when you do not need a long-lived handle.
When a topic needs more depth, use the dedicated guides for reading files, Stat, environment variables, and working directory changes rather than expanding this page into package documentation. Keep path construction on path/filepath, and leave signals, subprocesses, and permission theory to articles that focus on those concerns.
References
- os package — Go standard library
- io/fs package — file system interfaces and
FileInfo - path/filepath package — portable path operations

