Reading and writing files is one of the most common tasks in programming projects. Using this skill we can store data persistently and decrease the memory consumption of our program.
Paths
The path/filepath
package provides filepath.Join()
function to create filesystem paths for files and directories. The same could also be achieved with string concatenation in Go but it won’t be interoperable between different operating systems (Linux uses the forward slash (/
) for separators in the path whereas Windows uses the backward slash (\
)).
Other functions provided by the path/filepath
package:
filepath.Dir()
: Returns the directory from the path (excluding filename).filepath.Base()
: Returns the filename from the path (excluding parent directories).filepath.Ext()
: Returns the extension of the file.filepath.IsAbs()
: Returnstrue
if thefilepath
is an absolute path otherwisefalse
.filepath.Rel()
: Returns relative path between its two argument paths.
package main
import (
"fmt"
"path/filepath"
)
func main() {
exampleFile := filepath.Join("/app", "file_handling.go")
fmt.Println("Example File Path:", exampleFile)
fmt.Println("Filename:", filepath.Base(exampleFile))
fmt.Println("Directory:", filepath.Dir(exampleFile))
fmt.Println("Extension in Filename:", filepath.Ext(exampleFile))
fmt.Println("Is exampleFile path absolute?:", filepath.IsAbs(exampleFile))
value, err := filepath.Rel("/tmp", exampleFile)
if err!=nil {
panic(err)
} else {
fmt.Println("Relative path between /tmp and exampleFile will be:",
value)
}
}
// Output
// Example File Path: /app/file_handling.go
// Filename: file_handling.go
// Directory: /app
// Extension in Filename: .go
// Is exampleFile path absolute?: true
// Relative path between /tmp and exampleFile will be: ../app/file_handling.go
defer
Keyword
When a file is opened by a program its contents are loaded on the system memory. That’s why it is crucial to close the file and release the occupied memory. This will also prevent data corruption on the file.
Any statement in Go prefixed with defer
keyword will be executed at the end of the parent function. These statements are generally used for cleanup tasks.
package main
import "fmt"
func testFunc() {
defer fmt.Println("This will be executed after testFunc() has ended")
fmt.Println("This is the last statement in testFunc()")
}
func main() {
defer fmt.Println("This will be executed after main() has ended")
testFunc()
fmt.Println("Last statement in main()")
}
// Output
// This is the last statement in testFunc()
// This will be executed after testFunc() has ended
// Last statement in main()
// This will be executed after main() has ended
We can define the functions for closing files & perform other resource cleanup, then call them with defer
keyword to ensure they are executed after our program has finished. The statements will be executed in the reverse order of their declaration.
Managing Directories
The os
package in Go provides functions analogous to common Linux/Windows commands used for managing directories.
os.Mkdir()
: Similar to themkdir
Linux command, it creates the directory given the filepath in string and the octal notation of directory permissions (like0755
).os.MkdirAll()
: Creates the complete hierarchy of directories similar to themkdir
command executed with--parents
or-p
flag.os.RemoveAll()
: Removes directory (including subfolders and files).os.Getwd()
: Returns the present working directory similar to thepwd
command.os.Chdir()
: Changes present working directory. Same as thecd
command.os.ReadDir()
:ls
command in Linux lists the contents of the current directory,os.ReadDir()
will do the same for the directory path passed as the argument. Afor
loop has to be defined on the slice returned by the function to access each file/directory with its name.
package main
import (
"fmt"
"os"
)
func removeDir(dirPath string) {
os.RemoveAll(dirPath)
fmt.Println(dirPath, "removed")
}
func getPWD() {
pwd, err := os.Getwd()
if err!=nil {
panic(err)
}
fmt.Println("Present Working Directory is:", pwd)
}
func main() {
fmt.Println("Creating temp directory")
os.Mkdir("temp", 0755)
fmt.Println("Creating temp2/temp3/testDir directory hierarchy")
os.MkdirAll("temp2/temp3/testDir", 0755)
os.Mkdir("temp2/temp4", 0755)
os.Mkdir("temp2/temp5", 0755)
// This will be executed after the main() function is finished
defer removeDir("temp")
defer removeDir("temp2")
val, err := os.ReadDir("temp2")
if err!=nil {
panic(err)
} else {
fmt.Println("Contents of temp2 directory:")
for _, file := range val {
fmt.Print("\t",file.Name())
}
fmt.Print("\n")
}
getPWD()
fmt.Println("Moving one level up")
os.Chdir("../")
getPWD()
fmt.Println("Moving back to /app directory")
os.Chdir("/app")
getPWD()
}
// Output
// Creating temp directory
// Creating temp2/temp3/testDir directory hierarchy
// Contents of temp2 directory:
// temp3 temp4 temp5
// Present Working Directory is: /app
// Moving one level up
// Present Working Directory is: /
// Moving back to /app directory
// Present Working Directory is: /app
// temp2 removed
// temp removed
Reading from a File
The os.ReadFile()
function will load the entire content of a file into the system memory. If we want to save memory or read specific portions of a file we can use os.Open()
function. It returns an instance of os.File
struct that could be used for navigating the file in read-only mode.
The os.Seek()
method will offset all read and write operations to a specific location in an os.File
instance. For example exampleFile.Seek(10, 0)
will increase the offset by 10 bytes. Hence, all the read and write operations performed on the exampleFile
will skip the first 10 bytes. The second argument in os.Seek()
signifies the origin relative to the offset i.e.
exampleFile.Seek(10, 0)
: 10 bytes from the start of the fileexampleFile.Seek(10, 1)
: 10 bytes from the current offset locationexampleFile.Seek(10, 2)
: 10 bytes from the end of the file
exampleFile.Seek(0, 0)
will reset the offset to 0 (start of the file).
The os.Read()
method is used to read the contents of a file in a byte slice. It returns the number of bytes read as a value. In the following example, we will perform read operations on example.txt
.
$ cat example.txt
Hello, World
42 is the meaning of life, the universe, and everything
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("Reading the complete example.txt")
exampleFile, err := os.ReadFile("example.txt")
if err!=nil {
panic(err)
} else {
fmt.Println("Content from example.txt:")
// Converting the byte slice returned by ReadFile to string
fmt.Println(string(exampleFile))
fmt.Println()
}
exampleFile2, err := os.Open("example.txt")
if err!=nil {
panic(err)
}
defer exampleFile2.Close()
// Offsetting operations 13 bytes from the start
exampleFile2.Seek(13, 0)
byteArr := make([]byte, 17)
numBytes, err := exampleFile2.Read(byteArr)
if err!=nil {
panic(err)
}
fmt.Println("Bytes read from exampleFile2:", numBytes)
fmt.Printf("%v\n", string(byteArr[:numBytes]))
}
// Output
// Reading the complete example.txt
// Content from example.txt:
// Hello, World
// 42 is the meaning of life, the universe, and everything
//
// Bytes read from exampleFile2: 17
// 42 is the meaning
Writing to a File
The os.Create()
function will create a file or directory from the string path passed as its argument.
Similar to os.ReadFile()
, the os.WriteFile()
function will write the contents of a byte slice to the given file. It will also create the file if it doesn’t exist. To perform more granular writes we have os.Write()
method.
To open a file for read-write operations we have to use os.OpenFile()
instead of os.Open()
, with os.O_RDWR
as its argument. The changes written to the file by the program will not reflect on the system storage until the os.Sync()
method is called.
package main
import (
"fmt"
"os"
)
func readFile(filepath string) {
fmt.Println("Reading the contents of", filepath, ":")
byteSlice, err := os.ReadFile(filepath)
if err!=nil {
panic(err)
}
fmt.Println(string(byteSlice))
}
func main() {
fmt.Println("Writing to hello.txt")
byteSlice := []byte("Hello from Go program\n")
err := os.WriteFile("hello.txt", byteSlice, 0755)
if err!=nil {
panic(err)
}
fmt.Println("Write successful")
defer os.Remove("hello.txt")
readFile("hello.txt")
helloFile, err := os.OpenFile("hello.txt", os.O_RDWR, 0755)
if err!=nil {
panic(err)
}
defer helloFile.Close()
// Moving offset to the end of file
_, err = helloFile.Seek(0, 2)
if err!=nil {
panic(err)
}
byteSliceWrite := "Hey from WriteFile"
_, err = helloFile.WriteString(byteSliceWrite)
if err!=nil {
panic(err)
}
readFile("hello.txt")
// Syncing changes between memory and storage
helloFile.Sync()
}
// Output
// Writing to hello.txt
// Write successful
// Reading the contents of hello.txt :
// Hello from Go program
//
// Reading the contents of hello.txt :
// Hello from Go program
// Hey from WriteFile
Handling Common Filetypes
JSON, YAML, and XML are one of the most common file types for configuration, test results, and other structured data storage.
JSON
Encoding JSON
Encoding is the process of converting data into a specific format. To encode data into JSON format we have to use the encoding/json
package.
The json.Marshal()
function returns the JSON encoding for the struct instance passed as its argument. The fields in struct need to have their first character capitalized.
The Marshal-ed JSON could be written to file with the help of os.OpenFile()
and os.Write()
functions.
package main
import (
"fmt"
"encoding/json"
"os"
)
type Vehicle struct {
Name string
Color string
NumWheels int
}
func main() {
v := Vehicle{Name: "Nissan GTR", Color: "Grey", NumWheels: 4}
fmt.Println("Vehicle instance:", v)
jsonData, err := json.Marshal(v)
if err!=nil{
panic(err)
}
fmt.Println("Vehicle JSON:\n", string(jsonData))
file, err := os.OpenFile("vehicle.json", os.O_CREATE|os.O_RDWR, 0755)
if err!=nil{
panic(err)
}
defer file.Close()
file.Write(jsonData)
file.Sync()
}
// Output
// Vehicle instance: {Nissan GTR Grey 4}
// Vehicle JSON:
// {"Name":"Nissan GTR","Color":"Grey","NumWheels":4}
$ cat vehicle.json
{"Name":"Nissan GTR","Color":"Grey","NumWheels":4}
Decoding JSON
Decoding is the reverse of encoding, we take data from a specified format and convert it to its original form. The json
Go package provides json.Unmarshal()
function for decoding JSON into a struct.
package main
import (
"fmt"
"encoding/json"
"os"
)
type Vehicle struct {
Name string
Color string
NumWheels int
}
func main() {
var v Vehicle
jsonFile, err := os.ReadFile("vehicle.json")
if err!=nil {
panic(err)
}
defer jsonFile.Close()
err = json.Unmarshal(jsonFile, &v)
if err!=nil {
panic(err)
}
fmt.Println("Struct Instance from JSON:", v)
}
// Output
// Struct Instance from JSON: {Nissan GTR Grey 4}
YAML
Similar to the json
package, gopkg.in/yaml
also provides yaml.Marshal()
and yaml.Unmarshal()
functions for encoding and decoding data into YAML respectively.
Before using the gopkg.in/yaml
package we have to install it with the go get
command.
go get gopkg.in/yaml.v2
package main
import (
"fmt"
"gopkg.in/yaml.v2"
"os"
)
type VehicleUses struct {
F1SafetyCar bool
}
type Vehicle struct {
Name string
Color string
NumWheels int
Uses VehicleUses
}
func main() {
// Writing an instance of Vehicle to vehicle.yaml
v := Vehicle{
Name:"Aston Martin Vantage",
Color: "green",
NumWheels:4,
Uses:VehicleUses{F1SafetyCar: true}
}
fmt.Println("Vehicle Instance:", v)
yamlData, err := yaml.Marshal(&v)
if err!=nil {
panic(err)
}
fmt.Println("Vehicle in YAML format:")
fmt.Println(string(yamlData))
yamlFile, err := os.OpenFile("vehicle.yaml", os.O_CREATE|os.O_RDWR, 0755)
if err!=nil{
panic(err)
}
defer yamlFile.Close()
defer os.Remove("vehicle.yaml")
_, err = yamlFile.Write(yamlData)
if err!=nil{
panic(err)
}
// Creating an instance of Vehicle from vehicle.yaml
var v2 Vehicle
yamlFile2, err := os.ReadFile("vehicle.yaml")
if err!=nil{
panic(err)
}
err = yaml.Unmarshal(yamlFile2, &v2)
if err!=nil{
panic(err)
}
fmt.Println("Instance of Vehicle from vehicle.yaml:")
fmt.Println(v2)
}
// Output
// Vehicle Instance: {Aston Martin Vantage green 4 {true}}
// Vehicle in YAML format:
// name: Aston Martin Vantage
// color: green
// numwheels: 4
// uses:
// f1safetycar: true
//
// Instance of Vehicle from vehicle.yaml:
// {Aston Martin Vantage green 4 {true}}
XML
The XML format is commonly used to store test results and configuration. The encoding/xml
Go package provides xml.Marshal()
and xml.Unmarshal()
functions.
If we want the data in a prettified format (with indents on each level) then we can use the xml.MarshalIndent()
function. MarshalIndent()
is also present in json
and gopkg.in/yaml
packages.
package main
import (
"fmt"
"encoding/xml"
"os"
)
type Student struct{
Name string
IdNum int
Subjects []string
}
func main() {
// Storing an instance of Student in XML format
student1 := Student{
Name: "John Doe",
IdNum: 98,
Subjects:[]string{"English", "Maths", "Science"}
}
fmt.Println("Example of a Student instance:", student1)
xmlFile, err := os.OpenFile("student.xml", os.O_CREATE|os.O_RDWR, 0755)
if err!=nil {
panic(err)
}
defer xmlFile.Close()
defer os.Remove("student.xml")
xmlData, err := xml.MarshalIndent(student1, " ", " ")
if err!=nil {
panic(err)
}
fmt.Println("Student data in XML format:")
fmt.Println(string(xmlData))
_, err = xmlFile.Write(xmlData)
if err!=nil {
panic(err)
}
// Reading an instance of Student from XML File
var student2 Student
xmlFile2, err := os.ReadFile("student.xml")
if err!=nil {
panic(err)
}
err = xml.Unmarshal(xmlFile2, &student2)
fmt.Println("Student from student.xml:")
fmt.Println(student2)
}
// Output
// Example of a Student instance: {John Doe 98 [English Maths Science]}
// Student data in XML format:
// <Student>
// <Name>John Doe</Name>
// <IdNum>98</IdNum>
// <Subjects>English</Subjects>
// <Subjects>Maths</Subjects>
// <Subjects>Science</Subjects>
// </Student>
// Student from student.xml:
// {John Doe 98 [English Maths Science]}
Thank you for taking the time to read this blog post! If you found this content valuable and would like to stay updated with my latest posts consider subscribing to my RSS Feed.
Resources
Go by Example
JSON and Go
encoding/json
go-yaml/yaml
gopkg.in/yaml.v2
encoding/xml