diff --git a/.gitignore b/.gitignore index afbb2bb..e0b355c 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,8 @@ go.work.sum # Editor/IDE # .idea/ # .vscode/ -bin/ \ No newline at end of file +bin/ + +# Codacy +.codacy/ +.github/instructions/ \ No newline at end of file diff --git a/internal/exercises/catalog.yaml b/internal/exercises/catalog.yaml index 8d0a808..17118f6 100644 --- a/internal/exercises/catalog.yaml +++ b/internal/exercises/catalog.yaml @@ -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: ".*" diff --git a/internal/exercises/solutions/28_stateful_goroutines/stateful_goroutines.go b/internal/exercises/solutions/28_stateful_goroutines/stateful_goroutines.go new file mode 100644 index 0000000..24f93b9 --- /dev/null +++ b/internal/exercises/solutions/28_stateful_goroutines/stateful_goroutines.go @@ -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) +} diff --git a/internal/exercises/templates/28_stateful_goroutines/stateful_goroutines.go b/internal/exercises/templates/28_stateful_goroutines/stateful_goroutines.go new file mode 100644 index 0000000..a962c7e --- /dev/null +++ b/internal/exercises/templates/28_stateful_goroutines/stateful_goroutines.go @@ -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 +} diff --git a/internal/exercises/templates/28_stateful_goroutines/stateful_goroutines_test.go b/internal/exercises/templates/28_stateful_goroutines/stateful_goroutines_test.go new file mode 100644 index 0000000..8db3acf --- /dev/null +++ b/internal/exercises/templates/28_stateful_goroutines/stateful_goroutines_test.go @@ -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) + } +}