Unveiling the Powerhouse: A Closer Look at Go 1.21

In the ever-evolving landscape of programming languages, Go (Golang) continues to stand out as a stalwart, combining efficiency, simplicity, and robust performance.

Venture into this blog as we uncover some of the most exciting additions to Go 1.21 that will elevate our development experience.

The New Builtin Functions: Min, Max, and Clear

In this section, we will explore some applications of the newly introduced min, max, and clear builtin functions. These functions are crafted to address specific challenges encountered in software development, providing an elegant and standardised approach to common coding scenarios.

The min and max functions

As per the official documentation:

"The built-in functions min and max compute the smallest—or largest, respectively—value of a fixed number of arguments of ordered types. There must be at least one argument"

An ordered type is a type that is included into the new type constraint Ordered

type Ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
        ~float32 | ~float64 |
        ~string
}

And the new min and max functions are defined as:

func min[T cmp.Ordered](x T, y ...T) T
func max[T cmp.Ordered](x T, y ...T) T

The rationale behind not accepting a variadic argument (such as func min[T cmp.Ordered](x ...T) T) is rooted in the absence of a defined behavior when len(x)==0.

When T is a floating point value, the functions are implemented so that if any of the values is NaN, then NaN is returned. When T is a string, the result for min is the first argument with the smallest (or for max, largest) value, compared lexically byte-wise

Let's see some example:

v := min(9, 8, 7, 6, 5, 4, 3, 2, 1)                                             // v is 1
v := min(0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9)                           // v is 0.1
v := min(math.Inf(-1), 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9)             // v is -Inf
v := min(math.NaN(), math.Inf(-1), 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9) // v is NaN
v := min("Hello", "Beautiful", "World")                                         // v is "Beautiful"

Run the code here

The clear function

func clear[T ~[]Type | ~map[Type]Type1](t T)

The clear built-in function works for both slices and maps although the behaviour is a bit different between the two types.

When the passed in value is a map, calling clear will zero the map: all the elements will be removed. Let's see an example:

func main() {
    data := map[string]string{"key1": "value1", "key2": "value2", "key3": "value3"}
    fmt.Println("data: ", data)
    fmt.Println("len(data): ", len(data))
    clear(data)
    fmt.Println("\nAfter clearing:")
    fmt.Println("data: ", data)
    fmt.Println("len(data): ", len(data))
}

// output:
// data:  map[key1:value1 key2:value2 key3:value3]
// len(data):  3

// After clearing:
// data:  map[]
// len(data):  0

When the passed in value is a slice, clear will zero all the elements of the slice (not the slice itself). Try this code:

func main() {
    data := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
    fmt.Println("data: ", data)
    fmt.Println("len(data): ", len(data))
    clear(data)
    fmt.Println("\nAfter clearing:")
    fmt.Println("data: ", data)
    fmt.Println("len(data): ", len(data))
}

// output:
// data:  [1 2 3 4 5 6 7 8 9 0]
// len(data):  10

// After clearing:
// data:  [0 0 0 0 0 0 0 0 0 0]
// len(data):  10

The new packages

With GO 1.21, 5 new packages has been added:

  • log/slog
  • testing/slogtest
  • slices
  • maps
  • cmp

The log/slog package

The new log/slog package brings structured logging to the standard library. To write log statements the usual log methods are defined:

func Debug(msg string, args ...any)
func Info(msg string, args ...any)
func Warn(msg string, args ...any)
func Error(msg string, args ...any)

The first parameter is the log message, and the args parameter is a list of key-value pairs

For example, to log an error message, we can use:

