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.
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.
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
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
With GO 1.21, 5 new packages has been added:
log/slog
testing/slogtest
slices
maps
cmp
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
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)
}
}
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
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 contains the new Ordered
type accompanied by a set of functions tailored for handling Ordered
types:
The full documentation can be found here
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
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!