

Disclaimer: This post includes Amazon affiliate links. Clicking on them earns me a commission and does not affect the final price.
Hello π Itβs that time of the year again! Another minor version of Go was released, superseding Go 1.22, released a few weeks ago on August 13th; this is Go 1.23! π₯ π π
π₯³ Go 1.23.0 is released!
— Go (@golang) August 13, 2024
π Release notes: https://t.co/GXY18wS0ec
β¬οΈ Download: https://t.co/KjiWuBUQxl#golang pic.twitter.com/L3NMD3RAWx
Although this new version was released less than a month ago, most of the popular operating systems and package managers already have a way to upgrade to Go 1.23; so the typical packages for Windows, Linux and MacOS (including Homebrew!) already have updates available; and also the corresponding Docker images for Debian and Alpine:
For concrete examples:
docker pull golang:1.23.0-bookworm
docker pull golang:1.23.0-alpine3.20
Most of the changes in Go 1.23 are related to the toolchain, runtime, and libraries; the most significant change in this release was adding to the language iterators support when using the for
and range
keywords. Please consider reading the release notes to get familiar with all the new changes, as well as the official blog post describing the release.
Let’s talk about some of the new features that were added in Go 1.23.
Code for this example is available on Github.
The range over func implementation is the highlight of Go 1.23, because it defines a standard way to implement iterators while maintaining backwards compatibility. The way to implement this feature is pretty straightforward: you have to implement one of the following three types:
func(func() bool)
func(func(K) bool)
func(func(K, V) bool)
For example, the following code:
60func MultiplyBy2Iter(vals ...int) func(func(int, int) bool) {
61 return func(yield func(int, int) bool) {
62 for i, v := range vals {
63 if !yield(i, v*2) {
64 return
65 }
66 }
67 }
68}
Will multiply the received values in vals
by 2, one by one. This a big difference compared to what we used to do before if we wanted to use for
/range
:
52func MultiplyBy2(vals ...int) []int {
53 res := make([]int, len(vals))
54 for i, v := range vals {
55 res[i] = v * 2
56 }
57 return res
58}
Where the returned value is allocated in advance without considering if the user of our function will be using all results or not, this change is by far the most significant improvement when using iterators, where use cases that need to be memory efficient and deal will collections of values can perform better while keeping the code looking like it used before.
104 for i, v := range MultiplyBy2(1, 2, 3, 4, 5, 6) {
105 fmt.Println(i, v)
106 }
107
108 fmt.Println("---")
109
110 for i, v := range MultiplyBy2Iter(1, 2, 3, 4, 5, 6) {
111 fmt.Println(i, v)
112 }
unique
packageCode for this example is available on Github.
The unique
package provides facilities for canonicalizing (“interning” or “hash consing”) comparable values. According to Wikipedia Hash Consing:
In computer science, particularly in functional programming, hash consing is a technique used to share values that are structurally equal. When a value is constructed, such as a cons cell, the technique checks if such a value has been constructed before, and if so reuses the previous value, avoiding a new memory allocation
The key part to understand this package is the comparable
piece, this means only those types that can be compared can be used. For example:
8// `comparable`
9type User struct {
10 Name string
11 LastName string
12}
Is comparable
because all its fields are also comparable, review the spec to understand what types are comparable. Using unique
is as easy as calling unique.Make
, for example:
21 h1 := unique.Make(User{Name: "Mario", LastName: "Carrion"})
22 h2 := unique.Make(User{Name: "Mario", LastName: "Carrion"})
23
24 fmt.Println("same values?", h1 == h2)
25 fmt.Printf("addresses: %v - %v\n", h1, h2)
Will print out:
same values? true
addresses: {0xADDRESS} - {0xADDRESS}
Of course 0xADDRESS is not the literal value that is printed out, it will change, but the point I’m making here is that both will have the same memory address.
Trying to use non-comparable type, for example:
14// Not `comparable`
15type Group struct {
16 Name string
17 Users []User
18}
Will not work because the Users
field is a slice, a non comparable type.
iter
packageCode for this example is available on Github.
The iter
package defines the conventions and guidelines to follow when implementing iterator as well as types that are used to work with iterators, and are referred by other packages (such as slices
and maps
) in the standard library:
iter.Seq[V any] func(yield func(V) bool) // One value
iter.Seq2[K, V any] func(yield func(K, V) bool) // Two values: key-value or index-value pairs
Besides those two types we have two new functions called iter.Pull
and iter.Pull2
that correspond to iter.Seq
and iter.Seq2
respectively. Pull functions exist to provide a different way to interact with iterators, the standard iterators are “push” iterators, meaning they “push” values back to the for
/range
, for example an iterator Iter
:
13func Iter() func(func(int, int) bool) {
14 return func(yield func(int, int) bool) {
15 var v int
16
17 for {
18 if !yield(v, v+10) {
19 return
20 }
21 v++
22 }
23 }
24}
Will “push” the values back when used as:
27 for i, v := range Iter() {
28 if i == 5 {
29 break
30 }
31
32 fmt.Println(i, v, time.Now())
33 }
However, if the use case you’re trying to implement works better as a “pull” then, that’s where those two functions (Pull
/Pull2
) are used, for example:
37 next, stop := iter.Pull2(Iter())
38 defer stop()
39
40 for {
41 i, v, ok := next()
42 if !ok {
43 break
44 }
45
46 if i == 5 {
47 break
48 }
49
50 fmt.Println(i, v, time.Now())
51 }
Notice how:
iter.Pull2
uses Iter()
to return next()
and stop()
, two functions to literally do what they are named,for
keyword is still used, but the values are pulled using next()
next()
returns the key-value pair as well as an ok
to indicate whether there are more pairs to pull or not,break
the for
is completed and the stop
function is called therefore indicating the iterator the job is done.slices
package to support iteratorsCode for this example is available on Github.
The slices
package adds 9 functions that support iterators:
These new functions take advantage of the new iter.Seq
and iter.Seq2
types. For example:
9 numbers := []string{"5", "4", "3", "2", "1"}
10 fmt.Printf("numbers = %v\n", numbers)
11
12 // slices.Chunk
13 fmt.Printf("\nslices.Chunk(numbers, 2)\n")
14 for c := range slices.Chunk(numbers, 2) {
15 fmt.Printf("\t%s\n", c)
16 }
17
18 // slices.Collect
19 fmt.Printf("\nslices.Collect(slices.Chunk(numbers, 2))\n")
20 numbers1 := slices.Collect(slices.Chunk(numbers, 2))
21 fmt.Printf("\t%s\n", numbers1)
22
23 // slices.Sorted + slices.Values
24 fmt.Printf("\nslices.Sorted(slices.Values(numbers))\n")
25 sorted := slices.Sorted(slices.Values(numbers))
26 fmt.Printf("\t%v\n", sorted)
27
28 fmt.Printf("\nslices.Backward(numbers)\n")
29 for i, v := range slices.Backward(numbers) {
30 fmt.Printf("\ti: %d, v: %s\n", i, v)
31 }
slices.Chunk
returns slices that have the size of the parameter, in this case it will return 3 slices with a length of 2.slices.Collect
will return a new slice with using the iterator parameter, so in this case it will return a slice of with a length of 3 that includes the other slices.slices.Values
: returns the values of a slice as an iterator, andslices.Sorted
: sorts the received values in the iterator.slices.Backward
creates an iterator that iterates the values from last one to first one.Running the program prints out the following:
numbers = [5 4 3 2 1]
slices.Chunk(numbers, 2)
[5 4]
[3 2]
[1]
slices.Collect(slices.Chunk(numbers, 2))
[[5 4] [3 2] [1]]
slices.Sorted(slices.Values(numbers))
[1 2 3 4 5]
slices.Backward(numbers)
i: 4, v: 1
i: 3, v: 2
i: 2, v: 3
i: 1, v: 4
i: 0, v: 5
maps
package to support iteratorsCode for this example is available on Github.
The maps
package adds 5 functions that support iterators:
Similar to the functions added to slices
, these new functions take advantage of the new iter.Seq
and iter.Seq2
types. For example:
10 numbers := map[string]int{
11 "one": 1,
12 "two": 2,
13 "three": 3,
14 "four": 4,
15 "five": 5,
16 }
17 fmt.Printf("numbers = %v\n", numbers)
18
19 // maps.Values
20 fmt.Printf("\nslices.Sorted(maps.Values(numbers))\n")
21 sortedValues := slices.Sorted(maps.Values(numbers))
22 fmt.Printf("\t%v\n", sortedValues)
23
24 // maps.Keys
25 fmt.Printf("\nslices.Sorted(maps.Keys(numbers))\n")
26 sortedKeys := slices.Sorted(maps.Keys(numbers))
27 fmt.Printf("\t%q\n", sortedKeys)
maps.Values
will create an iterator that returns the values of the map, then we take advantage of slices.Sorted
to sort those values.maps.Keys
will create an iterator that returns the keys of the map, then we take advantage of slices.Sorted
to sort those keys.The output of this program is:
numbers = map[five:5 four:4 one:1 three:3 two:2]
slices.Sorted(maps.Values(numbers))
[1 2 3 4 5]
slices.Sorted(maps.Keys(numbers))
["five" "four" "one" "three" "two"]
slices.Repeat
and net/http.Request.Pattern
Code for this example is available on Github.
Finally the last two additions are minor changes to the standard library, specifically two things:
net/http.Request.Pattern
: ServeMux pattern that matched the request.slices.Repeat
: Repeat returns a new slice that repeats the provided slice the given number of times.13 mux := http.NewServeMux()
14 mux.HandleFunc("GET /hi", func(w http.ResponseWriter, req *http.Request) {
15 // New function: slices.Repeat + http.Request.Pattern
16 v, _ := json.Marshal(slices.Repeat([]string{req.Pattern}, 2))
17 w.Write(v)
18 })
Will write a JSON array with two values matching "GET /hi"
, so literally something like:
39 valStr := string(val)
40 if valStr != `["GET /hi","GET /hi"]` {
41 t.Fatalf("invalid value: %v", valStr)
42 }
I appreciate that the Go team keeps innovating and adding new features. However, I feel we are slowly starting to reach a point where we can consider the language itself to be “completed” (unless things such as Sum Types or real Enumerators ever come to fruition), not that I see the language moving into maintenance mode but adding new features while keeping the backwards compatibility promise makes adding new changes more challenging.
Every new release excites me because I want to learn about the latest features. However, it takes a while to properly evaluate them because, in some cases, this requires rewriting something that already exists and works to a new way of doing it (take Generics before and now Iterators). However, again, I’m not complaining; it’s nice to see Google is still investing in the language.
Great job, Go team; I’m looking forward to Go 1.24 next year!
If you’re looking to sink your teeth into more Go-related topics I recommend the following books: