Right, let’s talk about files. Not the digital abstraction, but the raw, honest bytes sitting on your disk. In Go, the os.File type is your gateway to them. It’s a workhorse, not a show pony. It gives you a direct, unfiltered connection to the operating system’s file handles, which means it’s powerful but also makes you responsible for the details. Forget to clean up after yourself here, and you’ll have a memory leak that would make a C programmer feel right at home.

Think of an os.File not as the data itself, but as a handle or a cursor. You use this handle to position yourself within a stream of bytes and to read from or write to that position. The operating system is the bouncer; it gives you the handle (*os.File) after you ask nicely (os.Open), and you absolutely must give it back (file.Close()) when you’re done.

Opening a File: It’s All About the Flags

You get a file handle with os.OpenFile. This is the mothership function; os.Open and os.Create are just convenient wrappers around it with predefined flags. The flags are how you tell the OS your intentions—are you reading? writing? appending? Do you want the file created if it doesn’t exist? This is where you avoid stepping on your own feet.

package main

import (
    "log"
    "os"
)

func main() {
    // Just read. Fails if file doesn't exist.
    readOnlyFile, err := os.Open("data.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer readOnlyFile.Close()

    // Create a file for writing. Truncates if it already exists (DANGER!).
    newFile, err := os.Create("new_data.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer newFile.Close()

    // The full control: open for reading and writing, append to the end, create if needed.
    // This is the pattern for a log file.
    logFile, err := os.OpenFile("app.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
    if err != nil {
        log.Fatal(err)
    }
    defer logFile.Close()
}

The key takeaway: os.Create is destructive. It will blithely wipe out an existing file. I’ve done it. You’ll do it. It’s a rite of passage. For anything that requires more nuance, you must use os.OpenFile and bitwise OR (|) your flags together. The third argument is the permission mode, which only matters if os.O_CREATE is used. 0666 (read and write for everyone) is a common choice, but tighten it up (0600) if you’re dealing with secrets.

Reading: The Simple, the Manual, and the Weird

You can read in a few ways. For small files, the one-liner is a godsend. For everything else, you need a strategy.

// 1. The "I'm lazy and this file is tiny" method (uses io.ReadAll).
data, err := os.ReadFile("small_file.txt")
if err != nil {
    log.Fatal(err)
}
// `data` is a []byte. Do with it as you please.

// 2. The "I'm a responsible adult" method: read into a buffer.
f, _ := os.Open("large_file.txt")
defer f.Close()

buf := make([]byte, 32*1024) // 32KB buffer. A good starting point.
for {
    n, err := f.Read(buf)
    if err != nil {
        break // We'll get to error handling in a sec.
    }
    process(buf[:n]) // Process the slice that was actually filled.
}

The Read method is straightforward but manual. It fills your buffer and tells you how many bytes it read (n). You must only use buf[0:n], because the rest of the buffer is just old, stale data from the previous read. The real fun begins with error handling. Read returns io.EOF when it hits the end of the file. This is expected, not a failure. It returns other errors for actual problems (file deleted, permission lost, etc.).

Writing: Don’t Forget to Sync

Writing uses the same buffer-oriented logic. The Write method takes a slice of bytes and returns the number of bytes written. Simple, right? Here’s the catch: the OS is lying to you. Well, not lying, but caching.

f, _ := os.Create("output.txt")
defer f.Close()

data := []byte("Hello, world!")
n, err := f.Write(data)
if err != nil {
    log.Fatal(err)
}
if n != len(data) {
    log.Fatal("couldn't write all data")
}

// This is the important part for critical writes.
// Flushes the OS's internal buffers to disk.
err = f.Sync()
if err != nil {
    log.Fatal(err)
}

When your Write call returns, the data has usually just been copied into a kernel buffer, not written to the physical disk. This is great for performance but terrible if your application crashes five milliseconds later and you thought your data was safe. Calling Sync() forces the OS to flush its buffers to the disk platter (or SSD cell). It’s slow, so use it judiciously—for database transaction logs, yes; for temporary cache files, probably not.

Closing: The Defer Your Way to Salvation

This is non-negotiable. You must close files. An open file handle is a finite OS resource. Leak enough of them, and your program will crash. The easiest and safest way to do this is with defer. The moment you have a successful open, defer the close.

f, err := os.OpenFile("myfile.txt", os.O_RDWR, 0666)
if err != nil {
    // Handle the error. Maybe the file doesn't exist.
    return
}
// Schedule the close to happen when this function returns.
// Now you can forget about it and focus on your logic.
defer f.Close()

// ... all your reading and writing code ...

defer is your best friend here. It ensures the file gets closed even if your function panics or has multiple early returns. It’s cleaner and safer than trying to remember to call Close() at the end of a function with three different error paths.

Seeking: Moving That Cursor

Remember I called os.File a cursor? Seek is how you move it. You can jump to the beginning, the end, or anywhere in between. This is essential for working with structured file formats.

f, _ := os.Open("database.file")
defer f.Close()

// Jump to the 1024th byte from the start of the file
offset, err := f.Seek(1024, io.SeekStart)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Now at offset %d\n", offset)

// Now read the record that starts here
record := make([]byte, 128)
_, err = f.Read(record)

The second argument is your reference point: io.SeekStart (0), io.SeekCurrent (1), or io.SeekEnd (2). It’s a clear and explicit API, which is something I wish more file-related APIs would emulate.