13 minutes read

Sometimes, you may need to interact with the OS (operating system) you are working on and access its features when creating your programs. In this topic, we'll learn how to perform such interactions using functions from the os package. Specifically, we'll look at the functions that help us work with file attributes and permissions.

Current working directory

Before we start looking at the functions from the os package that help us interact with files, let's set up a Go project directory named example — it will contain the following files and directories:

$ tree -hF
example
├── [4.0K]  files/
│   ├── [  32]  bikeshare.csv
│   └── [ 13K]  goland.svg
├── [  35]  info.txt
└── [ 240]  main.go

You can download the example project files here.

Now it's time to take a look at the os.Getwd() function — it returns a rooted path name corresponding to the current directory our main.go file is within:

package main

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

func main() {
    path, err := os.Getwd()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(path)
}

// Output:
// Linux/macOS -> ~/GolandProjects/example
// Windows -> C:\GolandProjects\example

As previously mentioned, Linux/macOS and Windows have different file paths! So please take notice of the different outputs of the os.Getwd() function in the above example!

Getting file attributes

You've already seen how the os package allows us to interact with our project directory path. However, what if we needed to get specific attributes from the files in our project directory?

Enter the os.Stat() function — it returns a FileInfo interface, describing the file or directory and an error. Let's take a look at how we can use it within our Go program to get the FileInfo from the info.txt file:

...

func main() {
    fileInfo, err := os.Stat("info.txt")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("File name:", fileInfo.Name())        // File name: info.txt
    fmt.Println("Size:", fileInfo.Size(), "bytes")    // Size: 35 bytes
    fmt.Println("Permission mode:", fileInfo.Mode())  // Permission mode: -rw-r--r--
    fmt.Println("Last modified:", fileInfo.ModTime()) // Last modified: 2022-03-10 11:15:22 -0500 EST
    fmt.Println("Is directory:", fileInfo.IsDir())    // Is directory: false
}

The above methods, such as Name(), Size(), Mode() etc. from the FileInfo interface are self-explanatory. Additionally, there is one more method — Sys() — that allows us to access the OS stat result directly, but take notice that we'll need to import the syscall package, and the interface might hold a different type depending on the OS:

In Linux/macOS, we can access attributes from the *syscall.Stat_t struct:

    stat, ok := fileInfo.Sys().(*syscall.Stat_t)
    if ok {
        fmt.Println("User identifier:", stat.Uid)  //  User identifier: 1000
        fmt.Println("Group identifier:", stat.Gid) // Group identifier: 1000
    }

To simplify our code, we'll assign stat to Stat_t; then we can access other file attributes of info.txt, such as the user identifier Uid or the group identifier Gid. You can take a look at other fields of the Stat_t struct in the syscall package documentation.

In Windows, we can access attributes from the *syscall.Win32FileAttributeData struct:

    stat, ok := fileInfo.Sys().(*syscall.Win32FileAttributeData)
    if ok {
        fmt.Println(time.Unix(0, stat.CreationTime.Nanoseconds())) // 2022-03-10 11:15:26 -0500 -05
        fmt.Println("Size:", stat.FileSizeLow, "bytes")            // Size: 35 bytes
    }

The Win32FileAttributeData struct allows us to access file attributes like CreationTime, FileSizeLow and a few others. You can also take a look at other fields of the Win32FileAttributeData struct in the syscall package docs.

Getting file modes and permission bits

In the previous section, we've learned how to get a file's permission mode via the Mode() method of the FileInfo interface. The Mode() method returns a FileMode type that represents both a file's permission mode and permission bits:

...

func main() {
    fileInfo, err := os.Stat("files")
    if err != nil {
        log.Fatal(err)
    }
    mode := fileInfo.Mode() // assign the FileMode value returned by the Mode() method to `mode`.
    fmt.Printf("File perm. bits: %#o\n", mode.Perm()) // File perm. bits: 0775
    fmt.Println("File type bits:", mode.Type())       // File type bits: d---------
    fmt.Println("Is regular:", mode.IsRegular())      // Is regular: false
}

