Using golang fsnotify package as watcher
In golang we have fsnotify package which can be used to monitor files and directories for any types of changes. fsnotify is not available in standard library of gloang so we need to install it using go get
command:
# go get "github.com/fsnotify/fsnotify"
go: finding module for package github.com/fsnotify/fsnotify
go: downloading github.com/fsnotify/fsnotify v1.6.0
go: found github.com/fsnotify/fsnotify in github.com/fsnotify/fsnotify v1.6.0
Following are the list of events which currently are supported by fsnotify
at the time of writing this tutorial:
fsnotify.Create
: triggered when a file or directory is createdfsnotify.Write
: triggered when a file is modifiedfsnotify.Remove
: triggered when a file or directory is removedfsnotify.Rename
: triggered when a file or directory is renamedfsnotify.Chmod
: triggered when the permissions of a file or directory are modified
For updated content you may refer fsnotify README
These events are defined as constants in the fsnotify package, and you can use them to check for specific events in your code. For example, you can check if an event is a write event using event.Op&fsnotify.Write == fsnotify.Write
.
Although you must understand that the events that are generated by the filesystem notifications are OS-specific and might not always be accurate. Also, the events generated by the filesystem notifications are not guaranteed to be in order.
You can use these events to monitor and take some action based on the file system changes. Please let me know if you have further questions.
Example-1: Using fsnotify to monitor changes in a file
In this example we use fsnotify
package to watch for changes in a text file:
package main
import (
"fmt"
"log"
"github.com/fsnotify/fsnotify"
)
func main() {
// setup watcher
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
done := make(chan bool)
// use goroutine to start the watcher
go func() {
for {
select {
case event := <-watcher.Events:
// monitor only for write events
if event.Op&fsnotify.Write == fsnotify.Write {
fmt.Println("Modified file:", event.Name)
}
case err := <-watcher.Errors:
log.Println("Error:", err)
}
}
}()
// provide the file name along with path to be watched
err = watcher.Add("file.txt")
if err != nil {
log.Fatal(err)
}
<-done
}
In this example, we first create a new watcher using the fsnotify.NewWatcher()
function. Then we start a goroutine that waits for events on the watcher's Events and Errors channels. When an event is received, we check if the event is a write event using the fsnotify.Write
constant and if so, we print the name of the modified file.
We then add the file that we want to watch to the watcher using the watcher.Add("file.txt")
function. In my case since the file exist in local directory hence I have not give absolute path.
You can also watch for other file system events like fsnotify.Create
, fsnotify.Remove
, fsnotify.Rename
and fsnotify.Chmod
as well.
On one terminal we execute our code
# go run main.go
On another terminal I will make some changes to file.txt
:
# echo hello >> file.txt
Immediately we get a notification on our code's STDOUT
# go run main.go Modified file: file.txt ^Csignal: interrupt
Press Ctrl+C
to exit the watcher.
Example-2: Using fsnotify to monitor a directory for changes
Similar to our previous example, we can also configure a watcher for a directory instead of file:
package main
import (
"fmt"
"log"
"github.com/fsnotify/fsnotify"
)
func main() {
// setup watcher
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
done := make(chan bool)
// use goroutine to start the watcher
go func() {
for {
select {
// provide the list of events to monitor
case event := <-watcher.Events:
if event.Op&fsnotify.Create == fsnotify.Create {
fmt.Println("File created:", event.Name)
}
if event.Op&fsnotify.Write == fsnotify.Write {
fmt.Println("File modified:", event.Name)
}
if event.Op&fsnotify.Remove == fsnotify.Remove {
fmt.Println("File removed:", event.Name)
}
if event.Op&fsnotify.Rename == fsnotify.Rename {
fmt.Println("File renamed:", event.Name)
}
if event.Op&fsnotify.Chmod == fsnotify.Chmod {
fmt.Println("File permissions modified:", event.Name)
}
case err := <-watcher.Errors:
log.Println("Error:", err)
}
}
}()
// provide the directory to monitor
err = watcher.Add("/tmp")
if err != nil {
log.Fatal(err)
}
<-done
}
In this example, we use the fsnotify.NewWatcher()
function to create a new watcher and then add the directory that we want to monitor to the watcher using watcher.Add("/tmp")
. You can provide your own directory instead of /tmp
.
We then start a goroutine that waits for events on the watcher's Events and Errors channels, and when an event is received, we check if the event is a create, write, remove, rename or chmod event using the corresponding constants provided by fsnotify
package, and if so, we print the name of the file or directory that was modified and the type of modification.
Output:
# go run main.go
File created: /tmp/file
File permissions modified: /tmp/file
File removed: /tmp/file
File created: /tmp/go-build1867431975
File removed: /tmp/go-build1867431975
File created: /tmp/staticcheck2690929288
File removed: /tmp/staticcheck2690929288
File modified: /tmp/staticcheck2690929288
Example-3: Monitor a directory recursively using fsnotify()
and filepath.Walk()
By default fsnotify
package does not support recursively monitoring a directory and its subdirectories. It only watches for changes in the immediate children of the directory that you added to the watcher, and it doesn't recursively watch subdirectories and their children.
So we will use fsnotify
with filepath.Walk()
function to recursively walk through the directory tree and add each file and directory to the watcher individually.
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/fsnotify/fsnotify"
)
func main() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
fmt.Println(err)
return
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event := <-watcher.Events:
if event.Op&fsnotify.Create == fsnotify.Create {
fmt.Println("File created:", event.Name)
}
if event.Op&fsnotify.Write == fsnotify.Write {
fmt.Println("File modified:", event.Name)
}
if event.Op&fsnotify.Remove == fsnotify.Remove {
fmt.Println("File removed:", event.Name)
}
if event.Op&fsnotify.Rename == fsnotify.Rename {
fmt.Println("File renamed:", event.Name)
}
if event.Op&fsnotify.Chmod == fsnotify.Chmod {
fmt.Println("File permissions modified:", event.Name)
}
case err := <-watcher.Errors:
log.Println("Error:", err)
}
}
}()
filepath.Walk("/tmp", func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
watcher.Add(path)
}
return nil
})
<-done
}
In this example, we first create a new watcher using the fsnotify.NewWatcher()
function and start a goroutine that waits for events on the watcher's Events and Errors channels, and when an event is received, we check if the event is a write event using fsnotify.Write constant and if so, we print the name of the modified file.
Then we use the filepath.Walk
function to recursively walk through the directory tree and for each directory, we add it to the watcher using the watcher.Add(path)
function.
We execute the code and in parallel terminal we will make some changes to sub-directories of the path which is being monitored"
# go run main.go File created: /tmp/gopls-workspace-mod1600390560/file1 File modified: /tmp/gopls-workspace-mod1600390560/file1 File removed: /tmp/gopls-workspace-mod1600390560/file1
The changes performed:
[root@server goexamples]# touch /tmp/gopls-workspace-mod1600390560/file1 [root@server goexamples]# echo hello > /tmp/gopls-workspace-mod1600390560/file1 [root@server goexamples]# rm -f /tmp/gopls-workspace-mod1600390560/file1
Example-4: Monitor symbolic link file using fsnotify()
We can use the fsnotify
package to watch for changes in a symbolic link. The process is similar to monitoring a regular file or directory, but you need to add the path of the symbolic link to the watcher. Here is our example:
package main
import (
"fmt"
"log"
"github.com/fsnotify/fsnotify"
)
func main() {
// create a new watcher
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
// close the watcher when the function exits
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event := <-watcher.Events:
// monitor write event.
// You can add more events supported by fsnotify
if event.Op&fsnotify.Write == fsnotify.Write {
fmt.Println("Modified symlink:", event.Name)
}
case err := <-watcher.Errors:
log.Println("Error:", err)
}
}
}()
// add your symbolic link path and file to monitor
err = watcher.Add("/tmp/file")
if err != nil {
log.Fatal(err)
}
<-done
}
This code uses the fsnotify
package to watch for changes in a symbolic link. It first creates a new watcher using the fsnotify.NewWatcher()
function, and it starts a goroutine that waits for events on the watcher's Events channel. Inside the goroutine, it uses a select statement to handle the events, checking if the event is a write event using the fsnotify.Write
constant and if so, it prints the name of the modified symbolic link.
The defer statement before the watcher.Close()
function is used to close the watcher when the function exits.
It then adds the symbolic link to the watcher using the watcher.Add("/tmp/file")
function, and it starts the goroutine to wait for events.
We have created a symbolic link for the sake of this code:
# ln -s /root/goexamples/file.txt /tmp/file
# ls -l /tmp/file
lrwxrwxrwx 1 root root 25 Jan 15 18:34 /tmp/file -> /root/goexamples/file.txt
Let's start our monitor script:
# go run main.go Modified symlink: /tmp/file Modified symlink: /tmp/file
On a different terminal I did below changes to both original file and the symbolic link
# echo hello >> /tmp/file # echo hello >> /root/goexamples/file.txt
as we can see in the go run main.go
output above, both the events have been captured.
Example-5: Monitor FIFO (named pipe) using fsnotify()
We can use same code as we used for regular file and symbolic link to also monitor a FIFO (named PIPE) file. Here is a sample code to monitor WRITE events into the FIFO file:
package main
import (
"fmt"
"log"
"github.com/fsnotify/fsnotify"
)
func main() {
// create a new watcher
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
// close the watcher when the function exits
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event := <-watcher.Events:
// monitor write event.
// You can add more events supported by fsnotify
if event.Op&fsnotify.Write == fsnotify.Write {
fmt.Println("Modified FIFO file:", event.Name)
}
case err := <-watcher.Errors:
log.Println("Error:", err)
}
}
}()
// provide the path to your fifo file
err = watcher.Add("/tmp/fifo")
if err != nil {
log.Fatal(err)
}
<-done
}
In this example, we first create a new watcher using the fsnotify.NewWatcher()
function. Then we start a goroutine that waits for events on the watcher's Events channel, and when an event is received, it checks if the event is a write event using fsnotify.Write constant and if so, it prints the name of the modified FIFO file.
Then we use the watcher.Add("/path/to/fifo")
function to add the FIFO file to the watcher and start the goroutine to wait for events.
I hope you are familiar with how FIFO works, as unless there is an active reader for the FIFO, the watcher will not work.
Let's create a temporary FIFO file:
# mkfifo /tmp/fifo
On a different terminal, I will add a reader to this FIFO file using tail
command:
# tail -f /tmp/fifo
On a different terminal I will start this watcher program:
# go run main.go Modified FIFO file: /tmp/fifo Modified FIFO file: /tmp/fifo Modified FIFO file: /tmp/fifo Modified FIFO file: /tmp/fifo
Now from a different terminal we can add contents to our FIFO file and it should be visible to tail command output and the same event should be captured by our golang code.
Summary
We shared multiple examples using fsnotify
to create a watcher service which can watch different resources and report the event. We learned how to monitor,
- single file
- symbolic link
- FIFO File
- single directory
- directory and files recursively
References
Go language how detect file changing? - Stack Overflow
How to listen to fifo wait for a fifo file's output in golang
Watch recursive directories in go-inotify - linux - Stack Overflow