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
Updating
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:
- MacOS Homebrew
- Official Docker Images:
- Debian 12 (Bookworm)
docker pull golang:1.23.0-bookworm
- Alpine 3.20:
docker pull golang:1.23.0-alpine3.20
- Debian 12 (Bookworm)
What is new?
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.
Range over func implementation
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 }
New unique
package
Code 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.
New iter
package
Code 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:
- L37:
iter.Pull2
usesIter()
to returnnext()
andstop()
, two functions to literally do what they are named, - L40-L51: The
for
keyword is still used, but the values are pulled usingnext()
next()
returns the key-value pair as well as anok
to indicate whether there are more pairs to pull or not,
- L46-L48: Because of the
break
thefor
is completed and thestop
function is called therefore indicating the iterator the job is done.
9 new functions added to the slices
package to support iterators
Code for this example is available on Github.
The slices
package adds 9 functions that support iterators:
- All returns an iterator over slice indexes and values.
- AppendSeq appends values from an iterator to an existing slice.
- Backward returns an iterator that loops over a slice backward.
- Chunk returns an iterator over consecutive sub-slices of up to n elements of a slice.
- Collect collects values from an iterator into a new slice.
- Sorted Sorted collects values from seq into a new slice, sorts the slice, and returns it.
- SortedFunc is like Sorted but with a comparison function.
- SortedStableFunc is like SortFunc but uses a stable sort algorithm.
- Sorted collects values from an iterator into a new slice, and then sorts the slice.
- Values returns an iterator over slice elements.
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 }
- L12-L16:
slices.Chunk
returns slices that have the size of the parameter, in this case it will return 3 slices with a length of 2. - L18-L21:
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. - L23-L26:
slices.Values
: returns the values of a slice as an iterator, andslices.Sorted
: sorts the received values in the iterator.
- L28-L31:
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
5 new functions added to the maps
package to support iterators
Code for this example is available on Github.
The maps
package adds 5 functions that support iterators:
- All returns an iterator over key-value pairs from a map.
- Collect collects key-value pairs from an iterator into a new map and returns it.
- Insert adds the key-value pairs from an iterator to an existing map.
- Keys returns an iterator over keys in a map.
- Values returns an iterator over values in a map.
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)
- L19-L22:
maps.Values
will create an iterator that returns the values of the map, then we take advantage ofslices.Sorted
to sort those values. - L24-L27:
maps.Keys
will create an iterator that returns the keys of the map, then we take advantage ofslices.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"]
2 minor changes: 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 }
Conclusion
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!
Recommended reading
If you’re looking to sink your teeth into more Go-related topics I recommend the following books: