Going For a Loop
I mentioned in Server Mux Rabbit Hole that go 1.22 brings some for
loop changes. It was interesting to see because JS went through a similar:
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}
The above would print "10" every time. Since the i
variable gets updated at every loop, by the time the setTimeout
callback runs, the loop has completed so i == 10
which is why it gets printed for every log. You could fix that with closures:
for (var i = 0; i < 10; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 0);
})(i);
}
or with "modern" js (it's been a while that we have let
):
for (let i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}
In this case let
creates a new block scoped variable which is different at every iteration. This means that the i
referenced by each setTimeout
callback is different (very simply put). Turns out (for folks who have not fallen into this trap yet) that go has the same issue:
s := []string{"test0", "test1", "test2", "test3", "test4"}
ch := make(chan bool)
for i := range s {
go func() {
fmt.Println(i)
ch <- true
}()
}
for _ = range s {
<-ch
}
This would print "4" every time. Without going into detail about how goroutines work, by the time they execute the loop is completed and the variable i
(referenced by each goroutine) is exactly 4. Fixing this feels more natural in go than it does in js because the go
keyword requires you to call a function right there. All you gotta do is pass the i
as an argument:
s := []string{"test0", "test1", "test2", "test3", "test4"}
ch := make(chan bool)
for i := range s {
go func(j int) {
fmt.Println(j)
ch <- true
}(i)
}
for _ = range s {
<-ch
}
Switching to go 1.22 you don't need to do that since the i
is now a different instance on each iteration. Worries be gone!
The additional change is the preview of range-over-func iterators. Simply put this means we can pass a function to the range
operator. As long as it adheres to the iterator signature we can use it! The simple example clarifies this a bit.
package slices
func Backward[E any](s []E) func(func(int, E) bool) {
return func(yield func(int, E) bool) {
for i := len(s)-1; i >= 0; i-- {
if !yield(i, s[i]) {
return
}
}
}
}
With the proposal it means that we can range over the function:
s := []string{"hello", "world"}
for i, x := range slices.Backward(s) {
fmt.Println(i, x)
}
Which the program translates to:
slices.Backward(s)(func(i int, x string) bool {
fmt.Println(i, x)
return true
})
Say what you want but in order to internalize this I like to do a little refactor. When reading the Backward
function the first time (also being later in the evening didn't help), it wasn't immediately obvious how it all comes together. We defined a function Backward
that returns a function that takes a function as an argument. Let's break it down a little bit and use a concrete type.
// rangeFn would take as arguments the usual values
// returned by a range so an index and, in this case,
// a string or whichever value of the type we are
// ranging over.
// The interesting part is the return type of bool.
// That's used to indicate when we should exit the
// loop. When it returns true it is equivalent to
// "continue" when it returns false it is equivalent
// to "break" so:
// true <-> "continue"
// false <-> "break"
type rangeFn = func(int, string) bool
func Backward(s []string) func(rangeFn) {
// Maybe cb is not an ideal name, yield got me
// confused for a hot second since it's used in JS.
return func(cb rangeFn) {
// This is the custom logic of how I want to
// iterate over the data structure. In our case
// a simple slice of strings.
for i := len(s) - 1; i >= 0; i-- {
// Now if you remember we call this with the
// index and the value. As you see if the
// returned value is true, we continue to the
// next iteration (true <-> "continue").
// If it returns false, we return and interrupt
// the loop (false <-> "break").
if !cb(i, s[i]) {
return
}
}
}
}
Thoughts
Even simple abstractions can make certain concepts more complex to understand than necessary. Grabbing a practical example can often help get a good understanding which then allows you to abstract again. The moment you can move back and forth between abstraction and practical example you develop a deeper understanding.
I almost feel like this proposal went a little bit under the radar but it can affect how expressive go can be quite a lot. I haven't had the chance to play with generics much but excited to see what people will do with this addition.