Go: Exploring a Brand New Approach to Type Assertions

This article introduces a new type assertion syntax in Go 1.18 using generics, simplifying the process from multi-line to single-line code.

Introduction

In the early days of Go programming, type assertions were typically performed in the following manner:

var v interface{}
v = 10
if _, ok := v.(int); ok {
    fmt.Println("v is an int")
}

However, with the progression of Go, particularly the introduction of generics in version 1.18, we can now use a much more concise method for type assertions:

if IsType[int](v) {
    fmt.Println("v is an int")
}

Here is the implementation of the IsType function:

func IsType[T interface{}](value interface{}) bool {
    _, ok := value.(T)
    return ok
}

We can also write unit tests for this function:

func TestIsType(t *testing.T) {
    assert.True(t, IsType[int](10))
    assert.False(t, IsType[int](int8(10)))
    assert.True(t, IsType[string]("foo"))
    assert.False(t, IsType[string](10))
    assert.True(t, IsType[time.Time](time.Now()))
    assert.False(t, IsType[time.Time](10))
    // More tests...
}

Test results:

=== RUN   TestIsType
--- PASS: TestIsType (0.00s)
PASS

In essence, the IsType function leverages the generic features introduced in Go 1.18, allowing us to conveniently determine the type of a value. What used to take three lines of code can now be done in just one.

Performance Comparison

Considering the use of generics, let's compare the performance overhead between the native approach and the generic approach. The related test scripts can be found on GitHub.

Native approach:

func testIsType(value interface{}) bool {
    _, ok := value.(int)
    return ok
}
func BenchmarkIsType_Native(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            testIsType(10)
        }
    })
}

Native approach test results:

BenchmarkIsType_Native-8   500000000               2330 ns/op

Generic approach:

func BenchmarkIsType_Generics(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            IsType[int](10)
        }
    })
}

Generic approach test results:

BenchmarkIsType_Generics-8   500000000               2403 ns/op

The test results show that the performance overhead is negligible.

Practical Application

In practical applications, similar to the instanceof operation in many languages, we can use this method to determine the type. For example:

type namedInterface interface {
    Name() string
}
type Person struct {
    Name string
}
func (p Person) Name() string {
    return p.Name
}
func main() {
    var n namedInterface = Person{"foo"}
    if IsType[Person](n) {
        fmt.Println(n.Name())
    }
}

References

With this approach, we can handle type assertions more concisely and efficiently, marking a significant advancement in the evolution of the Go language.