14. Working with Files

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 fs1 package, and finally, embed files into your Go binaries to create a truly self-contained application.

1. https://pkg.go.dev/fs

Directory Entries and File Information

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

Reading a Directory

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.ReadDir2 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.DirEntry3 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.DirEntry4 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 FileInfo Interface

The main source for metadata about files is the fs.FileInfo5 interface, Listing 14.7. In go1.16, the os.FileInfo6 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

Stating a File

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.Stat7 function, Listing 14.9.

7. https://pkg.go.dev/os#Stat

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

Walking Directories

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.IsDir11 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

Skipping Directories and Files

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.

Skipping Directories

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 error13 type, Go is using this error as a sentinel value to indicate that the directory should be skipped. This is similar to how io.EOF14 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

14. https://pkg.go.dev/io#EOF

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

Creating Directories and Subdirectories

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.Mkdir15 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.FileMode16 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.RemoveAll17 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

File Path Helpers

The filepath18 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

Getting a File’s Extension

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

Getting a File’s Directory

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

Getting a File/Directory’s Name

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

Using File Path Helpers

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

Checking the Error

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.Is22 to Check for the fs.ErrExit23 Error

22. https://pkg.go.dev/errors#Is

23. https://pkg.go.dev/io/fs#ErrExit

Creating Multiple Directories

To create multiple directories at once, you can use the os.MkdirAll24 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

Creating Files

Before you can read a file, you need to create it. To create a new file, you can use the os.Create25 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.File26 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.ReadFile27 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

Truncation

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

Fixing the Walk Tests

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

Creating the Files

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

Appending to Files

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.OpenFile28 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

Reading Files

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.Reader29 interface. We are passing the Read function to an io.Writer30 as an argument. We can make use of both of these interfaces to use the io.Copy31 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.Buffer32 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

Beware of Windows

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.Join33 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 fs35 package, we can use / as the separator, and the filepath is converted to the correct format for the current operating system.

35. https://pkg.go.dev/fs

The FS Package

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

The FS Interface

At the core of the fs package are two interfaces: the fs.FS36 interface and the fs.File37 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

39. https://pkg.go.dev/testing/fstest#MapFS

40. https://pkg.go.dev/embed#FS

The File Interface

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

Using the FS Interface

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.WalkDir43 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.WalkFunc44 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

File Paths

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

Mocking a File System

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

Using MapFS

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

Embedding Files

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

Using Embedded Files

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

Embedded Files in a Binary

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

Modifying Embedded Files

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

Embedding Files as a String or Byte Slice

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

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset