Iterators (TODO)
Beginning with Go 1.23, the standard library supports custom iterators. An iterator is a function that standardizes the way you push values from a sequence (e.g. a slice or channel) to a consumer. This gives consumers more control to decide how they retrieve values from the iterator.
Prefer using an iterator rather than a slice because the consumer doesn’t have to preallocate memory for a slice that might be large.
Read the Range Over Function Types blog post for more information.
Push iterators
Push iterators push values from a sequence of values into a yield function that the iterator’s consumer provides. Here is the signature:
type Seq[V any] func(yield func(V) bool)
The return value of yield tells the iterator to push more values or stop.
Implementation
This declaration creates an iterator function named Results that pushes a sequence of Result values:
- This type definition creates a new iterator function type named
Results.
type Results iter.Seq[Result]
type Result struct {
Status int
Bytes int64
Duration time.Duration
Error error
}
Conceptually, Results has this function signature:
type Results func(yield func(Result) bool)
This function type accepts a callback yield that takes a Result and returns a Boolean. This means that Results does not return Result values, it pushes each Result into the callback.
Producer
The iterator function definition is not yet implemented—we need to write a function that returns an iterator with the required signature.
SendN returns a Results iterator as a closure.
An anonymous function that captures the
SendNparameters,nandreq. It returns aResultsiterator and anerror.It “captures” these values because the
yieldsignature cannot explicitly accept them. Instead, it uses them in its logic. AfterSendNreturns, these variables remain inside the closure.For example, you call
SendNlike this:results, err := SendN(100, req)This returns an anonymous
yieldfunction that includes100andreqas values, but the function has not been executed.Range over
n, the number of requests.This line is where the value is pushed. Get the
resultof theSendfunction.Sendtakes the capturedreqvalue.If
yieldreturns false, stop consuming.
func SendN(n int, req *http.Request) (Results, error) {
if n <= 0 {
return nil, fmt.Errorf("n must be positive: got %d,", n)
}
return func(yield func(Result) bool) {
for range n {
result := Send(http.DefaultClient, req)
if !yield(result) {
return
}
}
}, nil
}
- SendN returns a Results iterator
- Consumers provide the
yieldfunction to the iterator - Iterator generates a Result for each request, then calls the consumers
yieldfunction. This function pushes the Result to the consumer. - Step 3 continues until the iterator pushes each Result or the consumer’s yield function returns false.
Send mimics an HTTP call that returns a Result struct:
func Send(_ *http.Client, _ *http.Request) Result {
const roundTripTime = 100 * time.Millisecond
time.Sleep(roundTripTime)
return Result{
Status: http.StatusOK,
Bytes: 10,
Duration: roundTripTime,
}
}
SendN returns a Results iterator, which is a closure.
An anonymous function that captures the
SendNparameters,nandreq. It returns aResultsiterator and anerror.It “captures” these values because the
yieldsignature cannot explicitly accept them. Instead, it uses them in its logic. AfterSendNreturns, these variables remain inside the closure.For example, you call
SendNlike this:results, err := SendN(100, req)This returns an anonymous
yieldfunction that includes100andreqas values, but the function has not been executed.Range over
n, the number of requests.Get the
resultof theSendfunction.Sendtakes the capturedreqvalue.If
yieldreturns false, stop consuming.
// SendN sends N requests using [Send].
// It returns a single-use [Results] iterator that pushes a
// [Result] for each [http.Request] sent.
func SendN(n int, req *http.Request) (Results, error) {
if n <= 0 {
return nil, fmt.Errorf("n must be positive: got %d,", n)
}
return func(yield func(Result) bool) {
for range n {
result := Send(http.DefaultClient, req)
if !yield(result) {
return
}
}
}, nil
}
Consumer
- A
niliterator is results in a panic - Compiler has built-in support for iterators, so you can use a
for rangeloop.
// result.go
type Summary struct {
Requests int
Errors int
Bytes int64
RPS float64
Duration time.Duration
Fastest time.Duration
Slowest time.Duration
Success float64
}
// Summarize returns a [Summary] of [Results].
func Summarize(results Results) Summary {
var s Summary
if results == nil {
return s
}
started := time.Now()
for r := range results {
s.Requests++
s.Bytes += r.Bytes
if r.Error != nil || r.Status != http.StatusOK {
s.Errors++
}
if s.Fastest == 0 {
s.Fastest = r.Duration
}
if r.Duration < s.Fastest {
s.Fastest = r.Duration
}
if r.Duration > s.Slowest {
s.Slowest = r.Duration
}
}
if s.Requests > 0 {
s.Success = (float64(s.Requests-s.Errors) /
float64(s.Requests)) * 100
}
s.Duration = time.Since(started) // makes sure you don't return a nil
s.RPS = float64(s.Requests) / s.Duration.Seconds()
return s
}