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

> Implement cross-platform file locking with the flock package. Securely manage file access on Unix and Windows with goroutine-safe TryLock and Lock methods.

- Repository: [Aperture Robotics/util](https://github.com/aperturerobotics/util)
- Tags: how-to-guide
- Published: 2026-02-25

---

**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`](https://github.com/aperturerobotics/util/blob/main/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:

```go
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`](https://github.com/aperturerobotics/util/blob/main/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`](https://github.com/aperturerobotics/util/blob/main/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`](https://github.com/aperturerobotics/util/blob/main/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`](https://github.com/aperturerobotics/util/blob/main/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:

```go
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:

```go
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:

```go
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`](https://github.com/aperturerobotics/util/blob/main/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`](https://github.com/aperturerobotics/util/blob/main/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`](https://github.com/aperturerobotics/util/blob/main/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`](https://github.com/aperturerobotics/util/blob/main/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.