fsnotify provides a cross-platform interface for file system notifications in Go. It enables monitoring of filesystem events (create, write, remove, rename, chmod) across multiple operating systems with a unified API. The library handles platform-specific implementations transparently, supporting Linux (inotify), BSD/macOS (kqueue), Windows (ReadDirectoryChangesW), and illumos (FEN).
github.com/fsnotify/fsnotifygo get github.com/fsnotify/fsnotifyimport "github.com/fsnotify/fsnotify"package main
import (
"log"
"github.com/fsnotify/fsnotify"
)
func main() {
// Create new watcher
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
// Start listening for events
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
log.Println("event:", event)
if event.Has(fsnotify.Write) {
log.Println("modified file:", event.Name)
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("error:", err)
}
}
}()
// Add a path to watch
err = watcher.Add("/tmp")
if err != nil {
log.Fatal(err)
}
// Block main goroutine
<-make(chan struct{})
}Create a new filesystem watcher that monitors paths for changes.
func NewWatcher() (*Watcher, error)Creates a watcher with default buffer size. Returns an error if the underlying OS watcher cannot be created.
func NewBufferedWatcher(sz uint) (*Watcher, error)Creates a watcher with a buffered Events channel. The sz parameter sets the channel buffer size. This is useful for situations with very large numbers of events where the kernel buffer size can't be increased. An unbuffered watcher (NewWatcher) performs better for most use cases.
Add paths to monitor for filesystem changes.
func (w *Watcher) Add(path string) errorStarts monitoring the specified path for changes. A path can only be watched once; watching it multiple times is a no-op. Paths that do not exist cannot be watched. Returns ErrClosed if the watcher has been closed.
Directory Watching: All files in a directory are monitored, including new files created after the watcher starts. Subdirectories are NOT watched (non-recursive).
File Watching: Not recommended. Many programs update files atomically by writing to a temporary file and then moving it, which breaks the watch. Instead, watch the parent directory and filter events by filename.
func (w *Watcher) AddWith(path string, opts ...addOpt) errorLike Add, but allows passing options. Available options:
WithBufferSize(bytes int) - Sets the ReadDirectoryChangesW buffer size (Windows only, no-op on other platforms)Stop monitoring a path for changes.
func (w *Watcher) Remove(path string) errorStops monitoring the specified path. Directories are always removed non-recursively. Returns ErrNonExistentWatch if the path was not being watched. Returns nil if the watcher has been closed.
Get all currently watched paths.
func (w *Watcher) WatchList() []stringReturns all paths explicitly added with Add or AddWith that have not been removed. The order is undefined and may differ between calls. Returns nil if the watcher has been closed.
Clean up and release watcher resources.
func (w *Watcher) Close() errorRemoves all watches and closes the Events channel. After calling Close, no more events will be sent.
Events are delivered through channels on the Watcher struct.
type Watcher struct {
Events chan Event // Filesystem change events
Errors chan error // Error notifications
}Important: You must read from both channels in a goroutine. If you don't read from the channels, the watcher will block and stop processing events.
Check which operations triggered an event.
func (e Event) Has(op Op) boolReturns true if the event contains the specified operation. Use this method instead of direct comparison since Op is a bitmask and multiple operations can be set simultaneously.
func (o Op) Has(h Op) boolReturns true if this operation contains the specified operation flag.
Options for configuring watcher behavior.
func WithBufferSize(bytes int) addOptSets the ReadDirectoryChangesW buffer size for Windows. This is a no-op on other platforms. The default is 64K (65536 bytes). Increase this if you encounter ErrEventOverflow errors. Only affects Windows systems.
type Watcher struct {
// Events sends the filesystem change events.
// Must be read in a goroutine to prevent blocking.
Events chan Event
// Errors sends any errors that occur during watching.
// Must be read in a goroutine to prevent blocking.
Errors chan error
}The main watcher type. Should not be copied; pass by pointer. A watch is automatically removed if the watched path is deleted or renamed (except on Windows, which doesn't remove on renames).
Platform-specific notes:
fs.inotify.max_user_watches and fs.inotify.max_user_instances sysctls.type Event struct {
Name string // Path to the file or directory
Op Op // File operation that triggered the event (bitmask)
}Represents a filesystem notification. The Name field contains the path relative to the input (e.g., if you Add("dir"), events will have Name "dir/file").
The Op field is a bitmask that may contain multiple operations. Always use the Has() method to check for operations instead of direct comparison.
func (e Event) String() stringReturns a string representation of the event including its operations and path.
type Op uint32Describes file operations as a bitmask. Multiple operations can be combined.
Operation Constants:
const (
Create Op = 1 << iota // New pathname created
Write // Pathname written to (doesn't mean write finished)
Remove // Path removed (watch automatically removed)
Rename // Path renamed (watch automatically removed)
Chmod // File attributes changed
)Operation Descriptions:
func (o Op) String() stringReturns a string representation of the operation(s).
var (
// Returned when Remove() is called on a path that hasn't been added
ErrNonExistentWatch = errors.New("fsnotify: can't remove non-existent watch")
// Returned when operating on a closed Watcher
ErrClosed = errors.New("fsnotify: watcher already closed")
// Sent on Errors channel when there are too many events:
// - inotify: IN_Q_OVERFLOW (increase fs.inotify.max_queued_events)
// - Windows: Buffer too small (use WithBufferSize to increase)
// - kqueue/fen: Not used
ErrEventOverflow = errors.New("fsnotify: queue or buffer overflow")
)Set the FSNOTIFY_DEBUG environment variable to "1" to enable debug output to stderr. This prints all events with minimal processing as they occur, which is useful for troubleshooting.
Example debug output:
FSNOTIFY_DEBUG: 11:34:23.633087586 256:IN_CREATE → "/tmp/file-1"
FSNOTIFY_DEBUG: 11:34:23.633202319 4:IN_ATTRIB → "/tmp/file-1"
FSNOTIFY_DEBUG: 11:34:28.989728764 512:IN_DELETE → "/tmp/file-1"When a file is removed, a Remove event won't be emitted until all file descriptors are closed. Deletes always emit a Chmod first:
fp := os.Open("file")
os.Remove("file") // Triggers Chmod event
fp.Close() // Triggers Remove eventResource Limits:
fs.inotify.max_user_watches: Max watches per userfs.inotify.max_user_instances: Max watcher instances per user/proc/sys/fs/inotify/max_user_watches and /proc/sys/fs/inotify/max_user_instancesTo increase limits:
sysctl fs.inotify.max_user_watches=124983
sysctl fs.inotify.max_user_instances=128To persist across reboots, edit /etc/sysctl.conf:
fs.inotify.max_user_watches=124983
fs.inotify.max_user_instances=128Reaching the limit results in "no space left on device" or "too many open files" errors.
kqueue requires opening a file descriptor for every watched file. Watching a directory with 5 files requires 6 file descriptors. You'll hit the system's "max open files" limit faster.
Resource Limits:
kern.maxfiles: System-wide max open fileskern.maxfilesperproc: Max open files per process/etc/login.conf: Per-user limits (BSD)C:\path\to\dir) or forward slashes (C:/path/to/dir)WithBufferSize if hitting overflow errorsFile Events Notification API support for illumos-based systems.
Always watch directories, not individual files. Filter events by checking the Event.Name field:
watcher.Add("/path/to/dir")
for {
select {
case event := <-watcher.Events:
if event.Name == "/path/to/dir/specific-file.txt" {
// Handle event for specific file
}
}
}Events can contain multiple operations. Check each one:
event := <-watcher.Events
if event.Has(fsnotify.Write) {
// Handle write
}
if event.Has(fsnotify.Chmod) {
// Handle attribute change
}A single user action can generate many Write events. Consider debouncing:
timer := time.NewTimer(time.Millisecond * 100)
for {
select {
case event := <-watcher.Events:
if event.Has(fsnotify.Write) {
timer.Reset(time.Millisecond * 100)
}
case <-timer.C:
// No more events received, perform action
}
}fsnotify does not watch subdirectories automatically. To watch recursively, walk the directory tree and add each directory:
err := filepath.Walk("/path/to/watch", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return watcher.Add(path)
}
return nil
})Remember to add new directories as they're created by watching for Create events and checking if they're directories.