slog.Error("An internal error has occurred"
// output
// 2023/12/30 12:43:14 ERROR An internal error has occurred

If we need to specify a list of key-pair values, we can use the list of arguments:

slog.Error("An internal error has occurred", "component", "dashboard", "page-name", "main", "action", "add-node")
// output
// 2023/12/30 12:43:54 ERROR An internal error has occurred component=dashboard page-name=main action=add-node

One nice feature of the log/slog package is the ability to specify an handler to change the output of the logger:

logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
logger.Error("An internal error has occurred", "component", "dashboard", "page-name", "main", "action", "add-node")
// output
// time=2023-12-30T12:48:13.763+01:00 level=ERROR msg="An internal error has occurred" component=dashboard page-name=main action=add-node

logger = slog.New(slog.NewJSONHandler(os.Stderr, nil))
logger.Error("An internal error has occurred", "component", "dashboard", "page-name", "main", "action", "add-node")
// output
// {"time":"2023-12-30T12:48:13.763878+01:00","level":"ERROR","msg":"An internal error has occurred","component":"dashboard","page-name":"main","action":"add-node"}

Attributes can be specified at logger level too, so that we don't have to pass them everytime we need to log a message. Look at the following example:

logger := slog.New(slog.NewTextHandler(os.Stderr, nil)).WithGroup("admin").With("username", "wile-coyote")
logger.Info("Trying to add node my-fancy-node")
logger.Error("An internal error has occurred", "component", "dashboard", "page-name", "main", "action", "add-node")
// output
// time=2023-12-30T13:02:40.750+01:00 level=INFO msg="Trying to add node my-fancy-node" admin.username=wile-coyote
// time=2023-12-30T13:02:40.750+01:00 level=ERROR msg="An internal error has occurred" admin.username=wile-coyote admin.component=dashboard admin.page-name=main admin.action=add-node

Another feature is the ability to group attributes. For example, to group all the attributes for the admin component:

logger := slog.New(slog.NewTextHandler(os.Stderr, nil)).WithGroup("admin")
logger.Error("An internal error has occurred", "component", "dashboard", "page-name", "main", "action", "add-node")
// output
// time=2023-12-30T12:56:55.644+01:00 level=ERROR msg="An internal error has occurred" admin.component=dashboard admin.page-name=main admin.action=add-node

logger = slog.New(slog.NewJSONHandler(os.Stderr, nil)).WithGroup("admin")
logger.Error("An internal error has occurred", "component", "dashboard", "page-name", "main", "action", "add-node")
// output
// {"time":"2023-12-30T12:56:55.645164+01:00","level":"ERROR","msg":"An internal error has occurred","admin":{"component":"dashboard","page-name":"main","action":"add-node"}}

Full example code here

The testing/slogtest package

This package facilitates the seamless implementation of tests for custom log handler implementations, streamlining the testing process. An example is provided in the Go documentation:

package main

import (
    "bytes"
    "encoding/json"
    "log"
    "log/slog"
    "testing/slogtest"
)

func main() {
    var buf bytes.Buffer
    h := slog.NewJSONHandler(&buf, nil)

    results := func() []map[string]any {
        var ms []map[string]any
        for _, line := range bytes.Split(buf.Bytes(), []byte{'\n'}) {
            if len(line) == 0 {
                continue
            }
            var m map[string]any
            if err := json.Unmarshal(line, &m); err != nil {
                panic(err) // In a real test, use t.Fatal.
            }
            ms = append(ms, m)
        }
        return ms
    }
    err := slogtest.TestHandler(h, results)
    if err != nil {
        log.Fatal(err)
    }

}

The slices package

This new package offers a variety of generic functions designed for performing common operations on slices.

For example, to sort a slice we can now use this code:

package main

import (
    "fmt"
    "slices"
)

func main() {
    data := []int{1, 0, 2, 9, 3, 8, 4, 7, 5, 6}

    fmt.Println("data: ", data)
    fmt.Println("Is data already sorted? ", slices.IsSorted(data))
    slices.Sort(data)
    fmt.Println("data: ", data)
    fmt.Println("Is data sorted? ", slices.IsSorted(data))
}
// data:  [1 0 2 9 3 8 4 7 5 6]
// Is data already sorted?  false
// data:  [0 1 2 3 4 5 6 7 8 9]
// Is data sorted?  true

or, to sort backward(code here):

package main

import (
    "cmp"
    "fmt"
    "slices"
)

func main() {
    data := []int{1, 0, 2, 9, 3, 8, 4, 7, 5, 6}

    fmt.Println("data: ", data)
    slices.SortFunc(data, func(a, b int) int { return cmp.Compare(b, a) })
    fmt.Println("data: ", data)
}
// output
// data:  [1 0 2 9 3 8 4 7 5 6]
// data:  [9 8 7 6 5 4 3 2 1 0]

Explore the list of these functions along with examples of their usage here

The maps package

This new package offers a variety of generic functions designed for performing common operations on maps.

For example, to check if two maps contains the same values we can use the following code:

package main

import (
    "fmt"
    "maps"
)

func main() {
    data1 := map[string]string{"key1": "value1", "key2": "value2", "key3": "value3"}
    data2 := map[string]string{"key2": "value2", "key1": "value1", "key3": "value3"}
    data3 := map[string]string{"key1": "value1"}
    fmt.Println("data1 == data2: ", maps.Equal(data1, data2))
    fmt.Println("data1 == data3: ", maps.Equal(data1, data3))
}
// output:
// data1 == data2:  true
// data1 == data3:  false

Explore the list of these functions along with examples of their usage here

The cmp package

The cmp package contains the new Ordered type accompanied by a set of functions tailored for handling Ordered types:

  • Compare
  • Less

The full documentation can be found here

Experimental features: loop variables

Consider the following code:

package main

import "fmt"

func printIntPointerArray(label string, data []*int) {
    fmt.Printf("%s: ", label)
    for _, v := range data {
        fmt.Printf("%d ", *v)
    }
    fmt.Println()
}

func main() {
    data := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    var odd, even []*int

    for _, v := range data {
        if v%2 == 0 {
            even = append(even, &v)
        } else {
            odd = append(odd, &v)
        }
    }

    fmt.Println("data:", data)
    printIntPointerArray("odd", odd)
    printIntPointerArray("even", even)
}
// output:
// data: [1 2 3 4 5 6 7 8 9]
// odd: 9 9 9 9 9 
// even: 9 9 9 9 

Upon inspection of the output, it becomes evident that the expected result is not being produced. The underlying reason is straightforward: in all Go version (1.21 comprised), the default behaviour of the range loop involves recycling the range variable. This implies that all elements within both odd and even point to the same variable, with its value reflecting the final iteration of the range loop. The Go team is currently contemplating a modification to this behaviour, aiming to create a new loop variable with each iteration (to be introduced into Go 1.22). By activating the new loopvar experimental feature (GOEXPERIMENT=loopvar), we observe the desired behaviour:

package main

import "fmt"

func printIntPointerArray(label string, data []*int) {
    fmt.Printf("%s: ", label)
    for _, v := range data {
        fmt.Printf("%d ", *v)
    }
    fmt.Println()
}

func main() {
    data := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    var odd, even []*int

    for _, v := range data {
        if v%2 == 0 {
            even = append(even, &v)
        } else {
            odd = append(odd, &v)
        }
    }

    fmt.Println("data:", data)
    printIntPointerArray("odd", odd)
    printIntPointerArray("even", even)
}
// output:
// data: [1 2 3 4 5 6 7 8 9]
// odd:: 1 3 5 7 9 
// even:: 2 4 6 8

You can run it here

Conclusions

With the debut of Go 1.21, a slew of exciting features has been ushered in to enhance the development journey. Although this blog has highlighted several noteworthy improvements, there's an array of additional features yet to be explored. For an in-depth look, consult the comprehensive list provided in the official Go 1.21 documentation.

Happy coding!