Documentation
¶
Overview ¶
Package flock implements a simple interface to flock(2) and fcntl(2) open file description locks on Linux.
Lock Semantics ¶
These locks are linked to file descriptors not processes. That means a lock can be inherited by child processes. It can be sent to unrelated processes via sendmsg(2) or similar. The file descriptor duplicated via dup(2) similarly inherits the lock. In all these cases, the lock is then shared by all file descriptors. If a lock is held this way by a number of file descriptors, it is released either when ANY one calls *Lock.Unlock or *Lock.UnlockRange (or the corresponding syscalls) or ALL of the file descriptors are closed.
These locks are advisory locks. They do not prevent any operation on the related files. The locks implemented by *Lock.LockEx, *Lock.LockSh and *Lock.Unlock represent exactly one lock per file. They are based on flock(2) and are somewhat portable across POSIX OS.
Range locks as provided by *Lock.LockRangeEx, *Lock.LockRangeSh and *Lock.UnlockRange are based on fcntl(2) and specific to Linux. They allow multiple independent locks on the same file descriptor.
fcntl(2) implements similar process based locks, see F_SETLK, F_SETLKW, and F_GETLK on the man page. These locks are NOT implemented by this package because their semantic is different.
Range Locks ¶
Range locks are good if you want to tie multiple locks to the same file. Every file position can be an independent lock. The lock range is not related to the file size. The file can have a size of 0 while you lock the range from 100 to 200. If you just need a single lock file, use flock(2) based lock.
Blocking ¶
This package has Lock... functions and TryLock... versions. They behave the same except that the Lock version waits until the lock becomes available while TryLock returns an error in this case.
In most cases, you likely don't want to use the blocking version in a Go program.
There is no context.Context based version or any other way to wait for a lock with a timeout. The only reliable way to implement that is by polling.
Ownership ¶
A Lock object might or might not OWN its file descriptor. If the object owns it, then the file descriptor is closed when *Lock.Close is called. Otherwise, the owner of the descriptor is responsible for closing it. See *Lock.File and WithFd for more information.
As a general rule, the Lock object owns the file descriptor if it has opened it.
Thread safety ¶
The internal data structure is NOT protected by any means.
Example (Flock) ¶
//go:build linux
/*
Copyright 2026 Torsten Foertsch
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"github.com/tfoertsch123/flock"
"log"
"os"
"os/exec"
"sync"
)
func start(cmdline string) *exec.Cmd {
cmd := exec.Command("sh", "-c", cmdline)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
log.Fatalf(`"%s" failed: %v`, cmdline, err)
}
return cmd
}
func main() {
var wg sync.WaitGroup
lck := flock.New(
flock.WithPath("."),
flock.WithIsDir(), // require path to be a directory
)
// Open the file/directory and acquire a shared lock.
if err := lck.TryLockSh(); err != nil {
log.Fatalf(`Couldn't lock(p1) .: %v`, err)
}
// The shell inherits the lock. (no WithCloseOnExec())
p1 := start("sleep .5; echo P1: slept")
wg.Go(func() { p1.Wait() })
// this only closes the file. It does not release the lock
lck.Close()
lck = flock.New(
flock.WithPath("."),
)
// Just open, no lock
if err := lck.Open(); err != nil {
log.Fatalf(`Couldn't open(p2) .: %v`, err)
}
// "flock -s" at the shell level is the same as flock.LockSh().
// This acquires a shared lock with does not conflict with the
// lock held by p1. So, the "echo" will not wait for p1 to finish.
p2 := start(fmt.Sprintf("flock -s %d; echo P2: lock acquired", lck.Fd()))
wg.Go(func() { p2.Wait() })
lck.Close()
// Open the file yet another time
lck = flock.New(
flock.WithPath("."),
)
// Just open
if err := lck.Open(); err != nil {
log.Fatalf(`Couldn't open(p3) .: %v`, err)
}
// "flock -x" at the shell level is the same as flock.LockEx().
// This acquires an exclusive lock with does conflict with the
// lock held by p1. So, the "echo" will wait for p1 to finish.
p3 := start(fmt.Sprintf("flock -x %d; echo P3: lock acquired", lck.Fd()))
wg.Go(func() { p3.Wait() })
lck.Close()
wg.Wait()
}
// Local Variables:
// tab-width: 4
// End:
Output: P2: lock acquired P1: slept P3: lock acquired
Index ¶
- Constants
- Variables
- type Lock
- func (l *Lock) Close() error
- func (l *Lock) Fd() int
- func (l *Lock) File() *os.File
- func (l *Lock) LockEx() error
- func (l *Lock) LockRangeEx(start, len int64, whence ...int16) error
- func (l *Lock) LockRangeSh(start, len int64, whence ...int16) error
- func (l *Lock) LockSh() error
- func (l *Lock) Open() error
- func (l *Lock) OwningFd() bool
- func (l *Lock) Path() string
- func (l *Lock) TryLockEx() error
- func (l *Lock) TryLockRangeEx(start, len int64, whence ...int16) error
- func (l *Lock) TryLockRangeSh(start, len int64, whence ...int16) error
- func (l *Lock) TryLockSh() error
- func (l *Lock) Unlock() error
- func (l *Lock) UnlockRange(start, len int64, whence ...int16) error
- type Option
- func WithAppend() Option
- func WithCloseOnExec() Option
- func WithCreate(perms ...uint32) Option
- func WithFd(param int) Option
- func WithIsDir() Option
- func WithPath(param string) Option
- func WithPathAt(param int) Option
- func WithRdWr() Option
- func WithTruncate() Option
- func WithWrOnly() Option
- func WithoutIntrRetry() Option
Examples ¶
Constants ¶
const SEEK_CUR int16 = unix.SEEK_CUR
const SEEK_END int16 = unix.SEEK_END
const SEEK_SET int16 = unix.SEEK_SET
SEEK_SET, SEEK_CUR and SEEK_END represent the same values as used in Linux' lseek(2) syscall. They are used as parameters for range locks like *Lock.TryLockRangeEx.
Variables ¶
var ErrAlreadyOpen error = errors.New("file is already open")
ErrAlreadyOpen is returned by *Lock.Open if the underlying file descriptor is deemed to be open. There is no syscall performed to actually check if the file descriptor is already open. Just if the stored file descriptor number is negative, the error is triggered.
Functions ¶
This section is empty.
Types ¶
type Lock ¶
type Lock struct {
// contains filtered or unexported fields
}
A Lock represents the lock object.
func New ¶
New creates the Lock object. At this time the file is not opened, yet. The file is only opened (unless an open file descriptor is passed), when either *Lock.Open is called or any of the Lock functions is called for the first time.
Example:
lck := flock.New(
flock.WithPath("/my/lock/file"),
flock.WithCreate(unix.S_IRUSR, unix.S_IWUSR),
flock.WithRdWr(),
flock.WithCloseOnExec(),
)
Note, if you use WithFd to pass an open file descriptor, that file descriptor will not be closed by *Lock.Close.
It is cheap to create these objects. No syscall involved.
func (*Lock) Close ¶
Close closes the underlying file descriptor if it is owned by the Lock object. It is not an error to close an already closed object. A subsequent *Lock.Open or any of the locking functions will re-open the file according to the parameters passed to New.
Note, Close does NOT unlock anything prior to closing the file. So, if the file descriptor is still open as in has been inherited by a child process or dup(2)ed or similar, the corresponding locks are still being held.
func (*Lock) Fd ¶
Fd returns the underlying file descriptor number. If the file is closed, -1 is returned.
func (*Lock) File ¶
File returns an *os.File object representing the underlying file. Calling this function disowns the file descriptor. After that, the *os.File object is responsible for closing it. If the file descriptor is already closed nil is returned.
func (*Lock) LockEx ¶
LockEx exclusively locks the file using flock(2). If the file is not yet opened it is opened automatically. The returned error represents either an error in flock(2) itself or a failed open(2). On success nil is returned.
Note, this is a blocking syscall. If the lock is not available, it will block the calling GO routine. There is no good way to interrupt that syscall.
func (*Lock) LockRangeEx ¶
LockRangeEx exclusively locks the range from start to start+len-1. The position at start+len is not locked. So, LockRangeEx(10, 1) locks exactly 1 byte position.
If len==0, the range from the start position up to positive infinity is locked. This does not depend on the file size.
The optional whence parameter defines how to interpret the start parameter. If omitted, SEEK_SET is used. If multiple whence parameters are passed only the first one is used.
- SEEK_SET: start is interpreted relative to the beginning of the file.
- SEEK_CUR: start is interpreted relative to the current file position.
- SEEK_END: start is interpreted relative to the current end of the file.
Both, start and len can be negative as long as the resulting starting position is not negative.
func (*Lock) LockRangeSh ¶
LockRangeSh acquires a shared lock. The parameters are the same as for *Lock.LockRangeEx
func (*Lock) LockSh ¶
LockSh represents a shared lock on the file using flock(2). If the file is not yet opened it is opened automatically. The returned error represents either an error in flock(2) itself or a failed open(2). On success nil is returned.
Note, this is a blocking syscall. If the lock is not available, it will block the calling GO routine. There is no good way to interrupt that syscall.
func (*Lock) Open ¶
Open opens the underlying file. If the file is deemed to be already open, ErrAlreadyOpen is returned. Otherwise, the underlying operating system error is returned. On success, nil is returned.
This function is called automatically when a locking function is called and the file is not yet open.
func (*Lock) OwningFd ¶
OwningFd can be used to determine if *Lock.Close will close the underlying file descriptor.
func (*Lock) TryLockEx ¶
TryLockEx is the non-blocking version of *Lock.LockEx. If the lock cannot be acquired unix.EWOULDBLOCK is returned.
func (*Lock) TryLockRangeEx ¶
TryLockRangeEx is the non-blocking version of *Lock.LockRangeEx. If the lock cannot be acquired unix.EWOULDBLOCK is returned.
func (*Lock) TryLockRangeSh ¶
TryLockRangeSh is the non-blocking version of LockRangeSh. The parameters are the same as for *Lock.LockRangeEx. If the lock cannot be acquired unix.EWOULDBLOCK is returned.
func (*Lock) TryLockSh ¶
TryLockSh is the non-blocking version of *Lock.LockSh. If the lock cannot be acquired unix.EWOULDBLOCK is returned.
func (*Lock) Unlock ¶
Unlock releases the lock. It is not an error to unlock an already released lock.
If the underlying file is closed, it will not be opened and nil is returned.
Any error comes from the underlying flock(2) syscall.
func (*Lock) UnlockRange ¶
UnlockRange releases a range lock. The parameters are the same as for *Lock.LockRangeEx.
If you simply want to release all existing range locks for a file, you can do this:
lck.UnlockRange(0, 0)
This releases all locks no matter if exclusive or shared. Since len==0, the entire range from start up to infinity is released. Since start==0 and no whence parameter is passed implying SEEK_SET, this identifies the start of the file.
type Option ¶
type Option func(*opts)
An Option represents an option passed to the constructor New. All options are functions starting with With...
func WithCloseOnExec ¶
func WithCloseOnExec() Option
WithCloseOnExec corresponds to O_CLOEXEC in open(2).
func WithCreate ¶
WithCreate corresponds to O_CREAT in open(2). The parameters are ORed together to build the file mode. If completely omitted, the default file permissions are 0o600 or unix.S_IRUSR + unix.S_IWUSR or "rw-------".
The example below sets the file mode to 0o603 or "rw-----wx" (which does not make much sense, though)
Example:
lck := flock.New(
flock.WithCreate(unix.S_IRUSR, unix.S_IWUSR, 3),
...
)
func WithFd ¶
WithFd allows you to pass an open file descriptor to the constructor New. This can be combined with WithPath. However, the path in this case will never be used for anything. If a file descriptor is passed this way, it is not owned by the Lock object. So, *Lock.Close will not close the file. All the modifiers for opening the file, will also be ignored, WithRdWr, WithTruncate, ...
Passing a negative parameter, which is an invalid file descriptor, is ignored. *Lock.Fd will return -1.
func WithPath ¶
WithPath passes the name of the file, see New. This is optional. However, if no path is passed, a file descriptor number needs to be provided, see WithFd.
func WithPathAt ¶ added in v1.1.0
WithPathAt optionally passes a directory file descriptor relative to which the path passed with WithPath is interpreted.
func WithoutIntrRetry ¶
func WithoutIntrRetry() Option
WithoutIntrRetry turns off the internal retry mechanism if the syscall comes back with EINTR. Mainly, this applies to the blocking syscalls.