74 lines
2.9 KiB
Markdown
74 lines
2.9 KiB
Markdown
# custom-rwmutex
|
||
|
||
## Overview
|
||
`CustomRwMutex` is a lightweight, type-safe wrapper around Go’s standard `sync.RWMutex` that provides safer, more ergonomic access patterns through handler functions. Instead of manually calling `Lock()`/`RLock()` and `Unlock()`/`RUnlock()`, you pass a function to be executed under the appropriate lock, ensuring proper resource management and reducing the risk of deadlocks or unlocked access.
|
||
|
||
This pattern enforces **scope-bound locking**, similar to a `defer`-based RAII pattern, making concurrent code more readable and less error-prone.
|
||
|
||
## Features
|
||
- ✅ **Thread-safe**: Built on `sync.RWMutex`, guarantees safe concurrent access.
|
||
- ✅ **Handler-based API**: Prevents manual lock/unlock mistakes.
|
||
- ✅ **Error propagation**: Functions passed in can return errors; they’re propagated up cleanly.
|
||
- ✅ **No external dependencies**: Pure Go, zero dependencies.
|
||
- ✅ **Minimal overhead**: Just a thin wrapper around standard library primitives.
|
||
|
||
### Real-World Example: Shared Config
|
||
|
||
```go
|
||
type Config struct {
|
||
Host string
|
||
Port int
|
||
}
|
||
|
||
func main() {
|
||
config := &Config{Host: "localhost", Port: 8080}
|
||
mu := custom_rwmutex.NewCustomRwMutex()
|
||
|
||
// Update config (writer)
|
||
err := mu.WriteHandler(func() error {
|
||
config.Host = "api.example.com"
|
||
config.Port = 443
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
// Read config (reader)
|
||
err = mu.ReadHandler(func() error {
|
||
fmt.Printf("Current config: %s:%d\n", config.Host, config.Port)
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
}
|
||
```
|
||
|
||
> **Note**: Since `config` is captured in closures, ensure it’s not modified outside the lock context. For full safety, consider returning copies of data from `ReadHandler`.
|
||
|
||
## Why Use `CustomRwMutex`?
|
||
|
||
| Problem with `sync.RWMutex` | How `CustomRwMutex` Solves It |
|
||
|-----------------------------|-------------------------------|
|
||
| Manual `Lock()`/`Unlock()` can be forgotten or mismatched | Automatically handled via `defer` |
|
||
| Risk of leaving locks held due to early returns | `defer` guarantees unlock even on error |
|
||
| No built-in error handling for operations | Functions return errors; easily propagated |
|
||
| Verbose and error-prone patterns | Clean, functional-style API |
|
||
|
||
## Best Practices
|
||
|
||
- ✅ **Keep handlers short and fast** — long operations under write locks block all readers and writers.
|
||
- ✅ **Avoid blocking calls** like `time.Sleep()` or HTTP requests in locked sections.
|
||
- ✅ **Return errors from handlers** — they’re preserved and returned from `ReadHandler`/`WriteHandler`.
|
||
- ✅ **Do not hold references to external data** outside the handler unless you are *certain* they are thread-safe or copied.
|
||
- ✅ **For heavy read workloads**, consider returning a copy of the data instead of exposing references:
|
||
|
||
```go
|
||
var config Data
|
||
err := mu.ReadHandler(func() error {
|
||
result := config // copy the data
|
||
// do work on result
|
||
return nil
|
||
})
|
||
``` |