# What's New in Go 1.24

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](https://tip.golang.org/doc/go1.24) 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!

<iframe width="100%" height="315" src="https://www.youtube.com/embed/cmtFI9eZ_UE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

## 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.

```go
[label main.go]
package main

import "fmt"

[highlight]
type MyString = string // Type alias
[/highlight]

func main() {
	var a MyString = "hello"
	var b string = a  // No conversion needed
	fmt.Println(a, b)
}
```

```text
[output]
hello hello
```

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:

```go
package main

import "fmt"

[highlight]
type MyString string // Type definition
[/highlight]

func main() {
	var a MyString = "hello"
	var b string = a  // compile-time error
	fmt.Println(a, b)
}
```

```text
[output]
./main.go:9:17: cannot use a (variable of type MyString) as string value in variable declaration
```

Even though `MyString` is based on `string`, they can not be used
interchangeably. Explicit conversion is required:

```go
package main

import "fmt"

type MyString string

func main() {
	var a MyString = "hello"
    [highlight]
	var b string = string(a) // Conversion is needed
    [/highlight]
	fmt.Println(a, b)
}
```

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:

```go
type Set[T comparable] = map[T]struct{}

mySet := Set[string]{
    "apple":  {},
    "banana": {},
    "cherry": {},
}

fmt.Printf("mySet is of type: %T\n", mySet)
```

```text
[output]
mySet is of type: map[string]struct {}
```

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:

```go
[label tools.go]
// go:build tools

package tools

import (
    _ "github.com/golangci/golangci-lint/cmd/golangci-lint"
    _ "golang.org/x/tools/cmd/goimports"
)
```

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:

```command
go get -tool github.com/golangci/golangci-lint/cmd/golangci-lint
```

This will add the tool directive to your `go.mod` file, and ensure the necessary
require directives are present:

```go
[label go.mod]
module github.com/betterstack-community/myProject

go 1.24

[highlight]
tool github.com/golangci/golangci-lint/cmd/golangci-lint
[/highlight]

require (
	. . .
    )
```

You can then run the tool with:

```command
go tool golangci-lint
```

You can also view all the available tools with:

```command
go tool
```

```text
[output]
. . .
pack
pprof
preprofile
test2json
trace
vet
github.com/golangci/golangci-lint/cmd/golangci-lint
```

See the [documentation](https://go.dev/doc/modules/managing-dependencies#tools)
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:

```command
go build --json
```

```json
[output]
{"ImportPath":"github.com/betterstack-community/go1.24","Action":"build-output","Output":"# github.com/betterstack-community/go1.24\n"}
{"ImportPath":"github.com/betterstack-community/go1.24","Action":"build-output","Output":"./main.go:36:11: undefined: Set\n"}
{"ImportPath":"github.com/betterstack-community/go1.24","Action":"build-output","Output":"./main.go:37:13: missing type in composite literal\n"}
{"ImportPath":"github.com/betterstack-community/go1.24","Action":"build-output","Output":"./main.go:38:13: missing type in composite literal\n"}
{"ImportPath":"github.com/betterstack-community/go1.24","Action":"build-output","Output":"./main.go:39:13: missing type in composite literal\n"}
{"ImportPath":"github.com/betterstack-community/go1.24","Action":"build-fail"}
```

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:

```command
go test --json
```

```json
[output]
# github.com/betterstack-community/go1.24 [github.com/betterstack-community/go1.24.test]
./main.go:4:2: undefined: fmt
{"Time":"2025-01-19T22:14:40.432917134+01:00","Action":"start","Package":"github.com/betterstack-community/go1.24"}
{"Time":"2025-01-19T22:14:40.432977663+01:00","Action":"output","Package":"github.com/betterstack-community/go1.24","Output":"FAIL\tgithub.com/betterstack-community/go1.24 [build failed]\n"}
{"Time":"2025-01-19T22:14:40.432990132+01:00","Action":"fail","Package":"github.com/betterstack-community/go1.24","Elapsed":0}
```

But in Go 1.24, you'll see these logs in JSON format as `build-output` and
`build-failed` `Action` types:

```json
[output]
[highlight]
{"ImportPath":"github.com/betterstack-community/go1.24 [github.com/betterstack-community/go1.24.test]","Action":"build-output","Output":"# github.com/betterstack-community/go1.24 [github.com
/betterstack-community/go1.24.test]\n"}
{"ImportPath":"github.com/betterstack-community/go1.24 [github.com/betterstack-community/go1.24.test]","Action":"build-output","Output":"./main.go:4:2: undefined: fmt\n"}
{"ImportPath":"github.com/betterstack-community/go1.24 [github.com/betterstack-community/go1.24.test]","Action":"build-fail"}
[/highlight]
{"Time":"2025-01-19T22:14:29.236391906+01:00","Action":"start","Package":"github.com/betterstack-community/go1.24"}
{"Time":"2025-01-19T22:14:29.236441887+01:00","Action":"output","Package":"github.com/betterstack-community/go1.24","Output":"FAIL\tgithub.com/betterstack-community/go1.24 [build failed]\n"}
{"Time":"2025-01-19T22:14:29.236450409+01:00","Action":"fail","Package":"github.com/betterstack-community/go1.24","Elapsed":0,"FailedBuild":"github.com/betterstack-community/go1.24 [github.c
om/betterstack-community/go1.24.test]"}
```

For further details, see the `go help buildjson`.

[ad-logs]

## 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.

```go
[label main.go]
package main

import (
	"encoding/json"
	"fmt"
	"time"
)

type Invoice struct {
	ID       int       `json:"id"`
	Customer string    `json:"customer"`
	DueDate  time.Time `json:"due_date,omitempty"`
	Amount   float64   `json:"amount"`
}

func main() {
	invoice := Invoice{ID: 123, Customer: "Acme Corp", Amount: 100.00}
	b, _ := json.Marshal(invoice)
	fmt.Println(string(b))
}
```

The `omitempty` tag does not work here because the zero value for `time.Time`
fields is a valid date:

```json
[output]
{"id":123,"customer":"Acme Corp","due_date":"0001-01-01T00:00:00Z","amount":100}
```

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:

```go
type Invoice struct {
	ID       int       `json:"id"`
	Customer string    `json:"customer"`
[highlight]
	DueDate  time.Time `json:"due_date,omitzero"`
[/highlight]
	Amount   float64   `json:"amount"`
}
```

With this change, the `DueDate` field will be excluded from the JSON output if
its not explicitly initialized:

```json
[output]
{"id":123,"customer":"Acme Corp","amount":100}
```

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](https://go.dev/ref/spec#The_zero_value).

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).

```go
type Invoice struct {
	ID       int       `json:"id"`
	Customer string    `json:"customer"`
[highlight]
	DueDate  time.Time `json:"due_date,omitzero,omitempty"`
[/highlight]
	Amount   float64   `json:"amount"`
}
```

## 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
  `TestXxx` format.
- **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 a `sync.Mutex` or 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:

```go
func BenchmarkReverseString(b *testing.B) {
    input := getString()
    b.ResetTimer()

    for range b.N {
        _ = reverseString(input)
    }
}
```

This approach, while functional, had some drawbacks:

- The `input` string 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:

```go
func BenchmarkReverseString(b *testing.B) {
    input := getString()

    b.Loop(func() {
        reverseString(input)
    })
}
```

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.

```go
package main

import (
    "io"
    "log/slog"
)

func main() {
    log := slog.New(
        slog.NewJSONHandler(io.Discard, nil),
    )
    log.Info("This will not be printed")
}
```

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:

```go
package main

import (
    "log/slog"
)

func main() {
    log := slog.New(slog.DiscardHandler)
    log.Info("This will not be printed")
}
```

## 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:

```go
logData := "INFO: Started\nWARN: Disk almost full\nERROR: Out of memory"
for line := range strings.Lines(logData) {
    fmt.Println("Log:", line)
}
```

```text
[output]
Log: INFO: Started
Log: WARN: Disk almost full
Log: ERROR: Out of memory
```

### SplitSeq()

The `strings.SplitSeq()` function returns an iterator that splits a string by a
specified delimiter.

```go
csvData := "apple,banana,cherry"
for value := range strings.SplitSeq(csvData, ",") {
    fmt.Println("Value:", value)
}
```

```text
[output]
Value: apple
Value: banana
Value: cherry
```

### SplitAfterSeq

The `strings.SplitAfterSeq()` function returns an iterator that splits a string
after each occurrence of the delimiter.

```go
logData := "INFO: Started|WARN: Disk almost full|ERROR: Out of memory|"
for part := range strings.SplitAfterSeq(logData, "|") {
    fmt.Println("Log segment:", part)
}
```

```text
[output]
Log segment: INFO: Started|
Log segment: WARN: Disk almost full|
Log segment: ERROR: Out of memory|
```

### FieldsSeq

The `strings.FieldsSeq()` function splits a string into substrings around runs
of whitespace and provides them as an iterator.

```go
command := "run --verbose --output=file.txt"
for arg := range strings.FieldsSeq(command) {
    fmt.Println("Argument:", arg)
}
```

```text
[output]
Argument: run
Argument: --verbose
Argument: --output=file.txt
```

### FieldsFuncSeq

The `strings.FieldsFuncSeq()` function splits a string based on Unicode code
points that satisfy a given predicate function.

```go
text := "Hello, world! How are you?"
isPunctuation := func(c rune) bool {
    return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}

for token := range strings.FieldsFuncSeq(text, isPunctuation) {
    fmt.Println("Token:", token)
}
```

```text
[output]
Token: Hello
Token: world
Token: How
Token: are
Token: you
```

## 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:

```go
package main

import (
	"fmt"
	"runtime/debug"
)

func main() {
[highlight]
	info, _ := debug.ReadBuildInfo()
[/highlight]
	fmt.Println("Go version:", info.GoVersion)
[highlight]
	fmt.Println("App version:", info.Main.Version)
[highlight]
}
```

```command
go build -o myapp && ./myapp
```

```text
[output]
Go version: go1.24rc2
App version: v1.0.0+dirty
```

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:

```command
go build -buildvcs=false -o myapp
```

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](https://github.com/golang/go/blob/master/src/testing/synctest/synctest.go),
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:

```go
// Retry retries the provided function f until it succeeds or the timeout is reached.
// It uses exponential backoff for retries, starting at 1 second and doubling each attempt.
func Retry(f func() error, timeout time.Duration) error {
    start := time.Now()
    delay := time.Second

    for {
        if err := f(); err == nil {
            return nil
        }

        if time.Since(start)+delay > timeout {
            return errors.New("operation timed out")
        }

        time.Sleep(delay)
        delay *= 2 // Exponential backoff
    }
}
```

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.

```go
func TestRetryTimeout(t *testing.T) {
    synctest.Run(func() {
        attempts := 0

        // Mock function that always fails
        f := func() error {
            attempts++
            return errors.New("failure")
        }

        // Set a timeout of 10 seconds
        timeout := 10 * time.Second

        // Call Retry and expect a timeout error
        err := Retry(f, timeout)
        if err == nil {
            t.Fatal("expected timeout error, got nil")
        }

        // Verify that the retry logic respected the timeout
        if attempts != 4 {
            t.Fatalf("expected 4 attempts, got %d", attempts)
        }
    })
}
```

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:

```command
GOEXPERIMENT=synctest go test
```

The output confirms that the function respects the timeout and that retries
occur as expected:

```text
[output]
PASS
ok      github.com/betterstack-community/go1.24 0.002s
```

## 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:

```go
[label main.go]
package main

import (
    "context"
    "os"
    "time"
)

type Processor struct {
    done chan struct{}
}

func (p *Processor) Process() int {
    return 42 // Simulated processing result
}

// Done returns a channel that is closed when the processor stops.
func (p *Processor) Done() <-chan struct{} {
    return p.done
}

// StartProcessor starts a processor that runs until the context is canceled.
func StartProcessor(ctx context.Context) *Processor {
    p := &Processor{done: make(chan struct{})}
    go func() {
        defer close(p.done)
        <-ctx.Done()
        // Simulate cleanup delay
        time.Sleep(100 * time.Millisecond)
    }()
    return p
}
```

To ensure the processor cleans up resources properly, we can use the `T.Context`
method to manage its lifecycle during testing:

```go
[label main_test.go]
package main

import (
    "testing"
)

func TestProcessor(t *testing.T) {
    // Use t.Context to manage processor lifecycle.
    processor := StartProcessor(t.Context())

[highlight]
    t.Cleanup(func() {
        <-processor.Done() // Wait for the processor to complete cleanup.
    })
[/highlight]

    if result := processor.Process(); result != 42 {
        t.Fatalf("unexpected result: %d", result)
    }
}
```

### 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.

```go
package main

import (
    "os"
    "testing"
)

func TestWorkingDirectoryIsolation(t *testing.T) {
    t.Run("temp_dir", func(t *testing.T) {
        // Change the working directory for this test.
        tmpDir := t.TempDir()
[highlight]
        t.Chdir(tmpDir)
[/highlight]

        cwd, _ := os.Getwd()
        if cwd != tmpDir {
            t.Fatalf("expected cwd to be %s, got %s", tmpDir, cwd)
        }

        // Create a temporary file in the new working directory.
        if _, err := os.Create("temp_file.txt"); err != nil {
            t.Fatalf("failed to create file: %v", err)
        }
    })

    t.Run("original_dir", func(t *testing.T) {
        // Ensure this test runs in the original working directory.
        cwd, _ := os.Getwd()
        if cwd == "/tmp" { // Replace with the temporary directory if needed
            t.Fatalf("unexpected cwd: %s", cwd)
        }
    })
}
```

Here's how you can test the processor and its file interactions in an isolated
environment:

```go
func TestProcessorWithFiles(t *testing.T) {
    t.Run("process_files", func(t *testing.T) {
        // Use a temporary directory for this test.
        tmpDir := t.TempDir()
        t.Chdir(tmpDir)

        // Create a sample file.
        if _, err := os.Create("input.txt"); err != nil {
            t.Fatalf("failed to create input file: %v", err)
        }

        // Start the processor with a managed context.
        processor := StartProcessor(t.Context())
        t.Cleanup(func() {
            <-processor.Done() // Ensure the processor completes cleanup.
        })

        // Verify processing result.
        if result := processor.Process(); result != 42 {
            t.Fatalf("unexpected result: %d", result)
        }
    })
}
```

## 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!
