You already know what a filesystem is and that it usually contains many directories and files. The next step is to find out how to use Go to navigate this system.
In this topic, we'll learn about the filepath package in Go; it allows us to parse and manipulate file paths, create paths compatible with your current operating system (either Linux/macOS or Windows), and traverse the rooted file tree with one particular function.
Getting the path's base and directory
Before we start looking at the functions from the filepath package, let's set up a Go project directory named example — it will contain the following files and directories:
$ tree -hF
example
├── [4.0K] files/
│ ├── [4.0K] img/
│ │ └── [ 13K] goland.svg
│ ├── [ 145] index.js
│ └── [ 455] index.test.js
└── [ 276] main.go
You can download the example project files here.
filepath package functions do not actually check if a certain path exists within your computer; we suggest you follow the example project directory. It will help you better understand this topic and make sense of how Go "walks" the rooted file tree in the last section of this topic.Now it's time to take a look at the two most basic file path operations:
- Getting the path's base, or the last element, using the
filepath.Base()function; - Getting all but the last element of a path, typically the path's directory, with the
filepath.Dir()function.
package main
import (
"fmt"
"path/filepath"
)
func main() {
paths := []string{"/example/files/img/goland.svg", "example/files", "..files//img///", "", "///"}
for _, path := range paths {
fmt.Printf("Base:%12s | Dir: %s\n", filepath.Base(path), filepath.Dir(path))
}
}
// Output:
// Base: goland.svg | Dir: /example/files/img
// Base: files | Dir: example
// Base: img | Dir: ..files/img
// Base: . | Dir: .
// Base: / | Dir: /
Take notice of the output of different paths. When the last element is a file, filepath.Base() will return a file name; otherwise, it will just return a directory.
Bothfilepath.Base() and filepath.Dir() functions take only one argument — a string path that contains a specific path in our computer. Here are some important details regarding these functions:
- Trailing path separators are removed before extracting the last element, e.g. —
../files//img///➡..files/img; - If we pass an empty
""path, these functions will return a dot.; - If the path consists entirely of separators, these functions return a single separator, e.g. —
///➡/.
Getting the file name extension
Before diving into more advanced file path operations, we'll take a look at another one that is very simple — getting the file name extension used by the path with the filepath.Ext() function:
...
func main() {
fmt.Println(filepath.Ext("main")) // ""
fmt.Println(filepath.Ext("main.go")) // .go
fmt.Println(filepath.Ext("../files/index.test.js")) // .js
}
The extension that the filepath.Ext() function returns is the suffix, beginning at the final dot . in the final element of the path. In case there is no dot, it will return an empty string "".
Constructing a path
You've already seen how to get a path's base and a path's directory, and now it's time to learn how to construct a path with the filepath.Join() function.
The filepath.Join() function joins any number of string path elements, separating them with an OS-specific separator: / for Linux/macOS, \ for Windows:
...
func main() {
fmt.Println(filepath.Join("example", "files", "img"))
fmt.Println(filepath.Join("example", "", "files/img"))
fmt.Println(filepath.Join("home/User/example", "../../../../files"))
fmt.Println(filepath.Join("", "")) // returns an empty string!
}
// Output:
// Linux/macOS | Windows
// example/files/img | example\files\img
// example/files/img | example\files\img
// ../files | ..\files
//
Take notice that any empty "" path element passed to filepath.Join() will be ignored. And if all its elements are empty, it will return an empty string.
Getting and checking absolute paths
An absolute path refers to the complete details required to locate a file or folder in a file system; it always contains the root element and the complete directory list to locate a specific file or folder:
# Linux/macOS -> /home/User/GolandProjects/example
# Windows -> C:\GolandProjects\example
To work with absolute paths, the filepath package provides two useful functions: filepath.Abs() and filepath.IsAbs().
The filepath.Abs() function returns a string with the absolute representation of the path and an error.
If the path is empty "", it will return the absolute path of the current project directory, and in case the path is not absolute, it will be joined with the current working directory to make it an absolute path:
...
func main() {
paths := []string{"/", ".", "", "./among_us"} // ./among_us is NOT an absolute path
for _, path := range paths {
abs, err := filepath.Abs(path)
if err != nil {
log.Fatal(err)
}
fmt.Println(abs)
}
}
// Output:
// Linux/macOS | Windows
// / | C:\
// /home/User/GolandProjects/example | C:\GolandProjects\example
// /home/User/GolandProjects/example | C:\GolandProjects\example
// /home/User/GolandProjects/example/among_us | C:\GolandProjects\example\among_us
filepath.Abs() function is that it calls the filepath.Clean() function on the result. The filepath.Clean() function returns a string with the shortest path name equivalent by lexical processing. If you want to take a look at the rules that filepath.Clean() follows, you can do this in the official Go documentation.On the other hand, the filepath.IsAbs() returns either true or false reporting if a specific path is absolute or not:
...
func main() {
fmt.Println(filepath.IsAbs("/home/User/GolandProjects/example")) // true
fmt.Println(filepath.IsAbs("/.bashrc")) // true
fmt.Println(filepath.IsAbs(".bashrc")) // false
fmt.Println(filepath.IsAbs("/")) // true Linux/macOS | false Windows
fmt.Println(filepath.IsAbs("")) // false
}
filepath.IsAbs() will vary according to the OS! For example, a path like "/" will return true in Linux/macOS and false in Windows. However, a path like "C:\GolandProjects\example" returns false in Linux/macOS but true in Windows!Walking the file tree
It's time to use Go to walk or traverse a file tree. The concept of "walking the file tree" is basically calling all the files and directories contained within a specific file tree.
To walk the file tree in Go, we can use the filepath.Walk() function, along with the WalkFunc function type. To further explain how these functions work, let's take a look at an example of how we can walk the example project directory:
...
func main() {
err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Fatal(err)
}
if info.IsDir() {
fmt.Println("Directory:", path, "size:", info.Size(), "bytes")
} else {
fmt.Println("File:", path, "size:", info.Size(), "bytes")
}
return nil
})
if err != nil {
log.Fatal(err)
}
}
// Output:
// Directory: . size: 4096 bytes
// Directory: files size: 4096 bytes
// Directory: files/img size: 4096 bytes
// File: files/img/goland.svg size: 13685 bytes
// ...
The filepath.Walk() function requires two arguments. The first one is the file tree we want to walk; in this case "." — the root of the example project directory.
The second argument is a function of the WalkFunc type that takes the following arguments:
path— a string containing a specific path in our computer;info— an interface of thefs.FileInfotype; it provides methods such asIsDir()to check if the file is a directory andSize()to get the file size in bytes.err— it reports an error related to path, signaling thatfilepath.Walk()will not walk into that directory. In caseWalkFuncreturns an error, it will causefilepath.Walk()to stop walking the entire tree.
An important detail regarding the filepath.Walk() function is its method of walking the files in lexical order, which makes the output deterministic; this means the function processes files in a consistent order each time it's run, assuming no changes in the file tree. However, to achieve this, filepath.Walk() must read the entire directory into memory before proceeding to walk that directory.
filepath.Walk() function to check them out!Conclusion
In this topic, we've learned about the filepath package and the most common path operations we can perform with its functions. In particular, we've learned about the following operations:
- Getting a path's base and a path's directory with the
filepath.Base()and thefilepath.Dir()functions, respectively; - Getting the file name extension used by the path with the
filepath.Ext()function; - Using the
filepath.Join()function to take different string path elements and construct a path; - Getting and checking absolute paths with the
filepath.Abs()andfilepath.IsAbs()functions, respectively; - Walking a specific file tree using the
filepath.Walk()function along with theWalkFuncfunction type.
Enough reading for now; it's time for some action! Let's go and test our newly acquired knowledge with some theory and coding tasks!