Go 1.27
Yay! Go 1.27 was released yesterday! Every Go release there’s something I have been excitedly anticipating, but there’s often nice surprises too.
- Anticipated: portable simd package (experiment), &
json/v2 - Surprises: generic methods +
uuid&maphashpackages
Generic methods could have easily been added in Go 1.18, when generics first appeared, but (long story) due to an abundance of caution, and because we couldn’t find a way to (efficiently) allow generic interface methods, it was dropped. Nothing has really changed since, but it’s now been decided that there are some useful use-cases, even with the interfaces limitation.
More significant to me, are the standard library additions for UUIDs, SIMD, generic containers, etc,
Most happily we finally get to see how the “portable” simd package works – it is very cool, but still has a lot of rough edges. SIMD is a big task, and I think it will be a while before it is ready to graduate from GOEXPERIMENT mode.
There is a new uuid package which is great - I explain how to use it properly below. Also, the new JSON package has graduated (after being an experiment in the last 2 releases), which is fantastic, but there are a few gotchas if you simply change your import from encoding/json to encoding/json/v2 - see Subtle Differences below.
One very interesting addition was the new hash/maphash package. At first, I could not understand the point until I realised it is a prelude to the new generic containers - see Generic Collection Types. Like uuid these are long overdue.
Background
General - UUID
Unique Identifiers and UUIDs
A perennial problem in data processing is being able to uniquely identify something. When identifying people, simply using their name as their ID will result in collisions (multiple people with the same name). Even using name + date of birth gives a surprising number of collisions.
My habit has just been to use an incrementing integer for IDs. I’d start at a fairly big number, in case someone sees that they are user number 3 which would give them a (hopefully) misleading impression about the maturity -> quality of the software.
Most relational databases will let you use an auto-incrementing primary key, which makes obtaining a unique ID simple.
Nowadays, it’s generally recommended to use (V4) UUIDs, to avoid leaking information because incrementing numerical IDs (if public) can be used to infer information such as how many users you have (for user IDs), turnover (for order/invoice IDs), or to guess IDs, etc. But if all you expose in the public API is a random number (UUID v4) then nothing can leak.
UUIDs are useful for other things too. If you just used an incrementing numeric ID in a distributed database you would need to coordinate the generation of new IDs to avoid duplicates, which is tedious and has performance and scalability implications.
However, using random numbers for IDs also has disadvantages. To avoid the possibility of random collisions they need to be large - a lot more than 64 bits. (UUID V4 has 122 random bits which is more than enough.) If you stick to smaller auto-inc ints (of 64, 32, or even less bits) your database will be smaller and faster. Also, UUIDs are much harder to work with and remember when debugging or inspecting logs, etc.
UUID Versions
Which type of UUID should I use? There are several variations as specified by the IETF in RFC 9562.
My rule of thumb is to use V4 or V7 UUIDs only. Why? The other variants are not as good for various reasons.
The new Go1.27 uuid package currently only supports V4 and V7.
For example, the original (V1) UUIDs included the network MAC address of the computer (where it was generated) plus a timestamp (when it was generated). Since MAC addresses are unique this precluded any possibility of collisions between machines. However, this collision prevention has been known to fail (clock issues + multiple running software instances at the same address).
But the main issue with V1 is security - it leaks information about the machine that the UUID was generated on, and the time it was generated. (Though this “feature” of V1 UUIDs has been used to track down virus creators so maybe this is a good thing :)
V4 vs V7
So when do you use V4 or V7?
For utmost security use V4, since it can’t leak anything. However, it may cause performance issues with database indexes, a problem that V7 was designed to address, though this many not be an issue depending on the indexing algorithm of your database and it’s size.
V7 starts with a 48-bit (millisecond) timestamp (most of the other bits being random). This help assists many database indexing methods to be faster to update and use less space. Moreover, just sorting the UUIDs will arrange them in the order they were created, which can be useful.
V7 UUIDs do leak info. though - it’s easy to get the exact time they were created. On the plus side, you can’t infer much else such as how often they are generated, or guess possible values.
UUID alternatives
There are many shorter (usually 64-bit) UUID alternatives, that are faster, smaller and easier for humans. One of these (Snowflake, NanoID, CUDI2, WUID, ULID, …) might be better for performance at the expense of security.
Summary
Understand your security and performance requirements, esp. how leaked V7 creation times could be exploited.
- Use V4 for highest security – it does not leak any info
- Use V7 for better database performance (if OK to leak creation time)
Go
Better Generics
Go has had generics for a few years now. There was a lot of debate by Gophers about how they should be implemented in Go and whether Go even needed them.
I have been meaning, for a while now, to review the state of generics in light of the original debate. In brief, it seems that generics have been a success (though personally I don’t use them as much as I thought I would).
Since I started using Go in 2017 (and even before that) I said that Go desperately needed “templates” (as they are called in C++) or parametric polymorphism, now what we call generics. But now I just think they are nice to have on occasion – maybe this is because I work on less technical software (more backend stuff) than I did with C++.
In 2020 I compared the Go generics proposal to the C++ kitchen sink approach, listing the things Go left out - see my old blog C++ Templates vs Go Generics. In summary, I think Go’s simpler approach not only allows simpler coding and faster build times but also discourages overuse of them.
That said, there were a few things that I would have liked to see namely:
- Non-type parameters
- Generic methods
Non-type Parameters
When I talk about non-type parameters most Gophers seem confused. I will try to explain it using the analogy of arrays as an example of a generic type. If you declare a new array type like this:
type array [10]string
you are providing two “generic” parameters to the compiler: string (a type) and 10 (an integer constant). (The square brackets indicate to the compiler that it’s an array.) The 10 is a “parameter” of the array declaration, but it is an integer not a type parameter, whereas string is a type parameter.
Unfortunately, you can’t define your own generic types like this in Go as you can currently only supply type parameters. You can’t use non-type generic parameters.
How would it be useful?
One thing I have always wanted to do in Go is create a generic geometry library which works for any number of dimensions. For example, the distance between 2 points is the square root of the sum on the squares of the differences of each coordinate irrespective of the number of dimensions.
If Go had non-type parameters it would look like this:
// WARNING: This is not valid Go: Dim is a non-type parameter of type Int
// (integer) and Go does not (yet) support non-type generic parameters.
type Point[T Numeric, Dim Int] [Dim]T // array of coords
func Distance[T Numeric, Dim Int](p1, p2 Point[T, Dim]) T {
distSq := T(0)
for i := range len(p1) {
diff = p1[i] - p2[i]
distSq += diff * diff
}
return SquareRoot[T](distSq)
}
...
dist2D := Distance([2]float32{0, 0}, [2]float32{1, 1})
dist3D := Distance([3]float32{0, 0, 0}, [3]float32{1, 1, 1})
Currently, in Go, you need to provide different versions of Distance() for each variation (2D, 3D etc) you want to use. This is easy enough for one function but there would be a lot of duplicate code if you want to support a complete geometry package.
Generic Methods
Also in my 2020 blog I discussed Generic Methods - Member Function Templates as they are called in C++.
In Go methods have basically two purposes:
- syntactic shorthand for a function call
- allow types to implement an interface
When generics were added in Go 1.18, generic methods were considered but not added due to issues with interfaces. It would cause strange performance/consistency issues to allow generic interface methods as mentioned here.
The main purpose of methods is (according to the Go Developers) for interfaces, so it was left out of Go 1.18.
It’s since been decided that generic concrete methods are useful even without generic interface methods. See Robert Greisemer’s Proposal: Generic Methods for Go for details.
Personally, I can’t recall every having a need for a generic method (in Go). I tried to find a useful example - see Generic Methods below.
Standard Library
The big news is the major JSON rewrite has finally arrived (being in GOEXPERIMENT=jsonv2 mode for the last 2 releases). There has also been a lot done for simd, but this is still an experiment (and I think will continue to be for a few more releases).
One thing that does not get mentioned much is the new uuid package.
uuid package
A UUID package in the standard library is long overdue. Most software uses UUIDs (or should be using them) especially if they use a database.
Using the new uuid package is simple, though understanding which version of UUID to use (e.g. V4 vs V7) can be tricky. You should understand your requirements and how the different versions work.
See my explanation in the Background section above, but generally you should uuid.New(). Or use uuid.NewV4() to be explicit - though these are currently the same and I doubt will ever change.
You may consider using uuid.NewV7() with a database where UUIDs are the primary key or are indexed. This will give better performance for large indexes for many species of database (like Postgres). You need to carefully consider if leaking UUID creation times poses any security risk.
simd package
There is a new simd package plus improvements to the simd/archsimd package (see below). Both still require GOEXPERIMENT=simd.
The simd package allows you to take advantage of SIMD instructions if available (on the hardware) and supported (by the code). Currently, there is good support for X86 (GOARCH=amd64), Arm and even WASM, with RISCV etc to come.
Vector Length
The crucial part about the new portable package is that you write your SIMD code oblivious to the length of the SIMD vectors you are using. This allows your code to work transparently on different hardware. Moreover, the code will work when built for any architecture (as determined by GOARCH). In either case, it will fall back to (slower) non-SIMD code.
As an example, let’s look at taking the sum of corresponding elements of two slices, putting the result into a third slice. First, here is some code to create the slices and fill them with random integers.
// Create 2 slices filled with random values
s1 := make([]int16, 32) // *** see NOTE ***
s2 := make([]int16, len(s1))
for i := range len(s1) {
s1[i] = rand.N[int16](100)
s2[i] = rand.N[int16](100)
}
// Create slice for the result
result := make([]int16, len(s1))
// Add each pair of elements (see below)
...
// Display the result
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(result)
NOTE: For simplicity we work with a slice length that’s a multiple of the vector size (16). If you use slices of other lengths some of the archsimd functions below will panic.
For the general case, you should handle slices of any length, and for better performance you might also look at alignment of vectors. I discussed this last time in Alignment and Length.
First, lets look at non-portable SIMD code as I discussed in the previoud (Go 1.26) post. This code assumes we are using x86 (amd64) architecture and that the CPU supports AVX2 (256 bit vector) instructions.
//go:build amd64 && go1.26 && goexperiment.simd
...
import "simd/archsimd"
...
// vLen is 16 - assuming AVX2 (256 bit vectors)
const vLen = 16
if len(s1) % vLen != 0 {
log.Fatalln("slices must be a multiple of", vLen)
}
if !archsimd.X86.AVX2() {
log.Fatalln("CPU does not support 16x16 SIMD Add")
}
// Loop through the slices a vector-full at a time
for i := 0; i < len(s1); i += vLen {
v1 := archsimd.LoadInt16x16Slice(s1[i : i+vLen])
v2 := archsimd.LoadInt16x16Slice(s2[i : i+vLen])
v1.AddSaturated(v2).StoreSlice(result[i : i+vLen])
}
The load/store functions in archsimd have been renamed in Go 1.27. LoadInt16x16Slice() becomes LoadInt16x16(), StoreSlice() becomes Store(), etc.
Now you can do the same thing portably using the simd package:
//go:build go1.27 && goexperiment.simd
...
import "simd"
...
// Work out the length of the int16 SIMD vector
vLen := simd.LoadInt16s(s1).Len()
//vLen := simd.VectorBitSize() / 16
// Loop through the slices a vector-full at a time
for i := 0; i < len(s1); i += vLen {
v1 := simd.LoadInt16s(s1[i : i+vLen])
v2 := simd.LoadInt16s(s2[i : i+vLen])
v1.AddSaturated(v2).Store(result[i : i+vLen])
}
To work out vLen I create a dummy vector then call it’s Len() method. An easier way may be to just divide simd.VectorBitSize() by the size of the element type but it’s unclear from the documentation that this is the correct approach.
This should run just as fast on an X86 machine as the previous (non-portable) version. In fact, it could run faster if the CPU supports AVX512, whence 512-bit instructions/registers would be used and vLen would be 32 (since 512 = 16x32 for 32 x 16-bit values).
Fallback
On the other hand if the above portable SIMD code was run on an older CPU model (not supporting AVX2) it might have to fall back to using 128-bit SIMD instructions. In the worst case (no SIMD support) it will fall back to non-SIMD code which would be much slower.
In general your code could fall back to non-SIMD code if:
- not supported by the hardware, where the available instructions are detected at run-time
- the architecture used at build-time has no SIMD instructions (or not yet implemented)
Either way, by using the simd package your code will run anywhere (even if slowly). You should not get panics like you can with simd/archsimd.
Compile Time
Build tags are used within the simd/archsimd package to add the SIMD code for the architecture being built for.
At compile-time the functions relevant to you architecture (as determined by GOARCH) are added from the simd/archsimd source files. This is done using file name suffixes or build tags - see Build Tags to understand how this works.
BTW It’s really cool that WASM now supports SIMD as I discuss below.
Run Time
Just because an architecture has SIMD support does not mean the hardware (CPU that your executeable is running on) supports it. Once you build your software for a particular architecture the simd package still has to pick the best SIMD instructions available, falling back to non-SIMD Go code if necessary.
Just to reiterate how cool this is – the portable simd package looks at the hardware on which you are running and:
- automatically chooses the fastest code, or
- (no SIMD at all) it still works (more slowly).
Even if you are developing for a single architecture this saves a lot of tedium and worry that your code could panic if you are using a SIMD instruction that is not supported by the hardware. I gave an example last time (see SIMD Runtime Considerations) where I was using 256-bit vectors (archsimd.Int16x16) so I assumed that checking for AVX2 (archsimd.X86.AVX2()) was sufficient which meant calling archsimd.Int16x16.ToBits() panicked because this was only added in AVX-512.
simd/archsimd
In Go 1.26 simd/archsimd was added as an experiment. It’s intended to support SIMD on different architectures, but initially it was just X86 (GOARCH=amd64 on Windows/Linux). In Go 1.27 it now has support for WASM and ARM64.
It is for “non-portable” code, though you could stumble across some combinations that will compile for more than one architecture.
The following will build for any architecture that supports 128-bit vectors (as 8 16-bit ints) and has an instruction for Saturated Addition.
// OK for AMD64 (AVX) ARM64 (Neon), WASM
var x, y archsimd.Int16x8
...
result := x.AddSaturated(y)
However, this is not recommended, as it will panic if run on hardware that does not provide the SIMD registers/instruction. For example, this will panic if run on an X86 CPU that does not have AVX. (See SIMD Runtime Considerationshttps://andrewwphillips.github.io/blog/go1p26.html#runtime-considerations) for how to handle this.)
Instead, you should use the portable simd package (see above) which falls back to emulated (non-SIMD) instructions if the hardware does not support it. It will also build for any architecture, but will run slower on those where SIMD instructions are emulated.
I explained a lot more about archsimd in Go 1.26 SIMD (Experiment) and X86 SIMD generally in Go 1.26 Background.
Using archsimd with WASM and ARM64 architectures is similar to X86. The major difference is that only 128-bit (16 byte) vectors are supported – so only 4 types for integers (Int8x16, etc), 4 types for unsigned integers, and 2 types for floats (Float32x4, Float64x2) are supported. That is, there are no 256-bit and 512-bit vectors as for X86.
encoding/json/v2
If you are like me, you have to deal a lot with JSON data. The faithful old encoding/json package is good, but its API could be better, there are some niggling problems, and decoding could be faster.
Note: I prefer the term decode to unmarshal as the meaning is more obvious, and the terms are commonly used interchangeably. Technically, the Go developers now have a stance that decoding refers to processing of JSON syntax, while unmarshaling refers to semantics or how the JSON data becomes Go values.
To deal with the problems of encoding/json the best way was to create a new package encoding/json/v2 with an improved, yet similar, API. The opportunity was also taken to make some other refinements and performance improvements. If you want to convert your code to the new package you may just need to change the import statement but there can be subtly different behaviour which may require code changes such as use of new options (see below).
Decode Benchmarks
I did some benchmarks on the new package in Go 1.25, just to verify the claims, e.g. that decoding is twice as fast.
One cool thing is that the original (v1) package has been reimplemented using the “v2” package. You will get better performance, without doing anything, just keep using encoding/json and rebuild with 1.27.
Here’s a decode benchmark you can try at home:
//go:build go1.27
package __
import (
"encoding/json/v2"
"testing"
)
func BenchmarkDecodeV2(b *testing.B) {
var person struct{ Name, Address string }
for b.Loop() {
if err := json.Unmarshal([]byte(`{"Name":"A","Address":"B"}`), &person); err != nil {
b.Fatalf("Unmarshal v2: %v", err)
}
}
}
This takes about 450 nsecs/op on my machine. Changing the import to be encoding/json (v1 package) gives about 550 nsecs/op – I’m not sure why this is a bit slower because it’s using the same code underneath.
However, using the old v1 code (setting GOEXPERIMENT=nojsonv2 or using an earlier Go release) the above benchmark takes more than twice as long (920 nsecs/op).
New Options
I had a detailed look at V2 a year ago (released as an experiment in Go 1.25) see Go 1.25 - JSON v2. But here are a few things I missed or want to emphasize.
BTW Below I discuss decoding/unmarshaling in detail, but most things usually apply (in reverse) to encoding/marshaling, except as noted.
One confusing thing I always found with v1 was knowing when to use json.Unmarshal vs json.Decoder.Decode. They do very similar things, but I usually just use json.Unmarshal(). However, using the json.Decoder type has some advantages like being able to use an io.Reader (such as an http.Request.body) whence you don’t necessarilly need all the JSON in memory at once.
The main advantage of the json.Decoder in v1, is that there are a lot of options not available with json.Unmarshal() such as DisallowUnknownFields(), UseNumber(), etc.
With encoding/json/v2 there is no longer a json.Decoder (and json.Encoder), though the lower level jsontext package has something similar now. But now you can do most things with Unmarshal() (and Marshal()) because they now take a list of (variadic) options including:
- MatchCaseInsensitiveNames - override (default = names must match exactly)
- RejectUnknownMembers - same as old
DisallowUnknownFields()[decode only] - StringifyNumbers - numbers are encoded as JSON strings
- FormatNilSliceAsNull - override (default = empty array) [encode only]
- OmitZeroStructFields - don’t encode zero/nil/empty fields of a
struct - etc
Note that you have to use some of these options to get the old (v1) behaviour. For example, field names are no longer matched case-insensitively by default (see example below) - this was done for reasons of security. Also, empty slices and maps are no longer encoded as a JSON NULL but instead as an empty array (since most Gophers prefer this).
Subtle Differences
If you are lucky you may be able to migrate to V2 simply by changing your import from “encoding/json” to “encoding/json/v2”. But this may entail differences in behaviour.
- encoding of maps no longer sorts the keys
- invalid UTF-8 strings cause a decode error (v1 ignored them)
- JSON duplicate field names are rejected (v1 used the last one)
For example, in v1 this invalid JSON will return a map with a single element (and no error), whereas in v2 it returns the error jsontext: duplicate object member name "a":
v := make(map[string]int)
err := json.Unmarshal([]byte(`{ "a": 1, "a": 2 }`), &v)
- extra characters (after end of JSON text) is a decode error
- JSON field names must match the case of Go structs
Strict (case-sensitive) name matching apparently avoids certain security vulnerabilities, but subtly changes a lot of existing code (like mine :). For example, this code works as expected in v1:
var person struct{ Name, Address string }
err := json.Unmarshal([]byte(`{"name":"A","address":"B"}`), &person)
but the fields are silently ignored in v2 because the JSON fields names (name and address) do not match the Go field names (Name and Address). Of course, even in v1, you should probably use tags.
var person struct{
Name string, `json:"name"`
Address string, `json:"address"`
}
err := json.Unmarshal([]byte(`{"name":"A","address":"B"}`), &person)
You can’t make the Go field names lowercase as then they are “private” and completely ignored by the JSON parser.
Using tags (like “name” above) is safer, otherwise renaming the Go field name affects the JSON parsing (which uses reflection). This has caught a lot of Gophers who don’t expect that renaming identifiers will change code behaviour.
In v2, you can use the case:ignore field option or the new MatchCaseInsensitiveNames option to Unmarshal like this:
var person struct{ Name, Address string }
err := json.Unmarshal([]byte(`{"name":"A","address":"B"}`), &person, json.MatchCaseInsensitiveNames(true))
If, instead you want to know (generate an error) instead of ignoring unknown fields, you can use the new RejectUnknownMembers option.
- One thing that was not done is that
omitemptystill encodes an uninitialised (zero)time.Timeas “0001-01-01T00:00:00Z”. Yes, this would be another subtle difference but would avoid issues in the long term.
Always use the JSON omitzero tag for dates that could be uninitialised or set to the zero value in some way.
JSON Package Split
Another cool thing is that a way was found to efficiently decouple the JSON parsing (decoding) from the unmarshaling of code values (as well as the reverse encoding/marshaling). This is done with a new encoding/json/jsontext package upon which encoding/json/v2 is built.
You can also use it directly for efficient low-level JSON processing. For example, you could use it to process a JSON file without actually “unmarshaling” to Go values. See the example in the package at jsontext/example_test.go.
You can also use it for extra performance and options like:
- efficient custom marshalers that avoid memory allocations
- parse JSON, skipping memory allocations for unneeded fields
- low-level options:
EscapeForHTML,AllowInvalidUTF8, etc
An important feature, if you use very large JSON files, or process a continuous stream, is that you no longer have to read the whole file into memory to process it.
To find out more about streaming, custom encoding/decoding, etc see A new experimental Go API for JSON.
hash/mapHash
This is a really interesting one that I almost missed. This seems to be related to the new containers proposal.
math/big
I’ve used the math/big package quite a bit over the years (only for Ints not Rats or Floats). It’s a bit clunky to use, but it is comprehensive and efficient. So I was surprised to see a new division method since we already have these methods (for big.Int division) that return the quotient and/or remainder:
Quo()- quotient using truncation (same as Go integers)Rem()- remainder “ “ (same as Go integer%operator)QuoRem()- returns quotient and remainderDiv()- quotient using Euclidean divisionMod()- remainder (Euclidian)DivMod()- quotient and remainder (Euclidian)
The new big.Int.Divide() method returns the quotient and remainder and gives six rounding options:
- round - halves round away from zero
- round to even - halves round to nearest even
- truncate (floor)
- away from zero (ceil)
- to -ve inf. (+ve floor, -ve ceil)
- to +ve inf. (+ve ceil, -ve floor)
math/rand/v2
I use rand.IntN() a lot, so it’s nice to now have a simple generic function rand.N[]() that allows you to get a random number of any integer type, which obviates the need for rand.Int32N() and its ilk.
import "math/rand/v2"
...
type MyRand int32
var n MyRand
n = rand.Int32N(100) // ugly
n = MyRand(rand.IntN(100)) // cast int to int32
n = rand.N[MyRand](100) // Go 1.27
n = rand.N(n) // type inferred
The rand.Rand type also has a new generic method that does the same thing.
Generic methods are new in Go 1.27 (see below).
rng := rand.New(rand.NewChaCha8([32]byte{}))
n := rng.N[int8](100) // generic method
n = rng.N[int8](200) // ERROR: 200 is too big for int8
The above code seeds the ChaCha8 random number generator with an array of 32 zero bytes. You would typically use a better seed value than this.
net/http/httptest
Here’s another gem I discovered, which makes testing an HTTP server even easier. I hope you know that it’s dead-easy to create a server in Go and unconscious-easy to test it using the net/http/httptest package.
TODO
testing/synctest
TODO
Language
The main changes to the language are in generics - generic methods (see next) but also some more improvements to type inference (where the compiler saves you from having to add type parameters like [int32]])
Generic Methods
When generics were introduced in Go 1.18 only functions (and types) could have type parameters. Methods were excluded (even though they are just functions really), due to their relationship with interfaces. We can now add type parameters to methods. This is useful but really just syntactic sugar since interface methods are excluded.
This addition to Go seems like it could be useful, so I thought I’d give a realistic example of how it could be used, but it took a long time. I looked through my old C++ code to try to find a bit of code that made sense in Go, but all the examples (class member function templates) were on large classes or for something that would be more easily done another way in Go.
Next I looked at some blogs. There are a lot of blogs that are already raving about Go 1.27’s generic methods. None provided an example of a useful application of generic methods. Please, send me a good example, if you have one.
The best I could find was from the standard library math/rand/v2 package, which (as mentioned above) has a new generic method for generating random integers ((*Rand).N[Int]()). This can be used to generate a random value for any integer type (though I’m not sure why you would use it with uintptr :).
So I’ll just have to repeat my example from my blog on the generics proposal from 2020 - see (Member Function Templates)[https://devmethodologies.blogspot.com/2020/10/go-generics.html#:~:text=Member%20function%20templates]….
This example shows a bool container (BoolStack) and a method (PushEqual[]()) that compares two values to add to the container. The type of the two parameters can be of any type as long as they can be compared for equality. This is ensured using the comparable type parameter.
type BoolStack []bool
func (s *BoolStack) PushEqual[T comparable](v1, v2 T) {
*s = append(*s, v1 == v2)
}
Struct Literal Embedded Fields
I first encountered this awkwardness about a decade ago. I’m surprised, if it’s worth fixing, that it’s only now. I guess it took this long because it does not add anything new, but just simplifies things a little. For example, if you have an inner struct embedded in an outer like this:
type (
inner struct{ num int }
outer struct{ inner }
)
you can use the fields of the embedded struct directly in a literal like this:
v := outer{num: 1} // Go 1.27
v = outer{inner: inner{num: 1}} // before
Runtime
There is not a lot that has changed in the runtime, but Go 1.27 continues the recent theme of speeding up garbage collection, though in this case it’s the allocation not the collection that has been improved. The Green Tea GC was a major improvement (see Go 1.26 - Green Tea GC) and Go 1.27 has a further refinement…
Faster Small Allocations
Much Go software deals with lots of small objects which necessarily need to be stored on the heap. For this type of software allocations have been optimised which could make it about 1% faster overall.
Without getting too deep in the details, the compiler now emits code to directly allocate objects of common small sizes, rather than funneling through a general allocation routine.
Tools
goroutine leak profiler
I discussed the new goroutine leak profiler last time (see Goroutine Leak Profiler).
This can help you detect a problem that is often hidden and so quite common. Unfortunately, it only tells you about proveable problemes.
go mod tidy
go mod tidy does a much nicer job on cleaning up require blocks by sorting, removing duplicates and creating two groups (direct and indirect), while preserving your comments (as much as possible).
go fix
I talked about go fix last time - see Go fix. There are several new fixers or modernizers. These are useful to make your code simpler, faster, more idiomatic or to replace deprecated function calls.
Unfortunately, there is no encoding/json to v2 modernizer (yet?).
go test
It’s not obvious but running tests with go test also runs a lot of go vet checks. One that was previously missing is the stdversion check which will tell you if you are using library features no yet available for the language version you are targeting.
This avoids releasing a package that works for you but fails for users of your package who are using an earlier Go version (as permitted by your go.mod file).
Conclusion
Most people will say that generic methods are the highlight of this package but, for me, it’s the standard library additions.
The new JSON packages are brilliantly designed and implemented. There is no urgency to port your existing “v1” code to “v2” (especially as the old v1 package gets performance improvements for free) but you should use it for new stuff. Personally, I have already ported some code to v2 to use new unknown tag to tidy up some ugly, difficult to maintain code.
A bonus is the jsontext package with several cool features to work with JSON directly and efficiently, such as zero-allocation custom en/decoding.
It’s also great to have UUID support in the standard library. I especially like the top-notch support for V4 and V7 UUIDs but make sure you know which one is best for your requirements (security, performance, etc).
An intriguing inclusion is new mapHash package, which puzzled me until I discovered this issue from the Go Collections working group that appeared 3 weeks ago.
Finally, there has been a more work on the massive task of SIMD support, though this is still flagged as an experiment. The significant additions are a portable, high-level simd package and (of course) ARM and WASM support.
Also see the release notes for details - Go 1.27 Release Notes
Comments