# File Handling

Course: [Go](https://www.devtutorials.ai/learn/go) · Section: Files and JSON
Source: https://www.devtutorials.ai/learn/go/files-and-json/file-handling

> Create, read, write and append files with os and bufio, make and list directories, read file info and permissions, and handle missing-file and permission errors.

## What Is File Handling?

**File** = data saved on a disk with a name, like `marks.csv` or `notes.txt`.

**File handling** = creating, reading, writing and deleting files from your program.

**Simple lesson:** file handling lets a program save data and read it back later.

### A Real-World Example

Think of a class notebook kept in the staff room.

- Without it, the teacher would forget the marks when the bell rings.
- She **writes** the marks in the notebook.
- Next week, she **reads** them back.
- At the end of term, she **adds** new pages.

Now the mapping:

- **The notebook** = a file.
- **The staff room shelf** = a folder (directory).
- **Writing, reading, adding pages** = `WriteFile`, `ReadFile`, appending.

### Why File Handling Is Important

- **Variables vanish** when the program ends. Files stay.
- **Settings** are kept in files, like `config.json`.
- **Data is shared** between programs through files: reports, logs, CSV sheets.
- **Logs** in files help you find out what went wrong later.

In this lab, every program gets the same **pretend disk** in its folder:

- `notes.txt`, `marks.csv`, and a read-only `locked.txt`
- a `data` folder with `students.json` and `config.json`

Each run starts fresh. On your computer, the same code reads and writes your real disk.

## What You Will Learn

In this lesson, you will learn:

1. How to create, read, write and append files.
2. The quick helpers `os.ReadFile()` and `os.WriteFile()`.
3. How to create and read directories.
4. How to read file information and permissions.
5. How to handle missing files and permission errors.

## File Operations

### The Quick Way: os.ReadFile() and os.WriteFile()

```go runnable title="Read a whole file"
package main

import (
	"fmt"
	"os"
)

func main() {
	data, err := os.ReadFile("notes.txt")
	if err != nil {
		fmt.Println("could not read:", err)
		return
	}
	fmt.Print(string(data))
	fmt.Println("bytes:", len(data))
}
```

```text
Bring your geometry box.
Maths test on Friday.
bytes: 47
```

- `os.ReadFile` reads the **whole** file into a `[]byte`.
- `string(data)` turns the bytes into text.
- Always check `err`. See **Error Handling**.

```go runnable title="Write a file"
package main

import (
	"fmt"
	"os"
)

func main() {
	text := "Annual day: 20 December\n"
	err := os.WriteFile("notice.txt", []byte(text), 0644)
	fmt.Println(err)

	data, _ := os.ReadFile("notice.txt")
	fmt.Print(string(data))
}
```

```text
<nil>
Annual day: 20 December
```

- `os.WriteFile` creates the file, or **replaces** everything in it.
- `0644` is the **permission**: you can read and write, others can only read. See **Permissions**.

Try every action on the pretend disk.

Compare these:

- **WriteFile twice** replaces the old text.
- **Append** adds a line at the end.
- **ReadFile on a missing name** gives `no such file or directory`.
- **WriteFile on `locked.txt`** gives `permission denied`.

### Create Files

**os.Create** = make a new empty file (or empty an old one), and open it for writing.

```go runnable title="Create and write step by step"
package main

import (
	"fmt"
	"os"
)

func main() {
	f, err := os.Create("attendance.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer f.Close()

	fmt.Fprintln(f, "Roll 1: present")
	fmt.Fprintln(f, "Roll 2: absent")
	f.WriteString("Roll 3: present\n")

	data, _ := os.ReadFile("attendance.txt")
	fmt.Print(string(data))
}
```

```text
Roll 1: present
Roll 2: absent
Roll 3: present
```

- `os.Create` gives an `*os.File`.
- `fmt.Fprintln(f, ...)` writes a line into the file, like `Println` writes to the screen.
- `defer f.Close()` makes sure the file is closed when `main` ends. See **Functions**.

### Read Files Line by Line

For big files, read one line at a time with **bufio.Scanner**.

- `bufio.Scanner` uses little memory, even for a huge file.
- Use `strings.Split(sc.Text(), ",")` to cut a CSV line into fields. See **Strings**.

### Append Files

**Append** = add to the end of a file, keeping what is already there.

```go runnable title="Add a line to a log"
package main

import (
	"fmt"
	"os"
)

func main() {
	f, err := os.OpenFile("notes.txt", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Fprintln(f, "Science project due Monday.")
	f.Close()

	data, _ := os.ReadFile("notes.txt")
	fmt.Print(string(data))
}
```

```text
Bring your geometry box.
Maths test on Friday.
Science project due Monday.
```

| Flag | Means |
|---|---|
| `os.O_APPEND` | write at the end |
| `os.O_WRONLY` | open for writing only |
| `os.O_CREATE` | create the file if it is missing |
| `os.O_TRUNC` | empty the file first |

- Join flags with `|`. See **Operators**.

**Simple lesson:** use `ReadFile` and `WriteFile` for small files, a `Scanner` for line-by-line reading, and `O_APPEND` to add to the end.

## Directory Operations

**Directory** = a folder that holds files and other folders.

### Create Directories

```go runnable title="Make folders"
package main

import (
	"fmt"
	"os"
)

func main() {
	err := os.Mkdir("reports", 0755)
	fmt.Println(err)
	err = os.MkdirAll("reports/2025/term1", 0755)
	fmt.Println(err)
	err = os.Mkdir("reports", 0755)
	fmt.Println(err)
}
```

```text
<nil>
<nil>
mkdir reports: file exists
```

- `os.Mkdir` makes one folder. It fails if the folder exists.
- `os.MkdirAll` makes every missing folder on the path, and does not fail if they exist.

### Read Directories

```go runnable title="List a folder"
package main

import (
	"fmt"
	"os"
)

func main() {
	entries, err := os.ReadDir(".")
	if err != nil {
		fmt.Println(err)
		return
	}
	for _, e := range entries {
		kind := "file"
		if e.IsDir() {
			kind = "folder"
		}
		fmt.Println(kind, e.Name())
	}
}
```

```text
folder data
file locked.txt
file marks.csv
file notes.txt
```

- `"."` means "the current folder".
- `os.ReadDir` gives the entries sorted by name.

### Deleting

- `os.Remove(name)` deletes one file, or one **empty** folder.
- `os.RemoveAll(path)` deletes a folder and everything inside. Use it with great care.
- `os.Rename(old, new)` renames or moves a file.

**Simple lesson:** `MkdirAll` makes folders, `ReadDir` lists them, and `Remove` deletes.

## File Metadata

**Metadata** = data about a file: its name, size, type, permissions and when it changed.

### File Info

```go runnable title="os.Stat"
package main

import (
	"fmt"
	"os"
)

func main() {
	info, err := os.Stat("marks.csv")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("Name:", info.Name())
	fmt.Println("Size:", info.Size(), "bytes")
	fmt.Println("Folder?", info.IsDir())
	fmt.Println("Mode:", info.Mode())
}
```

```text
Name: marks.csv
Size: 53 bytes
Folder? false
Mode: -rw-r--r--
```

- `info.ModTime()` gives the last time the file changed.

### Permissions

**Permission** = who may read, write or run a file.

- Written as three digits, in **octal**: `0644`, `0755`.
- The digits are for: **owner**, **group**, **everyone else**.
- Each digit adds: read = 4, write = 2, run (execute) = 1.

| Number | Letters | Meaning |
|---|---|---|
| `0644` | `-rw-r--r--` | owner reads and writes, others read |
| `0755` | `drwxr-xr-x` (folder) | owner does everything, others read and enter |
| `0444` | `-r--r--r--` | everyone can only read |

- On Windows, Go keeps only "read-only or not". The numbers matter most on Linux and macOS.

**Simple lesson:** `os.Stat` tells you a file's size, type and permissions.

## File Error Handling

### Missing Files

```go runnable title="Check for a missing file"
package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
)

func main() {
	_, err := os.ReadFile("homework.txt")
	fmt.Println(err)
	if errors.Is(err, fs.ErrNotExist) {
		fmt.Println("No homework file yet. Creating one.")
		os.WriteFile("homework.txt", []byte("none\n"), 0644)
	}
	data, _ := os.ReadFile("homework.txt")
	fmt.Print(string(data))
}
```

```text
open homework.txt: no such file or directory
No homework file yet. Creating one.
none
```

- `errors.Is(err, fs.ErrNotExist)` checks for "file does not exist", even when wrapped.
- `os.IsNotExist(err)` is an older way to ask the same question.

### Permission Errors

```go runnable title="A read-only file"
package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
)

func main() {
	err := os.WriteFile("locked.txt", []byte("changed"), 0644)
	fmt.Println(err)
	fmt.Println(errors.Is(err, fs.ErrPermission))

	data, _ := os.ReadFile("locked.txt")
	fmt.Print(string(data))
}
```

```text
open locked.txt: permission denied
true
Only the office can change this.
```

- Reading still works. Writing is refused.
- The file is not changed.

Always close files you open: `defer f.Close()` right after a successful `Open` or `Create`.

An open file uses system resources, and written data may not be saved until the file is closed.

**Easy way to remember**

- Small file: `os.ReadFile`, `os.WriteFile`
- Line by line: `os.Open` + `bufio.Scanner`
- Add to the end: `os.OpenFile` with `O_APPEND`
- Missing: `errors.Is(err, fs.ErrNotExist)`

**Simple lesson:** check every file error, and use `errors.Is` to tell "missing" from "not allowed".

## TL;DR

1. **File handling** = save and load data that outlives the program.
2. **Read all** = `os.ReadFile(name)`. **Write all** = `os.WriteFile(name, data, 0644)`.
3. **Create** = `os.Create(name)`, then `fmt.Fprintln(f, ...)`, `defer f.Close()`.
4. **Line by line** = `os.Open` + `bufio.NewScanner` + `sc.Scan()` / `sc.Text()`.
5. **Append** = `os.OpenFile(name, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)`.
6. **Folders** = `os.Mkdir`, `os.MkdirAll`, `os.ReadDir`, `os.Remove`, `os.RemoveAll`.
7. **Metadata** = `os.Stat` → `Name()`, `Size()`, `IsDir()`, `Mode()`.
8. **Errors** = `fs.ErrNotExist`, `fs.ErrPermission`, checked with `errors.Is`.

## Common mistakes

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Ignoring the error from `Open` | The file is always there | Check `err` first | A nil-file panic later. |
| Forgetting `f.Close()` | Go closes files for you | Close with `defer f.Close()` | Leaked files, lost writes. |
| `os.WriteFile` to add a line | It appends | It replaces the whole file | Old content is lost. |
| `os.Mkdir("a/b/c", 0755)` | It makes every folder | Use `MkdirAll` | `no such file or directory` |
| Permission `644` without `0` | Same as `0644` | Without `0`, it is decimal 644 | Strange permissions. |
| `err == os.ErrNotExist` | Errors compare with `==` | Use `errors.Is` | False for wrapped errors. |
| `string(data)` on a huge file | It is fine for any size | Read big files line by line | Slow, high memory use. |

## Check Yourself

## Exercises

## What to Remember

- Files keep data after the program ends.
- `os.ReadFile` and `os.WriteFile` handle small files in one call.
- Use `os.Open` + `bufio.Scanner` for line-by-line reading.
- `O_APPEND` adds to the end. `WriteFile` replaces everything.
- `MkdirAll`, `ReadDir` and `Stat` work with folders and file details.
- Close files with `defer f.Close()`, and check errors with `errors.Is`.

Next: **JSON**. You will learn how to turn Go structs into JSON text, and read JSON back into structs.
