Alright, let’s talk about the workhorses. You want to read a file, write a file, and make sure the directory for that file exists. You could open a file, get a reader, buffer it, read chunks, check for EOF, and close it deferfully. And sometimes you should! But 80% of the time? You just want the dang contents of the file in a byte slice. That’s where os.ReadFile and friends come in. They’re the Go standard library’s concession to the fact that we’re all busy people with better things to do than write the same file-handling boilerplate for the millionth time.

The Beautiful Simplicity of os.ReadFile

os.ReadFile is your one-stop shop for getting data from a file on disk into memory. It’s a fantastic example of Go’s “better to have a hundred functions than one clever generic function” philosophy. It does one thing, and it does it well.

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    data, err := os.ReadFile("hello.txt")
    if err != nil {
        log.Fatal(err) // Handle this better in real code, obviously.
    }

    // Remember: data is a []byte, not a string.
    fmt.Printf("File contents: %s\n", data)
    
    // If you need it as a string, just convert it.
    strContent := string(data)
    fmt.Println("As a string:", strContent)
}

Why is this great? It handles the entire lifecycle for you: opens the file, reads all the contents (efficiently, I might add), and then closes the file. The closing part is crucial. If you forget to close files yourself, you leak file descriptors, which is a fantastic way to eventually bring your application to a grinding halt. os.ReadFile removes that pitfall entirely. It’s like a friend who not only borrows your car but also returns it with a full tank of gas and gets the wash.

The Direct Power of os.WriteFile

os.WriteFile is os.ReadFile’s less cautious sibling. It takes a filename, a byte slice ([]byte), and a permission mode (os.FileMode), and it just… writes the data. It will create the file if it doesn’t exist, and it will truncate it to zero bytes if it does. This last part is the “less cautious” bit. It will blithely overwrite whatever was in that file before. No questions asked.

message := []byte("Hello, Gophers!\n")
err := os.WriteFile("greeting.txt", message, 0644)
if err != nil {
    log.Fatal(err)
}

Let’s talk about that 0644. That’s the Unix-style permission bits. You’re telling the OS, “I, the owner, can read and write this file (6), and everyone else can only read it (4).” It’s a good default for non-executable files. If you’re on Windows, this mode is mostly ignored for the actual file permissions but still used to determine the file’s read-only attribute. The designers made a choice here: this function requires you to think about permissions. It’s a bit of a pain, but it’s the right kind of pain—it forces you to be explicit about a security-adjacent decision.

The “Just Make It Happen” of os.MkdirAll

Here’s where we get to my personal favorite. os.Mkdir is fine for creating a single directory, but it fails miserably if any parent directory in the path doesn’t exist. os.MkdirAll is the solution to this absurdity. It’s the function that says, “You want this directory path to exist? Yeah, me too. I’ll make it happen.”

Need to create /a/b/c? If /a doesn’t exist, os.Mkdir("/a/b/c") fails. os.MkdirAll("/a/b/c", 0755) just creates /a, then /a/b, then /a/b/c. It’s idempotent, meaning if /a/b/c already exists, it does nothing and returns no error. This is incredibly useful for making sure your application has the directory structure it needs to write its logs, data files, or whatever else.

// This just works. It creates all necessary parents.
err := os.MkdirAll("/tmp/my/app/data", 0755)
if err != nil {
    log.Fatal(err)
}

// Now we can safely write to a file in that new directory.
err = os.WriteFile("/tmp/my/app/data/config.json", configData, 0644)
if err != nil {
    log.Fatal(err)
}

The Glaring Omission and The Pitfall

Notice anything missing from os.WriteFile and os.MkdirAll? That’s right. os.WriteFile doesn’t check if the directory it’s writing to exists. It’s a fantastic way to get a "no such file or directory" error. This is the rough edge. The standard library gives you the brilliant os.MkdirAll but doesn’t have a convenient os.WriteFileFull(path, data, mode) that calls it for you. You have to compose them yourself.

The classic pitfall is trying to use os.WriteFile without ensuring the directory exists first.

// This will fail if the 'logs' directory doesn't exist.
err := os.WriteFile("logs/app.log", logData, 0644)
if err != nil {
    log.Fatal(err) // "open logs/app.log: no such file or directory"
}

// The correct, composed approach:
os.MkdirAll("logs", 0755) // Ensure the directory exists
err := os.WriteFile("logs/app.log", logData, 0644)
if err != nil {
    log.Fatal(err)
}

It’s an extra line, but it’s a line that separates the rookies from the pros. Always think about the full path, not just the file. These functions are tools, not a complete framework. They expect you to know how to combine them to get the job done. And now, you do.