Golang watcher (fsnotify) Examples [In-Depth Tutorial]


GO, GOLANG Solutions

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 created
  • fsnotify.Write : triggered when a file is modified
  • fsnotify.Remove : triggered when a file or directory is removed
  • fsnotify.Rename : triggered when a file or directory is renamed
  • fsnotify.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

 

Deepak Prasad

Deepak Prasad

He is the founder of GoLinuxCloud and brings over a decade of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels in various domains, from development to DevOps, Networking, and Security, ensuring robust and efficient solutions for diverse projects. You can connect with him on his LinkedIn profile.

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can send mail to admin@golinuxcloud.com

Thank You for your support!!

Leave a Comment