In this case, we pass the files directory as an argument to the os.Stat function, and then we assign mode to the fileInfo.Mode() method. This returns a FileMode type with new methods that provide additional file information, such as:

  • Perm() — returns the file's permission bits, e.g. 0775 (read-write-execute), 0644 (read-write), or 0444 (read-only);
  • Type() — returns the type bits d--------- for a directory; ---------- for a regular or an executable file;
  • IsRegular() — returns false when the file is a directory and true when it's a regular or an executable file.

If you want to know about other methods of the FileMode type, you can take a look at the official docs.

Checking if a file exists

You already know how to get file attributes and file permissions information. However, what if we needed to check whether a specific file exists or not within our project directory? There are a few ways to do this; here, we'll showcase the most common ones.

For one thing, we can check if a file exists by using os.Stat() along with the os.IsNotExist() function:

...

func main() {
    fileName := "impostor.png"
    fileInfo, err := os.Stat(fileName)
    if os.IsNotExist(err) {
        log.Fatal("The file ", fileName, " does not exist!")
    }
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(fileInfo.Name(), "exists!")
}

The above program checks if impostor.png exists within the example project directory; if the file doesn't exist, it logs the error and immediately exits the program.

Another way to do this is with the errors.Is() function; to use it, you'll need to import the errors package:

...

func main() {
    fileName := "impostor.png"
    fileInfo, err := os.Stat(fileName)
    if errors.Is(err, os.ErrNotExist) {
        log.Fatal("The file ", fileName, " does not exist!")
    }
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(fileInfo.Name(), "exists!")
}

Since impostor.png doesn't exist within the example project directory, both programs will log the same custom error message and immediately exit the program:

// 2022/03/16 09:16:21 The file impostor.png does not exist!

The io/fs package

In Go version 1.16, os.Stat(), os.FileInfo and os.FileMode were moved to the io/fs package. The new io/fs package defines the fs.FS interface, an abstraction for read-only trees of files.

An important detail is that fs.FileInfo and fs.FileMode preserve the same methods as os.FileInfo and os.FileMode.

However, the new fs.Stat() function has one change: in contrast to os.Stat(), it takes two arguments instead of one — fsys, a file system of the fs.FS interface, and name, a string containing the name of the file:

package main

import (
    "fmt"
    "io/fs"
    "log"
    "os"
)

func main() {
    fileInfo, err := fs.Stat(os.DirFS("files"), "goland.svg")
    if err != nil {
        log.Fatal(err)
    }
    mode := fileInfo.Mode()
    fmt.Println("File name:", fileInfo.Name())      // File name: goland.svg
    fmt.Printf("File perm. bits: %#o", mode.Perm()) // File perm. bits: 0775
}

Since the file system argument requires the fs.FS interface; we can get it via the os.DirFS() function, which returns a file system of the fs.FS type. In this case, the returned file system will be the files directory, and the second argument is the file's name: goland.svg.

Finally, we'll once again call the Mode(), Name(), and Perm() methods on the fs.FileInfo interface. As previously mentioned, it keeps the same methods as os.FileInfo, so we'll get the same output as in the previous examples.

Summary

In this topic, we have learned how to use Go to interact with and get information from files in the OS we are working on. Particularly, we've learned how to perform the following operations:

  • Getting the current working directory with the os.Getwd() function;
  • Getting basic file attributes, such as the file name, file size, and file permission mode with the Name(), Size() and Mode() methods of the FileInfo interface;
  • Getting file permission bits and file type bits with the Perm() and Type() methods from the FileMode type;
  • Checking if a specific file or directory exists with the os.IsNotExist() and errors.Is() functions.

We've also learned that os.Stat(), os.FileInfo, and os.FileMode were moved to the io/fs package in Go version 1.16, and that the only changed function was fs.Stat(), because it now takes two arguments instead of just the file name: a file system fsys and a file name name.

Wow! This sure was a long topic! And now, it's time to test our newly acquired knowledge with some theory and coding tasks!

23 learners liked this piece of theory. 2 didn't like it. What about you?
Report a typo