Question

How to Read and Write Files in Go

gofile

Question

I am learning Go and want to understand how to read from and write to regular files.

I can open a file like this:

inFile, err := os.Open(INFILE)
if err != nil {
    // handle error
}
defer inFile.Close()

However, I do not understand how to actually read the file contents. The Read method takes a []byte parameter, and that part is confusing to me:

func (file *File) Read(b []byte) (n int, err error)

How do you use Read properly, and what is the standard way to read from and write to a file in Go?

Short Answer

By the end of this page, you will understand how file reading and writing work in Go, why Read uses a byte slice, and how to use common standard library functions to load whole files, read in chunks, and write data safely.

Concept

In Go, files are treated as streams of bytes. That means when you read from a file, Go gives you raw bytes, and when you write to a file, you send bytes back.

The key idea is this:

  • A file on disk stores data as bytes
  • Go's Read method fills a byte slice you provide
  • The method returns how many bytes were actually written into that slice
  • You then convert those bytes into a string or process them directly

This design matters because it is flexible:

  • Text files can be read as bytes and converted to strings
  • Binary files can be processed without conversion
  • Large files can be read in small chunks instead of loading everything into memory

In beginner code, there are usually two common approaches:

  1. Read the whole file at once
    • Best for small text files
    • Simple and readable
  2. Read the file in chunks
    • Better for large files
    • Uses Read([]byte) directly

For writing, Go follows the same byte-based model:

  • You can write a string by converting it to bytes
  • Or use helper functions that write strings directly

Modern Go code often uses the os and io packages together for this.

Mental Model

Think of a file like water coming through a pipe.

  • The file itself is the pipe
  • Your []byte buffer is a bucket
  • Read(buffer) fills the bucket with some amount of water
  • The returned n tells you how full the bucket is this time

If the file is bigger than the bucket, you have to keep filling the bucket until there is no more data.

For writing, imagine the reverse:

  • You have water to send into the pipe
  • Write pushes your bytes into the file

So the important thing is not that Read gives you data directly. Instead, you give Read a place to put the data.

Take Quiz

Syntax and Examples

Reading an entire file

For small files, the easiest approach is:

package main

import (
    "fmt"
    "os"
)

func main() {
    data, err := os.ReadFile("example.txt")
    if err != nil {
        fmt.Println("Error reading file:", err)
        return
    }

    fmt.Println(string(data))
}

Explanation

  • os.ReadFile reads the whole file into memory
  • data is a []byte
  • string(data) converts the bytes into readable text

Writing an entire file

package main

import (
    "fmt"
    "os"
)

func main() {
    content := []byte("Hello from Go!\n")

    err := os.WriteFile("output.txt", content, 0644)
    if err != nil {
        fmt.Println("Error writing file:", err)
        return
    }

    fmt.Println("File written successfully")
}

Explanation

  • os.WriteFile writes all bytes to the file
  • 0644 is the file permission mode on Unix-like systems
  • If the file does not exist, it is created
  • If it exists, it is overwritten

Reading with Read([]byte) directly

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    file, err := os.Open("example.txt")
    if err != nil {
        fmt.Println("Error opening file:", err)
        return
    }
    defer file.Close()

    buffer := make([]byte, 8)

    for {
        n, err := file.Read(buffer)
        if err != nil {
            if err == io.EOF {
                break
            }
            fmt.Println("Error reading file:", err)
            return
        }

        fmt.Print(string(buffer[:n]))
    }
}

Explanation

  • make([]byte, 8) creates a buffer with room for 8 bytes
  • file.Read(buffer) fills that buffer
  • n tells you how many bytes were actually read
  • buffer[:n] is important because the buffer may not be completely filled every time
  • io.EOF means the file is finished

