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.

Nullbody Notes
Nullbody Notes
Nullbody Notes
How to Build an Expiring Single-Node Lock in Go

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ConcurrencyGoLockExpirationsingle-process
Nullbody Notes
Written by

Nullbody Notes

Go backend development, learning open-source project source code together, focusing on simplicity and practicality.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.