From e066a67dc0367a79376dd0d20ef41547b961294a Mon Sep 17 00:00:00 2001 From: Aaron Hopkins Date: Sun, 31 May 2026 20:14:50 -0700 Subject: [PATCH 1/3] refactor: drop all third-party dependencies for the standard library Switch the generic constraint from golang.org/x/exp/constraints.Ordered to the standard library's cmp.Ordered, and rewrite the tests to use only the testing package instead of testify. This leaves the package with zero external dependencies, so go.sum and dependabot.yml are removed and the minimum Go version is raised to 1.22 (for cmp.Ordered, math/rand/v2, and integer range loops). - intervals.go: use cmp.Ordered in place of constraints.Ordered - intervals_test.go: replace testify assertions with t.Error/t.Fatal and slices.Equal, add a local assertPanics helper, and switch to math/rand/v2 - intervals_test.go: add Search benchmarks and a gap parameter so Insert benchmarks can exercise disjoint, mergeable, and overlapping inputs - README.md: expand documentation (concepts, API, concurrency, example), refresh benchmark numbers and note the zero-dependency property --- .github/dependabot.yml | 6 -- README.md | 210 +++++++++++++++++++++++++++++++++-------- go.mod | 13 +-- go.sum | 12 --- intervals.go | 7 +- intervals_test.go | 129 +++++++++++++++++++------ 6 files changed, 272 insertions(+), 105 deletions(-) delete mode 100644 .github/dependabot.yml delete mode 100644 go.sum diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index f1b219b..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "gomod" - directory: "/" - schedule: - interval: "weekly" diff --git a/README.md b/README.md index d39582f..bad77db 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,188 @@ Intervals [![Build Status](https://github.com/die-net/intervals/actions/workflows/go-test.yml/badge.svg)](https://github.com/die-net/intervals/actions/workflows/go-test.yml) [![Coverage Status](https://coveralls.io/repos/github/die-net/intervals/badge.svg?branch=main)](https://coveralls.io/github/die-net/intervals?branch=main) [![Go Report Card](https://goreportcard.com/badge/github.com/die-net/intervals)](https://goreportcard.com/report/github.com/die-net/intervals) ========= -Package intervals provides a fast insertion-sort based solution to the merge -overlapping intervals problem. - -An Interval represents a range of the form `[Start, End)`, which contains -all `x` in `Start <= x < End`. `x` can be any type that is ordered, -including the various sizes of `int`, `uint`, `float` types, `uintptr` and -`string`. - -The `Intervals` type is a slice of `Interval` as maintained by the `Insert()` -method, which keeps them in ascending order and with any two overlapping -`Interval` merged. Two `Interval` overlap if the `End` of one is `>=` the -`Start` from the next one. - -An example use case for this might be trying to do a concurrent download, -such as chunks of a large file from S3. You could kick off `n` goroutines, -each waiting to download `1/n` of the file, and use something like -`sync.WaitGroup` to wait until they are all done until you start to use -them. However, that would require waiting until the last byte is received -before start sending the first byte. If you want to start streaming the -answer from the beginning as soon as the relevant chunks are written to -disk, you could maintain a `done Intervals[int64]` and `done.Insert()` the -byte range written after each successful `Write()`, start streaming as soon -as `done.Search(0)` returns true, up to the whatever the returned Interval's -`End - 1` is. - -The overhead of `Insert()` is very low, with the worst case of `Insert()`ing -10,000 non-overlapping intervals only taking 147ns on my Apple Mac M1: +Package `intervals` provides a fast, insertion-sort based, zero-dependency +solution to the "merge overlapping intervals" problem. It keeps a slice of +ranges sorted and non-overlapping as you insert new ranges, and lets you +quickly look up which range (if any) contains a given value. +The package is generic: it works with any [ordered](https://pkg.go.dev/cmp#Ordered) +type, including the various sizes of `int`, `uint`, and `float`, as well as +`uintptr` and `string`. + +Concepts +-------- + +### Interval + +An `Interval[T]` represents a half-open range of the form `[Start, End)`, +which contains all `x` where `Start <= x < End`. Half-open ranges compose +cleanly: the `End` of one interval can equal the `Start` of the next without +the two values overlapping. + +```go +type Interval[T cmp.Ordered] struct { + Start T + End T +} +``` + +An interval is considered *empty* when `Start == End`. An interval where +`Start > End` is invalid and will panic when inserted. If `T` is a float, +neither bound may be `NaN` (because `NaN` is unordered); a `NaN` bound is +treated as empty. + +### Intervals + +`Intervals[T]` is a slice of `Interval[T]` kept in ascending order with no two +intervals overlapping. You build and maintain it through the `Insert` method, +which: + +- inserts a new interval in sorted position, or +- extends an adjacent or overlapping interval, or +- merges and compacts any intervals the new range now spans. + +Two intervals are merged when the `End` of one is `>=` the `Start` of the next, +so adjacent ranges like `[0, 2)` and `[2, 3)` collapse into `[0, 3)`. + +API +--- + +| Method | Description | +| --- | --- | +| `(Interval[T]) Empty() bool` | Reports whether the interval is empty (`Start == End`). Panics if `Start > End`. | +| `(Intervals[T]) Insert(v Interval[T]) Intervals[T]` | Returns a new `Intervals` with `v` added, keeping order and merging overlaps. An empty `v` is a no-op; an invalid `v` panics. | +| `(Intervals[T]) Search(off T) (Interval[T], bool)` | Returns the interval containing `off` and `true`, or the zero `Interval` and `false`. | + +Because `Insert` may reallocate the underlying slice (just like the built-in +`append`), always assign its result back: + +```go +done = done.Insert(iv) +``` + +Concurrency +----------- + +Neither `Insert` nor `Search` is safe for concurrent access. `Insert` mutates +the receiver, and `Search` requires the slice to remain correctly ordered while +it runs. If you share an `Intervals` across goroutines, guard it with a +`sync.RWMutex` (or equivalent): a write lock around `Insert`, and a read lock +around `Search`. + +Installation +------------ + +```sh +go get github.com/die-net/intervals +``` + +This package requires Go 1.22 or newer and pulls in no third-party +dependencies. + +Example +------- + +A motivating use case is a concurrent download, such as fetching chunks of a +large file from S3 with `n` goroutines. Rather than waiting for the last byte +before sending the first, you can record each completed byte range as it lands +and start streaming the contiguous prefix as soon as it is ready. + +```go +package main + +import ( + "fmt" + + "github.com/die-net/intervals" +) + +func main() { + var done intervals.Intervals[int64] + + // Chunks complete out of order; record each finished byte range. + done = done.Insert(intervals.Interval[int64]{Start: 0, End: 1024}) + done = done.Insert(intervals.Interval[int64]{Start: 2048, End: 4096}) + done = done.Insert(intervals.Interval[int64]{Start: 1024, End: 2048}) // fills the gap + + fmt.Println(done) // [{0 4096}] + + // How much contiguous data is ready starting at offset 0? + if iv, ok := done.Search(0); ok { + fmt.Printf("bytes [%d, %d) are ready to stream\n", iv.Start, iv.End) + } +} ``` -$ go test -cpu=1 -bench=. + +Performance +----------- + +Both `Insert` and `Search` use a binary search (`sort.Search`) to locate the +region of interest. `Insert` reuses the existing slice whenever possible, +copying in-place in the common cases, aggressively merging intervals where +possible and only allocating to double the size of the underlying slice when +it is full. `Search` doesn't allocate. + +The `Insert` benchmarks cover three scenarios: inserting disjoint intervals +(the worst case, since each must be kept separate), inserting mergeable +adjacent intervals, and inserting heavily overlapping intervals. + +The `Search` benchmarks cover the disjoint case (many separate intervals to +binary-search across) and the merged case (a single interval). `Search` is +read-only and runs in roughly logarithmic time in the number of unmerged +intervals remaining. + +``` +$ go test -cpu=1 -bench=. -benchmem goos: darwin goarch: arm64 pkg: github.com/die-net/intervals -BenchmarkInsertNonOverlapping/1 115100521 10.30 ns/op -BenchmarkInsertNonOverlapping/10 45857023 26.01 ns/op -BenchmarkInsertNonOverlapping/100 29844249 39.27 ns/op -BenchmarkInsertNonOverlapping/1000 21177404 57.57 ns/op -BenchmarkInsertNonOverlapping/10000 8115252 147.0 ns/op -BenchmarkInsertOverlapping/1 91738153 12.94 ns/op -BenchmarkInsertOverlapping/10 52171077 23.19 ns/op -BenchmarkInsertOverlapping/100 54989328 22.00 ns/op -BenchmarkInsertOverlapping/1000 47051826 25.75 ns/op -BenchmarkInsertOverlapping/10000 36945717 32.76 ns/op +cpu: Apple M2 Pro +BenchmarkInsertDisjoint/1 107374227 10.96 ns/op 91.23 MB/s 0 B/op 0 allocs/op +BenchmarkInsertDisjoint/10 45950748 26.01 ns/op 38.44 MB/s 0 B/op 0 allocs/op +BenchmarkInsertDisjoint/100 26269006 45.47 ns/op 21.99 MB/s 0 B/op 0 allocs/op +BenchmarkInsertDisjoint/1000 12517956 95.94 ns/op 10.42 MB/s 0 B/op 0 allocs/op +BenchmarkInsertDisjoint/10000 2251401 532.3 ns/op 1.88 MB/s 0 B/op 0 allocs/op +BenchmarkInsertMergeable/1 100000000 10.96 ns/op 91.22 MB/s 0 B/op 0 allocs/op +BenchmarkInsertMergeable/10 48691087 24.37 ns/op 41.04 MB/s 0 B/op 0 allocs/op +BenchmarkInsertMergeable/100 30892900 38.76 ns/op 25.80 MB/s 0 B/op 0 allocs/op +BenchmarkInsertMergeable/1000 19887729 60.10 ns/op 16.64 MB/s 0 B/op 0 allocs/op +BenchmarkInsertMergeable/10000 7792966 157.4 ns/op 6.35 MB/s 0 B/op 0 allocs/op +BenchmarkInsertOverlapping/1 70237390 16.43 ns/op 60.88 MB/s 0 B/op 0 allocs/op +BenchmarkInsertOverlapping/10 47493560 25.14 ns/op 39.78 MB/s 0 B/op 0 allocs/op +BenchmarkInsertOverlapping/100 48296054 24.92 ns/op 40.12 MB/s 0 B/op 0 allocs/op +BenchmarkInsertOverlapping/1000 40313540 29.73 ns/op 33.63 MB/s 0 B/op 0 allocs/op +BenchmarkInsertOverlapping/10000 30862311 38.81 ns/op 25.77 MB/s 0 B/op 0 allocs/op +BenchmarkSearchDisjoint/1 479366676 2.506 ns/op 399.11 MB/s 0 B/op 0 allocs/op +BenchmarkSearchDisjoint/10 279172508 4.494 ns/op 222.52 MB/s 0 B/op 0 allocs/op +BenchmarkSearchDisjoint/100 194244874 6.083 ns/op 164.39 MB/s 0 B/op 0 allocs/op +BenchmarkSearchDisjoint/1000 145232559 8.269 ns/op 120.94 MB/s 0 B/op 0 allocs/op +BenchmarkSearchDisjoint/10000 110869339 10.91 ns/op 91.70 MB/s 0 B/op 0 allocs/op +BenchmarkSearchMerged/1 477927547 2.552 ns/op 391.81 MB/s 0 B/op 0 allocs/op +BenchmarkSearchMerged/10 478269381 2.502 ns/op 399.74 MB/s 0 B/op 0 allocs/op +BenchmarkSearchMerged/100 479289055 2.503 ns/op 399.54 MB/s 0 B/op 0 allocs/op +BenchmarkSearchMerged/1000 477311384 2.504 ns/op 399.28 MB/s 0 B/op 0 allocs/op +BenchmarkSearchMerged/10000 475930947 2.507 ns/op 398.91 MB/s 0 B/op 0 allocs/op PASS ``` -And there's 100% test coverage, with a bunch of weird corner cases tested. +(Numbers above were measured on an Apple M2 Pro and will vary by machine.) + +Testing +------- + +The package has 100% statement coverage, including a randomized fuzz-style test +and a number of awkward corner cases (empty ranges, `NaN` bounds, gap filling, +and full replacement). + +```sh +go test -cover ./... +``` License ------- -Copyright 2021-2024 Aaron Hopkins and contributors +Copyright 2021-2026 Aaron Hopkins and contributors 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 diff --git a/go.mod b/go.mod index 1a6474e..9c459cd 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,3 @@ module github.com/die-net/intervals -go 1.18 - -require ( - github.com/stretchr/testify v1.9.0 - golang.org/x/exp v0.0.0-20240707233637-46b078467d37 -) - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) +go 1.22 diff --git a/go.sum b/go.sum deleted file mode 100644 index e4cf63b..0000000 --- a/go.sum +++ /dev/null @@ -1,12 +0,0 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w= -golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/intervals.go b/intervals.go index 8c8ad1f..73d6d10 100644 --- a/intervals.go +++ b/intervals.go @@ -3,9 +3,8 @@ package intervals import ( + "cmp" "sort" - - "golang.org/x/exp/constraints" ) // Interval represents a range of the form `[Start, End)`, which contains @@ -14,7 +13,7 @@ import ( // // If T is a float, neither Start nor End can be NaN, since NaN is not // ordered. -type Interval[T constraints.Ordered] struct { +type Interval[T cmp.Ordered] struct { Start T End T } @@ -37,7 +36,7 @@ func (v Interval[T]) Empty() bool { // Intervals is an ascending ordered representation of a slice of // non-overlapping Interval. -type Intervals[T constraints.Ordered] []Interval[T] +type Intervals[T cmp.Ordered] []Interval[T] // Search will return the Interval in the given Intervals containing the // value of off and true if found. Otherwise, it will return an empty diff --git a/intervals_test.go b/intervals_test.go index 153a89c..a16da35 100644 --- a/intervals_test.go +++ b/intervals_test.go @@ -2,11 +2,10 @@ package intervals import ( "math" - "math/rand" + "math/rand/v2" + "slices" "strconv" "testing" - - "github.com/stretchr/testify/assert" ) func TestEmpty(t *testing.T) { @@ -30,8 +29,9 @@ func TestEmpty(t *testing.T) { } for _, test := range tests { - empty := test.interval.Empty() - assert.Equal(t, test.empty, empty) + if empty := test.interval.Empty(); empty != test.empty { + t.Errorf("Empty(%v) = %v, want %v", test.interval, empty, test.empty) + } } } @@ -54,8 +54,9 @@ func TestSearchExpected(t *testing.T) { for _, test := range tests { v, ok := test.intervals.Search(test.offset) - assert.Equal(t, test.expected, v) - assert.Equal(t, test.ok, ok) + if v != test.expected || ok != test.ok { + t.Errorf("%v.Search(%d) = (%v, %v), want (%v, %v)", test.intervals, test.offset, v, ok, test.expected, test.ok) + } } } @@ -85,19 +86,21 @@ func TestInsertExpected(t *testing.T) { for _, v := range test.inserts { vs = vs.Insert(Interval[int64]{v.Start, v.End}) } - assert.Equal(t, test.expected, vs) + if !slices.Equal(vs, test.expected) { + t.Errorf("inserting %v = %v, want %v", test.inserts, vs, test.expected) + } } } func TestInsertRandom(t *testing.T) { - for n := 0; n < 1000; n++ { + for range 1000 { vs := Intervals[int64]{} - count := rand.Intn(10) + 1 + count := rand.IntN(10) + 1 //nolint:gosec // Not security sensitive. start := int64(10000) end := int64(-1) - for i := 0; i < count; i++ { - s := rand.Int63n(1024) - e := s + rand.Int63n(128) + for range count { + s := rand.Int64N(1024) //nolint:gosec // Not security sensitive. + e := s + rand.Int64N(128) //nolint:gosec // Not security sensitive. v := Interval[int64]{s, e} vs = vs.Insert(v) @@ -109,24 +112,32 @@ func TestInsertRandom(t *testing.T) { if s < start { start = s } - assert.Equal(t, start, vs[0].Start) + if vs[0].Start != start { + t.Fatalf("vs[0].Start = %d, want %d", vs[0].Start, start) + } // Track our own idea of the last end and verify it. if e > end { end = e } - assert.Equal(t, end, vs[len(vs)-1].End) + if vs[len(vs)-1].End != end { + t.Fatalf("vs[%d].End = %d, want %d", len(vs)-1, vs[len(vs)-1].End, end) + } } - assert.LessOrEqual(t, len(vs), count) + if len(vs) > count { + t.Fatalf("len(vs) = %d, want <= %d", len(vs), count) + } - for i := 0; i < len(vs); i++ { + for i := range vs { // Make sure each of the records looks legit. - assert.Less(t, vs[i].Start, vs[i].End) + if vs[i].Start >= vs[i].End { + t.Fatalf("vs[%d] = %v, want Start < End", i, vs[i]) + } // And there's a gap to the next one. - if i < len(vs)-1 { - assert.Less(t, vs[i].End, vs[i+1].Start) + if i < len(vs)-1 && vs[i].End >= vs[i+1].Start { + t.Fatalf("vs[%d].End = %d >= vs[%d].Start = %d, want a gap", i, vs[i].End, i+1, vs[i+1].Start) } } } @@ -134,21 +145,37 @@ func TestInsertRandom(t *testing.T) { func TestInsertPanic(t *testing.T) { // End before start should panic. - assert.Panics(t, func() { Intervals[int64]{}.Insert(Interval[int64]{2, 1}) }) + assertPanics(t, func() { Intervals[int64]{}.Insert(Interval[int64]{2, 1}) }) } -func BenchmarkInsertNonOverlapping(b *testing.B) { - benchInsert(b, 1024, 0) +// assertPanics fails the test if fn does not panic when called. +func assertPanics(t *testing.T, fn func()) { + t.Helper() + defer func() { + if recover() == nil { + t.Error("expected panic, but function returned normally") + } + }() + fn() +} + +func BenchmarkInsertDisjoint(b *testing.B) { + benchInsert(b, 1024, 1, 0) +} + +func BenchmarkInsertMergeable(b *testing.B) { + benchInsert(b, 1024, 0, 0) } func BenchmarkInsertOverlapping(b *testing.B) { - benchInsert(b, 1024, 10240) + benchInsert(b, 1024, 0, 10240) } -func benchInsert(b *testing.B, step, overlap int64) { +func benchInsert(b *testing.B, step, gap, overlap int64) { b.ReportAllocs() for _, num := range []int{1, 10, 100, 1000, 10000} { b.Run(strconv.Itoa(num), func(sb *testing.B) { + sb.SetBytes(1) sb.RunParallel(func(pb *testing.PB) { ovs := make(Intervals[int64], 0, num) @@ -159,7 +186,7 @@ func benchInsert(b *testing.B, step, overlap int64) { n++ if n >= len(ivs) { ovs = ovs[:0] - randIntervals(ivs, step, overlap) + randIntervals(ivs, step, gap, overlap) n = 0 } @@ -170,22 +197,62 @@ func benchInsert(b *testing.B, step, overlap int64) { } } -func randIntervals(vs Intervals[int64], step, overlap int64) { +func randIntervals(vs Intervals[int64], step, gap, overlap int64) { s := int64(0) - for i := 0; i < len(vs); i++ { - l := rand.Int63n(step) + for i := range vs { + l := rand.Int64N(step) //nolint:gosec // Not security sensitive. e := s + l v := Interval[int64]{s, e} if overlap > 0 { - v.End += rand.Int63n(overlap) + v.End += rand.Int64N(overlap) //nolint:gosec // Not security sensitive. } vs[i] = v - s = e + s = e + gap } rand.Shuffle(len(vs), func(i, j int) { vs[i], vs[j] = vs[j], vs[i] }) } + +func BenchmarkSearchDisjoint(b *testing.B) { + benchSearch(b, 1024, 1, 0) +} + +func BenchmarkSearchMerged(b *testing.B) { + benchSearch(b, 1024, 0, 0) +} + +func benchSearch(b *testing.B, step, gap, overlap int64) { + b.ReportAllocs() + for _, num := range []int{1, 10, 100, 1000, 10000} { + b.Run(strconv.Itoa(num), func(sb *testing.B) { + sb.SetBytes(1) + sb.RunParallel(func(pb *testing.PB) { + ivs := make(Intervals[int64], num) + randIntervals(ivs, step, gap, overlap) + + ovs := make(Intervals[int64], 0, len(ivs)) + for _, iv := range ivs { + ovs = ovs.Insert(iv) + } + + start := ovs[0].Start + end := ovs[len(ovs)-1].End + + n := end + + for pb.Next() { + n++ + if n >= end { + n = start + } + + _, _ = ovs.Search(n) + } + }) + }) + } +} From 62a8f95536f77e6cbb434941944180de6d3b2ec6 Mon Sep 17 00:00:00 2001 From: Aaron Hopkins Date: Sun, 31 May 2026 20:26:11 -0700 Subject: [PATCH 2/3] Update to current golangci-lint config. --- .github/workflows/golangci-lint.yml | 7 +- .golangci.yml | 400 +++++++++++----------------- 2 files changed, 154 insertions(+), 253 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 0a0b973..b107336 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -11,8 +11,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - uses: actions/setup-go@v6 + with: + go-version: 1.26 - name: golangci-lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v9 with: - version: latest + version: v2.12 \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 9152b29..ba3ae90 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,33 +1,24 @@ -# Config file for golangci-lint +version: "2" -# options for analysis running -run: - # timeout for analysis, e.g. 30s, 5m, default is 1m. This has to be long - # enough to handle an empty cache on a slow machine. - timeout: 2m - - # include test files or not, default is true - tests: true - -# Run "golangci-lint linters" for a list of available linters. Don't enable -# any linters here, or they can't be disabled on the commandline. linters: - disable-all: true + default: none enable: - asciicheck - bodyclose - - err113 + - copyloopvar + - dogsled + - dupl - errcheck - errorlint - exhaustive - - exportloopref + - gocheckcompilerdirectives + - gochecknoinits - gocritic - - gofumpt - - goimports + - godox - goprintffuncname - gosec - - gosimple - govet + - intrange - ineffassign - makezero - misspell @@ -37,242 +28,149 @@ linters: - prealloc - predeclared - revive - - rowserrcheck - - sqlclosecheck - staticcheck - - tparallel - - typecheck + - testifylint - unconvert + - unparam - unused - wastedassign - - fast: false - -# all available settings of specific linters -linters-settings: - errcheck: - # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; - # default is false: such cases aren't reported by default. - check-blank: false - - # List of functions to exclude from checking, where each entry is a single function to exclude. - # See https://github.com/kisielk/errcheck#excluding-functions for details. - # exclude-functions: - exhaustive: - # If enum-like constants don't use all cases in a switch statement, - # consider a default good enough. - default-signifies-exhaustive: true - gocritic: - # all checks list: https://github.com/go-critic/checkers - # Enable all checks by enabling all tags, then disable a few. - enabled-tags: - - diagnostic - - experimental - - opinionated - - performance - - style - disabled-checks: - # Don't be aggressive with comments. - - commentedOutCode - # Catches legit uses of case checking. - - equalFold - # Many defers can be skipped when exiting. Could fix this with a log.Fatal replacement. - - exitAfterDefer - # Disagree with the style recommendations for these three. - - ifElseChain - - octalLiteral - - unnamedResult - - filepathJoin - - tooManyResultsChecker - settings: - captLocal: - paramsOnly: false - hugeParam: - # Allowing 512 byte parameters. - sizeThreshold: 512 - nestingReduce: - # How many nested blocks before suggesting early exit. - bodyWidth: 4 - rangeExprCopy: - # Avoid copying arrays larger than this in range statement. - sizeThreshold: 512 - rangeValCopy: - # Avoid copying range values larger than this on every iteration. - sizeThreshold: 128 - truncateCmp: - skipArchDependent: false - underef: - skipRecvDeref: false - goimports: - # put imports beginning with prefix after 3rd-party packages; - # it's a comma-separated list of prefixes - local-prefixes: github.com/die-net/ - govet: - # Enable most of the non-default linters too. - enable: - - asmdecl - - assign - - atomic - - atomicalign - - bools - - buildtag - - cgocall - - composite - - copylock - - durationcheck - - errorsas - - findcall - - httpresponse - - ifaceassert - - loopclosure - - lostcancel - - nilfunc - - nilness - - printf - - shadow - - shift - - sortslice - - stdmethods - - stringintconv - - structtag - - testinggoroutine - - tests - - unmarshal - - unreachable - - unsafeptr - - unusedresult - disable: - # We need to fix a few tests that rely on this first. - - deepequalerrors - nakedret: - # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 - max-func-lines: 6 - nolintlint: - allow-unused: false - allow-leading-space: false - require-explanation: true - revive: - # Show all issues, not just those with a high confidence - confidence: 0.0 + - whitespace + + settings: + dupl: + threshold: 100 + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + disabled-checks: + - unnamedResult + settings: + hugeParam: + sizeThreshold: 100 + godox: + keywords: + - FIXME + govet: + enable: + - asmdecl + - assign + - atomic + - atomicalign + - bools + - buildtag + - cgocall + - composites + - copylocks + - errorsas + - findcall + - httpresponse + - ifaceassert + - loopclosure + - lostcancel + - nilfunc + - nilness + - printf + - shadow + - shift + - sortslice + - stdmethods + - stringintconv + - structtag + - testinggoroutine + - tests + - unmarshal + - unreachable + - unsafeptr + - unusedresult + errorlint: + asserts: false + misspell: + locale: US + nolintlint: + allow-unused: false # report any unused nolint directives + require-explanation: true # require an explanation for nolint directives + require-specific: true # require nolint directives to be specific about which linter is being skipped + revive: + rules: + - name: atomic + - name: bare-return + - name: blank-imports + - name: confusing-naming + - name: context-as-argument + - name: context-keys-type + - name: datarace + - name: deep-exit + - name: defer + - name: dot-imports + - name: duplicated-imports + - name: early-return + - name: empty-block + - name: empty-block + - name: empty-lines + - name: error-naming + - name: error-return + - name: error-strings + - name: errorf + - name: exported + - name: forbidden-call-in-wg-go + - name: get-return + - name: if-return + - name: import-shadowing + - name: increment-decrement + - name: indent-error-flow + - name: inefficient-map-lookup + - name: modifies-value-receiver + - name: package-directory-mismatch + - name: range-val-address + - name: range-val-in-closure + - name: range + - name: receiver-naming + - name: redefines-builtin-id + - name: string-of-int + - name: struct-tag + - name: superfluous-else + - name: time-date + - name: time-equal + - name: time-naming + - name: unconditional-recursion + - name: unexported-return + - name: unnecessary-if + - name: unnecessary-format + - name: unnecessary-stmt + - name: unreachable-code + - name: unused-parameter + - name: unused-receiver + - name: use-any + - name: use-errors-new + - name: use-waitgroup-go + - name: useless-break + - name: useless-fallthrough + - name: var-declaration + - name: var-naming + - name: waitgroup-by-value + exclusions: rules: - - name: atomic - # - name: bare-return - - name: blank-imports - # - name: confusing-naming - # - name: confusing-results - - name: constant-logical-expr - - name: context-as-argument - - name: context-keys-type - # - name: deep-exit - # - name: defer - - name: dot-imports - # - name: early-return - # - name: empty-block - - name: error-naming - - name: error-return - - name: error-strings - - name: errorf - - name: exported - # - name: get-return - - name: identical-branches - - name: if-return - - name: increment-decrement - - name: indent-error-flow - # - name: import-shadowing - - name: modifies-parameter - # - name: modifies-value-receiver - - name: package-comments - - name: range - - name: range-val-in-closure - - name: range-val-address - - name: receiver-naming - # - name: redefines-builtin-id - - name: string-of-int - # - name: struct-tag - - name: superfluous-else - - name: time-naming - # - name: var-naming - # - name: var-declaration - - name: unconditional-recursion - # - name: unexported-naming - - name: unexported-return - - name: unnecessary-stmt - - name: unreachable-code - # - name: unused-parameter - # - name: unused-receiver - - name: waitgroup-by-value - -# output configuration options -output: - # colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number" - formats: - - format: line-number - - # print lines of code with issue, default is true - print-issued-lines: false - - # sorts results by: filepath, line and column - sort-results: true - -issues: - # List of regexps of issue texts to exclude, empty list by default. - # But independently from this option we use default exclude patterns, - # it can be disabled by `exclude-use-default: false`. To list all - # excluded by default patterns execute `golangci-lint run --help` - exclude: - # revive: We don't require comments, only that they be properly formatted - - should have( a package)? comment - - # revive: Don't force variable scope changes - - (indent-error-flow|superfluous-else).*drop this else and outdent its block .move short variable declaration to its own line if necessary. - - # govet: Allow the most common form of shadowing - - declaration of .err. shadows declaration - - # gocritic: We use named Err return as part of our defer handling pattern. - - captLocal. .Err. should not be capitalized - - # govet: Allow an unused noCopy struct field to disallow copying - - .noCopy. is unused - - # gosec: Let errcheck complain about this instead - - G104. Errors unhandled - - # gosec: All URLs are variable in our code; this isn't useful - - G107. Potential HTTP request made with variable url - - # gosec: Complaining about every exec.Command() is annoying; we'll audit them - - G204. Subprocess launching should be audited - - G204. Subprocess launched with variable - - G204. Subprocess launched with function call as argument or cmd arguments - - # gosec: Too many false positives for legit uses of files and directories - - G301. Expect directory permissions to be 0750 or less - - G302. Expect file permissions to be 0600 or less - - G306. Expect WriteFile permissions to be 0600 or less - - # gosec: False positive is triggered by 'src, err := ioutil.ReadFile(filename)' - - G304. Potential file inclusion via variable - - # gosec: Complaining about every use of math/rand is annoying. - - G404. Use of weak random number generator - - # gosec: We're allowing SHA1 for now, but MD5, DES, and RC4 need to be audited - - G401. Use of weak cryptographic primitive - - G505. Blocklisted import crypto/sha1. weak cryptographic primitive - - # Exclude some linters from running on template-generated code, where we - # can't fix the output. - exclude-rules: - - # Independently from option `exclude` we use default exclude patterns, - # it can be disabled by this option. To list all - # excluded by default patterns execute `golangci-lint run --help`. - # Default value for this option is true. - exclude-use-default: false - - # Maximum issues count per one linter. Set to 0 to disable. Default is 50. - max-issues-per-linter: 0 + - linters: + - govet + text: "shadow: declaration of \"err\" shadows declaration at" + - linters: + - errcheck + text: "Error return value of .*\\.Close. is not checked" + +formatters: + enable: + - gofumpt + - goimports + settings: + gofmt: + rewrite-rules: + - pattern: 'interface{}' + replacement: 'any' + goimports: + local-prefixes: + - github.com/die-net/intervals - # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. - max-same-issues: 0 From 755aafa2f584bbf807fa2b20fc29f8b181a379f2 Mon Sep 17 00:00:00 2001 From: Aaron Hopkins Date: Sun, 31 May 2026 20:35:16 -0700 Subject: [PATCH 3/3] Update go-test and golangci-lint workflows to newer Go and newer actions versions. --- .github/workflows/go-test.yml | 6 +++--- .github/workflows/golangci-lint.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index 45d7424..d0470a0 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -12,13 +12,13 @@ jobs: strategy: fail-fast: false matrix: - go: ['1.18', '1.19', '1.20', '1.21', '1.22'] + go: ['1.22', '1.23', '1.24', '1.25', '1.26'] steps: - - uses: actions/setup-go@v2 + - uses: actions/setup-go@v6 with: go-version: ${{ matrix.go }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - run: go test -v -short -race -coverprofile=profile.cov ./... - name: Send coverage uses: shogo82148/actions-goveralls@v1 diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index b107336..9f824d9 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -10,7 +10,7 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version: 1.26