Golang os Package: Files, Directories, and Environment Examples

Deepak Prasad
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:

bash
printf 'line one\nline two\n' > notes.txt

printf 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:

bash
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)
	}
}
EOF

Run the scanner example against notes.txt:

bash
go run read_open.go
output
line one
line two

Each 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:

go
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:

bash
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")
}
EOF

Execute both write patterns and confirm the program finishes cleanly:

bash
go run create_write.go
output
created and wrote

out.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:

bash
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)
	}
}
EOF

Append a second line, then print out.txt to verify both lines survived:

bash
go run append.go && cat out.txt
output
first
second

The 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:

bash
printf 'line one\nline two\n' > notes.txt

Save and run a small program that checks a missing path and inspects notes.txt:

bash
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))
}
EOF

Run the Stat snippet:

bash
go run stat_demo.go

Sample output (your modification time will differ):

output
missing: true
size=18 mod=2026-08-23T07:15:42Z

missing: 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.

bash
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())
	}
}
EOF

List what MkdirAll created under data:

bash
go run dirs.go
output
cache

cache 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.

bash
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)
}
EOF

Run the working-directory and LookupEnv demo:

bash
go run env_wd.go

Sample output (your working-directory path will differ):

output
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:

go
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


Frequently Asked Questions

1. What is the Go os package used for?

The os package provides platform-independent access to operating-system services: file and directory paths, environment variables, process arguments, and working directory. Most everyday file work uses functions such as Open, ReadFile, WriteFile, Stat, MkdirAll, and ReadDir.

2. What is the difference between os.Open and os.OpenFile?

os.Open opens a path read-only, equivalent to OpenFile with O_RDONLY. os.OpenFile is the general form where you pass open flags such as O_RDWR, O_CREATE, O_APPEND, and O_TRUNC plus a permission bitmask used when new files are created.

3. What is the difference between os.Create and os.WriteFile?

Both truncate an existing file or create a new one. os.Create returns *os.File for incremental writes, Seek, or mixed read/write. os.WriteFile writes a complete byte slice in one call and closes the file internally.

4. How do I append to a file in Go?

Open with os.OpenFile using os.O_APPEND|os.O_CREATE|os.O_WRONLY and your permission mode, then write through the returned *os.File. os.WriteFile always replaces existing contents.

5. How do I check whether a file exists?

Call os.Stat(path) and use errors.Is(err, os.ErrNotExist). Avoid matching error strings such as no such file or directory because wording differs by platform.

6. What is the difference between os and path/filepath?

os operates on paths you already have and opens or creates filesystem objects. path/filepath builds and cleans path strings portably, for example filepath.Join and filepath.WalkDir.
Tuan Nguyen

Data Scientist

Proficient in Golang, Python, Java, MongoDB, Selenium, Spring Boot, Kubernetes, Scrapy, API development, Docker, Data Scraping, PrimeFaces, Linux, Data Structures, and Data Mining. With expertise spanning these technologies, he develops robust solutions and implements efficient data processing and management strategies across various projects and platforms.

  • Go (programming language)
  • Python (programming language)
  • Java (programming language)
  • MongoDB
  • Kubernetes