cover image for article

Golang: File Write

Edit

Author: Meet Rajesh Gor

#go

Introduction

In the 24th post of the series, we will be taking a look at how we can perform write operations to a file using golang. We will be using the os package in most operations along with bufio text manipulations. We will be performing write operations like appending, deleting, and replacing a file using golang. We will be heavily leveraging standard library packages like os, bufio, bytes and fmt. We will also be looking into overwriting and string formatting to a file.

Write to a File

The first part of this section is the basic write operation to a file, we assume we are writing to a fresh file and overriding the contents of the existing file. The next section will cover the appending of content to the file and so on. In this example, we will see how we perform basic writing operations to write a string, a slice of string to a file.


go
package main

import (
	"log"
	"os"
)

func HandleError(err){
    if err != nil {
        log.Fatal(err)
    }
}

func main() {
	str := "Hi, I am a gopher!\n"
	f, err := os.OpenFile("abc.txt", os.O_WRONLY, 0660)
    // f, err := os.Create("abc.txt")
	HandleError(err)
	_, err = f.Write([]byte(str))
	HandleError(err)
	defer f.Close()
}

bash
$ cat abc.txt

$ go run main.go

$ cat abc.txt
Hi, I am a gopher!

So, we have used a simple golang script to write to a file that exists/has already been created. If you don't want errors while having to write on a file that does not exist, use the Create method instead which is similar to the Open method but creates a file if it doesn't exist. We use the Write method to overwrite contents to the file, it takes in a parameter as a slice of byte, so we typecast the string str into []byte using the []byte(str) syntax. Thereby we write the contents of the string into the file. We use the defer keyword for closing the file at the end of the script or the end of the main function scope.

Write slice of strings to file

We can even write slice of string to file using a for loop and appending each string with a new line character.


go
package main

import (
	"log"
	"os"
)

func HandleError(err){
    if err != nil {
        log.Fatal(err)
    }
}

func main() {
	f, err := os.Create("abc.txt")
	//f, err := os.Open("abc.txt", os.O_WRONLY, 0660)
	langs := []string{"golang", "python", "rust", "javascript", "ruby"}
	for _, lang := range langs {
		_, err := f.WriteString(lang + "\n")
		HandleError(err)
	}
	defer f.Close()
}

plaintext
$ cat abc.txt

$ go run main.go

$ cat abc.txt
golang
python
rust
javascript
ruby

We have used the WriteString method which will take in a string as a parameter instead of a slice of bytes. So, we don't have to type cast into slice of bytes. So, as we can see we have written the string slice into a file.

Over Write

The minimal code to write to a file is the WriteFile function in the os package, it overrides the content of the file with the provided slice of bytes, the name of the file, and the necessary permission to write. The funciton additionally creates a file if it does not exist, which is one less reason for the error. Though it returns an error object, the error might be created due to not right permissions to write to the file, encoding issues, etc.


go
package main

import (
	"log"
	"os"
)

func main() {
	data := []byte{115, 111, 109, 101, 65}
	err := os.WriteFile("test.txt", data, 0660)
    log.Println(err)

	s := "Hello"
	err = os.WriteFile("test.txt", []byte(s), 0660)
    log.Println(err)
}

plaintext
$ go run main.go
2022/12/17 19:24:13 <nil>
2022/12/17 19:24:13 <nil>

$ cat test.txt
Hello

So, we have used the WriteFile method two times in the script, it first takes in a slice of bytes as it is defined as data which corresponds to 115 -> s, 111 -> o, 65 -> A, ASCII mapped to strings. The slice of bytes can be taken as a string like someA as the literal value of the underlying slice of the byte. So, we take that slice of byte and parse it to the second parameter of the WriteFile function. The first parameter is a string path of the file we want to write the contents to, the third parameter is the file permission. We have set it as 0660 indicating read(4) + write(2) to the group and the user and no permission to the other users. The function will return an error if any, or else it simply overwrites the data in the file.

In this case, we have called the WriteFile method with string s type cast to slice of bytes at the end of the script so we see the file has contents as Hello instead of someA. If we reverse the action, we don't see the Hello string in the file.


go
package main

import (
	"log"
	"os"
)

func main() {
	s := "Hello"
	err := os.WriteFile("test.txt", []byte(s), 0660)
    log.Println(err)

	data := []byte{115, 111, 109, 101, 65}
	err = os.WriteFile("test.txt", data, 0660)
    log.Println(err)
}

plaintext
$ go run main.go
2022/12/17 19:24:13 <nil>
2022/12/17 19:24:13 <nil>

$ cat test.txt
someA

As we can see the Hello has been overwritten by someA.