Writing using a file handle

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Create("output.txt")
    if err != nil {
        fmt.Println("Error creating file:", err)
        return
    }
    defer file.Close()

    text := "First line\nSecond line\n"

    _, err = file.Write([]byte(text))
    if err != nil {
        fmt.Println("Error writing file:", err)
        return
    }

    fmt.Println("Done")
}

This is useful when you want more control over the file handle.

Step by Step Execution

Consider this example:

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    file, err := os.Open("example.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()

    buffer := make([]byte, 4)

    for {
        n, err := file.Read(buffer)
        if err == io.EOF {
            break
        }
        if err != nil {
            fmt.Println(err)
            return
        }

        fmt.Println("bytes read:", n)
        fmt.Println("chunk:", string(buffer[:n]))
    }
}

Assume example.txt contains:

HelloGo

What happens

  1. os.Open("example.txt") opens the file for reading.
  2. defer file.Close() schedules the file to be closed when main ends.
  3. buffer := make([]byte, 4) creates space for 4 bytes.
  4. First Read(buffer) call reads Hell
    • n = 4
    • buffer[:n] is Hell
  5. Second Read(buffer) call reads oGo
    • n = 3
    • only the first 3 bytes are new data
    • buffer[:n] is oGo
  6. Next Read reaches the end of the file and returns io.EOF.
  7. The loop stops.

Why buffer[:n] matters

If you print the whole buffer every time, you may include leftover bytes from the previous read. Always use the number returned by Read.

Real World Use Cases

File reading and writing appear in many common programs:

Reading use cases

  • Loading a configuration file such as JSON, YAML, or plain text
  • Reading templates or static content from disk
  • Processing CSV or log files line by line
  • Importing data for scripts and command-line tools

Writing use cases

  • Saving reports to a text file
  • Writing log output
  • Exporting CSV or JSON data
  • Creating cache files or generated code

Example scenarios

  • A CLI tool reads .env or config files at startup
  • A data-processing script reads a large input file in chunks
  • A web server writes uploaded files to disk
  • A batch job generates output files for other systems
Take Quiz

Real Codebase Usage

In real Go projects, developers usually choose the file API based on file size and purpose.

Common patterns

1. Read the whole file for small inputs

data, err := os.ReadFile("config.json")
if err != nil {
    return err
}

Used for:

  • config files
  • templates
  • test fixtures

2. Use guard clauses for errors

file, err := os.Open(path)
if err != nil {
    return err
}
defer file.Close()

This keeps code clean and avoids deep nesting.

3. Stream large files

buffer := make([]byte, 1024)
for {
    n, err := file.Read(buffer)
    if err == io.EOF {
        break
    }
    if err != nil {
        return err
    }

    process(buffer[:n])
}

Used for:

  • logs
  • large exports
  • uploads
  • binary processing

4. Validate before writing

Developers often check:

  • whether the directory exists
  • whether overwriting is safe
  • whether the data format is valid

5. Use helpers when possible

Examples:

  • os.ReadFile for simplicity
  • os.WriteFile for full-file writes
  • bufio.Scanner for reading line by line
  • bufio.Writer for buffered output

These patterns make code easier to maintain and less error-prone.

Common Mistakes

1. Ignoring the number of bytes read

Broken code:

buffer := make([]byte, 100)
file.Read(buffer)
fmt.Println(string(buffer))

Problem:

  • The file may contain fewer than 100 bytes
  • The buffer may contain extra zero values or old data

Correct approach:

n, err := file.Read(buffer)
if err != nil && err != io.EOF {
    fmt.Println(err)
    return
}
fmt.Println(string(buffer[:n]))

2. Forgetting to close the file

Broken code:

file, err := os.Open("example.txt")
if err != nil {
    return
}
// missing file.Close()

Fix:

file, err := os.Open("example.txt")
if err != nil {
    return
}
defer file.Close()

3. Ignoring errors

Broken code:

data, _ := os.ReadFile("missing.txt")
fmt.Println(string(data))

Problem:

  • If the file does not exist, you lose useful error information

Fix:

data, err := os.ReadFile("missing.txt")
if err != nil {
    fmt.Println(err)
    return
}

4. Using old Go signatures

Older Go code may show outdated function signatures such as Error instead of error or older os.Open arguments. Modern Go uses simpler forms like:

file, err := os.Open("example.txt")

5. Loading huge files into memory unnecessarily

os.ReadFile is easy, but not always appropriate.

Avoid this for very large files:

data, err := os.ReadFile("huge.log")

Prefer chunked reading for large inputs.

6. Writing strings without converting when needed

Broken code:

file.Write("hello")

Fix:

file.Write([]byte("hello"))

Or use WriteString when available:

file.WriteString("hello")

Comparisons

ApproachBest forReads whole file?Memory usageExample
os.ReadFileSmall files, simple programsYesHigherConfig files
File.Read with bufferLarge files, streamingNoLowerLogs, big data
bufio.ScannerReading line by lineNoLowText processing
os.WriteFileSimple full writesWrites all at onceDepends on data sizeReports, configs
File.WriteIncremental writesNoLowerGenerated output

os.ReadFile vs File.Read

  • Use os.ReadFile when simplicity matters and the file is small.
  • Use File.Read when you need control, streaming, or better memory efficiency.

os.WriteFile vs File.Write

  • Use os.WriteFile when you already have all content ready.
  • Use File.Write when writing piece by piece.

Bytes vs strings

TypeUse case
[]byteRaw file data, binary data, low-level I/O
stringHuman-readable text

You will often convert between them:

[]byte("hello")
string(data)
Take Quiz

Cheat Sheet

Quick reference

Open a file for reading

file, err := os.Open("input.txt")
if err != nil {
    return
}
defer file.Close()

Read entire file

data, err := os.ReadFile("input.txt")

Read into a buffer

buffer := make([]byte, 1024)
n, err := file.Read(buffer)

Use only bytes actually read

buffer[:n]

Detect end of file

if err == io.EOF {
    break
}

Create or overwrite a file

file, err := os.Create("output.txt")

Write bytes

_, err = file.Write([]byte("hello"))

Write whole file at once

err := os.WriteFile("output.txt", []byte("hello"), 0644)

Rules to remember

  • Files are read and written as bytes
  • Read fills a buffer you provide
  • Always check n from Read
  • Always handle errors
  • Usually defer file.Close() right after opening
  • Use os.ReadFile and os.WriteFile for simple cases
  • Use buffered or chunked reading for large files

FAQ

Why does Go file reading use []byte?

Because files are byte streams. This works for both text and binary data.

How do I read a file as a string in Go?

Use os.ReadFile, then convert the result:

data, err := os.ReadFile("file.txt")
text := string(data)

What is the difference between os.ReadFile and file.Read?

os.ReadFile reads the entire file at once. file.Read reads into a buffer you provide, which is better for streaming or large files.

How do I write text to a file in Go?

You can use:

err := os.WriteFile("out.txt", []byte("hello"), 0644)

or create a file and call Write.

Do I always need to close files in Go?

Yes, when you open or create a file handle, you should usually close it with defer file.Close().

What does io.EOF mean?

It means the reader has reached the end of the file. It is not usually treated as a fatal error in a read loop.

Should I use os.ReadFile for large files?

Usually no. For large files, prefer reading in chunks so you do not load everything into memory at once.

Take Quiz

Mini Project

Description

Build a small Go program that reads a text file, counts its lines, and writes a short report to another file. This project demonstrates both reading and writing files in a practical way.

Goal

Create a program that reads an input text file and writes a summary report containing the file contents and line count.

Requirements

  1. Read the contents of a text file named input.txt.
  2. Count how many lines are in the file.
  3. Write a new file named report.txt.
  4. Include the original content and the line count in the report.
  5. Handle file errors properly.
Take Quiz

Keep learning

Related questions

View more