Skip to content

Commit 4d0cf6c

Browse files
committed
Initial implementation
1 parent e242d08 commit 4d0cf6c

6 files changed

Lines changed: 245 additions & 0 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: CI
2+
3+
on: [push]
4+
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- uses: actions/checkout@v4
11+
- name: Setup Go
12+
uses: actions/setup-go@v5
13+
with:
14+
go-version: '1.24'
15+
- name: Install dependencies
16+
run: go get .
17+
- name: Build
18+
run: go build -v ./...
19+
- name: Test with the Go CLI
20+
run: go test
21+

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# RingChan
2+
3+
[![CI](https://github.com/floatdrop/ringchan/actions/workflows/ci.yaml/badge.svg)](https://github.com/floatdrop/ringchan/actions/workflows/ci.yaml)
4+
[![Go Report Card](https://goreportcard.com/badge/github.com/floatdrop/ringchan)](https://goreportcard.com/report/github.com/floatdrop/ringchan)
5+
[![Go Reference](https://pkg.go.dev/badge/github.com/floatdrop/ringchan.svg)](https://pkg.go.dev/github.com/floatdrop/ringchan/v2)
6+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7+
8+
**RingChan** is a thread-safe, fixed-capacity ring buffer implemented as a channel in Go. It mimics Go's channel behavior while providing ring-buffer semantics — meaning **new items overwrite the oldest when full**.
9+
10+
## Features
11+
12+
- Fixed-size buffer with overwrite behavior
13+
- Range-friendly: can be iterated using `for ... range`
14+
- Safe for concurrent producers and consumers
15+
- Supports graceful shutdown via `Close()`
16+
17+
## Installation
18+
19+
```bash
20+
go get github.com/floatdrop/ringchan
21+
```
22+
23+
## Usage
24+
25+
```go
26+
package main
27+
28+
import (
29+
"fmt"
30+
"time"
31+
32+
"github.com/floatdrop/ringchan"
33+
)
34+
35+
func main() {
36+
rc := ringchan.New[string](3)
37+
38+
go func() {
39+
inputs := []string{"A", "B", "C", "D", "E"}
40+
for _, v := range inputs {
41+
rc.In <- v
42+
}
43+
rc.Close()
44+
}()
45+
46+
time.Sleep(50 * time.Millisecond)
47+
48+
for v := range rc.Out() {
49+
fmt.Println("Got:", v)
50+
}
51+
52+
// Output:
53+
// Got: C
54+
// Got: D
55+
// Got: E
56+
}
57+
```
58+
59+
## License
60+
61+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

example_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package ringchan_test
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/floatdrop/ringchan"
8+
)
9+
10+
func ExampleNew() {
11+
rc := ringchan.New[string](3)
12+
13+
go func() {
14+
inputs := []string{"A", "B", "C", "D", "E"}
15+
for _, v := range inputs {
16+
rc.In <- v
17+
}
18+
rc.Close()
19+
}()
20+
21+
time.Sleep(50 * time.Millisecond)
22+
23+
for v := range rc.Out {
24+
fmt.Println("Got:", v)
25+
}
26+
27+
// Output:
28+
// Got: C
29+
// Got: D
30+
// Got: E
31+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/floatdrop/ringchan
2+
3+
go 1.23

ringchan.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package ringchan
2+
3+
type Ring[T any] struct {
4+
in chan T
5+
out chan T
6+
7+
// In is the channel to send values to.
8+
In chan<- T
9+
// Out is the channel to receive values from.
10+
Out <-chan T
11+
}
12+
13+
// New creates a ring-buffered channel with fixed capacity.
14+
// When full, new inserts will drop the oldest items to make space.
15+
func New[T any](capacity int) *Ring[T] {
16+
in := make(chan T, capacity)
17+
out := make(chan T, capacity)
18+
19+
rc := &Ring[T]{
20+
in: in,
21+
out: out,
22+
In: in,
23+
Out: out,
24+
}
25+
26+
go rc.run()
27+
return rc
28+
}
29+
30+
func (rc *Ring[T]) run() {
31+
defer close(rc.out)
32+
33+
for v := range rc.in {
34+
select {
35+
case rc.out <- v:
36+
default:
37+
// Do non-blocking receive to drop the oldest item
38+
// if the buffer is full. This avoids blocking in case of an empty buffer.
39+
select {
40+
case <-rc.out:
41+
rc.out <- v
42+
default:
43+
rc.out <- v
44+
}
45+
}
46+
}
47+
}
48+
49+
// Len returns the number of items currently in the ring buffer.
50+
func (rc *Ring[T]) Len() int {
51+
return len(rc.out)
52+
}
53+
54+
// Close closes the input channel, ending the ring.
55+
func (rc *Ring[T]) Close() {
56+
close(rc.in)
57+
}

ringchan_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package ringchan
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func TestRingChanBasic(t *testing.T) {
9+
rc := New[int](3)
10+
11+
go func() {
12+
for i := 1; i <= 5; i++ {
13+
rc.In <- i
14+
}
15+
rc.Close()
16+
}()
17+
18+
time.Sleep(50 * time.Millisecond)
19+
20+
l := rc.Len()
21+
if l != 3 {
22+
t.Fatalf("expected Len()=%v, got %v", 3, l)
23+
}
24+
25+
var got []int
26+
for v := range rc.Out {
27+
got = append(got, v)
28+
}
29+
30+
// Only last 3 values should be kept due to overwrite
31+
want := []int{3, 4, 5}
32+
if len(got) != len(want) {
33+
t.Fatalf("expected %v values, got %v", len(want), len(got))
34+
}
35+
for i := range want {
36+
if got[i] != want[i] {
37+
t.Errorf("expected %v at index %d, got %v", want[i], i, got[i])
38+
}
39+
}
40+
}
41+
42+
func TestRingChanBlockingReceive(t *testing.T) {
43+
rc := New[int](1)
44+
45+
go func() {
46+
time.Sleep(100 * time.Millisecond)
47+
rc.In <- 42
48+
rc.Close()
49+
}()
50+
51+
val := <-rc.Out
52+
if val != 42 {
53+
t.Errorf("expected 42, got %v", val)
54+
}
55+
}
56+
57+
func TestRingChanRangeAfterClose(t *testing.T) {
58+
rc := New[string](2)
59+
60+
rc.In <- "foo"
61+
rc.In <- "bar"
62+
rc.Close()
63+
64+
var results []string
65+
for v := range rc.Out {
66+
results = append(results, v)
67+
}
68+
69+
if len(results) != 2 || results[0] != "foo" || results[1] != "bar" {
70+
t.Errorf("unexpected results: %v", results)
71+
}
72+
}

0 commit comments

Comments
 (0)