Write formatted string

We can even use fmt to write formatted strings to a file. Just like we can take inputs with Scanf, we can use Fprint and other similar functions like Fprintf, and Fprintln functions to print/add contents to the file.


go
package main

import (
	"fmt"
	"log"
	"os"
)

func HandleErr(err error) {
	if err != nil {
		log.Fatal(err)
	}
}
func main() {
	f, err := os.Create("temp.txt")
	HandleErr(err)
	name, lang, exp := "John", "go", 2
	_, err = fmt.Fprint(f, "Hi, I am ", name, "\n")
	HandleErr(err)
	_, err = fmt.Fprintf(f, "Language of choice: %s.\n", lang)
	HandleErr(err)
	_, err = fmt.Fprintln(f, "Having", exp, "years of experience.")
	HandleErr(err)
	defer f.Close()
}

plaintext
$ cat temp.txt
cat: temp.txt: No such file or directory

$ go run format.go

$ cat test.txt
Hi, I am John.
Language of choice: go.
Having 2 years of experience.

So, we can see that we have used all three methods having their own use cases, we can use Fprint for simple strings, Fprintf for formatting the block of a string with multiple placeholders, and the Fprintln which works simply like Fprint but it adds a new line itself, we don't need to specify it explicitly.

Append

If we want to append text to a file, we can use the OpenFile function and provide a few parameters to append the contents instead of overwriting.

Here, we have two steps, open the file and then write the contents in the file. So while opening the file, we provide a few options as parameters to make the fine-tuned system call like only open for read, write or append modes. These options are defined as constant int values in the os package.


go
package main

import (
	"log"
	"os"
)

