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
Readmethod 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:
- Read the whole file at once
- Best for small text files
- Simple and readable
- 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
[]bytebuffer is a bucket Read(buffer)fills the bucket with some amount of water- The returned
ntells 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
Writepushes 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.
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.ReadFilereads the whole file into memorydatais a[]bytestring(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.WriteFilewrites all bytes to the file0644is 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 bytesfile.Read(buffer)fills that bufferntells you how many bytes were actually readbuffer[:n]is important because the buffer may not be completely filled every timeio.EOFmeans 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
os.Open("example.txt")opens the file for reading.defer file.Close()schedules the file to be closed whenmainends.buffer := make([]byte, 4)creates space for 4 bytes.- First
Read(buffer)call readsHelln = 4buffer[:n]isHell
- Second
Read(buffer)call readsoGon = 3- only the first 3 bytes are new data
buffer[:n]isoGo
- Next
Readreaches the end of the file and returnsio.EOF. - 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
.envor 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
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.ReadFilefor simplicityos.WriteFilefor full-file writesbufio.Scannerfor reading line by linebufio.Writerfor 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
| Approach | Best for | Reads whole file? | Memory usage | Example |
|---|---|---|---|---|
os.ReadFile | Small files, simple programs | Yes | Higher | Config files |
File.Read with buffer | Large files, streaming | No | Lower | Logs, big data |
bufio.Scanner | Reading line by line | No | Low | Text processing |
os.WriteFile | Simple full writes | Writes all at once | Depends on data size | Reports, configs |
File.Write | Incremental writes | No | Lower | Generated output |
os.ReadFile vs File.Read
- Use
os.ReadFilewhen simplicity matters and the file is small. - Use
File.Readwhen you need control, streaming, or better memory efficiency.
os.WriteFile vs File.Write
- Use
os.WriteFilewhen you already have all content ready. - Use
File.Writewhen writing piece by piece.
Bytes vs strings
| Type | Use case |
|---|---|
[]byte | Raw file data, binary data, low-level I/O |
string | Human-readable text |
You will often convert between them:
[]byte("hello")
string(data)
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
Readfills a buffer you provide- Always check
nfromRead - Always handle errors
- Usually
defer file.Close()right after opening - Use
os.ReadFileandos.WriteFilefor 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.
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
- Read the contents of a text file named
input.txt. - Count how many lines are in the file.
- Write a new file named
report.txt. - Include the original content and the line count in the report.
- Handle file errors properly.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.