Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
26 changes: 26 additions & 0 deletions internal/exercises/catalog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,32 @@ concepts:
test_regex: ".*"
hints:
- Define a custom error type and return it from a function.
- slug: 28_defer
title: Defer
test_regex: ".*"
hints:
- Use `f.close()` with `defer` keyword.
- slug: 29_go_routines
title: Go Routines
test_regex: ".*"
hints:
- Use `go` keyword to execute functions concurrently using go routines.
- slug: 30_channels
title: Channels
test_regex: ".*"
hints:
- Use `make(chan T)` to create a channel and `<-` to send and receive on it.
- slug: 31_mutexes
title: Mutexes
test_regex: ".*"
hints:
- Use `sync.Mutex` to synchronize access to a shared resource.
- slug: 32_sorting
title: Sorting
test_regex: ".*"
hints:
- Use `slice.Sort` for sorting slices.

- slug: 37_xml
title: XML Encoding and Decoding
test_regex: ".*"
Expand Down
29 changes: 29 additions & 0 deletions internal/exercises/solutions/28_defer/defer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package deferr

import (
"fmt"
"os"
"sync"
)

var (
fileInstance *os.File
once sync.Once
)

func CreateFile() *os.File {
once.Do(func() {
f, err := os.CreateTemp("", "example.txt")
if err != nil {
panic(err)
}
fileInstance = f
})
return fileInstance
}

func WriteToFile(*os.File) {
f := CreateFile()
defer f.Close()
fmt.Fprintln(f, "data")
}
Comment on lines +25 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove unused parameter or use it instead of calling CreateFile().

The function signature accepts *os.File but ignores it, calling CreateFile() instead. This is confusing and creates an inconsistent API.

Consider one of these approaches:

  1. Remove the parameter if CreateFile() should always be used:

    func WriteToFile() {
        f := CreateFile()
        defer f.Close()
        fmt.Fprintln(f, "data")
    }
  2. Use the parameter if the file should be provided by the caller:

    func WriteToFile(f *os.File) {
        defer f.Close()
        fmt.Fprintln(f, "data")
    }

The second approach is more flexible and matches the function signature, but verify that the test expectations align with the chosen design.

🤖 Prompt for AI Agents
internal/exercises/solutions/28_defer/defer.go lines 8-12: the function
signature declares a *os.File parameter but ignores it and always calls
CreateFile(); update the implementation to use the provided parameter instead of
calling CreateFile() — remove the CreateFile() call, defer closing the passed-in
file (defer f.Close()), and write to that file with fmt.Fprintln(f, "data");
ensure tests expecting the caller to supply the file still pass.

25 changes: 25 additions & 0 deletions internal/exercises/solutions/29_go_routines/go_routines.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package go_routines

import (
"time"
)

func sleepForOneHundredMillisecond() {
time.Sleep(100 * time.Millisecond)
}

func sleepForTwoHundredMilliseconds() {
time.Sleep(200 * time.Millisecond)
}

func RunConcurrently() {
go sleepForOneHundredMillisecond()
go sleepForTwoHundredMilliseconds()

// go func() {
// sleepForOneHundredMillisecond()
// }()
// go func() {
// sleepForTwoHundredMilliseconds()
// }()
}
Comment on lines +15 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Goroutines launched without synchronization.

Lines 16-17 launch goroutines with go keyword but RunConcurrently() returns immediately without waiting for them to complete. This means:

  1. The goroutines may not execute before the program exits
  2. Tests calling this function may have flaky results
  3. The behavior is non-deterministic

For a solution/reference implementation, proper synchronization is essential:

+import (
+    "sync"
+    "time"
+)
+
 func RunConcurrently() {
-    go sleepForOneHundredMillisecond()
-    go sleepForTwoHundredMilliseconds()
+    var wg sync.WaitGroup
+    wg.Add(2)
+    
+    go func() {
+        defer wg.Done()
+        sleepForOneHundredMillisecond()
+    }()
+    go func() {
+        defer wg.Done()
+        sleepForTwoHundredMilliseconds()
+    }()
+    
+    wg.Wait()
-
-    // Alternatively : Anonymous Goroutines
-    // go func() {
-    //     sleepForOneHundredMillisecond()
-    // }()
-    // go func() {
-    //     sleepForTwoHundredMilliseconds()
-    // }()
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func RunConcurrently() {
go sleepForOneHundredMillisecond()
go sleepForTwoHundredMilliseconds()
// Alternatively : Anonymous Goroutines
// go func() {
// sleepForOneHundredMillisecond()
// }()
// go func() {
// sleepForTwoHundredMilliseconds()
// }()
}
import (
"sync"
"time"
)
func RunConcurrently() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
sleepForOneHundredMillisecond()
}()
go func() {
defer wg.Done()
sleepForTwoHundredMilliseconds()
}()
wg.Wait()
}
🤖 Prompt for AI Agents
In internal/exercises/solutions/29_go_routines/go_routines.go around lines 15 to
26, the function launches two goroutines and returns immediately, causing
non-deterministic behavior and flaky tests; fix by synchronizing: create a
sync.WaitGroup, Add(2) before starting goroutines, have each goroutine call
Done() when finished, and call wg.Wait() at the end of RunConcurrently to block
until both goroutines complete (alternatively use a two-value channel and block
reading two results) so the function only returns after both sleep routines
finish.

13 changes: 13 additions & 0 deletions internal/exercises/solutions/30_channels/channels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package channels

func ReadMessage() string {
chat := make(chan string)
go func() {
senderMessage := "Hi!"
chat <- senderMessage
}()

msg := <-chat

return msg
}
31 changes: 31 additions & 0 deletions internal/exercises/solutions/31_mutexes/mutexes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package mutexes

import "sync"

const numWorkers = 10_000

func Counting() int {

count := 0
done := make(chan bool, numWorkers)
var wg sync.WaitGroup
var mu sync.Mutex

for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
count += 1
mu.Unlock()
done <- true
}()
}

for i := 1; i <= numWorkers; i++ {
<-done
}

wg.Wait()
return count
}
18 changes: 18 additions & 0 deletions internal/exercises/solutions/32_sorting/sorting.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package sorting

import (
"slices"
)

var Years = []int{2017, 2003, 2026}
var Pets = []string{"Dog", "Cat", "Parrot"}
Comment on lines +7 to +8

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Refactor to avoid mutable global state.

Exported package-level variables that are mutated by the sorting functions create several issues:

  1. Non-idempotent behavior: After the first call, the data is already sorted, making subsequent calls redundant.
  2. Test reliability: If tests run multiple times or in parallel, shared mutable state can cause unpredictable results.
  3. Poor practice demonstration: For an educational exercise, this pattern teaches anti-patterns rather than best practices.

Consider refactoring to accept input parameters and return new sorted slices:

-var Years = []int{2017, 2003, 2026}
-var Pets = []string{"Dog", "Cat", "Parrot"}
-
-func SortNumbers() []int {
-	slices.Sort(Years)
-	return Years
+func SortNumbers(numbers []int) []int {
+	sorted := make([]int, len(numbers))
+	copy(sorted, numbers)
+	slices.Sort(sorted)
+	return sorted
 }
 
-func SortAnimals() []string {
-	slices.Sort(Pets)
-	return Pets
+func SortAnimals(animals []string) []string {
+	sorted := make([]string, len(animals))
+	copy(sorted, animals)
+	slices.Sort(sorted)
+	return sorted
 }

Alternatively, if in-place sorting is the intended teaching point, document this behavior clearly and accept the slice as a parameter.

🤖 Prompt for AI Agents
internal/exercises/solutions/32_sorting/sorting.go lines 7-8: the file currently
defines exported package-level mutable variables Years and Pets which causes
non-idempotent behavior and test flakiness; refactor by removing these exported
globals and instead implement functions that accept a slice parameter and return
a sorted copy (or, if demonstrating in-place sorting, accept a slice parameter
and clearly document/make the function name reflect that it mutates the input),
ensure you do not mutate package state, and update callers/tests to pass slices
and receive sorted results (or pass slices explicitly when in-place).


func SortNumbers() []int {
slices.Sort(Years)
return Years
}

func SortAnimals() []string {
slices.Sort(Pets)
return Pets
}
29 changes: 29 additions & 0 deletions internal/exercises/templates/28_defer/defer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package deferr

import (
"fmt"
"os"
"sync"
)

var (
fileInstance *os.File
once sync.Once
)

func CreateFile() *os.File {
once.Do(func() {
f, err := os.CreateTemp("", "example.txt")
if err != nil {
panic(err)
}
fileInstance = f
})
return fileInstance
}

func WriteToFile(*os.File) {
f := CreateFile()
// TODO: Add your code here
fmt.Fprintln(f, "data")
}
17 changes: 17 additions & 0 deletions internal/exercises/templates/28_defer/defer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package deferr

import (
"fmt"
"testing"
)

func TestClosingFileAfterWriting(t *testing.T) {
f := CreateFile()
WriteToFile(f)

// trying to write to the file after closing
_, err := fmt.Fprintln(f, "data")
if err == nil {
t.Fatal("File wasn't closed!")
}
}
Comment on lines +8 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Test relies on confusing API where WriteToFile ignores its parameter.

The test passes a file handle to WriteToFile(f) on line 10, but as noted in the solution file review, WriteToFile ignores this parameter and calls CreateFile() internally. This makes the test's intent unclear.

Additionally, since CreateFile() uses a singleton pattern (sync.Once), this test depends on:

  1. Being the first/only test to run, or
  2. WriteToFile closing the singleton file instance

Consider clarifying the API:

  • If WriteToFile should accept and use the file parameter, update the implementation
  • If WriteToFile should always use CreateFile(), remove the parameter from both the function signature and this test call

🏁 Script executed:

#!/bin/bash
# Locate WriteToFile and CreateFile implementations
rg -n --type=go 'func WriteToFile' -g '*.go'
rg -n --type=go 'func CreateFile' -g '*.go'

Length of output: 234


🏁 Script executed:

#!/bin/bash
# Extract WriteToFile implementation
sed -n '1,50p' internal/exercises/templates/28_defer/defer.go | sed -n '1,50p'
# Extract CreateFile and sync.Once usage
sed -n '1,200p' internal/exercises/templates/28_defer/utils.go | sed -n '1,200p'

Length of output: 424


Clarify WriteToFile API to use or remove its *os.File parameter

  • WriteToFile (internal/exercises/templates/28_defer/defer.go:8) ignores its *os.File argument and unconditionally calls CreateFile(), so the test’s call on line 10 is misleading.
  • CreateFile() uses a singleton (sync.Once), making the test’s behavior dependent on execution order or implicit closing.
    Choose one:
    • Have WriteToFile write to the passed-in file, or
    • Drop the file parameter (and its use in the test) to reflect its singleton design.
🤖 Prompt for AI Agents
In internal/exercises/templates/28_defer/defer_test.go around lines 8 to 17 the
test calls WriteToFile(f) but WriteToFile currently ignores its *os.File
parameter and instead calls CreateFile() (which itself is a singleton via
sync.Once), making the test misleading and order-dependent; fix by choosing one
of two options and applying matching changes: either (A) make WriteToFile
actually write to the provided *os.File (remove its internal CreateFile call)
and keep the test as-is so it creates a file, passes it to WriteToFile, then
closes it and asserts writes fail; or (B) remove the *os.File parameter from
WriteToFile and update the test to call WriteToFile() (no arg) to reflect the
singleton CreateFile usage; in either case update the function signature,
implementation, and the test call sites consistently and ensure file closing
behavior in the test remains correct.

19 changes: 19 additions & 0 deletions internal/exercises/templates/29_go_routines/go_routines.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package go_routines

import (
"time"
)

func sleepForOneHundredMillisecond() {
time.Sleep(100 * time.Millisecond)
}

func sleepForTwoHundredMilliseconds() {
time.Sleep(200 * time.Millisecond)
}

func RunConcurrently() {
// TODO: update following code
sleepForOneHundredMillisecond()
sleepForTwoHundredMilliseconds()
}
24 changes: 24 additions & 0 deletions internal/exercises/templates/29_go_routines/go_routines_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package go_routines

import (
"testing"
"time"
)

func TestGoRoutines(t *testing.T) {
start := time.Now()
RunConcurrently()
elapsed := time.Since(start)

// according to benchmark it is about ~2 microseconds
// x2 for more breathing room, so 4
if elapsed.Microseconds() >= 4 {
t.Fatal("Go routines was not used!")
}
}
Comment on lines +8 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Test logic is critically flawed and will produce false positives.

The test has multiple issues:

  1. Incorrect timing threshold: The test expects concurrent execution to complete in <3 microseconds, but goroutines that sleep for 100ms and 200ms will take at minimum ~200ms (the duration of the longer sleep) when properly concurrent. The test will pass for the wrong reason—if RunConcurrently() spawns goroutines without waiting for them (as the template version does), the function returns immediately (~microseconds), and the test passes even though the work isn't complete.

  2. Missing synchronization: The template's RunConcurrently() (lines 3-6 of go_routines.go) runs sequentially. The solution spawns goroutines but doesn't wait for them to complete. A correct concurrent implementation needs a time.Sleep() or sync.WaitGroup to ensure goroutines finish before returning.

  3. Inverted expectation: Sequential execution should take ~300ms (100ms + 200ms). Concurrent should take ~200ms (max of the two). The test should verify elapsed < sequentialTime, not check microsecond timing.

Apply this diff to fix the test logic:

 func TestGoRoutines(t *testing.T) {
 	start := time.Now()
 	RunConcurrently()
 	elapsed := time.Since(start)
 
-	// according to benchmark it is about ~1.5 microseconds
-	// x2 for more breathing room, so 3
-	if elapsed.Microseconds() >= 3 {
-		t.Fatal("Go routines was not used!")
+	// Sequential execution would take ~300ms (100ms + 200ms)
+	// Concurrent execution should take ~200ms (max of the two)
+	// Allow 250ms threshold to account for scheduling overhead
+	if elapsed.Milliseconds() >= 250 {
+		t.Fatalf("Go routines was not used! Elapsed: %v", elapsed)
 	}
 }

Note: The solution's RunConcurrently() also needs synchronization (e.g., time.Sleep(250*time.Millisecond) at the end) to wait for goroutines to complete.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestGoRoutines(t *testing.T) {
start := time.Now()
RunConcurrently()
elapsed := time.Since(start)
// according to benchmark it is about ~1.5 microseconds
// x2 for more breathing room, so 3
if elapsed.Microseconds() >= 3 {
t.Fatal("Go routines was not used!")
}
}
func TestGoRoutines(t *testing.T) {
start := time.Now()
RunConcurrently()
elapsed := time.Since(start)
// Sequential execution would take ~300ms (100ms + 200ms)
// Concurrent execution should take ~200ms (max of the two)
// Allow 250ms threshold to account for scheduling overhead
if elapsed.Milliseconds() >= 250 {
t.Fatalf("Go routines was not used! Elapsed: %v", elapsed)
}
}
🤖 Prompt for AI Agents
internal/exercises/templates/29_go_routines/go_routines_test.go lines 8-18: the
test’s timing and synchronization are wrong — it currently asserts elapsed < 3µs
which yields false positives if RunConcurrently returns before goroutines
complete. Fix by measuring the sequential baseline (e.g., sequential :=
100*time.Millisecond + 200*time.Millisecond), ensure RunConcurrently waits for
work to finish (prefer sync.WaitGroup or a final sleep in the implementation),
then assert the concurrent elapsed is less than the sequential baseline (use a
tolerant threshold like elapsed < 250*time.Millisecond) and fail otherwise.


func BenchmarkRunConcurrently(b *testing.B) {
for i := 0; i < b.N; i++ {
RunConcurrently()
}
}
15 changes: 15 additions & 0 deletions internal/exercises/templates/30_channels/channels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package channels

import "fmt"

func ReadMessage() string {
// TODO: update following code
// to receive senderMessage in main goroutine
// and return it from ReadMessage function
go func() {
senderMessage := "Hi!"
fmt.Println(senderMessage)
}()

return ""
}
12 changes: 12 additions & 0 deletions internal/exercises/templates/30_channels/channels_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package channels

import "testing"

func TestReadMessage(t *testing.T) {
got := ReadMessage()
want := "Hi!"

if got != want {
t.Errorf("Expected %s, got %s", want, got)
}
}
30 changes: 30 additions & 0 deletions internal/exercises/templates/31_mutexes/mutexes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package mutexes

import "sync"

const numWorkers = 10_000

func Counting() int {

// TODO: update following code to avoid race conditions
// using sync.Mutex
count := 0
done := make(chan bool, numWorkers)
var wg sync.WaitGroup

for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
count += 1
done <- true
}()
Comment on lines +15 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Synchronize the shared counter to avoid races.

count += 1 runs concurrently without a mutex, so increments are lost and Counting() can return less than numWorkers, making the template fail its own test. Please guard the increment with a sync.Mutex.

 	count := 0
 	done := make(chan bool, numWorkers)
 	var wg sync.WaitGroup
+	var mu sync.Mutex
 
 	for i := 1; i <= numWorkers; i++ {
 		wg.Add(1)
 		go func() {
 			defer wg.Done()
-			count += 1
+			mu.Lock()
+			count++
+			mu.Unlock()
 			done <- true
 		}()
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
count += 1
done <- true
}()
count := 0
done := make(chan bool, numWorkers)
var wg sync.WaitGroup
var mu sync.Mutex
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
count++
mu.Unlock()
done <- true
}()
}
🤖 Prompt for AI Agents
In internal/exercises/templates/31_mutexes/mutexes.go around lines 15 to 21, the
concurrent increment "count += 1" is not synchronized which causes data races
and lost increments; declare a sync.Mutex (e.g., var mu sync.Mutex) in the
surrounding scope and inside the goroutine lock the mutex before incrementing
and unlock it afterward (mu.Lock(); count += 1; mu.Unlock()), leaving wg.Done
and done <- true as-is so each worker still signals completion.

}

for i := 1; i <= numWorkers; i++ {
<-done
}

wg.Wait()
return count
}
12 changes: 12 additions & 0 deletions internal/exercises/templates/31_mutexes/mutexes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package mutexes

import "testing"

func TestCounter(t *testing.T) {
got := Counting()
want := numWorkers

if got != want {
t.Fatalf("Expected %d, got %d", want, got)
}
}
14 changes: 14 additions & 0 deletions internal/exercises/templates/32_sorting/sorting.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package sorting

var Years = []int{2017, 2003, 2026}
var Pets = []string{"Dog", "Cat", "Parrot"}

func SortNumbers() []int {
// TODO: sort Years
return Years
}

func SortAnimals() []string {
// TODO: sort Pets
return Pets
}
22 changes: 22 additions & 0 deletions internal/exercises/templates/32_sorting/sorting_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package sorting

import (
"slices"
"testing"
)

func TestSorting(t *testing.T) {
t.Run("Sorting numbers", func(t *testing.T) {
SortNumbers()
if !slices.IsSorted(Years) {
t.Fatal("Strings are not sorted!")
}
})
Comment on lines +9 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix incorrect error message.

Line 12 reports "Strings are not sorted!" when testing numbers. The message should accurately reflect that integers are being validated.

Apply this diff:

 	t.Run("Sorting numbers", func(t *testing.T) {
 		SortYears()
 		if !slices.IsSorted(Years) {
-			t.Fatal("Strings are not sorted!")
+			t.Fatal("Numbers are not sorted!")
 		}
 	})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
t.Run("Sorting numbers", func(t *testing.T) {
SortYears()
if !slices.IsSorted(Years) {
t.Fatal("Strings are not sorted!")
}
})
t.Run("Sorting numbers", func(t *testing.T) {
SortYears()
if !slices.IsSorted(Years) {
t.Fatal("Numbers are not sorted!")
}
})
🤖 Prompt for AI Agents
In internal/exercises/templates/32_sorting/sorting_test.go around lines 9 to 14,
the test's failure message incorrectly says "Strings are not sorted!" while
validating integer sorting; update the t.Fatal call to use an accurate message
such as "Numbers are not sorted!" or "Years are not sorted!" so the error
reflects that integers (Years) are being tested.


t.Run("Sorting strings", func(t *testing.T) {
SortAnimals()
if !slices.IsSorted(Pets) {
t.Fatal("Strings are not sorted!")
}
})
}
Loading