Skip to main content

Go File Handling

Introduction​

File handling is a fundamental skill for any programmer. Whether you're reading configuration files, processing data, or saving application state, understanding how to interact with the file system is essential. Go provides a rich set of packages in its standard library to make file operations straightforward and efficient.

In this tutorial, we'll explore how to:

  • Create and open files
  • Read file content using different methods
  • Write data to files
  • Handle file paths
  • Work with directories
  • Handle common file operation errors

By the end, you'll have a solid understanding of how to incorporate file handling in your Go applications.

Basic File Operations​

Importing Required Packages​

Go's file handling functionality is primarily contained in these packages:

go
import (
"os" // Core file operations
"io" // Basic I/O utilities
"io/ioutil" // Simplified I/O utilities (deprecated in Go 1.16+)
"bufio" // Buffered I/O
"path/filepath" // Path manipulation
)

For Go 1.16+, many ioutil functions have been moved to the os and io packages, but we'll mention both approaches.

Opening a File​

To open a file in Go, use the os.Open() function:

go
package main

import (
"fmt"
"os"
)

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

fmt.Println("Successfully opened example.txt")
}

The defer file.Close() statement ensures the file will be closed when the function exits, regardless of how it exits. This is a common pattern in Go for resource management.

Creating a File​

To create a new file or truncate an existing one, use os.Create():

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

fmt.Println("Successfully created newfile.txt")

Opening with Options​

For more control, use os.OpenFile() which allows you to specify flags and permissions:

go
// Open for read and write, create if not exists, append to file
file, err := os.OpenFile(
"logfile.txt",
os.O_RDWR|os.O_CREATE|os.O_APPEND,
0644,
)
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer file.Close()

Common flags include:

  • os.O_RDONLY: Read only
  • os.O_WRONLY: Write only
  • os.O_RDWR: Read and write
  • os.O_CREATE: Create file if it doesn't exist
  • os.O_APPEND: Append to file instead of truncating
  • os.O_TRUNC: Truncate file when opening

The permission value (0644) controls file access rights when creating files.

Reading Files​

Go offers several ways to read files, each suited for different scenarios.

Reading an Entire File at Once​

For small files, you can read the entire content into memory:

go
package main

import (
"fmt"
"os"
)

func main() {
// In Go 1.16+:
content, err := os.ReadFile("example.txt")

// For older Go versions:
// content, err := ioutil.ReadFile("example.txt")

if err != nil {
fmt.Println("Error reading file:", err)
return
}

// Convert bytes to string for display
fmt.Println("File content:")
fmt.Println(string(content))
}

Example Output:

File content:
Hello, this is an example file.
It has multiple lines.
This is the third line.

Reading Line by Line​

For larger files or when processing line-by-line, use a bufio.Scanner:

go
package main

import (
"bufio"
"fmt"
"os"
)

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

scanner := bufio.NewScanner(file)
lineNum := 1

// Scan through the file line by line
for scanner.Scan() {
fmt.Printf("Line %d: %s
", lineNum, scanner.Text())
lineNum++
}

// Check for errors during scanning
if err := scanner.Err(); err != nil {
fmt.Println("Error reading file:", err)
}
}

Example Output:

Line 1: Hello, this is an example file.
Line 2: It has multiple lines.
Line 3: This is the third line.

Reading Fixed Chunks​

To read a file in fixed-size chunks, use a buffer and Read() method:

go
package main

import (
"fmt"
"os"
)

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

// Create a buffer of 8 bytes
buffer := make([]byte, 8)

