Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ go.work.sum
# Editor/IDE
# .idea/
# .vscode/
bin/
bin/

# Codacy
.codacy/
.github/instructions/
8 changes: 8 additions & 0 deletions internal/exercises/catalog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ concepts:
test_regex: ".*"
hints:
- Define a custom error type and return it from a function.
- slug: 28_stateful_goroutines
title: Stateful Goroutines
test_regex: ".*"
hints:
- Use channels to send read and write operations to a state-owning goroutine.
- Create readOp and writeOp structs with response channels.
- The state-owning goroutine uses select to handle operations from channels.
- This pattern avoids mutexes by ensuring only one goroutine accesses shared state.
- slug: 37_xml
title: XML Encoding and Decoding
test_regex: ".*"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package stateful_goroutines

// readOp represents a read request
type readOp struct {
resp chan int
}

// writeOp represents a write request (increment)
type writeOp struct {
amount int
resp chan bool
}

type Counter struct {
reads chan readOp
writes chan writeOp
done chan struct{}
}

// NewCounter creates and starts a new stateful counter
func NewCounter() *Counter {
c := &Counter{
reads: make(chan readOp),
writes: make(chan writeOp),
done: make(chan struct{}),
}

// Start the state-owning goroutine
go func() {
var state int
for {
select {
case read := <-c.reads:
read.resp <- state
case write := <-c.writes:
state += write.amount
write.resp <- true
case <-c.done:
return
}
}
}()

return c
}

// Increment increments the counter by the given amount
func (c *Counter) Increment(amount int) {
write := writeOp{
amount: amount,
resp: make(chan bool),
}
c.writes <- write
<-write.resp
}

// GetValue returns the current counter value
func (c *Counter) GetValue() int {
read := readOp{
resp: make(chan int),
}
c.reads <- read
return <-read.resp
}

// Close stops the state-owning goroutine
func (c *Counter) Close() {
close(c.done)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package stateful_goroutines

// TODO:
// - Implement a Counter that manages state using a single goroutine and channels.
// - The counter should support Increment and GetValue operations.
// - State must be owned by a single goroutine to avoid race conditions.
// - Other goroutines communicate via channels to read or modify the state.

// readOp represents a read request
type readOp struct {
resp chan int
}

// writeOp represents a write request (increment)
type writeOp struct {
amount int
resp chan bool
}

type Counter struct {
reads chan readOp
writes chan writeOp
}

// NewCounter creates and starts a new stateful counter
func NewCounter() *Counter {
// TODO: initialize channels and start the state-owning goroutine
return &Counter{}
}

// Increment increments the counter by the given amount
func (c *Counter) Increment(amount int) {
// TODO: send a write operation and wait for confirmation
}

// GetValue returns the current counter value
func (c *Counter) GetValue() int {
// TODO: send a read operation and return the value
return 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package stateful_goroutines

import (
"sync"
"testing"
"time"
)

func TestCounterInitialization(t *testing.T) {
counter := NewCounter()
if counter == nil {
t.Fatal("NewCounter() returned nil")
}

value := counter.GetValue()
if value != 0 {
t.Errorf("Initial counter value = %d, want 0", value)
}
}

func TestCounterIncrement(t *testing.T) {
counter := NewCounter()

counter.Increment(5)
counter.Increment(3)

value := counter.GetValue()
if value != 8 {
t.Errorf("Counter value = %d, want 8", value)
}
}

func TestCounterConcurrentIncrements(t *testing.T) {
counter := NewCounter()

var wg sync.WaitGroup
numGoroutines := 100
incrementsPerGoroutine := 10

for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < incrementsPerGoroutine; j++ {
counter.Increment(1)
}
}()
}

wg.Wait()

expected := numGoroutines * incrementsPerGoroutine
value := counter.GetValue()
if value != expected {
t.Errorf("Counter value = %d, want %d", value, expected)
}
}

func TestCounterConcurrentReadsAndWrites(t *testing.T) {
counter := NewCounter()

var wg sync.WaitGroup
numReaders := 50
numWriters := 50

// Start writers
for i := 0; i < numWriters; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 5; j++ {
counter.Increment(1)
time.Sleep(time.Microsecond)
}
}()
}

// Start readers
for i := 0; i < numReaders; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 5; j++ {
_ = counter.GetValue()
time.Sleep(time.Microsecond)
}
}()
}

wg.Wait()

// Verify final value
expected := numWriters * 5
value := counter.GetValue()
if value != expected {
t.Errorf("Counter value = %d, want %d", value, expected)
}
}

func TestCounterNegativeIncrement(t *testing.T) {
counter := NewCounter()

counter.Increment(10)
counter.Increment(-3)

value := counter.GetValue()
if value != 7 {
t.Errorf("Counter value = %d, want 7", value)
}
}
Loading