pwshub.com

What is new in Go 1.23?

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!

📝 Release notes: https://t.co/GXY18wS0ec

âŹ‡ïž Download: https://t.co/KjiWuBUQxl#golang pic.twitter.com/L3NMD3RAWx

— Go (@golang) August 13, 2024

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:

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.

For example:

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 uses Iter() to return next() and stop(), two functions to literally do what they are named,
  • L40-L51: The 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,
  • L46-L48: Because of the break the for is completed and the stop 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, and
    • slices.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 of slices.Sorted to sort those values.
  • L24-L27: 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"]

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:

For example:

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!

If you’re looking to sink your teeth into more Go-related topics I recommend the following books:

Back to posts

Source: mariocarrion.com

Related stories
3 weeks ago - Introduction This is the blog post version of my talk at GopherCon 2024. Range over function types is a new language feature in the Go...
1 week ago - Go 1.23 provides a new way for you to help improve the Go toolchain. By enabling telemetry uploading, you can elect to share data about toolchain...
2 weeks ago - The standard library of Go 1.23 now includes the new unique package. The purpose behind this package is to enable the canonicalization of comparable...
1 month ago - And we’re go – Linux Mint 22 ‘Wilma’ has been officially released and made available to download. This major update is the first version to be based on the latest Ubuntu 24.04 LTS This major update is built on Ubuntu 24.04 LTS and sees...
1 month ago - An Employer of Record (EOR) service is a global employment solution provider. It helps businesses have a global workforce without having to deal with legal responsibilities and compliance. It hires employees from different countries and...
Other stories
1 hour ago - Hello, everyone! It’s been an interesting week full of AWS news as usual, but also full of vibrant faces filling up the rooms in a variety of events happening this month. Let’s start by covering some of the releases that have caught my...
2 hours ago - Nitro.js is a solution in the server-side JavaScript landscape that offers features like universal deployment, auto-imports, and file-based routing. The post Nitro.js: Revolutionizing server-side JavaScript appeared first on LogRocket Blog.
2 hours ago - Information architecture isn’t just organizing content. It's about reducing clicks, creating intuitive pathways, and never making your users search for what they need. The post Information architecture: A guide for UX designers appeared...
2 hours ago - Enablement refers to the process of providing others with the means to do something that they otherwise weren’t able to do. The post The importance of enablement for business success appeared first on LogRocket Blog.
3 hours ago - Learn how to detect when a Bluetooth RFCOMM serial port is available with Web Serial.