or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

index.md
tile.json

tessl/golang-github-com-fsnotify--fsnotify

Cross-platform file system notifications for Go

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
golangpkg:golang/github.com/fsnotify/fsnotify@1.9.x

To install, run

npx @tessl/cli install tessl/golang-github-com-fsnotify--fsnotify@1.9.0

index.mddocs/

fsnotify

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

Package Information

  • Package Name: fsnotify
  • Package Type: golang
  • Language: Go
  • Import Path: github.com/fsnotify/fsnotify
  • Installation: go get github.com/fsnotify/fsnotify
  • Minimum Go Version: 1.17

Core Imports

import "github.com/fsnotify/fsnotify"

Basic Usage

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{})
}

Capabilities

Creating a Watcher

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.

Adding Paths to Watch

Add paths to monitor for filesystem changes.

func (w *Watcher) Add(path string) error

Starts 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) error

Like Add, but allows passing options. Available options:

  • WithBufferSize(bytes int) - Sets the ReadDirectoryChangesW buffer size (Windows only, no-op on other platforms)

Removing Paths from Watch

Stop monitoring a path for changes.

func (w *Watcher) Remove(path string) error

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

Listing Watched Paths

Get all currently watched paths.

func (w *Watcher) WatchList() []string

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

Closing the Watcher

Clean up and release watcher resources.

func (w *Watcher) Close() error

Removes all watches and closes the Events channel. After calling Close, no more events will be sent.

Receiving Events

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.

Checking Event Operations

Check which operations triggered an event.

func (e Event) Has(op Op) bool

Returns 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) bool

Returns true if this operation contains the specified operation flag.

Configuration Options

Options for configuring watcher behavior.

func WithBufferSize(bytes int) addOpt

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

Types

Watcher

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:

  • Linux: Uses inotify. Limited by fs.inotify.max_user_watches and fs.inotify.max_user_instances sysctls.
  • BSD/macOS: Uses kqueue. Opens a file descriptor per watched file; limited by system file descriptor limits.
  • Windows: Uses ReadDirectoryChangesW. Default 64K buffer; can be increased with WithBufferSize.
  • Network filesystems: NFS, SMB, FUSE generally do not work.
  • Special filesystems: /proc, /sys do not work.

Event

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() string

Returns a string representation of the event including its operations and path.

Op

type Op uint32

Describes 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:

  • Create: A new path was created. May be followed by Write events if data is written.
  • Write: A file or named pipe was written to. Truncate also triggers Write. A single user action may generate multiple Write events. Some systems send Write for directories when directory content changes.
  • Remove: A path was removed. Watches on the path are automatically removed.
  • Rename: A path was renamed. The event contains the old path in Name. A Create event will be sent with the new name. Watches on the path are automatically removed. Only sent for paths currently watched.
  • Chmod: File attributes changed. Often triggered frequently by indexing software, anti-virus, backups. Generally not recommended for action. On Linux, also sent when a file is removed. On kqueue, sent when a file is truncated. Never sent on Windows.
func (o Op) String() string

Returns a string representation of the operation(s).

Error Values

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")
)

Debug Support

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"

Platform-Specific Behaviors

Linux (inotify)

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 event

Resource Limits:

  • fs.inotify.max_user_watches: Max watches per user
  • fs.inotify.max_user_instances: Max watcher instances per user
  • Each Watcher is an "instance", each path is a "watch"
  • Exposed in /proc as /proc/sys/fs/inotify/max_user_watches and /proc/sys/fs/inotify/max_user_instances

To increase limits:

sysctl fs.inotify.max_user_watches=124983
sysctl fs.inotify.max_user_instances=128

To persist across reboots, edit /etc/sysctl.conf:

fs.inotify.max_user_watches=124983
fs.inotify.max_user_instances=128

Reaching the limit results in "no space left on device" or "too many open files" errors.

BSD and macOS (kqueue)

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 files
  • kern.maxfilesperproc: Max open files per process
  • /etc/login.conf: Per-user limits (BSD)

Windows (ReadDirectoryChangesW)

  • Paths can use backslashes (C:\path\to\dir) or forward slashes (C:/path/to/dir)
  • When a watched directory is removed, it always sends an event for the directory itself
  • May not send events for all files in a removed directory (sometimes all, sometimes none, often partial)
  • Default buffer size is 64K, guaranteed to work with SMB filesystems
  • Increase buffer with WithBufferSize if hitting overflow errors

illumos (FEN)

File Events Notification API support for illumos-based systems.

Common Patterns

Watching Directories

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
        }
    }
}

Handling Multiple Operations

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
}

Deduplicating Write Events

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
    }
}

Recursive Watching

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.

Limitations

  • Network filesystems: NFS, SMB, FUSE generally do not support filesystem notifications
  • Special filesystems: /proc, /sys do not support notifications
  • Recursive watching: Not automatic; must manually add each subdirectory
  • File atomicity: Watching individual files doesn't work well due to atomic updates
  • Platform differences: Behavior varies across platforms (see platform-specific sections)