func HandleError(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

func main() {
	s := "Hello"
	err := os.WriteFile("test.txt", []byte(s), 0660)
    HandleError(err)

	s = "World"
	f, err := os.OpenFile("test.txt", os.O_APPEND|os.O_WRONLY, 0660)
	HandleError(err)
	_, err = f.WriteString(s)
	HandleError(err)
	defer f.Close()
}

plaintext
$ go run main.go

$ cat test.txt
HelloWorld

So, from the above example, we are able to append text into a file, we have first added the Hello world string into the file using the WriteFile method to indicate we overwrite the previous contents of the file. We then use the OpenFile method to open a file provided in the first parameter as a string path. The second parameter is the options to be passed for performing operations on the opened file, we always have to use them defer to close the file or other resource-locking operations.

We have specified the os.O_WRONLY and the os.O_APPEND options indicating we want to write to the file while the file is open and specifically append to the file. So this is fine-tuning the opened file operation. We can use the ReadFile or WriteFile operation which is just used for simple read and write operations respectively.

We use the WriteString method, but we can even use the Write method to write a slice of byte instead. This is just used for exploring the different options in the file types of the os package.

Append at a specific line

We can also add content to a specific line or a portion of the file. There are no direct functions in golang to do the same, we will have to do some manual fine-tuning of file operations to append a particular text at a specific line.


go
package main

import (
    "bufio"
    "bytes"
    "log"
    "os"
)

func HandleError(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

func main() {
    /* test.txt
    Hi
    Hello
    World
    Gopher
    */
	f, err = os.OpenFile("test.txt", os.O_RDWR, 0660)
	defer f.Close()
	HandleError(err)
	m := bufio.NewScanner(f)
	bytes_till := 0
	// line to be appended
	line_till := 2
	var lines_after string
	var lines_till string
	i := 0
	for m.Scan() {
		line := m.Text()
		if i < line_till {
			bytes_till += bytes.Count([]byte(line), []byte{})
			if i > 0 {
				lines_till += "\n"
			}
			lines_till += line
		} else {
			lines_after += "\n" + line
		}
		i += 1
	}
	HandleError(m.Err())
	insert_text := lines_till + "\nInserted content"
	insert_text_bytes := bytes.Count([]byte(insert_text), []byte{})
	lines_after_bytes := bytes.Count([]byte(lines_after), []byte{})

	err = os.WriteFile("test.txt", []byte(insert_text), 0660)
	HandleError(err)
	_, err = f.WriteAt([]byte(lines_after), int64(lines_after_bytes)+int64(insert_text_bytes))
	HandleError(err)
    /* test.txt
    Hi
    Hello
    Inserted content
    World
    Gopher
    */
}

plaintext
$ cat test.txt 
Hi
Hello
World
Gophers

$ go run append.go 

$ cat test.txt 
Hi
Hello
Inserted content
World
Gophers

We have inserted Inserted content after the second line because the line_till the variable is set to 2

So, in the above example, we have used a bunch of packages to append a string or any form of text to a particular line. We first read the contents of the file, by using the OpenFile method which will open the file with certain permissions. We need to close the file at the end of the script so we simply use the defer keyword before the f.Close() method call. We then start to scan the file buffer, by creating a Scanner object with the NewScanner method. Then, with the scanner object of the file content, we then can use the Scan() method to scan the file contents line by line. By converting the content at each line from a slice of bytes to a string using the Text, we append to a string line, this will be used for keeping the count of bytes for appending text before the newly inserted text.

The line_till variable is used for the line number from which we want to append to the text after.

We count the bytes for the current line and add it to the bytes_till variable indicating the number of bytes there before appending content. We have a simple if-else check for the first line that is for appending a new line of characters. We append the lines into a single string lines_till. The string insert_text is created by appending all the lines before the line number line_till with the actual content to be inserted. We calculate the number of bytes using the Count method in the bytes package. The separator is kept blank. The lines_after is also been created as a single string of lines after the line number in the file.

We add the insert_text (lines before + inserted text) into the file using the WriteFile which will override the contents of the file. Then we append the lines_after string as a slice of bytes to the insert_text_bytes + lines_after_bytes so we get the byte number position to append the lines_after string.

In short, we basically overwrite the file by creating two strings (slice of bytes) one which has the lines before the line number with the text to be inserted and the second string has all the lines after the line number.

Replace text in a file

Using the bytes.Replace method, we can first read all the bytes and replace the old with the new text, and store them as a slice of bytes. We then write these slices of bytes to the file again, so we first read the contents into slices of bytes, replace the content of the byte and then overwrite the contents with the slice of bytes. It's quite simple.


go
package main

import (
	"bytes"
	"log"
	"os"
)

func HandleError(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

func main() {
	filename := "test.txt"
	file, err := os.ReadFile(filename)
	HandleError(err)
	old_text := "Hello\nWorld"
	new_text := "Bye"
	new_content := bytes.Replace(file, []byte(old_text), []byte(new_text), -1)
	err = os.WriteFile(filename, new_content, 0660)
	HandleError(err)
}

plaintext
$ cat test.txt
Hi
Hello
World
Gophers

$ go run main.go


$ cat test.txt
Hi
Bye
Gophers

As we can see we have replaced Hello\nWorld it with Bye. So, the [Replace] method in the bytes package takes in the parameters like the slice of bytes which should be the actual contents of the file, the old text to be replaced with again as a slice of bytes, and the new text to replace also as the slice of bytes, the final parameter is the number of replacements to be made. Here -1 indicates there are no limits on how many replacements can be done, it can be 1, 2 for replacing the first n occurrence of the old text, depending on how many times you want to replace the content in the file.

Delete Text from a File

We can use the os.Truncate method to delete the contents of the file. The Truncate method takes in the parameters like the file path string and the size of the file to truncate or set to. If we set the second parameter to 0, the file size will be zero and all the contents will be deleted or removed.


go
package main

import (
	"log"
	"os"
)

func main() {
    /* test.txt
    Hi
    Hello
    World
    Gophers
    */
	err := os.Truncate("test.txt", 0)
	if err != nil {
		log.Fatal(err)
	}
    /* test.txt is empty
    */
}

plaintext
$ cat test.txt
Hi
Hello
World
Gophers

$ go run delete.go

$ cat test.txt

As we can see that the contents of the file are emptied if we set the second parameter(size) of the Truncate method as 0.

We can also set the value of the size as the number of bytes to keep, so instead of 0 we can set it to n positive integer to only save the first n bytes in the file.


go
package main

import (
	"log"
	"os"
)

func main() {
    /* test.txt
    Hi
    Hello
    World
    Gophers
    */
	err := os.Truncate("test.txt", 6)
	if err != nil {
		log.Fatal(err)
	}
    /* test.txt 
    Hi
    Hel
    */
}

plaintext
$ cat test.txt
Hi
Hello
World
Gophers

$ go run delete.go

$ cat test.txt
Hi
Hel

So, in the above example, if we set the size parameter to the Truncate method as 6, it will keep the size of the file to 6 bytes. So we only see Hi\nHel, the new line is a single byte. The rest of the content is deleted. This is how we previously deleted all the bytes from the file by setting the size to 0.

That's it from this part. Reference for all the code examples and commands can be found in the 100 days of Golang GitHub repository.

Conclusion

So, from this section of the series, we were able to perform write operations on a file using golang. We used the packages from the standard library and performed operations like write, append, overwrite, delete and replace to a simple text file, but it could have been any file format.

Thank you for reading. If you have any queries, questions, or feedback, you can let me know in the discussion below or on my social handles. Happy Coding :)