Help us shape the future of Go! Take the 2026 Go Developer Survey

lock

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package lock excludes other processes from a directory with an flock(2) advisory lock.

The kernel drops the lock when its holder exits or crashes, so there is no stale-lock detection. Locker is the seam: FileLocker is the flock implementation, and tests substitute their own. With runs a function under a Locker and is re-entrant through its context.

Index

Examples

Constants

View Source
const FileName = ".gotools.flock"

FileName is the lock file inside the locked directory. It is never deleted: removing it races with a waiting process.

Variables

View Source
var ErrHeld = errors.New("lock held by another process")

ErrHeld matches the error Lock returns when another holder is still there at the deadline. The error is a Lock error from package errors.

View Source
var ErrUnavailable = errors.New("lock unavailable")

ErrUnavailable matches the error Lock returns when the lock cannot be set up: the directory or file cannot be created, or locking fails. The error is an Environment error from package errors.

Functions

func With

func With(ctx context.Context, l Locker, dir string, fn func(context.Context) error) error

With runs fn while holding l, releasing it on return or panic. dir names the locked directory; it keys re-entrancy.

It is re-entrant through ctx: fn's ctx records dir, and a With on that ctx for the same directory runs fn directly. Without that, a nested call would open a second file description, which flock treats as another holder, and wait on itself. Code that drops the ctx is another holder.

locker := lock.NewFileLocker(dir, lock.WithTimeout(10*time.Second))

err := lock.With(ctx, locker, dir, func(ctx context.Context) error {
	// Work on dir. Pass ctx on: a nested lock.With for dir runs directly.
	return sync(ctx)
})
if errors.Is(err, lock.ErrHeld) {
	// another process is still running
}
Example

With holds the lock for the duration of fn. The ctx it passes to fn records the held directory, so a function fn calls can take the same lock again without waiting on itself.

package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/piusalfred/gotools/internal/lock"
)

func main() {
	dir, _ := os.MkdirTemp("", "lock-example")
	defer os.RemoveAll(dir)

	locker := lock.NewFileLocker(dir, lock.WithTimeout(time.Second))

	err := lock.With(context.Background(), locker, dir, func(ctx context.Context) error {
		fmt.Println("holding the lock")

		// Pass ctx down: this nested call runs directly instead of waiting.
		return lock.With(ctx, locker, dir, func(context.Context) error {
			fmt.Println("nested call runs without waiting")

			return nil
		})
	})
	fmt.Println("error:", err)

	held, _ := locker.Holder()
	fmt.Println("held after return:", held.Held)

}
Output:
holding the lock
nested call runs without waiting
error: <nil>
held after return: false
Example (Busy)

A caller that starts from a fresh context is another holder: while the lock is held, it gets an error matching ErrHeld once its timeout passes.

package main

import (
	"context"
	"errors"
	"fmt"
	"os"

	"github.com/piusalfred/gotools/internal/lock"
)

func main() {
	dir, _ := os.MkdirTemp("", "lock-example")
	defer os.RemoveAll(dir)

	locker := lock.NewFileLocker(dir)

	_ = lock.With(context.Background(), locker, dir, func(context.Context) error {
		err := lock.With(context.Background(), locker, dir, func(context.Context) error {
			return nil
		})
		fmt.Println("busy:", errors.Is(err, lock.ErrHeld))

		return nil
	})

}
Output:
busy: true

Types

type FileLocker

type FileLocker struct {
	// contains filtered or unexported fields
}

FileLocker is the Locker that holds an flock(2) on a file in a directory. Build it with NewFileLocker; the fields are private so that settings can be added as options without breaking callers.

func NewFileLocker

func NewFileLocker(dir string, opts ...Option) FileLocker

NewFileLocker returns the lock for the directory dir. It touches nothing on disk until Lock.

func (FileLocker) Holder

func (l FileLocker) Holder() (Holder, error)

Holder implements Locker. A missing lock file means nobody holds the lock, and nothing is created. The check takes the lock for an instant and gives it back. It ignores the disabled setting: it describes the directory, not this process's settings.

func (FileLocker) Lock

func (l FileLocker) Lock(ctx context.Context) (*Lock, error)

Lock implements Locker. It waits up to the configured timeout, in real time, for another holder. The holder's pid is written into the file for messages only; correctness never depends on it.

type HeldError

type HeldError struct {
	Path string // the lock file
	PID  int    // the recorded holder; 0 when unknown
}

HeldError is the error behind ErrHeld.

func (*HeldError) Error

func (e *HeldError) Error() string

func (*HeldError) Is

func (e *HeldError) Is(target error) bool

type Holder

type Holder struct {
	Held bool // another process holds the lock right now
	PID  int  // the recorded holder; 0 when nobody holds it or it is unknown
}

Holder describes who holds a directory's lock.

func (Holder) String

func (h Holder) String() string

String formats the holder for messages, e.g. " (pid 123)", or "" when the pid is unknown.

type Lock

type Lock struct {
	// contains filtered or unexported fields
}

Lock is a held lock. The zero value and a nil *Lock unlock as no-ops.

func New

func New(release func()) *Lock

New returns a Lock that calls release once, on the first Unlock. It is for Locker implementations; release must drop whatever Lock took.

func (*Lock) Unlock

func (l *Lock) Unlock()

Unlock drops the lock. It is safe to call more than once.

type Locker

type Locker interface {
	// Lock takes the lock, waiting for another holder as the implementation
	// is configured to. It returns an error matching ErrHeld when a holder
	// was still there at the deadline, ErrUnavailable when the lock could not
	// be set up, and ctx.Err() when ctx is done.
	Lock(ctx context.Context) (*Lock, error)
	// Holder reports whether another process holds the lock, without taking
	// it or creating anything.
	Holder() (Holder, error)
}

Locker is the lock on one directory. It excludes other processes, not other goroutines of this one.

type Option

type Option func(*FileLocker)

Option configures a FileLocker.

func WithDisabled

func WithDisabled(disabled bool) Option

WithDisabled makes Lock succeed without locking.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets how long Lock waits for another holder. The default, 0, tries exactly once.

type UnavailableError

type UnavailableError struct {
	Path string // the directory or file that failed
	Op   string // what was attempted
	Err  error
}

UnavailableError is the error behind ErrUnavailable.

func (*UnavailableError) Error

func (e *UnavailableError) Error() string

func (*UnavailableError) Is

func (e *UnavailableError) Is(target error) bool

func (*UnavailableError) Unwrap

func (e *UnavailableError) Unwrap() error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL