The Most Common Mistakes with Read File in Golang
Using a Filename as an Input
Another common mistake is to pass a filename to a function.
Let’s say we have to implement a function to count the number of empty lines in a file. The most natural implementation would be something like this:
filename
is given as an input, so we open it and then we implement our logic, right?
func count(filename string) (int, error) {
file, err := os.Open(filename)
if err != nil {
return 0, errors.Wrapf(err, "unable to open %s", filename)
}
defer file.Close()
scanner := bufio.NewScanner(file)
count := 0
for scanner.Scan() {
if scanner.Text() == "" {
count++
}
}
return count, nil
}
Now, let’s say we want to implement unit tests on top of this function to test with a normal file, an empty file, a file with a different encoding type, etc. It could easily become very hard to manage.
Also, if we want to implement the same logic but for an HTTP body, for example, we will have to create another function for that.
Go comes with two great abstractions: io.Reader
and io.Writer
. Instead of passing a filename, we can simply pass an io.Reader
that will abstract the data source.
Is it a file? An HTTP body? A byte buffer? It’s not important as we are still going to use the same Read
method.
In our case, we can even buffer the input to read it line by line. So, we can use bufio.Reader
and its ReadLine
method:
func count(reader *bufio.Reader) (int, error) {
count := 0
for {
line, _, err := reader.ReadLine()
if err != nil {
switch err {
default:
return 0, errors.Wrapf(err, "unable to read")
case io.EOF:
return count, nil
}
}
if len(line) == 0 {
count++
}
}
}
The responsibility of opening the file itself is now delegated to the count
client:
func ReadFile() {
filename := os.Getenv("fileExample")
file, err := os.Open(filename)
if err != nil {
return errors.Wrapf(err, "unable to open %s", filename)
}
defer file.Close()
count, err := count(bufio.NewReader(file))
}
With the second implementation, the function can be called regardless of the actual data source. Meanwhile, and it will facilitate our unit tests as we can simply create a bufio.Reader
from a string
:
count, err := count(bufio.NewReader(strings.NewReader("input")))