├── .gitignore ├── go.mod ├── go.sum ├── .github ├── dependabot.yml └── workflows │ └── go.yml ├── LICENSE ├── list.go ├── fifomu.go ├── README.md ├── fifomu_test.go └── .golangci.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/neilotoole/fifomu 2 | 3 | go 1.20 4 | 5 | require golang.org/x/sync v0.6.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 2 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | pull_request: 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: actions/setup-go@v4 19 | with: 20 | go-version-file: go.mod 21 | 22 | - name: Lint 23 | uses: golangci/golangci-lint-action@v3 24 | 25 | build-test: 26 | strategy: 27 | matrix: 28 | os: [ ubuntu-latest, macos-latest, windows-latest ] 29 | runs-on: ${{ matrix.os }} 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - uses: actions/setup-go@v4 35 | with: 36 | go-version-file: go.mod 37 | 38 | - name: Build 39 | run: go build -v ./... 40 | 41 | - name: Test 42 | run: go test -v ./... 43 | 44 | - name: Benchmark 45 | run: go test -bench=. 46 | 47 | 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Neil O'Toole 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /list.go: -------------------------------------------------------------------------------- 1 | package fifomu 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | var elementPool = sync.Pool{New: func() any { return new(element[waiter]) }} 8 | 9 | // list is a doubly-linked list of type T. 10 | type list[T any] struct { 11 | root element[T] 12 | len uint 13 | } 14 | 15 | func (l *list[T]) lazyInit() { 16 | if l.root.next == nil { 17 | l.root.next = &l.root 18 | l.root.prev = &l.root 19 | l.len = 0 20 | } 21 | } 22 | 23 | // front returns the first element of list l or nil. 24 | func (l *list[T]) front() *element[T] { 25 | if l.len == 0 { 26 | return nil 27 | } 28 | 29 | return l.root.next 30 | } 31 | 32 | // pushBackElem inserts a new element e with value v at 33 | // the back of list l and returns e. 34 | func (l *list[T]) pushBackElem(v T) *element[T] { 35 | l.lazyInit() 36 | 37 | e := elementPool.Get().(*element[T]) //nolint:errcheck 38 | e.Value = v 39 | l.insert(e, l.root.prev) 40 | return e 41 | } 42 | 43 | // pushBack inserts a new element e with value v at 44 | // the back of list l. 45 | func (l *list[T]) pushBack(v T) { 46 | l.lazyInit() 47 | 48 | e := elementPool.Get().(*element[T]) //nolint:errcheck 49 | e.Value = v 50 | l.insert(e, l.root.prev) 51 | } 52 | 53 | // remove removes e from l if e is an element of list l. 54 | func (l *list[T]) remove(e *element[T]) { 55 | if e.list == l { 56 | e.prev.next = e.next 57 | e.next.prev = e.prev 58 | e.next = nil // avoid memory leaks 59 | e.prev = nil // avoid memory leaks 60 | e.list = nil 61 | l.len-- 62 | } 63 | 64 | elementPool.Put(e) 65 | } 66 | 67 | // insert inserts e after at. 68 | func (l *list[T]) insert(e, at *element[T]) { 69 | e.prev = at 70 | e.next = at.next 71 | e.prev.next = e 72 | e.next.prev = e 73 | e.list = l 74 | l.len++ 75 | } 76 | 77 | // element is a node of a linked list. 78 | type element[T any] struct { 79 | next, prev *element[T] 80 | 81 | list *list[T] 82 | 83 | Value T 84 | } 85 | -------------------------------------------------------------------------------- /fifomu.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package fifomu provides a Mutex whose Lock method returns the lock to 6 | // callers in FIFO call order. This is in contrast to sync.Mutex, where 7 | // a single goroutine can repeatedly lock and unlock and relock the mutex 8 | // without handing off to other lock waiter goroutines (that is, until after 9 | // a 1ms starvation threshold, at which point sync.Mutex enters a FIFO 10 | // "starvation mode" for those starved waiters, but that's too late for some 11 | // use cases). 12 | // 13 | // fifomu.Mutex implements the exported methods of sync.Mutex and thus is 14 | // a drop-in replacement (and by extension also implements sync.Locker). 15 | // It also provides a bonus context-aware Mutex.LockContext method. 16 | // 17 | // Note: unless you need the FIFO behavior, you should prefer sync.Mutex. 18 | // For typical workloads, its "greedy-relock" behavior requires less goroutine 19 | // switching and yields better performance. 20 | package fifomu 21 | 22 | import ( 23 | "context" 24 | "sync" 25 | ) 26 | 27 | var _ sync.Locker = (*Mutex)(nil) 28 | 29 | // Mutex is a mutual exclusion lock whose Lock method returns 30 | // the lock to callers in FIFO call order. 31 | // 32 | // A Mutex must not be copied after first use. 33 | // 34 | // The zero value for a Mutex is an unlocked mutex. 35 | // 36 | // Mutex implements the same methodset as sync.Mutex, so it can 37 | // be used as a drop-in replacement. It implements an additional 38 | // method Mutex.LockContext, which provides context-aware locking. 39 | type Mutex struct { 40 | waiters list[waiter] 41 | cur int64 42 | mu sync.Mutex 43 | } 44 | 45 | // Lock locks m. 46 | // 47 | // If the lock is already in use, the calling goroutine 48 | // blocks until the mutex is available. 49 | func (m *Mutex) Lock() { 50 | m.mu.Lock() 51 | if m.cur <= 0 && m.waiters.len == 0 { 52 | m.cur++ 53 | m.mu.Unlock() 54 | return 55 | } 56 | 57 | w := waiterPool.Get().(waiter) //nolint:errcheck 58 | m.waiters.pushBack(w) 59 | m.mu.Unlock() 60 | 61 | <-w 62 | waiterPool.Put(w) 63 | } 64 | 65 | // LockContext locks m. 66 | // 67 | // If the lock is already in use, the calling goroutine 68 | // blocks until the mutex is available or ctx is done. 69 | // 70 | // On failure, LockContext returns context.Cause(ctx) and 71 | // leaves the mutex unchanged. 72 | // 73 | // If ctx is already done, LockContext may still succeed without blocking. 74 | func (m *Mutex) LockContext(ctx context.Context) error { 75 | m.mu.Lock() 76 | if m.cur <= 0 && m.waiters.len == 0 { 77 | m.cur++ 78 | m.mu.Unlock() 79 | return nil 80 | } 81 | 82 | w := waiterPool.Get().(waiter) //nolint:errcheck 83 | elem := m.waiters.pushBackElem(w) 84 | m.mu.Unlock() 85 | 86 | select { 87 | case <-ctx.Done(): 88 | err := context.Cause(ctx) 89 | m.mu.Lock() 90 | select { 91 | case <-w: 92 | // Acquired the lock after we were canceled. Rather than trying to 93 | // fix up the queue, just pretend we didn't notice the cancellation. 94 | err = nil 95 | waiterPool.Put(w) 96 | default: 97 | isFront := m.waiters.front() == elem 98 | m.waiters.remove(elem) 99 | // If we're at the front and there's extra tokens left, 100 | // notify other waiters. 101 | if isFront && m.cur < 1 { 102 | m.notifyWaiters() 103 | } 104 | } 105 | m.mu.Unlock() 106 | return err 107 | 108 | case <-w: 109 | waiterPool.Put(w) 110 | return nil 111 | } 112 | } 113 | 114 | // TryLock tries to lock m and reports whether it succeeded. 115 | func (m *Mutex) TryLock() bool { 116 | m.mu.Lock() 117 | success := m.cur <= 0 && m.waiters.len == 0 118 | if success { 119 | m.cur++ 120 | } 121 | m.mu.Unlock() 122 | return success 123 | } 124 | 125 | // Unlock unlocks m. 126 | // It is a run-time error if m is not locked on entry to Unlock. 127 | // 128 | // A locked Mutex is not associated with a particular goroutine. 129 | // It is allowed for one goroutine to lock a Mutex and then 130 | // arrange for another goroutine to unlock it. 131 | func (m *Mutex) Unlock() { 132 | m.mu.Lock() 133 | m.cur-- 134 | if m.cur < 0 { 135 | m.mu.Unlock() 136 | panic("sync: unlock of unlocked mutex") 137 | } 138 | m.notifyWaiters() 139 | m.mu.Unlock() 140 | } 141 | 142 | func (m *Mutex) notifyWaiters() { 143 | for { 144 | next := m.waiters.front() 145 | if next == nil { 146 | break // No more waiters blocked. 147 | } 148 | 149 | w := next.Value 150 | if m.cur > 0 { 151 | // Anti-starvation measure: we could keep going, but under load 152 | // that could cause starvation for large requests; instead, we leave 153 | // all remaining waiters blocked. 154 | break 155 | } 156 | 157 | m.cur++ 158 | m.waiters.remove(next) 159 | w <- struct{}{} 160 | } 161 | } 162 | 163 | var waiterPool = sync.Pool{New: func() any { return waiter(make(chan struct{})) }} 164 | 165 | type waiter chan struct{} 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Reference](https://pkg.go.dev/badge/github.com/neilotoole/fifomu.svg)](https://pkg.go.dev/github.com/neilotoole/fifomu) 2 | [![Go Report Card](https://goreportcard.com/badge/neilotoole/fifomu)](https://goreportcard.com/report/neilotoole/fifomu) 3 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/neilotoole/fifomu/blob/master/LICENSE) 4 | ![Pipeline](https://github.com/neilotoole/fifomu/actions/workflows/go.yml/badge.svg) 5 | 6 | # fifomu: mutex with FIFO lock acquisition 7 | 8 | [`fifomu`](https://pkg.go.dev/github.com/neilotoole/fifomu) is a Go 9 | package that provides a [`Mutex`](https://pkg.go.dev/github.com/neilotoole/fifomu#Mutex) 10 | whose [`Lock`](https://pkg.go.dev/github.com/neilotoole/fifomu#Mutex.Lock) method returns 11 | the lock to callers in FIFO call order. This is in contrast to [`sync.Mutex`](https://pkg.go.dev/sync#Mutex), 12 | where a single goroutine can repeatedly lock and unlock and relock the mutex 13 | without handing off to other lock waiter goroutines (that is, until after a 1ms 14 | starvation threshold, at which point `sync.Mutex` enters a FIFO "starvation mode" 15 | for those starved waiters, but that's too late for some use cases). 16 | 17 | `fifomu.Mutex` implements the exported methods of `sync.Mutex` and thus is 18 | a drop-in replacement (and by extension, also implements [`sync.Locker`](https://pkg.go.dev/sync#Locker)). 19 | It also provides a bonus context-aware [`LockContext`](https://pkg.go.dev/github.com/neilotoole/fifomu#Mutex.LockContext) 20 | method. 21 | 22 | Note: unless you need the FIFO behavior, you should prefer `sync.Mutex`. 23 | For typical workloads, its "greedy-relock" behavior requires less goroutine 24 | switching and yields better performance. See the [benchmarks](#benchmarks) 25 | section below. 26 | 27 | 28 | ## Usage 29 | 30 | Add the package to your `go.mod` via `go get`: 31 | 32 | ```shell 33 | go get github.com/neilotoole/fifomu 34 | ``` 35 | 36 | Then import it and use it as you would `sync.Mutex`: 37 | 38 | ```go 39 | package main 40 | 41 | import "github.com/neilotoole/fifomu" 42 | 43 | func main() { 44 | mu := &fifomu.Mutex{} 45 | mu.Lock() 46 | defer mu.Unlock() 47 | 48 | // ... Do something critical 49 | } 50 | ``` 51 | 52 | Additionally, `fifomu` provides a context-aware 53 | [`Mutex.LockContext`](https://pkg.go.dev/github.com/neilotoole/fifomu#Mutex.LockContext) 54 | method that blocks until the mutex is available or ctx is done, returning the 55 | value of [`context.Cause(ctx)`](https://pkg.go.dev/context#Cause). 56 | 57 | ```go 58 | func foo(ctx context.Context) error { 59 | mu := &fifomu.Mutex{} 60 | if err := mu.LockContext(ctx); err != nil { 61 | // Oh dear, ctx was cancelled 62 | return err 63 | } 64 | defer mu.Unlock() 65 | 66 | // ... Do something critical 67 | 68 | return nil 69 | } 70 | ``` 71 | 72 | ## Benchmarks 73 | 74 | The benchmark results below were obtained on a 2021 MacBook Pro (M1 Max) 75 | using Go 1.21.6. 76 | 77 | The benchmarks compare three mutex implementations: 78 | 79 | - `stdlib` is `sync.Mutex` 80 | - `fifomu` is `fifomu.Mutex` (this package) 81 | - `semaphoreMu` is a trivial mutex implementation built on top of 82 | [`semaphore.Weighted`](https://pkg.go.dev/golang.org/x/sync/semaphore); it 83 | exists only in the test code, as a comparison baseline. 84 | 85 | What conclusions can be drawn from the results below? Well, `fifomu` 86 | is always at least 1.5x slower than `sync.Mutex`, and often more. The 87 | worst results are in `BenchmarkMutexSpin`, where `fifomu` is 10x slower 88 | than `sync.Mutex`. On the plus side, `fifomu` is always faster than 89 | the baseline `semaphoreMu` implementation, and unlike that baseline, 90 | calls to `fifomu`'s `Mutex.Lock` method do not allocate. 91 | 92 | Benchmark your own workload before committing to `fifomu.Mutex`. In many 93 | cases you will be able to design around the need for FIFO lock acquisition. 94 | It's a bit of a code smell to begin with, but, that said, sometimes it's the 95 | most straightforward solution. 96 | 97 | ``` 98 | $ GOMAXPROCS=10 go test -bench . 99 | goos: darwin 100 | goarch: arm64 101 | pkg: github.com/neilotoole/fifomu 102 | BenchmarkMutexUncontended/stdlib-10 738603763 1.654 ns/op 0 B/op 0 allocs/op 103 | BenchmarkMutexUncontended/fifomu-10 361784637 3.266 ns/op 0 B/op 0 allocs/op 104 | BenchmarkMutexUncontended/semaphoreMu-10 344456692 3.298 ns/op 0 B/op 0 allocs/op 105 | BenchmarkMutex/stdlib-10 10600124 110.2 ns/op 0 B/op 0 allocs/op 106 | BenchmarkMutex/fifomu-10 7731985 153.9 ns/op 0 B/op 0 allocs/op 107 | BenchmarkMutex/semaphoreMu-10 5520886 217.4 ns/op 159 B/op 2 allocs/op 108 | BenchmarkMutexSlack/stdlib-10 11174293 109.4 ns/op 0 B/op 0 allocs/op 109 | BenchmarkMutexSlack/fifomu-10 6911655 164.0 ns/op 0 B/op 0 allocs/op 110 | BenchmarkMutexSlack/semaphoreMu-10 5330490 227.0 ns/op 159 B/op 2 allocs/op 111 | BenchmarkMutexWork/stdlib-10 10140280 120.0 ns/op 0 B/op 0 allocs/op 112 | BenchmarkMutexWork/fifomu-10 6797134 176.5 ns/op 0 B/op 0 allocs/op 113 | BenchmarkMutexWork/semaphoreMu-10 4979712 240.6 ns/op 159 B/op 2 allocs/op 114 | BenchmarkMutexWorkSlack/stdlib-10 10004154 119.4 ns/op 0 B/op 0 allocs/op 115 | BenchmarkMutexWorkSlack/fifomu-10 6362150 188.7 ns/op 0 B/op 0 allocs/op 116 | BenchmarkMutexWorkSlack/semaphoreMu-10 4678826 256.8 ns/op 160 B/op 3 allocs/op 117 | BenchmarkMutexNoSpin/stdlib-10 9500152 131.2 ns/op 12 B/op 0 allocs/op 118 | BenchmarkMutexNoSpin/fifomu-10 3132691 381.3 ns/op 12 B/op 0 allocs/op 119 | BenchmarkMutexNoSpin/semaphoreMu-10 2502936 461.0 ns/op 51 B/op 1 allocs/op 120 | BenchmarkMutexSpin/stdlib-10 5025486 233.7 ns/op 0 B/op 0 allocs/op 121 | BenchmarkMutexSpin/fifomu-10 514082 2362 ns/op 0 B/op 0 allocs/op 122 | BenchmarkMutexSpin/semaphoreMu-10 496808 2512 ns/op 159 B/op 2 allocs/op 123 | ``` 124 | 125 | ## Related 126 | 127 | - [`streamcache`](https://github.com/neilotoole/streamcache) uses `fifomu` to ensure fairness 128 | for concurrent readers of a stream. 129 | - [`sq`](https://github.com/neilotoole/sq) uses `fifomu` indirectly via `streamcache`. 130 | -------------------------------------------------------------------------------- /fifomu_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // GOMAXPROCS=10 go test 6 | 7 | package fifomu_test 8 | 9 | import ( 10 | "context" 11 | "runtime" 12 | "sync" 13 | "testing" 14 | "time" 15 | 16 | "golang.org/x/sync/semaphore" 17 | 18 | "github.com/neilotoole/fifomu" 19 | ) 20 | 21 | // Acknowledgement: Much of the test code in this file is 22 | // copied from stdlib sync/mutex_test.go. 23 | 24 | // mutexer is the exported methodset of sync.Mutex. 25 | type mutexer interface { 26 | sync.Locker 27 | TryLock() bool 28 | } 29 | 30 | var ( 31 | _ mutexer = (*fifomu.Mutex)(nil) 32 | _ mutexer = (*sync.Mutex)(nil) 33 | _ mutexer = (*semaphoreMutex)(nil) 34 | ) 35 | 36 | // newMu is a function that returns a new mutexer. 37 | // We set it to newFifoMu, newStdlibMu or newSemaphoreMu 38 | // for benchmarking. 39 | var newMu = newFifoMu 40 | 41 | func newFifoMu() mutexer { 42 | return &fifomu.Mutex{} 43 | } 44 | 45 | func newStdlibMu() mutexer { 46 | return &sync.Mutex{} 47 | } 48 | 49 | func newSemaphoreMu() mutexer { 50 | return &semaphoreMutex{sema: semaphore.NewWeighted(1)} 51 | } 52 | 53 | func benchmarkEachImpl(b *testing.B, fn func(b *testing.B)) { 54 | b.Cleanup(func() { 55 | // Restore to default. 56 | newMu = newFifoMu 57 | }) 58 | 59 | b.Run("stdlib", func(b *testing.B) { 60 | b.ReportAllocs() 61 | newMu = newStdlibMu 62 | fn(b) 63 | }) 64 | b.Run("fifomu", func(b *testing.B) { 65 | b.ReportAllocs() 66 | newMu = newFifoMu 67 | fn(b) 68 | }) 69 | b.Run("semaphoreMu", func(b *testing.B) { 70 | b.ReportAllocs() 71 | newMu = newSemaphoreMu 72 | fn(b) 73 | }) 74 | } 75 | 76 | func HammerMutex(m mutexer, loops int, cdone chan bool) { 77 | for i := 0; i < loops; i++ { 78 | if i%3 == 0 { 79 | if m.TryLock() { 80 | m.Unlock() 81 | } 82 | continue 83 | } 84 | m.Lock() 85 | m.Unlock() //nolint:staticcheck 86 | } 87 | cdone <- true 88 | } 89 | 90 | func TestMutex(t *testing.T) { 91 | if n := runtime.SetMutexProfileFraction(1); n != 0 { 92 | t.Logf("got mutexrate %d expected 0", n) 93 | } 94 | defer runtime.SetMutexProfileFraction(0) 95 | 96 | m := newMu() 97 | 98 | m.Lock() 99 | if m.TryLock() { 100 | t.Fatalf("TryLock succeeded with mutex locked") 101 | } 102 | m.Unlock() 103 | if !m.TryLock() { 104 | t.Fatalf("TryLock failed with mutex unlocked") 105 | } 106 | m.Unlock() 107 | 108 | c := make(chan bool) 109 | for i := 0; i < 10; i++ { 110 | go HammerMutex(m, 1000, c) 111 | } 112 | for i := 0; i < 10; i++ { 113 | <-c 114 | } 115 | } 116 | 117 | func TestMutexMisuse(t *testing.T) { 118 | t.Run("Mutex.Unlock", func(t *testing.T) { 119 | defer func() { 120 | if r := recover(); r == nil { 121 | t.Errorf("Expected panic due to Unlock of unlocked mutex") 122 | } 123 | }() 124 | 125 | mu := newMu() 126 | mu.Unlock() 127 | }) 128 | 129 | t.Run("Mutex.Unlock2", func(t *testing.T) { 130 | defer func() { 131 | if r := recover(); r == nil { 132 | t.Errorf("Expected panic due to Unlock of unlocked mutex") 133 | } 134 | }() 135 | 136 | mu := newMu() 137 | mu.Lock() 138 | mu.Unlock() //nolint:staticcheck 139 | mu.Unlock() 140 | }) 141 | } 142 | 143 | func TestMutexFairness(t *testing.T) { 144 | mu := newMu() 145 | stop := make(chan bool) 146 | defer close(stop) 147 | go func() { 148 | for { 149 | mu.Lock() 150 | time.Sleep(100 * time.Microsecond) 151 | mu.Unlock() 152 | select { 153 | case <-stop: 154 | return 155 | default: 156 | } 157 | } 158 | }() 159 | done := make(chan bool, 1) 160 | go func() { 161 | for i := 0; i < 10; i++ { 162 | time.Sleep(100 * time.Microsecond) 163 | mu.Lock() 164 | mu.Unlock() //nolint:staticcheck 165 | } 166 | done <- true 167 | }() 168 | select { 169 | case <-done: 170 | case <-time.After(10 * time.Second): 171 | t.Fatalf("can't acquire mutex in 10 seconds") 172 | } 173 | } 174 | 175 | func BenchmarkMutexUncontended(b *testing.B) { 176 | type PaddedMutex struct { 177 | mutexer 178 | pad [128]uint8 //nolint:unused 179 | } 180 | 181 | benchmarkEachImpl(b, func(b *testing.B) { 182 | b.RunParallel(func(pb *testing.PB) { 183 | var mu PaddedMutex 184 | mu.mutexer = newMu() 185 | for pb.Next() { 186 | mu.Lock() 187 | mu.Unlock() //nolint:staticcheck 188 | } 189 | }) 190 | }) 191 | } 192 | 193 | func benchmarkMutex(b *testing.B, slack, work bool) { 194 | b.ReportAllocs() 195 | mu := newMu() 196 | if slack { 197 | b.SetParallelism(10) 198 | } 199 | b.RunParallel(func(pb *testing.PB) { 200 | foo := 0 201 | for pb.Next() { 202 | mu.Lock() 203 | mu.Unlock() //nolint:staticcheck 204 | if work { 205 | for i := 0; i < 100; i++ { 206 | foo *= 2 207 | foo /= 2 208 | } 209 | } 210 | } 211 | _ = foo 212 | }) 213 | } 214 | 215 | func BenchmarkMutex(b *testing.B) { 216 | benchmarkEachImpl(b, func(b *testing.B) { 217 | benchmarkMutex(b, false, false) 218 | }) 219 | } 220 | 221 | func BenchmarkMutexSlack(b *testing.B) { 222 | benchmarkEachImpl(b, func(b *testing.B) { 223 | benchmarkMutex(b, true, false) 224 | }) 225 | } 226 | 227 | func BenchmarkMutexWork(b *testing.B) { 228 | benchmarkEachImpl(b, func(b *testing.B) { 229 | benchmarkMutex(b, false, true) 230 | }) 231 | } 232 | 233 | func BenchmarkMutexWorkSlack(b *testing.B) { 234 | benchmarkEachImpl(b, func(b *testing.B) { 235 | benchmarkMutex(b, true, true) 236 | }) 237 | } 238 | 239 | func BenchmarkMutexNoSpin(b *testing.B) { 240 | benchmarkEachImpl(b, func(b *testing.B) { 241 | // This benchmark models a situation where spinning in the mutex should be 242 | // non-profitable and allows to confirm that spinning does not do harm. 243 | // To achieve this we create excess of goroutines most of which do local work. 244 | // These goroutines yield during local work, so that switching from 245 | // a blocked goroutine to other goroutines is profitable. 246 | // As a matter of fact, this benchmark still triggers some spinning in the mutex. 247 | m := newMu() 248 | var acc0, acc1 uint64 249 | b.SetParallelism(4) 250 | b.RunParallel(func(pb *testing.PB) { 251 | c := make(chan bool) 252 | var data [4 << 10]uint64 253 | for i := 0; pb.Next(); i++ { 254 | if i%4 == 0 { 255 | m.Lock() 256 | acc0 -= 100 257 | acc1 += 100 258 | m.Unlock() 259 | } else { 260 | for i := 0; i < len(data); i += 4 { 261 | data[i]++ 262 | } 263 | // Elaborate way to say runtime.Gosched 264 | // that does not put the goroutine onto global runq. 265 | go func() { 266 | c <- true 267 | }() 268 | <-c 269 | } 270 | } 271 | }) 272 | }) 273 | } 274 | 275 | func BenchmarkMutexSpin(b *testing.B) { 276 | benchmarkEachImpl(b, func(b *testing.B) { 277 | // This benchmark models a situation where spinning in the mutex should be 278 | // profitable. To achieve this we create a goroutine per-proc. 279 | // These goroutines access considerable amount of local data so that 280 | // unnecessary rescheduling is penalized by cache misses. 281 | m := newMu() 282 | var acc0, acc1 uint64 283 | b.RunParallel(func(pb *testing.PB) { 284 | var data [16 << 10]uint64 285 | for i := 0; pb.Next(); i++ { 286 | m.Lock() 287 | acc0 -= 100 288 | acc1 += 100 289 | m.Unlock() 290 | for i := 0; i < len(data); i += 4 { 291 | data[i]++ 292 | } 293 | } 294 | }) 295 | }) 296 | } 297 | 298 | var _ sync.Locker = (*semaphoreMutex)(nil) 299 | 300 | // semaphoreMutex is a mutex built on a semaphore.Weighted. 301 | // It exists as a baseline for benchmarking. Like fifomu.Mutex, 302 | // its Lock method returns the lock to callers in FIFO call order. 303 | type semaphoreMutex struct { 304 | sema *semaphore.Weighted 305 | } 306 | 307 | // Lock implements sync.Locker. 308 | func (m *semaphoreMutex) Lock() { 309 | _ = m.sema.Acquire(context.Background(), 1) 310 | } 311 | 312 | // Unlock implements sync.Locker. 313 | func (m *semaphoreMutex) Unlock() { 314 | m.sema.Release(1) 315 | } 316 | 317 | // TryLock tries to lock m and reports whether it succeeded. 318 | func (m *semaphoreMutex) TryLock() bool { 319 | return m.sema.TryAcquire(1) 320 | } 321 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # This code is licensed under the terms of the MIT license. 2 | 3 | ## Golden config for golangci-lint v1.55.2 4 | # 5 | # This is the best config for golangci-lint based on my experience and opinion. 6 | # It is very strict, but not extremely strict. 7 | # Feel free to adopt and change it for your needs. 8 | # 9 | # @neilotoole: ^^ Well, it's less strict now! 10 | # Based on: https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322 11 | 12 | run: 13 | # Timeout for analysis, e.g. 30s, 5m. 14 | # Default: 1m 15 | timeout: 5m 16 | 17 | tests: true 18 | 19 | skip-dirs: 20 | 21 | skip-files: 22 | 23 | output: 24 | sort-results: true 25 | 26 | # This file contains only configs which differ from defaults. 27 | # All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml 28 | linters-settings: 29 | cyclop: 30 | # The maximal code complexity to report. 31 | # Default: 10 32 | max-complexity: 50 33 | # The maximal average package complexity. 34 | # If it's higher than 0.0 (float) the check is enabled 35 | # Default: 0.0 36 | package-average: 10.0 37 | 38 | errcheck: 39 | # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. 40 | # Such cases aren't reported by default. 41 | # Default: false 42 | check-type-assertions: true 43 | 44 | exhaustive: 45 | # Program elements to check for exhaustiveness. 46 | # Default: [ switch ] 47 | check: 48 | - switch 49 | - map 50 | 51 | funlen: 52 | # Checks the number of lines in a function. 53 | # If lower than 0, disable the check. 54 | # Default: 60 55 | lines: 150 56 | # Checks the number of statements in a function. 57 | # If lower than 0, disable the check. 58 | # Default: 40 59 | statements: 100 60 | 61 | gocognit: 62 | # Minimal code complexity to report 63 | # Default: 30 (but we recommend 10-20) 64 | min-complexity: 50 65 | 66 | gocritic: 67 | # Settings passed to gocritic. 68 | # The settings key is the name of a supported gocritic checker. 69 | # The list of supported checkers can be find in https://go-critic.github.io/overview. 70 | settings: 71 | captLocal: 72 | # Whether to restrict checker to params only. 73 | # Default: true 74 | paramsOnly: false 75 | underef: 76 | # Whether to skip (*x).method() calls where x is a pointer receiver. 77 | # Default: true 78 | skipRecvDeref: false 79 | 80 | gocyclo: 81 | # Minimal code complexity to report. 82 | # Default: 30 (but we recommend 10-20) 83 | min-complexity: 50 84 | 85 | gofumpt: 86 | # Module path which contains the source code being formatted. 87 | # Default: "" 88 | module-path: github.com/neilotoole/streamcache 89 | # Choose whether to use the extra rules. 90 | # Default: false 91 | extra-rules: true 92 | 93 | gomnd: 94 | # List of function patterns to exclude from analysis. 95 | # Values always ignored: `time.Date`, 96 | # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, 97 | # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. 98 | # Default: [] 99 | ignored-functions: 100 | - make 101 | - os.Chmod 102 | - os.Mkdir 103 | - os.MkdirAll 104 | - os.OpenFile 105 | - os.WriteFile 106 | - prometheus.ExponentialBuckets 107 | - prometheus.ExponentialBucketsRange 108 | - prometheus.LinearBuckets 109 | ignored-numbers: 110 | - "2" 111 | - "3" 112 | 113 | gomodguard: 114 | blocked: 115 | # List of blocked modules. 116 | # Default: [] 117 | modules: 118 | - github.com/golang/protobuf: 119 | recommendations: 120 | - google.golang.org/protobuf 121 | reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules" 122 | - github.com/satori/go.uuid: 123 | recommendations: 124 | - github.com/google/uuid 125 | reason: "satori's package is not maintained" 126 | - github.com/gofrs/uuid: 127 | recommendations: 128 | - github.com/google/uuid 129 | reason: "gofrs' package is not go module" 130 | 131 | govet: 132 | # Enable all analyzers. 133 | # Default: false 134 | enable-all: true 135 | # Disable analyzers by name. 136 | # Run `go tool vet help` to see all analyzers. 137 | # Default: [] 138 | disable: 139 | # - fieldalignment # too strict 140 | # Settings per analyzer. 141 | settings: 142 | shadow: 143 | # Whether to be strict about shadowing; can be noisy. 144 | # Default: false 145 | strict: false 146 | 147 | lll: 148 | # Max line length, lines longer will be reported. 149 | # '\t' is counted as 1 character by default, and can be changed with the tab-width option. 150 | # Default: 120. 151 | line-length: 120 152 | # Tab width in spaces. 153 | # Default: 1 154 | tab-width: 1 155 | 156 | nakedret: 157 | # Make an issue if func has more lines of code than this setting, and it has naked returns. 158 | # Default: 30 159 | max-func-lines: 0 160 | 161 | nestif: 162 | # Minimal complexity of if statements to report. 163 | # Default: 5 164 | min-complexity: 20 165 | 166 | nolintlint: 167 | # Exclude following linters from requiring an explanation. 168 | # Default: [] 169 | allow-no-explanation: [ funlen, gocognit, lll ] 170 | # Enable to require an explanation of nonzero length after each nolint directive. 171 | # Default: false 172 | require-explanation: false 173 | # Enable to require nolint directives to mention the specific linter being suppressed. 174 | # Default: false 175 | require-specific: true 176 | 177 | revive: 178 | # https://golangci-lint.run/usage/linters/#revive 179 | rules: 180 | 181 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#add-constant 182 | - name: add-constant 183 | disabled: true 184 | arguments: 185 | - maxLitCount: "3" 186 | allowStrs: '""' 187 | allowInts: "0,1,2" 188 | allowFloats: "0.0,0.,1.0,1.,2.0,2." 189 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#argument-limit 190 | - name: argument-limit 191 | disabled: false 192 | arguments: [ 7 ] 193 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#atomic 194 | - name: atomic 195 | disabled: false 196 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#banned-characters 197 | - name: banned-characters 198 | disabled: true 199 | arguments: [ "Ω", "Σ", "σ", "7" ] 200 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bare-return 201 | - name: bare-return 202 | disabled: false 203 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#blank-imports 204 | - name: blank-imports 205 | disabled: false 206 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bool-literal-in-expr 207 | - name: bool-literal-in-expr 208 | disabled: false 209 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#call-to-gc 210 | - name: call-to-gc 211 | disabled: false 212 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#cognitive-complexity 213 | - name: cognitive-complexity 214 | disabled: true 215 | arguments: [ 7 ] 216 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#comment-spacings 217 | - name: comment-spacings 218 | disabled: true 219 | arguments: 220 | - mypragma 221 | - otherpragma 222 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#confusing-naming 223 | - name: confusing-naming 224 | disabled: true 225 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#confusing-results 226 | - name: confusing-results 227 | disabled: false 228 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#constant-logical-expr 229 | - name: constant-logical-expr 230 | disabled: false 231 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#context-as-argument 232 | - name: context-as-argument 233 | disabled: false 234 | arguments: 235 | - allowTypesBefore: "*testing.T,*github.com/user/repo/testing.Harness" 236 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#context-keys-type 237 | - name: context-keys-type 238 | disabled: false 239 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#cyclomatic 240 | - name: cyclomatic 241 | disabled: true 242 | arguments: [ 3 ] 243 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#datarace 244 | - name: datarace 245 | disabled: false 246 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#deep-exit 247 | - name: deep-exit 248 | disabled: false 249 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#defer 250 | - name: defer 251 | disabled: false 252 | arguments: 253 | - [ "call-chain", "loop" ] 254 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#dot-imports 255 | - name: dot-imports 256 | disabled: false 257 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#duplicated-imports 258 | - name: duplicated-imports 259 | disabled: false 260 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#early-return 261 | - name: early-return 262 | disabled: false 263 | arguments: 264 | - "preserveScope" 265 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block 266 | - name: empty-block 267 | disabled: false 268 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines 269 | - name: empty-lines 270 | disabled: true # Covered by "whitespace" linter. 271 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#enforce-map-style 272 | - name: enforce-map-style 273 | disabled: true 274 | arguments: 275 | - "make" 276 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-naming 277 | - name: error-naming 278 | disabled: false 279 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-return 280 | - name: error-return 281 | disabled: false 282 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-strings 283 | - name: error-strings 284 | disabled: false 285 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#errorf 286 | - name: errorf 287 | disabled: false 288 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#file-header 289 | - name: file-header 290 | disabled: true 291 | # arguments: 292 | # - This is the text that must appear at the top of source files. 293 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#flag-parameter 294 | - name: flag-parameter 295 | disabled: true 296 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#function-result-limit 297 | - name: function-result-limit 298 | disabled: false 299 | arguments: [ 4 ] 300 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#function-length 301 | - name: function-length 302 | disabled: true 303 | arguments: [ 10, 0 ] 304 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#get-return 305 | - name: get-return 306 | disabled: false 307 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#identical-branches 308 | - name: identical-branches 309 | disabled: false 310 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#if-return 311 | - name: if-return 312 | disabled: false 313 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#increment-decrement 314 | - name: increment-decrement 315 | disabled: false 316 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#indent-error-flow 317 | - name: indent-error-flow 318 | disabled: false 319 | arguments: 320 | - "preserveScope" 321 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-alias-naming 322 | - name: import-alias-naming 323 | disabled: false 324 | arguments: 325 | - "^[a-z][a-z0-9]{0,}$" 326 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#imports-blacklist 327 | - name: imports-blacklist 328 | disabled: false 329 | arguments: 330 | - "crypto/md5" 331 | - "crypto/sha1" 332 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing 333 | - name: import-shadowing 334 | disabled: false 335 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#line-length-limit 336 | - name: line-length-limit 337 | disabled: true 338 | arguments: [ 80 ] 339 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#max-public-structs 340 | - name: max-public-structs 341 | disabled: true 342 | arguments: [ 3 ] 343 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#modifies-parameter 344 | - name: modifies-parameter 345 | disabled: false 346 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#modifies-value-receiver 347 | - name: modifies-value-receiver 348 | disabled: false 349 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#nested-structs 350 | - name: nested-structs 351 | disabled: false 352 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#optimize-operands-order 353 | - name: optimize-operands-order 354 | disabled: false 355 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#package-comments 356 | - name: package-comments 357 | disabled: false 358 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range 359 | - name: range 360 | disabled: false 361 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range-val-in-closure 362 | - name: range-val-in-closure 363 | disabled: false 364 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range-val-address 365 | - name: range-val-address 366 | disabled: false 367 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#receiver-naming 368 | - name: receiver-naming 369 | disabled: false 370 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#redundant-import-alias 371 | - name: redundant-import-alias 372 | disabled: false 373 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#redefines-builtin-id 374 | - name: redefines-builtin-id 375 | disabled: false 376 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-of-int 377 | - name: string-of-int 378 | disabled: false 379 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-format 380 | - name: string-format 381 | disabled: false 382 | arguments: 383 | - - 'core.WriteError[1].Message' 384 | - '/^([^A-Z]|$)/' 385 | - must not start with a capital letter 386 | - - 'fmt.Errorf[0]' 387 | - '/(^|[^\.!?])$/' 388 | - must not end in punctuation 389 | - - panic 390 | - '/^[^\n]*$/' 391 | - must not contain line breaks 392 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#struct-tag 393 | - name: struct-tag 394 | arguments: 395 | - "json,inline" 396 | - "bson,outline,gnu" 397 | disabled: false 398 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#superfluous-else 399 | - name: superfluous-else 400 | disabled: false 401 | arguments: 402 | - "preserveScope" 403 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#time-equal 404 | - name: time-equal 405 | disabled: false 406 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#time-naming 407 | - name: time-naming 408 | disabled: false 409 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#var-naming 410 | - name: var-naming 411 | disabled: false 412 | arguments: 413 | - [ "ID" ] # AllowList 414 | - [ "VM" ] # DenyList 415 | - - upperCaseConst: true 416 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#var-declaration 417 | - name: var-declaration 418 | disabled: false 419 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unconditional-recursion 420 | - name: unconditional-recursion 421 | disabled: false 422 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-naming 423 | - name: unexported-naming 424 | disabled: false 425 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-return 426 | - name: unexported-return 427 | disabled: false 428 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unhandled-error 429 | - name: unhandled-error 430 | disabled: false 431 | arguments: 432 | - "fmt.Printf" 433 | - "fmt.Fprintf" 434 | - "fmt.Fprint" 435 | - "fmt.Fprintln" 436 | - "bytes.Buffer.Write" 437 | - "bytes.Buffer.WriteByte" 438 | - "bytes.Buffer.WriteString" 439 | - "bytes.Buffer.WriteRune" 440 | - "strings.Builder.WriteString" 441 | - "strings.Builder.WriteRune" 442 | - "strings.Builder.Write" 443 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unnecessary-stmt 444 | - name: unnecessary-stmt 445 | disabled: false 446 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unreachable-code 447 | - name: unreachable-code 448 | disabled: false 449 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter 450 | - name: unused-parameter 451 | disabled: false 452 | arguments: 453 | - allowRegex: "^_" 454 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver 455 | - name: unused-receiver 456 | disabled: true 457 | arguments: 458 | - allowRegex: "^_" 459 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#useless-break 460 | - name: useless-break 461 | disabled: false 462 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#waitgroup-by-value 463 | - name: waitgroup-by-value 464 | disabled: false 465 | 466 | rowserrcheck: 467 | # database/sql is always checked 468 | # Default: [] 469 | packages: 470 | - github.com/jmoiron/sqlx 471 | 472 | tenv: 473 | # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. 474 | # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. 475 | # Default: false 476 | all: true 477 | 478 | 479 | linters: 480 | disable-all: true 481 | 482 | enable: 483 | ## enabled by default 484 | - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases 485 | - gosimple # specializes in simplifying a code 486 | - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string 487 | - ineffassign # detects when assignments to existing variables are not used 488 | - staticcheck # is a go vet on steroids, applying a ton of static analysis checks 489 | - typecheck # like the front-end of a Go compiler, parses and type-checks Go code 490 | - unused # checks for unused constants, variables, functions and types 491 | 492 | 493 | # ## disabled by default 494 | - asasalint # checks for pass []any as any in variadic func(...any) 495 | - asciicheck # checks that your code does not contain non-ASCII identifiers 496 | - bidichk # checks for dangerous unicode character sequences 497 | - bodyclose # checks whether HTTP response body is closed successfully 498 | - cyclop # checks function and package cyclomatic complexity 499 | - dupl # tool for code clone detection 500 | - durationcheck # checks for two durations multiplied together 501 | - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error 502 | - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 503 | - execinquery # checks query string in Query function which reads your Go src files and warning it finds 504 | - exhaustive # checks exhaustiveness of enum switch statements 505 | - exportloopref # checks for pointers to enclosing loop variables 506 | - forbidigo # forbids identifiers 507 | - funlen # tool for detection of long functions 508 | - gochecknoinits # checks that no init functions are present in Go code 509 | - gocognit # computes and checks the cognitive complexity of functions 510 | - goconst # finds repeated strings that could be replaced by a constant 511 | - gocritic # provides diagnostics that check for bugs, performance and style issues 512 | - gocyclo # computes and checks the cyclomatic complexity of functions 513 | - godot # checks if comments end in a period 514 | - gofumpt 515 | - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt 516 | # - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod 517 | - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations 518 | - goprintffuncname # checks that printf-like functions are named with f at the end 519 | - gosec # inspects source code for security problems 520 | - lll # reports long lines 521 | - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) 522 | - makezero # finds slice declarations with non-zero initial length 523 | - nakedret # finds naked returns in functions greater than a specified function length 524 | - nestif # reports deeply nested if statements 525 | - nilerr # finds the code that returns nil even if it checks that the error is not nil 526 | - nilnil # checks that there is no simultaneous return of nil error and an invalid value 527 | - noctx # finds sending http request without context.Context 528 | - nolintlint # reports ill-formed or insufficient nolint directives 529 | - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL 530 | - predeclared # finds code that shadows one of Go's predeclared identifiers 531 | - promlinter # checks Prometheus metrics naming via promlint 532 | - reassign # checks that package variables are not reassigned 533 | - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint 534 | - stylecheck # is a replacement for golint 535 | - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 536 | - testableexamples # checks if examples are testable (have an expected output) 537 | - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes 538 | - unconvert # removes unnecessary type conversions 539 | - unparam # reports unused function parameters 540 | - usestdlibvars # detects the possibility to use variables/constants from the Go standard library 541 | - whitespace # detects leading and trailing whitespace 542 | 543 | ## These three linters are disabled for now due to generics: https://github.com/golangci/golangci-lint/issues/2649 544 | #- rowserrcheck # checks whether Err of rows is checked successfully # Disabled because: https://github.com/golangci/golangci-lint/issues/2649 545 | #- sqlclosecheck # checks that sql.Rows and sql.Stmt are closed 546 | #- wastedassign # finds wasted assignment statements 547 | 548 | 549 | ## you may want to enable 550 | #- decorder # checks declaration order and count of types, constants, variables and functions 551 | #- exhaustruct # checks if all structure fields are initialized 552 | #- gochecknoglobals # checks that no global variables exist 553 | #- godox # detects FIXME, TODO and other comment keywords 554 | #- goheader # checks is file header matches to pattern 555 | #- gomnd # detects magic numbers 556 | #- interfacebloat # checks the number of methods inside an interface 557 | #- ireturn # accept interfaces, return concrete types 558 | #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated 559 | #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope 560 | #- wrapcheck # checks that errors returned from external packages are wrapped 561 | 562 | ## disabled 563 | #- containedctx # detects struct contained context.Context field 564 | #- contextcheck # [too many false positives] checks the function whether use a non-inherited context 565 | #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages 566 | #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) 567 | #- dupword # [useless without config] checks for duplicate words in the source code 568 | #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted 569 | #- forcetypeassert # [replaced by errcheck] finds forced type assertions 570 | #- goerr113 # [too strict] checks the errors handling expressions 571 | #- gofmt # [replaced by goimports] checks whether code was gofmt-ed 572 | #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed 573 | #- grouper # analyzes expression groups 574 | #- importas # enforces consistent import aliases 575 | #- maintidx # measures the maintainability index of each function 576 | #- misspell # [useless] finds commonly misspelled English words in comments 577 | #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity 578 | #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test 579 | #- tagliatelle # checks the struct tags 580 | #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers 581 | #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines 582 | 583 | ## deprecated 584 | #- deadcode # [deprecated, replaced by unused] finds unused code 585 | #- exhaustivestruct # [deprecated, replaced by exhaustruct] checks if all struct's fields are initialized 586 | #- golint # [deprecated, replaced by revive] golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes 587 | #- ifshort # [deprecated] checks that your code uses short syntax for if-statements whenever possible 588 | #- interfacer # [deprecated] suggests narrower interface types 589 | #- maligned # [deprecated, replaced by govet fieldalignment] detects Go structs that would take less memory if their fields were sorted 590 | #- nosnakecase # [deprecated, replaced by revive var-naming] detects snake case of variable naming and function name 591 | #- scopelint # [deprecated, replaced by exportloopref] checks for unpinned variables in go programs 592 | #- structcheck # [deprecated, replaced by unused] finds unused struct fields 593 | #- varcheck # [deprecated, replaced by unused] finds unused global variables and constants 594 | 595 | 596 | issues: 597 | # Maximum count of issues with the same text. 598 | # Set to 0 to disable. 599 | # Default: 3 600 | max-same-issues: 3 601 | 602 | exclude-rules: 603 | - source: "^//\\s*go:generate\\s" 604 | linters: [ lll ] 605 | - source: "(noinspection|TODO)" 606 | linters: [ godot ] 607 | - source: "//noinspection" 608 | linters: [ gocritic ] 609 | - source: "^\\s+if _, ok := err\\.\\([^.]+\\.InternalError\\); ok {" 610 | linters: [ errorlint ] 611 | - path: "_test\\.go" 612 | linters: 613 | - bodyclose 614 | - dupl 615 | - funlen 616 | - goconst 617 | - gosec 618 | - noctx 619 | - wrapcheck 620 | --------------------------------------------------------------------------------