The os package opens, creates, and manipulates files as *os.File values, which themselves implement the tiny io.Reader (one method: Read) and io.Writer (one method: Write) interfaces. Because so many types across the standard library implement these same two interfaces — network connections, in-memory buffers, compressors — code written against io.Reader/io.Writer works unchanged regardless of where the bytes actually come from or go.
Reading a whole file
For small-to-medium files, os.ReadFile reads the entire contents in one call — the simplest option when you don't need to stream.
data, err := os.ReadFile("config.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))Opening a file and using defer to close it
For more control (streaming large files, writing, appending), os.Open/os.Create return an *os.File that must be explicitly closed — idiomatically with defer right after the open succeeds, guaranteeing cleanup regardless of how the function returns.
f, err := os.Open("data.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close() // guaranteed to run when this function returns
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fmt.Println(scanner.Text()) // one line at a time, memory-efficient
}Writing files
os.WriteFile writes a full byte slice in one call, creating the file if needed; os.Create plus Write gives more control for streaming output.
err := os.WriteFile("output.txt", []byte("hello\n"), 0644)
if err != nil {
log.Fatal(err)
}