How to Implement Cross-Platform File Locking Using the flock Package

The flock package from aperturerobotics/util provides a lightweight, goroutine-safe abstraction for exclusive file locking that works on Unix, Windows, and unsupported platforms via a single Flock type with TryLock() and Lock() methods.

The aperturerobotics/util repository offers a production-ready solution for cross-platform file locking in Go. This compact package encapsulates platform-specific syscalls behind a unified API centered on the Flock struct. Whether you need immediate non-blocking checks or cancellable blocking waits, the implementation handles Unix, Windows, and graceful degradation on unsupported systems.

Core Architecture and API Design

The package centers around the Flock struct defined in flock/flock.go (lines 11-18), which encapsulates all state required to manage an exclusive lock safely across goroutines and processes.

The Flock Struct and Constructor

The struct holds the absolute file path, an internal sync.Mutex, the underlying *os.File handle, and a boolean flag tracking local ownership. You initialize a new instance via the New(path string) constructor located at lines 20-24:

fl := flock.New("/var/run/myapp.lock")

This allocates metadata immediately but defers actual file creation until the first lock attempt. The Path() method (lines 26-29) returns the resolved absolute path for debugging purposes.

Lock State Inspection

The Locked() method (lines 31-37) provides a thread-safe query of the in-process lock state. Important: This inspects only the local flag, not the kernel lock table, meaning the result can become stale immediately after reading.

Lock Acquisition Methods

The package offers two primary acquisition strategies implemented in flock/flock.go:

  • TryLock() (bool, error) – A non-blocking attempt implemented separately for each platform. It returns true if the lock is acquired or if this instance already holds it. On unsupported platforms, it returns false and ErrUnsupported.
  • Lock(ctx context.Context) error – A blocking acquisition that respects cancellation (lines 39-71). This method implements an exponential backoff loop (50 ms → 200 ms) calling TryLock() until success or context expiry.

Platform-Specific Implementation Details

The package uses build tags to select the correct syscall implementation while maintaining a consistent public API.

Unix Implementation

In flock/flock_unix.go (lines 15-41), the implementation opens the file with O_RDONLY and invokes golang.org/x/sys/unix.Flock with LOCK_EX|LOCK_NB for non-blocking exclusive locks. The Unlock() method (lines 43-63) releases the kernel lock and closes the file descriptor.

Windows Implementation

The Windows variant in flock/flock_windows.go (lines 15-53) requires the file to be opened with O_RDWR because the Windows API demands a writable handle. It calls windows.LockFileEx with LOCKFILE_EXCLUSIVE_LOCK|LOCKFILE_FAIL_IMMEDIATELY. The corresponding Unlock() (lines 55-81) invokes windows.UnlockFileEx.

Unsupported Platforms

For platforms without native support, flock/flock_other.go provides stub implementations that return ErrUnsupported, allowing applications to detect the limitation and implement alternative strategies.

Implementation Patterns

Non-Blocking Try-Lock Pattern

Use this pattern when you want to fail fast if another process holds the lock:

func doExclusiveWork(path string) error {
    f := flock.New(path)
    defer f.Unlock() // Safe no-op if lock was never obtained

    locked, err := f.TryLock()
    if err != nil {
        return fmt.Errorf("try lock error: %w", err)
    }
    if !locked {
        return fmt.Errorf("resource is already locked")
    }

    fmt.Println("exclusive work running")
    // ... critical section ...
    return nil
}

Blocking Lock with Context

For long-running operations where waiting is acceptable, use the context-aware blocking method:

func runWithLock(path string) error {
    f := flock.New(path)
    defer f.Unlock()

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := f.Lock(ctx); err != nil {
        return fmt.Errorf("could not obtain lock: %w", err)
    }

    fmt.Println("got lock, proceeding")
    // ... exclusive work ...
    return nil
}

Detecting Unsupported Platforms

Always handle the ErrUnsupported case for portable applications:

func tryLockPortable(path string) {
    f := flock.New(path)

    ok, err := f.TryLock()
    if errors.Is(err, flock.ErrUnsupported) {
        fmt.Println("file locking not supported on this OS")
        return
    }
    if err != nil {
        log.Fatalf("unexpected error: %v", err)
    }
    if ok {
        defer f.Unlock()
        fmt.Println("lock acquired")
    }
}

Summary

  • The Flock struct in flock/flock.go encapsulates all lock state with an internal mutex for goroutine safety.
  • TryLock() provides immediate non-blocking acquisition, while Lock(ctx) offers cancellable blocking waits with exponential backoff.
  • Unix systems use unix.Flock with read-only file handles; Windows requires LockFileEx with read-write handles.
  • The Unlock() method is idempotent and safe to defer even when the lock was never acquired.
  • Unsupported platforms return ErrUnsupported, allowing graceful fallback logic.

Frequently Asked Questions

How does the flock package ensure thread safety across goroutines?

The Flock struct embeds a sync.Mutex that protects the file handle and ownership flag during concurrent access. According to the source in flock/flock.go, this ensures that Unlock() remains idempotent and that state transitions are atomic within the process. However, the underlying OS lock provides the actual cross-process exclusivity.

What is the difference between Lock() and TryLock() in the flock package?

TryLock() attempts acquisition once and returns immediately with a boolean indicating success, making it suitable for fail-fast scenarios. Lock() accepts a context.Context and blocks indefinitely—retrying with exponential backoff (50 ms to 200 ms)—until the lock is obtained, the context is cancelled, or a terminal error occurs.

How does the package handle platforms that do not support file locking?

For unsupported operating systems, the build-constrained file flock/flock_other.go provides stub implementations that return ErrUnsupported. This allows code to compile on any platform while giving runtime visibility into the lack of native locking support, enabling applications to switch to alternative coordination mechanisms.

Is the Locked() method reliable for checking lock status in real-time?

No. As documented in flock/flock.go (lines 31-37), Locked() inspects only the local boolean flag, not the kernel lock table. The value can become stale instantly upon return, so it should be used only for diagnostic purposes, not for synchronization decisions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →