Skip to content

Commit dc172e1

Browse files
committed
Demonstrate a custom iterator from go1.23
1 parent 2a97c2b commit dc172e1

File tree

7 files changed

+233
-3
lines changed

7 files changed

+233
-3
lines changed

.github/workflows/go.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
- name: Set up Go
1919
uses: actions/setup-go@v5
2020
with:
21-
go-version: 1.22
21+
go-version: 1.23
2222

2323
- name: Build
2424
run: ./build.sh

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/quii/learn-go-with-tests
22

3-
go 1.22
3+
go 1.23
44

55
require (
66
github.com/approvals/go-approval-tests v0.0.0-20211008131110-0c40b30e0000

intro-to-acceptance-tests.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ import (
298298

299299
const (
300300
port = "8080"
301-
url = "<http://localhost:" +port
301+
url = "<http://localhost:" + port
302302
)
303303

304304
func TestGracefulShutdown(t *testing.T) {

iterators/iterators_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package iterators
2+
3+
import (
4+
"iter"
5+
"slices"
6+
"testing"
7+
)
8+
9+
func Concatenate(seq iter.Seq[string]) string {
10+
var result string
11+
for s := range seq {
12+
result += s
13+
}
14+
return result
15+
}
16+
17+
// annoyingly, there is no builtin way to go from seq2, to seq (e.g just get the values)
18+
func Values[K, V any](seq iter.Seq2[K, V]) iter.Seq[V] {
19+
return func(yield func(V) bool) {
20+
for _, v := range seq {
21+
if !yield(v) {
22+
return
23+
}
24+
}
25+
}
26+
}
27+
28+
// WIP!
29+
func TestConcatenate(t *testing.T) {
30+
t.Run("values of a slice", func(t *testing.T) {
31+
got := Concatenate(slices.Values([]string{"a", "b", "c"}))
32+
want := "abc"
33+
if got != want {
34+
t.Errorf("got %q want %q", got, want)
35+
}
36+
})
37+
38+
t.Run("values of a slice backwards", func(t *testing.T) {
39+
backward := slices.Backward([]string{"a", "b", "c"})
40+
41+
got := Concatenate(Values(backward))
42+
want := "cba"
43+
if got != want {
44+
t.Errorf("got %q want %q", got, want)
45+
}
46+
})
47+
48+
t.Run("values of a slice sorted", func(t *testing.T) {
49+
got := Concatenate(slices.Values(slices.Sorted(slices.Values([]string{"c", "a", "b"}))))
50+
want := "abc"
51+
if got != want {
52+
t.Errorf("got %q want %q", got, want)
53+
}
54+
})
55+
}

mocking.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,3 +636,45 @@ In this post about mocking we have only covered **Spies**, which are a kind of m
636636
> [Test Double is a generic term for any case where you replace a production object for testing purposes.](https://martinfowler.com/bliki/TestDouble.html)
637637
638638
Under test doubles, there are various types like stubs, spies and indeed mocks! Check out [Martin Fowler's post](https://martinfowler.com/bliki/TestDouble.html) for more detail.
639+
640+
## Bonus - Example of iterators from go 1.23
641+
642+
In Go 1.23 [iterators were introduced](https://tip.golang.org/doc/go1.23). We can use iterators in various ways, in this instance we can make a `countdownFrom` iterator, which will return the numbers to countdown in reverse order.
643+
644+
```go
645+
func Countdown(out io.Writer, sleeper Sleeper) {
646+
for i := range countDownFrom(3) {
647+
fmt.Fprintln(out, i)
648+
sleeper.Sleep()
649+
}
650+
651+
fmt.Fprint(out, finalWord)
652+
}
653+
```
654+
655+
Before we get into how we write custom iterators, let's see how we use it. Rather than writing a fairly imperative looking loop to count down from a number, we can make this code look more expressive by `range`-ing over our custom `countdownFrom` iterator.
656+
657+
To write an iterator, a function that can be used in a `range` loop, you need to write a function in a particular way. From the docs:
658+
659+
The “range” clause in a “for-range” loop now accepts iterator functions of the following types
660+
func(func() bool)
661+
func(func(K) bool)
662+
func(func(K, V) bool)
663+
664+
(The `K` and `V` stand for key and value types, respectively.)
665+
666+
In our case, we don't have keys, just values. Go also provides a convenience type `iter.Seq[T]` which is a type alias for `func(func(T) bool)`.
667+
668+
```go
669+
func countDownFrom(from int) iter.Seq[int] {
670+
return func(yield func(int) bool) {
671+
for i := from; i > 0; i-- {
672+
if !yield(i) {
673+
return
674+
}
675+
}
676+
}
677+
}
678+
```
679+
680+
This is a simple iterator, which will yield the numbers in reverse order - perfect for our usecase.

mocking/v6/countdown_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"reflect"
6+
"testing"
7+
"time"
8+
)
9+
10+
func TestCountdown(t *testing.T) {
11+
12+
t.Run("prints 3 to Go!", func(t *testing.T) {
13+
buffer := &bytes.Buffer{}
14+
Countdown(buffer, &SpyCountdownOperations{})
15+
16+
got := buffer.String()
17+
want := `3
18+
2
19+
1
20+
Go!`
21+
22+
if got != want {
23+
t.Errorf("got %q want %q", got, want)
24+
}
25+
})
26+
27+
t.Run("sleep before every print", func(t *testing.T) {
28+
spySleepPrinter := &SpyCountdownOperations{}
29+
Countdown(spySleepPrinter, spySleepPrinter)
30+
31+
want := []string{
32+
write,
33+
sleep,
34+
write,
35+
sleep,
36+
write,
37+
sleep,
38+
write,
39+
}
40+
41+
if !reflect.DeepEqual(want, spySleepPrinter.Calls) {
42+
t.Errorf("wanted calls %v got %v", want, spySleepPrinter.Calls)
43+
}
44+
})
45+
}
46+
47+
func TestConfigurableSleeper(t *testing.T) {
48+
sleepTime := 5 * time.Second
49+
50+
spyTime := &SpyTime{}
51+
sleeper := ConfigurableSleeper{sleepTime, spyTime.Sleep}
52+
sleeper.Sleep()
53+
54+
if spyTime.durationSlept != sleepTime {
55+
t.Errorf("should have slept for %v but slept for %v", sleepTime, spyTime.durationSlept)
56+
}
57+
}
58+
59+
type SpyCountdownOperations struct {
60+
Calls []string
61+
}
62+
63+
func (s *SpyCountdownOperations) Sleep() {
64+
s.Calls = append(s.Calls, sleep)
65+
}
66+
67+
func (s *SpyCountdownOperations) Write(p []byte) (n int, err error) {
68+
s.Calls = append(s.Calls, write)
69+
return
70+
}
71+
72+
const write = "write"
73+
const sleep = "sleep"
74+
75+
type SpyTime struct {
76+
durationSlept time.Duration
77+
}
78+
79+
func (s *SpyTime) Sleep(duration time.Duration) {
80+
s.durationSlept = duration
81+
}

mocking/v6/main.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"iter"
7+
"os"
8+
"time"
9+
)
10+
11+
// Sleeper allows you to put delays.
12+
type Sleeper interface {
13+
Sleep()
14+
}
15+
16+
// ConfigurableSleeper is an implementation of Sleeper with a defined delay.
17+
type ConfigurableSleeper struct {
18+
duration time.Duration
19+
sleep func(time.Duration)
20+
}
21+
22+
// Sleep will pause execution for the defined Duration.
23+
func (c *ConfigurableSleeper) Sleep() {
24+
c.sleep(c.duration)
25+
}
26+
27+
const finalWord = "Go!"
28+
29+
// Countdown prints a countdown from 3 to out with a delay between count provided by Sleeper.
30+
func Countdown(out io.Writer, sleeper Sleeper) {
31+
for i := range countDownFrom(3) {
32+
fmt.Fprintln(out, i)
33+
sleeper.Sleep()
34+
}
35+
36+
fmt.Fprint(out, finalWord)
37+
}
38+
39+
func countDownFrom(from int) iter.Seq[int] {
40+
return func(yield func(int) bool) {
41+
for i := from; i > 0; i-- {
42+
if !yield(i) {
43+
return
44+
}
45+
}
46+
}
47+
}
48+
49+
func main() {
50+
sleeper := &ConfigurableSleeper{1 * time.Second, time.Sleep}
51+
Countdown(os.Stdout, sleeper)
52+
}

0 commit comments

Comments
 (0)