With Go 1.24 set to launch in February, now's the perfect time to dive into the latest features and improvements. While the official release notes are comprehensive, they can be a bit dense.
That's why I've put together this example-packed guide to showcase what's new and how these changes affect real-world code.
Let's explore the updates together—you're in for a treat!
Generic type aliases
In Go, a type alias is an alternative name for an existing type. It's like giving a nickname to something, but the underlying thing remains the same.
This is different from a type definition where the new type is distinct from the original type, even if it has the same underlying structure:
Even though MyString is based on string, they can not be used
interchangeably. Explicit conversion is required:
In Go 1.24, its now possible to create generic type aliases, making it easier to create reusable abstractions without introducing new types.
For instance, you can use a type alias to define a generic data structure such as a set:
Here, Set[T] is simply an alias for map[T]struct{}. Since it doesn't create
a new type, mySet remains a map[string]struct{} under the hood, and
functions expecting a map[T]struct{} will work seamlessly with it.
Tracking executable dependencies
Before Go 1.24, the recommended approach for tracking executable dependencies is
by using a tools.go file that imports the packages providing the tools but
doesn't use them in the code:
The import statements enable the go command to accurately capture version
details for your tools in the module's go.mod file, while the build constraint
ensures that these tools are not included during normal builds.
In Go 1.24 and above, you can now add a tool directive for such dependencies
directly to your go.mod file instead of following the tools.go pattern.
Here's the command to run:
This will add the tool directive to your go.mod file, and ensure the necessary
require directives are present:
You can then run the tool with:
You can also view all the available tools with:
See the documentation for more details.
In Go 1.10, a new -json flag was added to go test to enable the production
of a machine-readable JSON-formatted description of test execution. This made it
easy the creation of rich presentations of test execution in IDEs and other
tools.
Now the same flag has been added to go build and go install to make their
output easier to parse and analyze programmatically.
For example, here's the output from a failed go build with the --json flag:
The JSON output from go test was also enhanced with build output and failures
in addition to the test results.
Prior to Go 1.24, a build failure will not be in JSON:
But in Go 1.24, you'll see these logs in JSON format as build-output and
build-failed Action types:
For further details, see the go help buildjson.
Omitting zero values in JSON
A common issue when working with JSON in Go is that the default marshaling behavior can include fields with zero values, which may not always be desired.
For example, a time.Time field with a zero value represents a valid date
(January 1, 1, 00:00:00 UTC), but in many cases, you might want to omit it from
the JSON output if it hasn't been explicitly set.
The omitempty tag does not work here because the zero value for time.Time
fields is a valid date:
Go 1.24 addresses this issue by introducing the omitzero option for JSON field
tags to provide a more precise way to exclude zero values during JSON
marshaling.
This option is clearer and less error-prone than omitempty when the intent is
specifically to exclude zero values:
With this change, the DueDate field will be excluded from the JSON output if
its not explicitly initialized:
If the field type has a IsZero() bool method, it will be used to determine if
the value is a zero value. Otherwise, the value is zero if it is
the zero value for its type.
You can also use omitempty and omitzero together if you'd like to omit a
field if its value is either empty or zero (or both).
Vet checks for safer code
Go 1.24 strengthens the go vet command with new and improved analyzers,
providing even more robust static analysis to catch potential issues in your
code and improve its safety. Here are some key updates:
New tests Analyzer
This new analyzer focuses specifically on your test code. It automatically examines your test functions, checking for common mistakes such as:
- Malformed test names: Ensures your test function names follow the correct
TestXxxformat. - Invalid example functions: Verifies that example functions are correctly named and don't reference non-existent identifiers.
By catching these errors early, the tests analyzer helps you write more
reliable and maintainable tests.
Enhanced analyzers
Several existing analyzers have also been improved:
printf: Now warns you if you use fmt.Printf with a runtime variable as the format string (e.g.,fmt.Printf(s)). This helps prevent potential panics if the variable contains unexpected format specifiers.buildtag: Flags invalid build constraints like//go:build go1.23.1, ensuring your build tags are correct.copylock: Helps prevent subtle concurrency bugs by warning you if a loop variable holds async.Mutexor similar lock. This addresses a potential issue introduced in Go 1.22 where such loop variables are copied in each iteration.
New benchmark function
Go 1.24 introduces a streamlined approach to writing benchmarks with
testing.B.Loop, addressing common pitfalls and making your benchmarks more
efficient and reliable.
Before Go 1.24, benchmarks typically used a b.N for loop:
This approach, while functional, had some drawbacks:
- The
inputstring is recreated every time the benchmark function is called, even though it's constant across iterations. - You must explicitly call
b.ResetTimer()to exclude the setup time from the measured results. - The
_ = reverseString(input)line is needed to ensure the compiler doesn't optimize away the function call
With Go 1.24's b.Loop, these problems are resolved:
Here, the input string is created only once, no matter how many iterations are
executed. The setup time is also automatically excluded so there's no need to
call b.ResetTimer(), and the compiler won't optimize away code within
b.Loop.
Suppressing log output is made easier
When testing or benchmarking code that uses slog, it's often necessary to
suppress log output to avoid cluttering the console. A common approach is to
create a logger that discards all log entries.
Here, the slog.JSONHandler is configured to send all log output to
io.Discard, effectively silencing it.
In Go 1.24, the slog.DiscardHandler now achieves this automatically:
Expanded iterators in strings and bytes Packages
Go 1.23 introduced a robust set of iterator functions, making string and byte processing more efficient and expressive. These iterators simplify common tasks like splitting strings or processing lines without requiring slices, offering a more memory-efficient approach.
In Go 1.24, a few new iterators have been added to both the strings and
bytes packages, but we'll only demonstrate the strings variant below:
Lines()
The strings.Lines() function returns an iterator that processes
newline-terminated lines in a string:
SplitSeq()
The strings.SplitSeq() function returns an iterator that splits a string by a
specified delimiter.
SplitAfterSeq
The strings.SplitAfterSeq() function returns an iterator that splits a string
after each occurrence of the delimiter.
FieldsSeq
The strings.FieldsSeq() function splits a string into substrings around runs
of whitespace and provides them as an iterator.
FieldsFuncSeq
The strings.FieldsFuncSeq() function splits a string based on Unicode code
points that satisfy a given predicate function.
Embedding module version in Go binaries
Go 1.24 enhances the go build command to automatically embed version control
information into your compiled binaries. This makes it easier to track and
identify the exact code version used to build an application.
You can access the build version with the following code:
The version is determined based on the version control system (VCS) tag or
commit. You can have tagged versions like v1.2.4 or v1.2.4+dirty, or
untagged versions such as v1.2.3-0.20240620130020-daa7c0413123. The +dirty
suffix is added if there are uncommitted changes in the repository at build
time.
To omit version control information from the binary, use the -buildvcs=false
flag:
In this case, the application version will always display (devel)
Testing with synthetic time
Testing time-sensitive code can be challenging, especially when dealing with long timeouts. Waiting for real-time durations in tests slows down development and can lead to unreliable results.
Go 1.24 addresses this with the experimental testing/synctest package, which introduces synthetic time for controlled and predictable testing.
Consider a function that retries an operation with exponential backoff until it succeeds or a maximum duration is reached:
Testing this function with real time would require waiting for the full timeout
duration, which is inefficient. Using the testing/synctest package, we can
speed up the process by simulating time progression.
With this setup, the time.Sleep() calls in Retry doesn't delay the test
because the synctest.Run() environment uses synthetic time. The synthetic
clock advances as each retry occurs, respecting the exponential backoff logic.
In the test, we assert that the retry logic performed four attempts, corresponding to the backoff sequence within the 10-second timeout.
Note that this feature is experimental and subject to change, so you must
explicitly enable it by setting GOEXPERIMENT=synctest at build time:
The output confirms that the function respects the timeout and that retries occur as expected:
Using test context and directory isolation
Go 1.24 introduces several enhancements for testing, including improved context handling and working directory management.
Context management and cleanup
T.Context(): This method returns a context that is automatically canceled when the test completes. This simplifies resource cleanup and ensures tests don't leak resources.T.Cleanup(): Register functions to be called when the test completes. This is useful for waiting for long-running operations to finish or cleaning up resources.
For example, imagine a data processor that processes files in the current working directory:
To ensure the processor cleans up resources properly, we can use the T.Context
method to manage its lifecycle during testing:
Working directory management
Tests that interact with files in the current directory often require directory
isolation. The t.Chdir() and b.Chdir() methods ensures each test runs in its
own temporary directory while restoring the original directory afterward.
Here's how you can test the processor and its file interactions in an isolated environment:
Final thoughts
Go 1.24 is packed with exciting new features and enhancements. Highlights include the introduction of weak pointers and finalizers, making it easier to manage memory in advanced use cases. Directory-scoped filesystem access offers a more secure and granular way to handle file operations.
Performance improvements take center stage with faster map implementations, a change that developers will appreciate across a wide range of applications. The release also emphasizes developer experience, introducing tools for safer and more efficient benchmarking, better support for testing concurrent code, and simplified integration of custom tools.
On the cryptographic front, the addition of SHA-3 support and random text generation utilities further strengthens Go's already robust security capabilities.
With this release, Go continues to evolve as a developer-friendly and high-performance language. It's a fantastic step forward!