Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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,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
}
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,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)
}
}
Loading