The file structure

The os.File type satisfies the reader interface and is the main actor that's used to interact with file contents. The most common way to obtain an instance for reading purposes is with the os.Open function. It's very important to remember to close a file when you're done using it – this will not be obvious with short-lived programs, but if an application keeps opening files without closing the ones that it's done with, the application will reach the limit of open files imposed by the operating system and start failing the opening operations.

The shell offers a couple of utilities, as follows:

  • One to get the limit of open files – ulimit -n 
  • Another to check how many files are open by a certain process – lsof -p PID

The previous example opens a file just to show its contents to standard output, which it does by loading all its content in memory. This can be easily optimized with the tools we just mentioned. In the following example, we are using a small buffer and printing its content before it gets overridden by the next read, using a small buffer to keep memory usage at a minimum.

An example of using a byte array as a buffer is shown in the following code:

func main() {
if len(os.Args) != 2 {
fmt.Println("Please specify a file")
return
}
f, err := os.Open(os.Args[1])
if err != nil {
fmt.Println("Error:", err)
return
}
defer f.Close() // we ensure close to avoid leaks

var (
b = make([]byte, 16)
)
for n := 0; err == nil; {
n, err = f.Read(b)
if err == nil {
fmt.Print(string(b[:n])) // only print what's been read
}
}
if err != nil && err != io.EOF { // we expect an EOF
fmt.Println("\n\nError:", err)
}
}

The reading loop, if everything works as expected, will continue executing read operations until the file content is over. In that case, the reading loop will return an io.EOF error, which shows that there is no more content available.