File Handling

Reading Files

1. Reading an Entire File into Memory

The simplest file operation is reading an entire file into memory using the os.ReadFile function. Below is an example.

Directory Structure

├── Workspace
│   └── filedemo
│       ├── main.go
│       ├── go.mod
│       └── sample.txt

Content of sample.txt:

Welcome to Go file handling!

Code in main.go:

package main

import (
	"fmt"
	"os"
)

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

Run Instructions:

cd ~/Workspace/filedemo/
go install
filedemo

Output:

File content: Welcome to Go file handling!

If you run the program from a different directory, you’ll encounter:

Error reading file: open sample.txt: no such file or directory
2. Using an Absolute File Path

Using an absolute path ensures the program works regardless of the current directory.

Updated Code in main.go:

package main

import (
	"fmt"
	"os"
)

func main() {
	content, err := os.ReadFile("/Users/user/Workspace/filedemo/sample.txt")
	if err != nil {
		fmt.Println("Error reading file:", err)
		return
	}
	fmt.Println("File content:", string(content))
}

Output:

File content: Welcome to Go file handling!
3. Passing the File Path as a Command-Line Argument

Using the flag package, we can pass the file path dynamically.

Code in main.go:

package main

import (
	"flag"
	"fmt"
	"os"
)

func main() {
	filePath := flag.String("file", "sample.txt", "Path of the file to read")
	flag.Parse()

	content, err := os.ReadFile(*filePath)
	if err != nil {
		fmt.Println("Error reading file:", err)
		return
	}
	fmt.Println("File content:", string(content))
}

Run Instructions:

filedemo --file=/path/to/sample.txt

Output:

File content: Welcome to Go file handling!
4. Bundling the File within the Binary

Using the embed package, we can include the file contents directly in the binary.

Code in main.go:

package main

import (
	_ "embed"
	"fmt"
)

//go:embed sample.txt
var fileData []byte

func main() {
	fmt.Println("File content:", string(fileData))
}

Compile and run the binary:

cd ~/Workspace/filedemo/
go install
filedemo

Output:

File content: Welcome to Go file handling!
5. Reading a File in Small Chunks

For large files, read them in chunks using the bufio package.

Code in main.go:

package main

import (
	"bufio"
	"flag"
	"fmt"
	"io"
	"os"
)

func main() {
	filePath := flag.String("file", "sample.txt", "Path of the file to read")
	flag.Parse()

	file, err := os.Open(*filePath)
	if err != nil {
		fmt.Println("Error opening file:", err)
		return
	}
	defer file.Close()

	reader := bufio.NewReader(file)
	buffer := make([]byte, 5)

	for {
		bytesRead, err := reader.Read(buffer)
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("Error reading file:", err)
			return
		}
		fmt.Print(string(buffer[:bytesRead]))
	}
	fmt.Println("\nFile read completed.")
}
6. Reading a File Line by Line

To process large files line by line, use a bufio.Scanner.

Code in main.go:

package main

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

func main() {
	filePath := flag.String("file", "sample.txt", "Path of the file to read")
	flag.Parse()

	file, err := os.Open(*filePath)
	if err != nil {
		fmt.Println("Error opening file:", err)
		return
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		fmt.Println(scanner.Text())
	}

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

Run Instructions:

filedemo --file=/path/to/sample.txt

Output:

Welcome to Go file handling!

Writing Files using Go

One of the simplest and most common operations is writing a string to a file. The process includes the following steps:

  1. Create a file.
  2. Write the string to the file.
package main

import (
	"fmt"
	"os"
)

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

	length, err := file.WriteString("Greetings, Universe!")
	if err != nil {
		fmt.Println("Error writing to file:", err)
		return
	}
	fmt.Println(length, "characters successfully written.")
}

This program creates a file named example.txt. If it already exists, it will be overwritten. The WriteString method writes the string to the file and returns the number of characters written along with any errors. On running the code, you’ll see:

21 characters successfully written.
Writing Bytes to a File

Writing raw bytes is similar to writing strings. The Write method is used for this purpose.

package main

import (
	"fmt"
	"os"
)

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

	data := []byte{72, 101, 108, 108, 111, 32, 98, 121, 116, 101, 115}
	bytesWritten, err := file.Write(data)
	if err != nil {
		fmt.Println("Error writing bytes:", err)
		return
	}
	fmt.Println(bytesWritten, "bytes successfully written.")
}

This code creates a file output_bytes, writes a slice of bytes corresponding to the string Hello bytes, and outputs the number of bytes written. Expected output:

11 bytes successfully written.
Writing Lines to a File

Often, we need to write multiple lines to a file. The Fprintln function makes this straightforward:

package main

import (
	"fmt"
	"os"
)

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

	lines := []string{
		"Go is fun to learn.",
		"It is concise and efficient.",
		"File handling is straightforward.",
	}

	for _, line := range lines {
		if _, err := fmt.Fprintln(file, line); err != nil {
			fmt.Println("Error writing line:", err)
			return
		}
	}
	fmt.Println("Lines written successfully.")
}

This will create a file named multi_lines.txt containing:

Go is fun to learn.
It is concise and efficient.
File handling is straightforward.
Appending to a File

To add content to an existing file, open it in append mode:

package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.OpenFile("multi_lines.txt", os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		fmt.Println("Error opening file:", err)
		return
	}
	defer file.Close()

	newLine := "Appending is simple!"
	if _, err := fmt.Fprintln(file, newLine); err != nil {
		fmt.Println("Error appending to file:", err)
		return
	}
	fmt.Println("Line appended successfully.")
}

This program appends a new line to multi_lines.txt, resulting in:

Go is fun to learn.
It is concise and efficient.
File handling is straightforward.
Appending is simple!
Concurrent File Writing

When multiple goroutines write to a file, coordination is necessary to avoid race conditions. This can be achieved using channels.

Here’s an example that generates 50 random numbers concurrently and writes them to a file:

package main

import (
	"fmt"
	"math/rand"
	"os"
	"sync"
)

func generateNumbers(data chan int, wg *sync.WaitGroup) {
	num := rand.Intn(1000)
	data <- num
	wg.Done()
}

func writeToFile(data chan int, done chan bool) {
	file, err := os.Create("random_numbers.txt")
	if err != nil {
		fmt.Println("Error creating file:", err)
		done <- false
		return
	}
	defer file.Close()

	for num := range data {
		if _, err := fmt.Fprintln(file, num); err != nil {
			fmt.Println("Error writing to file:", err)
			done <- false
			return
		}
	}
	done <- true
}

func main() {
	dataChannel := make(chan int)
	doneChannel := make(chan bool)
	var wg sync.WaitGroup

	for i := 0; i < 50; i++ {
		wg.Add(1)
		go generateNumbers(dataChannel, &wg)
	}

	go writeToFile(dataChannel, doneChannel)
	go func() {
		wg.Wait()
		close(dataChannel)
	}()

	if <-doneChannel {
		fmt.Println("Random numbers written successfully.")
	} else {
		fmt.Println("Failed to write random numbers.")
	}
}

This program creates a file random_numbers.txt containing 50 randomly generated numbers.