From 71f8d69f5078d83bda1ab3b07336b11db9b4729f Mon Sep 17 00:00:00 2001 From: aviralgarg05 Date: Thu, 2 Oct 2025 12:46:24 +0530 Subject: [PATCH 1/4] feat: Add Stateful Goroutines exercise (28_stateful_goroutines) This commit implements issue #71: Add exercise templates and tests for Stateful Goroutines concept. Changes: - Added exercise template at internal/exercises/templates/28_stateful_goroutines/ - stateful_goroutines.go: Incomplete template with TODO comments demonstrating the stateful goroutines pattern using channels for state management - stateful_goroutines_test.go: Comprehensive test suite covering: * Counter initialization * Basic increment operations * Concurrent increments from multiple goroutines * Concurrent reads and writes * Negative increment support - Added working solution at internal/exercises/solutions/28_stateful_goroutines/ - Implements the channel-based state management pattern - Uses a single goroutine to own state (avoiding race conditions) - readOp and writeOp structs for communication via channels - select statement for handling concurrent operations - Updated internal/exercises/catalog.yaml - Added slug: 28_stateful_goroutines - Title: Stateful Goroutines - Comprehensive hints explaining the pattern: * Channel-based communication * readOp and writeOp structs with response channels * State-owning goroutine with select * Avoiding mutexes through single-goroutine ownership Testing: - Template tests fail as expected (4/5 tests fail due to incomplete implementation) - Solution passes all tests (5/5 tests pass) - Tests validate concurrent safety and correctness This exercise teaches the Go concurrency pattern of using goroutines and channels to manage shared state, aligning with Go's principle of 'share memory by communicating' rather than using explicit locks. --- internal/exercises/catalog.yaml | 8 ++ .../stateful_goroutines.go | 60 ++++++++++ .../stateful_goroutines.go | 40 +++++++ .../stateful_goroutines_test.go | 113 ++++++++++++++++++ 4 files changed, 221 insertions(+) create mode 100644 internal/exercises/solutions/28_stateful_goroutines/stateful_goroutines.go create mode 100644 internal/exercises/templates/28_stateful_goroutines/stateful_goroutines.go create mode 100644 internal/exercises/templates/28_stateful_goroutines/stateful_goroutines_test.go 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..4a95126 --- /dev/null +++ b/internal/exercises/solutions/28_stateful_goroutines/stateful_goroutines.go @@ -0,0 +1,60 @@ +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 +} + +// NewCounter creates and starts a new stateful counter +func NewCounter() *Counter { + c := &Counter{ + reads: make(chan readOp), + writes: make(chan writeOp), + } + + // 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 + } + } + }() + + 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 +} 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..3036442 --- /dev/null +++ b/internal/exercises/templates/28_stateful_goroutines/stateful_goroutines_test.go @@ -0,0 +1,113 @@ +package stateful_goroutines + +import ( + "sync" + "testing" + "time" +) + +func TestCounterInitialization(t *testing.T) { + counter := NewCounter() + if counter == nil { + t.Fatal("NewCounter() returned nil") + } + + // Give goroutine time to start + time.Sleep(10 * time.Millisecond) + + 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) + } +} From 4cbb20ddb26d2684933e4657e30b981ab38e5a2a Mon Sep 17 00:00:00 2001 From: aviralgarg05 Date: Thu, 2 Oct 2025 12:48:37 +0530 Subject: [PATCH 2/4] chore: Fix whitespace formatting and update .gitignore - Fix whitespace formatting in solution file (remove trailing spaces) - Update .gitignore to exclude Codacy artifacts: - .codacy/ directory - .github/instructions/ directory These files are tool-generated and should not be committed to the repository. --- .gitignore | 6 +++++- .../solutions/28_stateful_goroutines/stateful_goroutines.go | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) 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/solutions/28_stateful_goroutines/stateful_goroutines.go b/internal/exercises/solutions/28_stateful_goroutines/stateful_goroutines.go index 4a95126..90393ce 100644 --- a/internal/exercises/solutions/28_stateful_goroutines/stateful_goroutines.go +++ b/internal/exercises/solutions/28_stateful_goroutines/stateful_goroutines.go @@ -22,7 +22,7 @@ func NewCounter() *Counter { reads: make(chan readOp), writes: make(chan writeOp), } - + // Start the state-owning goroutine go func() { var state int @@ -36,7 +36,7 @@ func NewCounter() *Counter { } } }() - + return c } From 6d8cc5a2d635bec743158920a071fd359d0d21f2 Mon Sep 17 00:00:00 2001 From: aviralgarg05 Date: Thu, 2 Oct 2025 12:54:01 +0530 Subject: [PATCH 3/4] fix: Format Go code according to Go standards Run 'go fmt ./...' to fix formatting issues in test file. This resolves the CI/CD pipeline failure caused by improper code formatting. Fixed file: - internal/exercises/templates/28_stateful_goroutines/stateful_goroutines_test.go (corrected whitespace formatting) --- .../stateful_goroutines_test.go | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/internal/exercises/templates/28_stateful_goroutines/stateful_goroutines_test.go b/internal/exercises/templates/28_stateful_goroutines/stateful_goroutines_test.go index 3036442..d67d16e 100644 --- a/internal/exercises/templates/28_stateful_goroutines/stateful_goroutines_test.go +++ b/internal/exercises/templates/28_stateful_goroutines/stateful_goroutines_test.go @@ -11,10 +11,10 @@ func TestCounterInitialization(t *testing.T) { if counter == nil { t.Fatal("NewCounter() returned nil") } - + // Give goroutine time to start time.Sleep(10 * time.Millisecond) - + value := counter.GetValue() if value != 0 { t.Errorf("Initial counter value = %d, want 0", value) @@ -23,10 +23,10 @@ func TestCounterInitialization(t *testing.T) { 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) @@ -35,11 +35,11 @@ func TestCounterIncrement(t *testing.T) { 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() { @@ -49,9 +49,9 @@ func TestCounterConcurrentIncrements(t *testing.T) { } }() } - + wg.Wait() - + expected := numGoroutines * incrementsPerGoroutine value := counter.GetValue() if value != expected { @@ -61,11 +61,11 @@ func TestCounterConcurrentIncrements(t *testing.T) { 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) @@ -77,7 +77,7 @@ func TestCounterConcurrentReadsAndWrites(t *testing.T) { } }() } - + // Start readers for i := 0; i < numReaders; i++ { wg.Add(1) @@ -89,9 +89,9 @@ func TestCounterConcurrentReadsAndWrites(t *testing.T) { } }() } - + wg.Wait() - + // Verify final value expected := numWriters * 5 value := counter.GetValue() @@ -102,10 +102,10 @@ func TestCounterConcurrentReadsAndWrites(t *testing.T) { 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) From 3657076537d322bd1508a31963a541c62d8673ce Mon Sep 17 00:00:00 2001 From: aviralgarg05 Date: Thu, 2 Oct 2025 13:10:32 +0530 Subject: [PATCH 4/4] fix: address PR review issues - Remove unnecessary time.Sleep from TestCounterInitialization test - Add Close() method to Counter to prevent goroutine leaks - Add done channel to properly terminate state-owning goroutine Fixes review comments from https://github.com/zhravan/golearn/pull/112#pullrequestreview-3292970924 --- .../28_stateful_goroutines/stateful_goroutines.go | 9 +++++++++ .../28_stateful_goroutines/stateful_goroutines_test.go | 3 --- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/exercises/solutions/28_stateful_goroutines/stateful_goroutines.go b/internal/exercises/solutions/28_stateful_goroutines/stateful_goroutines.go index 90393ce..24f93b9 100644 --- a/internal/exercises/solutions/28_stateful_goroutines/stateful_goroutines.go +++ b/internal/exercises/solutions/28_stateful_goroutines/stateful_goroutines.go @@ -14,6 +14,7 @@ type writeOp struct { type Counter struct { reads chan readOp writes chan writeOp + done chan struct{} } // NewCounter creates and starts a new stateful counter @@ -21,6 +22,7 @@ func NewCounter() *Counter { c := &Counter{ reads: make(chan readOp), writes: make(chan writeOp), + done: make(chan struct{}), } // Start the state-owning goroutine @@ -33,6 +35,8 @@ func NewCounter() *Counter { case write := <-c.writes: state += write.amount write.resp <- true + case <-c.done: + return } } }() @@ -58,3 +62,8 @@ func (c *Counter) GetValue() 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_test.go b/internal/exercises/templates/28_stateful_goroutines/stateful_goroutines_test.go index d67d16e..8db3acf 100644 --- a/internal/exercises/templates/28_stateful_goroutines/stateful_goroutines_test.go +++ b/internal/exercises/templates/28_stateful_goroutines/stateful_goroutines_test.go @@ -12,9 +12,6 @@ func TestCounterInitialization(t *testing.T) { t.Fatal("NewCounter() returned nil") } - // Give goroutine time to start - time.Sleep(10 * time.Millisecond) - value := counter.GetValue() if value != 0 { t.Errorf("Initial counter value = %d, want 0", value)