How to Build an Expiring Single-Node Lock in Go
This article explains how to implement a single‑process lock in Go that records an owner, allows only the owner to unlock, and automatically releases the lock after a specified timeout, providing full source code and a walkthrough of the key functions.
Purpose of the single‑machine lock
When a lock is acquired it records an owner identifier; only the recorded owner may release the lock; and, if a positive timeout is supplied, the lock is automatically released when the timeout expires, preventing forgotten locks.
Implementation overview
The implementation is a single Go source file (repository: https://github.com/gofish2020/expiredlock) that defines an ExpiredLocker type.
package expiredlock
import (
"bytes"
"context"
"fmt"
"os"
"runtime"
"strconv"
"sync"
"time"
)
/*
Implement a single‑machine lock with automatic expiration.
Only the lock owner can unlock.
*/
type ExpiredLocker struct {
mutex sync.Mutex // the lock itself
processMutex sync.Mutex // protects fields below
owner string // lock owner (process‑id + goroutine‑id)
cancel context.CancelFunc // cancels the expiration goroutine
}
func (l *ExpiredLocker) Lock(expireTime time.Duration) {
// acquire the lock
l.mutex.Lock()
l.processMutex.Lock()
defer l.processMutex.Unlock()
// unique identifier for the successful lock acquisition
l.owner = getPidGid()
if expireTime <= 0 { // no expiration needed
return
}
ctx, cancel := context.WithTimeout(context.Background(), expireTime)
l.cancel = cancel
go func() {
<-ctx.Done()
if ctx.Err() == context.DeadlineExceeded { // expiration triggered
l.unlocker(l.owner)
}
}()
}
func (l *ExpiredLocker) UnLock() {
// user‑initiated unlock; also stops the expiration goroutine if it was started
l.unlocker(getPidGid())
}
func (l *ExpiredLocker) unlocker(owner string) {
l.processMutex.Lock()
defer l.processMutex.Unlock()
// must be the lock owner
if l.owner != "" && l.owner == owner {
l.owner = ""
if l.cancel != nil { // this was an expiring lock
l.cancel() // stop the expiration goroutine
l.cancel = nil
}
l.mutex.Unlock() // release the lock
}
}
// combine process id and goroutine id as a unique identifier
func getPidGid() string {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
b = b[:bytes.IndexByte(b, ' ')]
n, _ := strconv.ParseUint(string(b), 10, 64)
return fmt.Sprintf("%d_%d", os.Getpid(), n)
}Step‑by‑step behavior
Lock acquisition : Lock first calls l.mutex.Lock() to obtain the exclusive lock, then records the caller’s identifier (process‑id + goroutine‑id) in l.owner.
Expiration handling : If expireTime is greater than zero, a context.WithTimeout is created. The returned cancel function is stored in l.cancel. A background goroutine blocks on <-ctx.Done(); when the deadline is reached it invokes l.unlocker(l.owner), which releases the lock automatically.
Manual unlock : UnLock calls unlocker with the current caller’s identifier. unlocker verifies that the supplied identifier matches l.owner, clears the owner field, cancels the expiration context if it exists, and finally calls l.mutex.Unlock() to free the lock.
Owner identifier generation : getPidGid obtains the current goroutine number from runtime.Stack, trims the prefix, parses the numeric ID, and formats it together with the process ID as "pid_gid" (e.g., 1234_56), guaranteeing uniqueness among goroutines in the same process.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Nullbody Notes
Go backend development, learning open-source project source code together, focusing on simplicity and practicality.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
