From 67e002728f8161750fdbbf2e9b3d9a498e05b198 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 31 Mar 2026 16:16:20 +0800 Subject: [PATCH 1/2] format --- .github/workflows/ci.yml | 6 ++++++ .github/workflows/release.yml | 27 +++++++++++++++++++++++++++ README.md | 3 +++ arr/sorting.go | 5 ++++- collection/mutation.go | 29 +++++++++++++++++++++++------ collection/sorting.go | 5 ++++- 6 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b737661..93ea036 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,12 @@ jobs: - name: Run tests run: go test ./... -coverprofile=coverage.out -covermode=atomic + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + files: coverage.out + fail_ci_if_error: false + - name: Validate coverage threshold shell: bash run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..38a086f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run tests + run: go test ./... -covermode=atomic + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true diff --git a/README.md b/README.md index 935abe2..c951a90 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ [![Go Reference](https://pkg.go.dev/badge/github.com/gocanto/collection.svg)](https://pkg.go.dev/github.com/gocanto/collection) [![Go Version](https://img.shields.io/badge/go-1.25-blue.svg)](https://golang.org/doc/devel/release.html#go1.25) +[![CI](https://github.com/gocanto/collection/actions/workflows/ci.yml/badge.svg)](https://github.com/gocanto/collection/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/gocanto/collection/branch/main/graph/badge.svg)](https://codecov.io/gh/gocanto/collection) +[![Go Report Card](https://goreportcard.com/badge/github.com/gocanto/collection)](https://goreportcard.com/report/github.com/gocanto/collection) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) A powerful Go port of [Laravel Collections](https://laravel.com/docs/collections) — fluent, type-safe, and powered by Go generics and `iter.Seq` lazy evaluation. diff --git a/arr/sorting.go b/arr/sorting.go index 778044f..0532dd6 100644 --- a/arr/sorting.go +++ b/arr/sorting.go @@ -63,5 +63,8 @@ func Random[T any](items []T, counts ...int) []T { return shuffled } - return shuffled[:count] + result := make([]T, count) + copy(result, shuffled[:count]) + + return result } diff --git a/collection/mutation.go b/collection/mutation.go index 5c742a4..89603c5 100644 --- a/collection/mutation.go +++ b/collection/mutation.go @@ -67,7 +67,10 @@ func (c *Collection[T]) Pop(counts ...int) (T, bool) { _ = count // For single pop item := c.items[len(c.items)-1] - c.items = c.items[:len(c.items)-1] + + newItems := make([]T, len(c.items)-1) + copy(newItems, c.items[:len(c.items)-1]) + c.items = newItems return item, true } @@ -82,8 +85,14 @@ func (c *Collection[T]) PopMany(count int) *Collection[T] { } idx := len(c.items) - count - popped := Collect(c.items[idx:]) - c.items = c.items[:idx] + + items := make([]T, count) + copy(items, c.items[idx:]) + popped := Collect(items) + + remaining := make([]T, idx) + copy(remaining, c.items[:idx]) + c.items = remaining return popped } @@ -98,7 +107,10 @@ func (c *Collection[T]) Shift() (T, bool) { } item := c.items[0] - c.items = c.items[1:] + + newItems := make([]T, len(c.items)-1) + copy(newItems, c.items[1:]) + c.items = newItems return item, true } @@ -112,8 +124,13 @@ func (c *Collection[T]) ShiftMany(count int) *Collection[T] { return shifted } - shifted := Collect(c.items[:count]) - c.items = c.items[count:] + items := make([]T, count) + copy(items, c.items[:count]) + shifted := Collect(items) + + remaining := make([]T, len(c.items)-count) + copy(remaining, c.items[count:]) + c.items = remaining return shifted } diff --git a/collection/sorting.go b/collection/sorting.go index 54c0a6d..44486e6 100644 --- a/collection/sorting.go +++ b/collection/sorting.go @@ -75,5 +75,8 @@ func (c *Collection[T]) Random(counts ...int) *Collection[T] { return shuffled } - return Collect(shuffled.items[:count]) + result := make([]T, count) + copy(result, shuffled.items[:count]) + + return Collect(result) } From 91be11d9497c943156eaea385e718c636e99049c Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Tue, 31 Mar 2026 16:26:58 +0800 Subject: [PATCH 2/2] format --- collection/mutation.go | 46 +++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/collection/mutation.go b/collection/mutation.go index 89603c5..4672141 100644 --- a/collection/mutation.go +++ b/collection/mutation.go @@ -68,9 +68,13 @@ func (c *Collection[T]) Pop(counts ...int) (T, bool) { _ = count // For single pop item := c.items[len(c.items)-1] - newItems := make([]T, len(c.items)-1) - copy(newItems, c.items[:len(c.items)-1]) - c.items = newItems + // Zero the removed element so the GC can reclaim it — without this, + // the backing array keeps a stale reference and the value is never collected. + // Reslicing in-place avoids the O(N) alloc+copy that a new slice would require. + var zero T + + c.items[len(c.items)-1] = zero + c.items = c.items[:len(c.items)-1] return item, true } @@ -90,9 +94,16 @@ func (c *Collection[T]) PopMany(count int) *Collection[T] { copy(items, c.items[idx:]) popped := Collect(items) - remaining := make([]T, idx) - copy(remaining, c.items[:idx]) - c.items = remaining + // Zero the removed tail so the GC can reclaim those elements — without this, + // the backing array keeps stale references that are never collected. + // Reslicing in-place avoids the O(N) alloc+copy that a new slice would require. + var zero T + + for i := idx; i < len(c.items); i++ { + c.items[i] = zero + } + + c.items = c.items[:idx] return popped } @@ -108,9 +119,13 @@ func (c *Collection[T]) Shift() (T, bool) { item := c.items[0] - newItems := make([]T, len(c.items)-1) - copy(newItems, c.items[1:]) - c.items = newItems + // Zero the removed element so the GC can reclaim it — without this, + // the backing array keeps a stale reference and the value is never collected. + // Reslicing in-place avoids the O(N) alloc+copy that a new slice would require. + var zero T + + c.items[0] = zero + c.items = c.items[1:] return item, true } @@ -128,9 +143,16 @@ func (c *Collection[T]) ShiftMany(count int) *Collection[T] { copy(items, c.items[:count]) shifted := Collect(items) - remaining := make([]T, len(c.items)-count) - copy(remaining, c.items[count:]) - c.items = remaining + // Zero the removed head so the GC can reclaim those elements — without this, + // the backing array keeps stale references that are never collected. + // Reslicing in-place avoids the O(N) alloc+copy that a new slice would require. + var zero T + + for i := 0; i < count; i++ { + c.items[i] = zero + } + + c.items = c.items[count:] return shifted }