File Lock Guard

Take an exclusive non-blocking flock around a critical section, reject a second holder, and reacquire after release.

GoSecurity Boundaries

Code

snippet
package main

import (
	"fmt"
	"os"
	"path/filepath"
	"syscall"
)

type FileLock struct {
	file *os.File
	path string
}

func AcquireFileLock(path string) (*FileLock, error) {
	file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
	if err != nil {
		return nil, fmt.Errorf("open lock file: %w", err)
	}
	if err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
		file.Close()
		return nil, fmt.Errorf("lock held elsewhere: %w", err)
	}
	return &FileLock{file: file, path: path}, nil
}

func (l *FileLock) Release() error {
	if err := syscall.Flock(int(l.file.Fd()), syscall.LOCK_UN); err != nil {
		l.file.Close()
		return fmt.Errorf("unlock: %w", err)
	}
	return l.file.Close()
}

func withLock(path string, work func() error) error {
	lock, err := AcquireFileLock(path)
	if err != nil {
		return err
	}
	defer lock.Release()
	return work()
}

func main() {
	path := filepath.Join(os.TempDir(), "devexamples-file-lock.lock")
	lock, err := AcquireFileLock(path)
	if err != nil {
		fmt.Println("first acquire failed:", err)
		return
	}
	fmt.Println("locked:", filepath.Base(lock.path))

	if _, err := AcquireFileLock(path); err != nil {
		fmt.Println("second acquire rejected:", err != nil)
	}

	if err := lock.Release(); err != nil {
		fmt.Println("release failed:", err)
	}
	reopened, err := AcquireFileLock(path)
	fmt.Println("reacquired:", err == nil)
	if err == nil {
		_ = reopened.Release()
	}
}