Working with files is a very common task in computer programming. You work with log files, HTML files, and a whole host of other file types in your programs. In this chapter, we explain how to read and write files, walk directories, use the fs
1 package, and finally, embed files into your Go binaries to create a truly self-contained application.
Before you can begin using files for your programs, you first need to understand the basics of how files work in Go.
Consider the file tree in Listing 14.1. We use this file tree to help you understand files in Go. This tree contains .txt
files as well as special directories testdata
, _ignore
, and .hidden
. We cover the importance of these files and directories as we progress.
Listing 14.1 File Tree Containing Special Folders: .hidden, _ignore
, and testdata
To know which files you can work with, you need to know which files are in a directory. To do this, you can use the os.ReadDir
2 function, Listing 14.2.
2. https://pkg.go.dev/os#ReadDir
Listing 14.2 The os.ReadDir
Function
The os.ReadDir
function takes a path as a parameter and returns a slice of os.DirEntry
3 values, Listing 14.3. Each os.DirEntry
value represents a file or directory in the directory and contains information about the file or directory.
3. https://pkg.go.dev/os#DirEntry
Listing 14.3 The os.DirEntry
Type
The fs
package was added in go1.16
, and it provides interface around the file system. The result of this was that many types were aliased to the fs
package. This means that you may need to hunt a little further to find the deeper documentation.
For example, os.DirEntry
was aliased to fs.DirEntry
4 in go1.16
, Listing 14.4.
4. https://pkg.go.dev/io/fs#DirEntry
Listing 14.4 The fs.DirEntry
Type
As shown in Listing 14.5, we can use the os.ReadDir
function to read the contents of the data
directory. We then print the names to the console. If the file is a directory, we prepend it with a ->
.
Listing 14.5 Using os.ReadDir
to Read the Contents of a Directory
As you can see from the output in Listing 14.6, only the files in the data
directory are listed. The os.ReadDir
function only reads the contents of the directory and not the contents of any subdirectories. To get a full list of files, including the contents of subdirectories, we need to walk the directories ourselves. We discuss this more later in this chapter.
Listing 14.6 Output of Listing 14.5
The main source for metadata about files is the fs.FileInfo
5 interface, Listing 14.7. In go1.16
, the os.FileInfo
6 type was aliased to fs.FileInfo
. From this interface, we can get the name of the file, the size of the file, the time the file was last modified, and the mode, or permissions, of the file. We can also tell whether the file is a directory or a regular file.
5. https://pkg.go.dev/io/fs#FileInfo
6. https://pkg.go.dev/os#FileInfo
Listing 14.7 The fs.FileInfo
Interface
Consider Listing 14.8. We read the contents of the data
directory and print the mode, size, and name of each file.
Listing 14.8 Using fs.FileInfo
to Print Information about Files
The os.ReadDir
function returns a slice of os.DirEntry
values, from which we can get the fs.FileInfo
: the os.DirEntry.Info
field. To get the fs.FileInfo
for a single file or directory, we can use the os.Stat
7 function, Listing 14.9.
Listing 14.9 The os.Stat
Function
Consider Listing 14.10, which prints the mode, size, and name of the data/a.txt
file.
Listing 14.10 Using os.Stat
to Get Information about a File
os.ReadDir
does not recurse into subdirectories. To be able to do that, you need to use filepath.WalkDir
,8 Listing 14.11 to walk through all files in a directory and all subdirectories.
8. https://pkg.go.dev/filepath#WalkDir
Listing 14.11 The filepath.WalkDir
Function
To use filepath.WalkDir
, you first need to give it a path to walk and then a function that will be called for each file. This function is the filepath.WalkFunc
,10 Listing 14.12.
10. https://pkg.go.dev/filepath#WalkFunc
Listing 14.12 The fs.WalkDirFunc
Function
Consider Listing 14.13, in which we walk through the data
directory. For each file and directory, including the root directory, the filepath.WalkFunc
is called.
Listing 14.13 Using filepath.WalkDir
to Recurse a Directory
First, we need to check the error
that is passed in via the filepath.WalkFunc
function. If this error is not nil
, it is most likely that the root directory could not be read. In this case, we can simply return the error, and the walk stops.
If there is no error, we can then check to see if what we’re dealing with is a directory or a file. If it is a directory, we can simply return nil
to indicate that we don’t want to continue processing the directory itself but walking of the directory will continue. The fs.DirEntry.IsDir
11 method can be used to check this, return true
for a directory, and return false
for a file.
11. https://pkg.go.dev/io/fs#DirEntry.IsDir
Finally, we print off the mode, size, and path of the file.
As we can see from Listing 14.14, the walk function is called for each file and directory in the data
directory. Printing out the file information is lexical order, which is the order in which the files and directories are listed in the directory.
Listing 14.14 The Output of Listing 14.13
When walking through a directory tree, you may want to skip certain directories and files. For example, you may want to skip hidden folders like .git
and .vscode
or skip large folders such as node_modules
. There may be other reasons to skip certain files and directories that are more specific to your application.
To illustrate this, let’s look at the excerpt from the go help test
command in Listing 14.15.
Listing 14.15 Excerpt from the go help test
Command
The documentation in Listing 14.15 says that there are certain files and directories that Go ignores when walking through a directory tree looking for test files:
Files/directories whose names begin with .
are ignored.
Files/directories whose names begin with _
are ignored.
The testdata
directory is ignored.
Let’s implement these same restrictions in our own code.
To tell Go to skip a directory, we can return fs.SkipDir
,12 Listing 14.16, from our fs.WalkFunc
function. Although technically this is an error
13 type, Go is using this error as a sentinel value to indicate that the directory should be skipped. This is similar to how io.EOF
14 is used to indicate that the end of a file has been reached and there is no more data to be read.
12. https://pkg.go.dev/io/fs#SkipDir
13. https://pkg.go.dev/builtin#error
Listing 14.16 The fs.SkipDir
Error
In Listing 14.17, instead of simply returning a nil
when the fs.DirEntry
is a directory, we can check the name of the directory and make a decision on whether to ignore that directory. If we want to ignore a directory, testdata
for example, we can return fs.SkipDir
from our fs.WalkFunc
function. Go then skips that directory and all of its children.
Listing 14.17 Using fs.SkipDir
to Skip Directories
Next, in Listing 14.18, we can write a small test that asserts that the testdata
directory and our other special directories are skipped.
Listing 14.18 Asserting Special Directories Are Skipped
Finally, when compared to the original file listing, Listing 14.19, we see that the testdata
directory, along with the other special directories, is no longer included in the file listing.
Listing 14.19 Original File Listing
Now that we have a working test suite that asserts our Walk
function returns the correct results, we can remove the hard-coded test data we’ve been using and generate the test data directly inside the test itself.
To create a single directory, we can use the os.Mkdir
15 function, as in Listing 14.20. The os.Mkdir
function creates a single directory at the path specified, and with the specified permissions.
15. https://pkg.go.dev/os#Mkdir
Listing 14.20 The os.Mkdir
Function
To create a directory, you need to provide a set of permissions for the directory. The permissions are specified in the form of an os.FileMode
16 value, Listing 14.21. Those familiar with Unix style permissions will feel right at home here being able to use permissions such as 0755
to specify the permissions for the directory.
16. https://pkg.go.dev/os#FileMode
Listing 14.21 The os.FileMode
Type
In Listing 14.22, we can create a helper that generates all of the files for us. In this helper, we first remove any test data that may have been left behind from a previous test run, by using the os.RemoveAll
17 function. This deletes the directory at the path specified; it also deletes all of its contents, including any subdirectories.
17. https://pkg.go.dev/os#RemoveAll
Listing 14.22 Using os.Mkdir
to Create Directories on Disk
Next, we use the os.Mkdir
function to create the parent data
directory, giving it the permissions 0755
that allow us to read and write to the directory.
Finally, we loop over a list of files and directories that we want to create and create each one using the os.Mkdir
function.
As you can see from the test output in Listing 14.23, this approach isn’t working as expected. We run into errors trying to create the necessary files and directories. The biggest problem is that we are trying to use os.Mkdir
to create files instead of directories.
Listing 14.23 Error Using os.Mkdir
to Create Files
The filepath
18 package, Listing 14.24, provides a number of functions that help you manipulate file paths.
18. https://pkg.go.dev/filepath
Listing 14.24 The filepath
Package
There are three functions in particular that are going to be useful in our test suite. These are filepath.Ext
,19 filepath.Dir
,20 and filepath.Base
.21
19. https://pkg.go.dev/filepath#Ext
20. https://pkg.go.dev/filepath#Dir
21. https://pkg.go.dev/filepath#Base
When presented with a file path in string
form, such as /path/to/file.txt
, you need to be able to tell whether the path is a file or a directory. The easiest, although not without potential errors, way to do this is to check whether the path has a file extension. If it does, then you can assume the path to be a file; if it doesn’t, then you can assume the path to be a directory. The filepath.Ext
function, Listing 14.25, returns the file extension of a file path. If the path is /path/to/file.txt
, then the function returns .txt
.
Listing 14.25 The filepath.Ext
Function
If the filepath.Ext
function returns an extension, such as .txt
, then you can assume the path is a file. To get the directory of a file, you can use the filepath.Dir
function, Listing 14.26. If the path is /path/to/file.txt
, then the function returns /path/to
.
Listing 14.26 The filepath.Dir
Function
Regardless of whether your path has an extension, it does have a “base” name. You can use the filepath.Base
function, shown in Listing 14.27, to return the name of the file or directory at the end of the file path. For example, if the path is /path/to/file.txt
, then “base” name would be file.txt
. If the path is /path/to/dir
, then the “base” name would be dir
.
Listing 14.27 The filepath.Base
Function
Now that you know how to find the directory, base, and extension of a file path, you can use these functions to update your helper to create only directories—not files. In Listing 14.28, we can use the filepath.Dir
and filepath.Ext
functions to parse the given file path to create the necessary folder structure on disk.
Listing 14.28 Helper Function to Create Test Folder and File Structures
When you look at the test output, Listing 14.29, you can see that the helper function is still not working as expected. It is trying to create a directory, data
, that already exists.
Listing 14.29 Test Output
To try to fix our helper, we should properly check the error returned by the os.Mkdir
function in Listing 14.30. If the error is not nil, we need to check whether the error is because the directory already exists. If it is, then we can ignore the error and continue. If the error is something else, then we need to return the error to the caller.
Listing 14.30 Helper Function to Check for Errors
When you look at the test output, Listing 14.31, you can see that the helper function is still not working as expected, but you have moved beyond the first error to a new one. We are trying to create nested subdirectories, data/e/f/_ignore
, using the os.Mkdir
function that is designed to create only one directory at a time.
Listing 14.31 Using errors.Is
22 to Check for the fs.ErrExit
23 Error
To create multiple directories at once, you can use the os.MkdirAll
24 function, Listing 14.32. This behaves identically to os.Mkdir
except that it creates all the directories in the path.
24. https://pkg.go.dev/os#MkdirAll
Listing 14.32 The os.MkdirAll
Function
In Listing 14.33, we can update our test helper to use os.MkdirAll
to create the directories, instead of os.Mkdir
, to ensure that all of the directories are created rather than just one.
Listing 14.33 Helper Function to Create Test Folder and File Structures
As you can see from the output in Listing 14.34, our tests are still not passing. This is because we have yet to create the necessary files.
A look at the file system itself confirms that the directories were indeed created.
Listing 14.34 The Directory Structure Created by the Test Helper
Before you can read a file, you need to create it. To create a new file, you can use the os.Create
25 function, Listing 14.35. The os.Create
function creates a new file at the specified path if one does not already exist. If the file already exists, the os.Create
function erases, or truncates, the existing file’s contents.
25. https://pkg.go.dev/os#Create
Listing 14.35 The os.Create
Function
Consider Listing 14.36. We have a Create
function that creates a new file at the specified path and writes the specified data to the file.
Listing 14.36 The Create
Function
If successful, os.Create
returns a os.File
26 value representing the new file. This file can then be written to and read from. In this case, we are writing the string Hello, World!
to the file.
26. https://pkg.go.dev/os#File
In Listing 14.37, we write a test that confirms the file was created and its contents are correct.
Listing 14.37 Testing the Create
Function from Listing 14.36
First, we need to make sure the file does not already exist. To do this, we can use the os.Stat
function to check if the file exists.
Next, we can read the contents of the file using the os.ReadFile
27 function. This function reads the entire contents of the file into a byte slice. If the file does not exist, the os.ReadFile
function, Listing 14.38, returns an error.
27. https://pkg.go.dev/os#ReadFile
Listing 14.38 The os.ReadFile
Function
Finally, we can compare the contents of the file to the expected contents. The test output, Listing 14.39, confirms that the file was created and its contents are correct.
Listing 14.39 Test Output from Listing 14.37
If we look directly at the file on disk, as shown in Listing 14.40, we can see that the file has been created and its contents are correct.
Listing 14.40 Contents of Listing 14.39
If a file already exists, the os.Create
function truncates the existing file’s contents. This means that if a file already exists and has contents, the file is erased before the new contents are written. This can lead to unexpected results.
Consider the example in Listing 14.41. In this test, we are creating a file and setting its contents. Then, we create the file again—this time with different contents. The original contents of the file are erased and replaced with the new contents.
Listing 14.41 Using os.Create
Truncates and Replaces an Existing File on Disk
To keep the test clean, we can use a helper function, Listing 14.42, to create a file, set its contents, and then assert that the contents are correct.
Listing 14.42 The fileHelper
Function
From the test output, Listing 14.42, you can see that the original contents of the file were erased, and the new contents were written.
A look at the file on disk, Listing 14.43, confirms that the new content was written.
Listing 14.43 Contents of Listing 14.42
When we last looked at the tests for our Walk
function, we saw that the tests were failing, Listing 14.44. Our tests are failing because we have yet to create the files necessary. We have only created the directories.
Listing 14.44 Failing Test Output
A look at the file system, Listing 14.45, confirms that the directories were created but the files were not.
Listing 14.45 Folders but Not Files Were Created in Listing 14.44
With knowledge of how to create files, we can now create the files necessary for our tests, Listing 14.46.
Listing 14.46 Create Files with os.Create
and Directories with os.MkdirAll
The tests are now passing, Listing 14.47, all files and directories are created as expected, and the Walk
function is working as expected.
Listing 14.47 The File System
When you use os.Create
to create a file, if that file already exists, the file is overwritten. This is expected behavior when creating a new file. It would be strange to find previously written contents in a new file.
There are plenty of times when you want to append to an existing file instead of overwriting it. For example, you might want to append to a log file with new entries and not overwrite the previous log entries. In this case, you can use os.OpenFile
28 to open the file for appending, as in Listing 14.48.
28. https://pkg.go.dev/os#OpenFile
Listing 14.48 The os.OpenFile
Function
Consider Listing 14.49. We have an Append
function that uses os.OpenFile
to open the file for appending. The os.OpenFile
function can be configured with flags to tell Go how to open the file. In this example, we create the file if it does not exist and append to it if it does.
Listing 14.49 The Append
Function
Next, we can write a test to confirm that the file is appended to properly, Listing 14.50. We can make use of our createTestFile
helper to create the initial file and fill it with some data, but we need a new helper to append to the file.
Listing 14.50 Testing the Append
Function
The appendTestFile
helper reads in the original contents of the file, appends the new data, and then reads the new contents of the file. Finally, we can compare to see if the new contents of the file are equal to the original contents plus the new data.
The tests in Listing 14.51 show that the file is appended to properly.
Listing 14.51 The appendTestFile
Helper
Finally, a look at the file system shows that the file is appended to properly, Listing 14.52.
Listing 14.52 The Contents of the File
So far we have been reading files directly into memory using the os.ReadFile
function. Although this is a very simple way to read a file, it is not the most efficient way to do so. If the file is very large, reading it into memory may not be feasible. It may also not be necessary. For example, if you have a media file, such as a video, you might only want to read its metadata at the beginning of the file and then stop reading it before you get to the actual video data.
Using interfaces, such as io.Reader
and io.Writer
, you can read and write files in a more efficient way.
Consider the example in Listing 14.53. We are opening a file using os.Open
, which returns an os.File
that implements the io.Reader
29 interface. We are passing the Read
function to an io.Writer
30 as an argument. We can make use of both of these interfaces to use the io.Copy
31 function to copy the contents of the file to the io.Writer
. If this io.Writer
is another os.File
, then io.Copy
streams the data directly from one file to the other.
29. https://pkg.go.dev/io#Reader
30. https://pkg.go.dev/io#Writer
31. https://pkg.go.dev/io#Copy
Listing 14.53 The Read
Function
In the test in Listing 14.54, we use a bytes.Buffer
32 as the io.Writer
we pass to the Read
function. This then allows us to assert the contents of the file were properly read.
32. https://pkg.go.dev/bytes#Buffer
Listing 14.54 Testing the Read
Function
When discussing file systems, special attention must be paid to Windows. Although Go has done a great job of abstracting away the differences between Windows and Unix file systems, it is still possible to run into some issues.
The largest issue is that Windows has a different file path system than Unix. In Unix, a file path is a series of directories separated by slashes, whereas in Windows, a file path is a series of directories separated by backslashes.
Because of this difference, whenever you want to use a nested filepath in Go, you need to use a function that converts the path to the correct format. The filepath.Join
33 function, Listing 14.55, does this.
33. https://pkg.go.dev/filepath#Join
Listing 14.55 The filepath.Join
Function
The filepath.Join
is a function that takes a variable number of paths and joins them together with the appropriate filepath.Separator
.34 The result is a string that is a valid path in the appropriate file system. In Listing 14.56, we can join multiple paths together to create a Windows or Unix file path, depending on the underlying operating system.
34. https://pkg.go.dev/filepath#Separator
Listing 14.56 Using filepath.Join
Function to Create Platform-Specific File Paths
When we use the fs
35 package, we can use /
as the separator, and the filepath is converted to the correct format for the current operating system.
In go1.16
, the Go team wanted to introduce a long-requested feature: the ability to embed files into a Go binary. There had been a variety of third-party tools that did this, but none of them were able to do it without a lot of work.
A lot of these tools behaved in a similar way. If the file was found in their in-memory store, they would return it. If their store was empty, or didn’t contain the file, the assumption was the file was in the file system, and they would read it from there. The Go team liked this approach because it was very friendly to developers. Using go run
to start you local web server, for example, would read HTML templates from disk, allowing for live updates. But if built with go build
, the binary would contain all of the HTML templates in memory, and the developer would have to manually update the binary to see file changes.
To enable this feature, the Go team had to introduce a new set of interfaces for working with the file system. For this, they introduced the fs
package, Listing 14.57. Although this package was introduced to help enable the new embedding feature, it provides a common interface for working with read-only file systems. This allows developers to mock out the file system for testing or create their own file system implementations. For example, you can implement the fs.FS
interface to create an Amazon S3 file system that is a drop-in replacement for the standard file system.
Listing 14.57 The fs
Package
At the core of the fs
package are two interfaces: the fs.FS
36 interface and the fs.File
37 interface.
36. https://pkg.go.dev/io/fs#FS
37. https://pkg.go.dev/io/fs#File
The fs.FS
interface is used to define a file system, Listing 14.58. To implement this interface, you must define a method, named Open
, that accepts a path and returns an fs.File
interface and a potential error.
Listing 14.58 The fs.FS
Interface
There are already examples of the fs.FS
interface in the standard library, such as os.DirFS
,38 fstest.MapFS
,39 and embed.FS
.40
38. https://pkg.go.dev/os#DirFS
The fs.File
interface is used to define a file, Listing 14.59. While the interface for fs.FS
is very simple, the fs.File
interface is more complex. A file needs to be able to be read, closed, and able to return its own fs.FileInfo
.41
41. https://pkg.go.dev/io/fs#FileInfo
Listing 14.59 The fs.File
Interface
There are examples of fs.File
in the standard library already, such as os.File
and fstest.MapFile
,42 Listing 14.60.
42. https://pkg.go.dev/testing/fstest#MapFile
Listing 14.60 The fstest.MapFile
Implements fs.File
Previously, we had been using filepath.WalkDir
to walk the directory tree. This worked by walking the file system directly. As a result, we have to make sure that the file system is in the correct state before we can use it. As we have seen, this can cause a lot of setup work to be done. Either we have to keep completely different folders for each test scenario we have, or we have to create all of the files and folders at the start of the test before we begin. This is a lot of work and is often prone to errors.
The filepath.WalkDir
function works directly with the underlying file system, whereas the fs.WalkDir
function takes an fs.FS
implementation. By using fs.WalkDir
instead, as shown in Listing 14.61, we can easily create in-memory fs.FS
implementations for testing.
Listing 14.61 The fs.WalkDir
43 Function
43. https://pkg.go.dev/io/fs#WalkDir
In Listing 14.62, we can continue to use the same code inside of the fs.WalkFunc
44 function as we did before. Our test code only needs two small changes to make it work. The first is we need an implementation of the fs.FS
interface to pass to the Walk
function. For now, we can use the os.DirFS
function, Listing 14.63. The implementation will be backed directly by the file system.
44. https://pkg.go.dev/io/fs#WalkFunc
Listing 14.62 Using the fs.WalkDir
Function
Listing 14.63 The os.DirFS
Function
The other change we need to make is the expected paths. Before, we were expecting paths such as /data/a.txt
returned from our Walk
function. However, when working with fs.FS
implementations, the paths returned are relative to the root of the implementation. In this case, we are using os.DirFS("data")
to create the fs.FS
implementation. This places data
at the root of the file system implementation and paths returned from the Walk
function will be relative to this root.
We need to update our test code for the relative paths, a.txt
, to be returned instead of /data/a.txt
.
As you can see from the test output in Listing 14.64, we have successfully updated our code to use the fs.FS
interface.
Listing 14.64 Testing the Walk
Function
One of the biggest advantages of having interfaces for the file system is that it allows us to mock it out for testing. We no longer need to either have the files already on disk or have to create them before we can test our code. Instead, we just need to provide an interface that has that information already in it.
To help make testing easier, we can use the fstest
package. In particular, we can use the fstest.MapFS
type, Listing 14.65, to mock out a file system using a map.
Listing 14.65 The fstest.MapFS
Type
The fstest.MapFile
type, Listing 14.66, is provided to help with creating files in memory and implementing the fs.File
interface.
Listing 14.66 The fstest.MapFile
Type
Because the Walk
function already takes the fs.FS
interface, we can use the fstest.MapFS
type to create a file system that has the files we need for our test in it.
In Listing 14.67, we create a helper function that creates a new fstest.MapFS
type, Listing 14.68, and fills it with the files we need.
Listing 14.67 Creating a Helper Function to Populate a fstest.MapFS
If we look at the file system directly, we can see that there are no additional files on disk for us to use in our tests. We are relying on the fstest.MapFS
type, Listing 14.68, to provide the files we need.
Listing 14.68 No Additional Test Files Needed When Using fstest.MapFS
Inside of the test itself, we can use the helper function to get the fstest.MapFS
and pass that to the Walk
function instead of the os.DirFS
implementation we were using before.
As you can see from our test output, Listing 14.69, we are now able to test our code without having to worry about the files on disk.
Listing 14.69 Using the Helper Function to Get the fstest.MapFS
As was mentioned earlier, the fs
package and the subsequent changes to the standard library were added to allow for the embedding of files, such as HTML, JavaScript, and CSS, into the final binary.
To use this feature, you can use the embed
package and the //go:embed
directive to define which file or files to embed, Listing 14.70.
Listing 14.70 The embed
Package
Per the documentation, we can embed a single file as either a string
or a []byte
. For our purposes, and more often than not, we will be embedding multiple files, so we need a fs.FS
implementation to hold the files. The embed.FS
type, Listing 14.71, implements the fs.FS
interface and provides a read-only file system for the embedded files.
Listing 14.71 The embed.FS
Type
In Listing 14.72, we can define a global variable of type embed.FS
and use the //go:embed
directive to define the directories and files to embed.
Listing 14.72 The //go:embed
Directive
The //go:embed data
directive tells Go to fill the DataFS
variable with the contents of the data
directory. Files beginning with .
and _
are ignored by the //go:embed
directive.
In the tests, Listing 14.73, we can use the DataFS
variable to pass to the Walk
function.
Listing 14.73 Testing Using the embed.FS
as an fs.FS
As you can see from the test output in Listing 14.74, the files we wanted were successfully embedded and added to the DataFS
variable.
Listing 14.74 Successful Test Output Using an embed.FS
To see the embedding in action, we can write a small application that uses our application.
Consider Listing 14.75. We are calling our demo.Walk
function from the main
function, using the demo.DataFS
variable, which is an embed.FS
implementation. We then print out the results of the Walk
function.
Listing 14.75 The main
Function
When we use go run
to run the application, Listing 14.76, we see that the output is as expected.
Listing 14.76 The go run
Output
In Listing 14.77, we compile a binary of our application. We use the go build
command and output the binary to the bin
directory.
Listing 14.77 Building a Binary with Embedded Files
Finally, when we run the binary, Listing 14.78, we see that the output is as expected. Our application is able to successfully embed the files we defined. We now have a fully self-contained application that can contain not just the necessary runtime needed to execute the binary on the desired GOOS
and GOARCH
combinations but also the files we defined.
Listing 14.78 Files Embedded in the Binary
While we only needed to embed one directory, we can also embed multiple directories and files. We can do this by using the //go:embed
directive multiple times, as in Listing 14.79.
Listing 14.79 The //go:embed
Directive
As you can see from the test output, Listing 14.80, the extra //go:embed
directives embedded the files we did not want, and, as a result, the test failed.
Listing 14.80 Using Multiple //go:embed
Directives
In addition to embedding files and directories to a embed.FS
, we can also embed the contents of a file as a string
or []byte
, as shown in Listing 14.81. This makes gaining access to the contents of a file easier.
Listing 14.81 The //go:embed
Directive with a string
and a []byte
In this chapter, we delved deep into using files with Go. We showed you how to create, read, and append to files. We discussed the fs.FS
and fs.File
interfaces that make working with readonly file systems easier. We covered how to mock out file systems for testing. We also explained how to read and walk directories. Finally, we covered how to use the embed
package to embed files into our Go binaries.