for {
// Read up to 8 bytes
bytesRead, err := file.Read(buffer)

// If we've reached the end of the file
if err != nil {
if err.Error() == "EOF" {
break // End of file reached
}
fmt.Println("Error reading file:", err)
return
}

// Print the bytes read
fmt.Printf("Bytes read: %d
", bytesRead)
fmt.Printf("Content: %s
", buffer[:bytesRead])
}
}

Example Output:

Bytes read: 8
Content: Hello, t
Bytes read: 8
Content: his is a
Bytes read: 8
Content: n exampl
...

Writing to Files​

Go provides several methods for writing data to files, similar to reading.

Writing a String​

To write a simple string to a file:

go
package main

import (
"fmt"
"os"
)

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

// Write a string to the file
bytesWritten, err := file.WriteString("Hello, Go file handling!
This is a test file.
")
if err != nil {
fmt.Println("Error writing to file:", err)
return
}

fmt.Printf("Wrote %d bytes to file
", bytesWritten)
}

Example Output:

Wrote 45 bytes to file

The resulting output.txt file contains:

Hello, Go file handling!
This is a test file.

Writing Bytes​

To write raw bytes to a file:

go
package main

import (
"fmt"
"os"
)

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

data := []byte{72, 101, 108, 108, 111, 33} // "Hello!" in ASCII
bytesWritten, err := file.Write(data)
if err != nil {
fmt.Println("Error writing to file:", err)
return
}

fmt.Printf("Wrote %d bytes to file
", bytesWritten)
}

Using a Buffered Writer​

For better performance with many small writes, use a buffered writer:

go
package main

import (
"bufio"
"fmt"
"os"
)

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

// Create a buffered writer
writer := bufio.NewWriter(file)

// Write strings to the buffer
writer.WriteString("Line 1: Buffered writing is efficient
")
writer.WriteString("Line 2: For multiple small writes
")
writer.WriteString("Line 3: Like this example
")

// Check how many bytes are waiting in the buffer
fmt.Printf("Buffered: %d bytes
", writer.Buffered())

// Write buffered data to disk
writer.Flush()

fmt.Println("Data successfully written to file")
}

Example Output:

Buffered: 89 bytes
Data successfully written to file

Writing an Entire File at Once​

For simple cases where you have all content ready:

go
package main

import (
"fmt"
"os"
)

func main() {
data := []byte("This entire content will be written to the file at once.")

// In Go 1.16+:
err := os.WriteFile("quickwrite.txt", data, 0644)

// For older Go versions:
// err := ioutil.WriteFile("quickwrite.txt", data, 0644)

if err != nil {
fmt.Println("Error writing file:", err)
return
}

fmt.Println("File written successfully")
}

Working with File Paths​

The path/filepath package provides functions for manipulating file paths in a platform-independent way.

Path Manipulation​

go
package main

import (
"fmt"
"path/filepath"
)

func main() {
// Join path elements
fullPath := filepath.Join("dir", "subdir", "file.txt")
fmt.Println("Joined path:", fullPath)

// Get the directory name
dir := filepath.Dir(fullPath)
fmt.Println("Directory:", dir)

// Get the base filename
base := filepath.Base(fullPath)
fmt.Println("Base name:", base)

// Split path into directory and file
dirPath, fileName := filepath.Split(fullPath)
fmt.Println("Dir path:", dirPath)
fmt.Println("File name:", fileName)

// Get file extension
ext := filepath.Ext(fullPath)
fmt.Println("Extension:", ext)

// Clean up path by removing extra separators, etc.
messy := "dir//subdir/..//./file.txt"
clean := filepath.Clean(messy)
fmt.Println("Cleaned path:", clean)

// Get absolute path
absPath, err := filepath.Abs("relative/path")
if err == nil {
fmt.Println("Absolute path:", absPath)
}
}

Example Output:

Joined path: dir/subdir/file.txt
Directory: dir/subdir
Base name: file.txt
Dir path: dir/subdir/
File name: file.txt
Extension: .txt
Cleaned path: dir/file.txt
Absolute path: /home/user/relative/path

Note: The actual output of filepath.Abs() will depend on your current working directory.

Working with Directories​

Go makes it easy to create, read, and manipulate directories.

Creating Directories​

go
package main

import (
"fmt"
"os"
)

func main() {
// Create a single directory
err := os.Mkdir("newdir", 0755)
if err != nil {
fmt.Println("Error creating directory:", err)
} else {
fmt.Println("Directory created successfully")
}

// Create directories with parent directories if needed
err = os.MkdirAll("parent/child/grandchild", 0755)
if err != nil {
fmt.Println("Error creating nested directories:", err)
} else {
fmt.Println("Nested directories created successfully")
}
}

Reading Directory Contents​

go
package main

import (
"fmt"
"os"
)

func main() {
// In Go 1.16+:
entries, err := os.ReadDir(".")

// For older Go versions:
// entries, err := ioutil.ReadDir(".")

if err != nil {
fmt.Println("Error reading directory:", err)
return
}

fmt.Println("Directory contents:")
for _, entry := range entries {
if entry.IsDir() {
fmt.Printf("📁 %s (directory)
", entry.Name())
} else {
fmt.Printf("📄 %s (file)
", entry.Name())
}
}
}

Example Output:

Directory contents:
📁 bin (directory)
📁 pkg (directory)
📁 src (directory)
📄 go.mod (file)
📄 main.go (file)

Walking a Directory Tree​

To recursively traverse directories:

go
package main

import (
"fmt"
"os"
"path/filepath"
)

func main() {
root := "."

err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
fmt.Printf("Error accessing path %s: %v
", path, err)
return err
}

// Print indentation based on path depth
indent := ""
for i := 0; i < len(filepath.SplitList(path))-1; i++ {
indent += " "
}

if info.IsDir() {
fmt.Printf("%s📁 %s
", indent, info.Name())
} else {
fmt.Printf("%s📄 %s (%d bytes)
", indent, info.Name(), info.Size())
}

return nil
})

if err != nil {
fmt.Printf("Error walking directory tree: %v
", err)
}
}

Example Output:

📁 .
📄 main.go (145 bytes)
📁 images
📄 logo.png (4582 bytes)
📄 banner.jpg (15783 bytes)
📁 docs
📄 readme.md (256 bytes)

File Information and Manipulation​

Getting File Info​

go
package main

import (
"fmt"
"os"
"time"
)

func main() {
fileInfo, err := os.Stat("example.txt")
if err != nil {
if os.IsNotExist(err) {
fmt.Println("File does not exist")
} else {
fmt.Println("Error:", err)
}
return
}

fmt.Println("File Information:")
fmt.Printf("Name: %s
", fileInfo.Name())
fmt.Printf("Size: %d bytes
", fileInfo.Size())
fmt.Printf("Mode: %s
", fileInfo.Mode())
fmt.Printf("Modified: %s
", fileInfo.ModTime().Format(time.RFC1123))
fmt.Printf("Is Directory: %t
", fileInfo.IsDir())
}

Example Output:

File Information:
Name: example.txt
Size: 75 bytes
Mode: -rw-r--r--
Modified: Mon, 02 Jan 2023 15:04:05 GMT
Is Directory: false

Renaming and Moving Files​

go
package main

import (
"fmt"
"os"
)

func main() {
// Create a test file
os.WriteFile("original.txt", []byte("Test content"), 0644)

// Rename/move the file
err := os.Rename("original.txt", "moved.txt")
if err != nil {
fmt.Println("Error renaming file:", err)
return
}

fmt.Println("File renamed successfully")
}

Removing Files and Directories​

go
package main

import (
"fmt"
"os"
)

func main() {
// Remove a file
err := os.Remove("unwanted.txt")
if err != nil {
fmt.Println("Error removing file:", err)
} else {
fmt.Println("File removed successfully")
}

// Remove directory and all contents
err = os.RemoveAll("tempdir")
if err != nil {
fmt.Println("Error removing directory:", err)
} else {
fmt.Println("Directory removed successfully")
}
}

Temporary Files and Directories​

Go provides built-in support for temporary files and directories that automatically handle unique naming.

go
package main

import (
"fmt"
"os"
)

func main() {
// Create a temporary file
tempFile, err := os.CreateTemp("", "example-*.txt")
if err != nil {
fmt.Println("Error creating temp file:", err)
return
}
defer os.Remove(tempFile.Name()) // Clean up
defer tempFile.Close()

fmt.Println("Created temporary file:", tempFile.Name())

// Write some data
tempFile.WriteString("This is temporary data")

// Create a temporary directory
tempDir, err := os.MkdirTemp("", "example-dir-*")
if err != nil {
fmt.Println("Error creating temp directory:", err)
return
}
defer os.RemoveAll(tempDir) // Clean up

fmt.Println("Created temporary directory:", tempDir)
}

Example Output:

Created temporary file: /tmp/example-123456.txt
Created temporary directory: /tmp/example-dir-789012

Error Handling in File Operations​

Go encourages explicit error handling, which is especially important for file operations. Here are common file-related errors and how to handle them:

go
package main

import (
"fmt"
"os"
)

func main() {
// Check if file exists
_, err := os.Stat("nonexistent.txt")
if os.IsNotExist(err) {
fmt.Println("The file doesn't exist")
}

// Check file permissions
file, err := os.Open("readonly.txt")
if os.IsPermission(err) {
fmt.Println("Permission denied")
}

// Try to create file in non-existent directory
_, err = os.Create("/nonexistent/test.txt")
if pathError, ok := err.(*os.PathError); ok {
fmt.Printf("Operation: %s, Path: %s, Error: %v
",
pathError.Op,
pathError.Path,
pathError.Err,
)
}

// Always close files
if file != nil {
file.Close()
}
}

Practical Examples​

Example 1: Simple Log Writer​

Here's a simple logger that appends to a log file:

go
package main

import (
"fmt"
"os"
"time"
)

// Logger writes log messages to a file
type Logger struct {
file *os.File
}

// NewLogger creates a new logger that writes to the specified file
func NewLogger(filename string) (*Logger, error) {
file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}

return &Logger{file: file}, nil
}

// Log writes a message to the log file
func (l *Logger) Log(message string) error {
timestamp := time.Now().Format("2006-01-02 15:04:05")
_, err := l.file.WriteString(fmt.Sprintf("[%s] %s
", timestamp, message))
return err
}

// Close closes the log file
func (l *Logger) Close() error {
return l.file.Close()
}

func main() {
logger, err := NewLogger("application.log")
if err != nil {
fmt.Println("Error creating logger:", err)
return
}
defer logger.Close()

logger.Log("Application started")
logger.Log("Processing data...")
logger.Log("Operation completed")

fmt.Println("Logs written to application.log")
}

This creates a log file with content like:

[2023-01-02 15:04:05] Application started
[2023-01-02 15:04:05] Processing data...
[2023-01-02 15:04:05] Operation completed

Example 2: CSV File Processing​

Here's how to read and write CSV data:

go
package main

import (
"encoding/csv"
"fmt"
"os"
"strconv"
)

type Product struct {
ID int
Name string
Price float64
}

func main() {
// Create sample data
products := []Product{
{1, "Laptop", 999.99},
{2, "Mouse", 24.99},
{3, "Keyboard", 59.99},
}

// Write CSV file
file, err := os.Create("products.csv")
if err != nil {
fmt.Println("Error creating file:", err)
return
}
defer file.Close()

writer := csv.NewWriter(file)
defer writer.Flush()

// Write header
header := []string{"ID", "Name", "Price"}
writer.Write(header)

// Write data
for _, product := range products {
record := []string{
strconv.Itoa(product.ID),
product.Name,
strconv.FormatFloat(product.Price, 'f', 2, 64),
}
writer.Write(record)
}

fmt.Println("CSV file written successfully")

// Read CSV file back
readFile, err := os.Open("products.csv")
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer readFile.Close()

reader := csv.NewReader(readFile)
records, err := reader.ReadAll()
if err != nil {
fmt.Println("Error reading CSV:", err)
return
}

var readProducts []Product

// Skip header row (index 0)
for i := 1; i < len(records); i++ {
id, _ := strconv.Atoi(records[i][0])
price, _ := strconv.ParseFloat(records[i][2], 64)

product := Product{
ID: id,
Name: records[i][1],
Price: price,
}

readProducts = append(readProducts, product)
}

// Display read products
fmt.Println("
Products read from CSV:")
for _, p := range readProducts {
fmt.Printf("ID: %d, Name: %s, Price: $%.2f
", p.ID, p.Name, p.Price)
}
}

Example Output:

CSV file written successfully

Products read from CSV:
ID: 1, Name: Laptop, Price: $999.99
ID: 2, Name: Mouse, Price: $24.99
ID: 3, Name: Keyboard, Price: $59.99

Example 3: Config File Reader​

Here's a simple configuration file reader:

go
package main

import (
"bufio"
"fmt"
"os"
"strings"
)

type Config map[string]string

func ReadConfig(filename string) (Config, error) {
config := make(Config)

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

scanner := bufio.NewScanner(file)
lineNum := 0

for scanner.Scan() {
lineNum++
line := scanner.Text()

// Skip empty lines and comments
if line == "" || strings.HasPrefix(line, "#") {
continue
}

// Split by '=' and trim spaces
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
fmt.Printf("Warning: Invalid config at line %d: %s
", lineNum, line)
continue
}

key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])

config[key] = value
}

if err := scanner.Err(); err != nil {
return nil, err
}

return config, nil
}

func main() {
// First, create a config file
configContent := `# Application Configuration
app_name = Go File Example
version = 1.0.0
debug = true

# Database Settings
db_host = localhost
db_port = 5432
db_name = myapp
`

err := os.WriteFile("config.ini", []byte(configContent), 0644)
if err != nil {
fmt.Println("Error creating config file:", err)
return
}

// Read the config
config, err := ReadConfig("config.ini")
if err != nil {
fmt.Println("Error reading config:", err)
return
}

// Use the config
fmt.Println("Configuration:")
fmt.Printf("App Name: %s
", config["app_name"])
fmt.Printf("Version: %s
", config["version"])
fmt.Printf("Debug Mode: %s
", config["debug"])
fmt.Printf("Database: %s:%s/%s
",
config["db_host"],
config["db_port"],
config["db_name"],
)
}

Example Output:

Configuration:
App Name: Go File Example
Version: 1.0.0
Debug Mode: true
Database: localhost:5432/myapp

File I/O Flow​

Here's a diagram showing the common file I/O operations in Go:



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)