]> Cypherpunks repositories - gostls13.git/commitdiff
reflect: add TypeAssert[T]
authorMateusz Poliwczak <mpoliwczak34@gmail.com>
Tue, 20 May 2025 15:39:22 +0000 (15:39 +0000)
committerKeith Randall <khr@golang.org>
Tue, 20 May 2025 19:40:33 +0000 (12:40 -0700)
This implementation is zero-alloc when T is a concrete type,
allocates when val contains a method or when T is a interface
and Value was obtained for example through Elem(), in which case
it has to be allocated to avoid sharing the same memory.

goos: linux
goarch: amd64
pkg: reflect
cpu: AMD Ryzen 5 4600G with Radeon Graphics
                                                                         │ /tmp/bench2 │
                                                                         │   sec/op    │
TypeAssert/TypeAssert[int](int)-12                                         2.725n ± 1%
TypeAssert/TypeAssert[uint8](int)-12                                       2.599n ± 1%
TypeAssert/TypeAssert[fmt.Stringer](reflect_test.testTypeWithMethod)-12    8.470n ± 0%
TypeAssert/TypeAssert[fmt.Stringer](*reflect_test.testTypeWithMethod)-12   8.460n ± 1%
TypeAssert/TypeAssert[interface_{}](int)-12                                4.181n ± 1%
TypeAssert/TypeAssert[interface_{}](reflect_test.testTypeWithMethod)-12    4.178n ± 1%
TypeAssert/TypeAssert[time.Time](time.Time)-12                             2.839n ± 0%
TypeAssert/TypeAssert[func()_string](func()_string)-12                     151.1n ± 1%
geomean                                                                    6.645n

                                                                         │ /tmp/bench2  │
                                                                         │     B/op     │
TypeAssert/TypeAssert[int](int)-12                                         0.000 ± 0%
TypeAssert/TypeAssert[uint8](int)-12                                       0.000 ± 0%
TypeAssert/TypeAssert[fmt.Stringer](reflect_test.testTypeWithMethod)-12    0.000 ± 0%
TypeAssert/TypeAssert[fmt.Stringer](*reflect_test.testTypeWithMethod)-12   0.000 ± 0%
TypeAssert/TypeAssert[interface_{}](int)-12                                0.000 ± 0%
TypeAssert/TypeAssert[interface_{}](reflect_test.testTypeWithMethod)-12    0.000 ± 0%
TypeAssert/TypeAssert[time.Time](time.Time)-12                             0.000 ± 0%
TypeAssert/TypeAssert[func()_string](func()_string)-12                     72.00 ± 0%
geomean                                                                               ¹

Fixes #62121

Change-Id: I0911c70c5966672c930d387438643f94a40441c4
GitHub-Last-Rev: ce89a53097b53fc59ff3ce3996917f8484ad3967
GitHub-Pull-Request: golang/go#71639
Reviewed-on: https://go-review.googlesource.com/c/go/+/648056
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Keith Randall <khr@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
api/next/62121.txt [new file with mode: 0644]
doc/next/6-stdlib/99-minor/reflect/62121.md [new file with mode: 0644]
src/reflect/all_test.go
src/reflect/value.go

diff --git a/api/next/62121.txt b/api/next/62121.txt
new file mode 100644 (file)
index 0000000..bb220a6
--- /dev/null
@@ -0,0 +1 @@
+pkg reflect, func TypeAssert[$0 interface{}](Value) ($0, bool) #62121
diff --git a/doc/next/6-stdlib/99-minor/reflect/62121.md b/doc/next/6-stdlib/99-minor/reflect/62121.md
new file mode 100644 (file)
index 0000000..f6148ce
--- /dev/null
@@ -0,0 +1,3 @@
+The new [TypeAssert] function permits converting a [Value] directly to a Go value
+of the given type. This is like using a type assertion on the result of [Value.Interface],
+but avoids unnecessary memory allocations.
index 16c361e53fcc10ba90304c6c7808e5c0a9498750..e86ee9d322f6399b2f7c4fbed6cf3ee12b9930c6 100644 (file)
@@ -8681,3 +8681,132 @@ func TestMapOfKeyPanic(t *testing.T) {
        var slice []int
        m.MapIndex(ValueOf(slice))
 }
+
+func TestTypeAssert(t *testing.T) {
+       testTypeAssert(t, int(123456789), int(123456789), true)
+       testTypeAssert(t, int(-123456789), int(-123456789), true)
+       testTypeAssert(t, int32(123456789), int32(123456789), true)
+       testTypeAssert(t, int8(-123), int8(-123), true)
+       testTypeAssert(t, [2]int{1234, -5678}, [2]int{1234, -5678}, true)
+       testTypeAssert(t, "test value", "test value", true)
+       testTypeAssert(t, any("test value"), any("test value"), true)
+
+       v := 123456789
+       testTypeAssert(t, &v, &v, true)
+
+       testTypeAssert(t, int(123), uint(0), false)
+
+       testTypeAssert[any](t, 1, 1, true)
+       testTypeAssert[fmt.Stringer](t, 1, nil, false)
+
+       vv := testTypeWithMethod{"test"}
+       testTypeAssert[any](t, vv, vv, true)
+       testTypeAssert[any](t, &vv, &vv, true)
+       testTypeAssert[fmt.Stringer](t, vv, vv, true)
+       testTypeAssert[fmt.Stringer](t, &vv, &vv, true)
+       testTypeAssert[interface{ A() }](t, vv, nil, false)
+       testTypeAssert[interface{ A() }](t, &vv, nil, false)
+       testTypeAssert(t, any(vv), any(vv), true)
+       testTypeAssert(t, fmt.Stringer(vv), fmt.Stringer(vv), true)
+
+       testTypeAssert(t, fmt.Stringer(vv), any(vv), true)
+       testTypeAssert(t, any(vv), fmt.Stringer(vv), true)
+       testTypeAssert(t, fmt.Stringer(vv), interface{ M() }(vv), true)
+       testTypeAssert(t, interface{ M() }(vv), fmt.Stringer(vv), true)
+
+       testTypeAssert(t, any(int(1)), int(1), true)
+       testTypeAssert(t, any(int(1)), byte(0), false)
+       testTypeAssert(t, fmt.Stringer(vv), vv, true)
+}
+
+func testTypeAssert[T comparable, V any](t *testing.T, val V, wantVal T, wantOk bool) {
+       t.Helper()
+
+       v, ok := TypeAssert[T](ValueOf(&val).Elem())
+       if v != wantVal || ok != wantOk {
+               t.Errorf("TypeAssert[%v](%#v) = (%#v, %v); want = (%#v, %v)", TypeFor[T](), val, v, ok, wantVal, wantOk)
+       }
+
+       // Additionally make sure that TypeAssert[T](v) behaves in the same way as v.Interface().(T).
+       v2, ok2 := ValueOf(&val).Elem().Interface().(T)
+       if v != v2 || ok != ok2 {
+               t.Errorf("reflect.ValueOf(%#v).Interface().(%v) = (%#v, %v); want = (%#v, %v)", val, TypeFor[T](), v2, ok2, v, ok)
+       }
+}
+
+type testTypeWithMethod struct{ val string }
+
+func (v testTypeWithMethod) String() string { return v.val }
+func (v testTypeWithMethod) M()             {}
+
+func TestTypeAssertMethod(t *testing.T) {
+       method := ValueOf(&testTypeWithMethod{val: "test value"}).MethodByName("String")
+       f, ok := TypeAssert[func() string](method)
+       if !ok {
+               t.Fatalf(`TypeAssert[func() string](method) = (,false); want = (,true)`)
+       }
+
+       out := f()
+       if out != "test value" {
+               t.Fatalf(`TypeAssert[func() string](method)() = %q; want "test value"`, out)
+       }
+}
+
+func TestTypeAssertPanic(t *testing.T) {
+       t.Run("zero val", func(t *testing.T) {
+               defer func() { recover() }()
+               TypeAssert[int](Value{})
+               t.Fatalf("TypeAssert did not panic")
+       })
+       t.Run("read only", func(t *testing.T) {
+               defer func() { recover() }()
+               TypeAssert[int](ValueOf(&testTypeWithMethod{}).FieldByName("val"))
+               t.Fatalf("TypeAssert did not panic")
+       })
+}
+
+func TestTypeAssertAllocs(t *testing.T) {
+       typeAssertAllocs[[128]int](t, ValueOf([128]int{}), 0)
+       typeAssertAllocs[any](t, ValueOf([128]int{}), 0)
+
+       val := 123
+       typeAssertAllocs[any](t, ValueOf(val), 0)
+       typeAssertAllocs[any](t, ValueOf(&val).Elem(), 1) // must allocate, so that Set() does not modify the returned inner iface value.
+       typeAssertAllocs[int](t, ValueOf(val), 0)
+       typeAssertAllocs[int](t, ValueOf(&val).Elem(), 0)
+
+       typeAssertAllocs[time.Time](t, ValueOf(new(time.Time)).Elem(), 0)
+       typeAssertAllocs[time.Time](t, ValueOf(*new(time.Time)), 0)
+}
+
+func typeAssertAllocs[T any](t *testing.T, val Value, wantAllocs int) {
+       t.Helper()
+       allocs := testing.AllocsPerRun(10, func() {
+               TypeAssert[T](val)
+       })
+       if allocs != float64(wantAllocs) {
+               t.Errorf("TypeAssert[%v](%v) unexpected amount of allocations = %v; want = %v", TypeFor[T](), val.Type(), allocs, wantAllocs)
+       }
+}
+
+func BenchmarkTypeAssert(b *testing.B) {
+       benchmarkTypeAssert[int](b, ValueOf(int(1)))
+       benchmarkTypeAssert[byte](b, ValueOf(int(1)))
+
+       benchmarkTypeAssert[fmt.Stringer](b, ValueOf(testTypeWithMethod{}))
+       benchmarkTypeAssert[fmt.Stringer](b, ValueOf(&testTypeWithMethod{}))
+       benchmarkTypeAssert[any](b, ValueOf(int(1)))
+       benchmarkTypeAssert[any](b, ValueOf(testTypeWithMethod{}))
+
+       benchmarkTypeAssert[time.Time](b, ValueOf(*new(time.Time)))
+
+       benchmarkTypeAssert[func() string](b, ValueOf(time.Now()).MethodByName("String"))
+}
+
+func benchmarkTypeAssert[T any](b *testing.B, val Value) {
+       b.Run(fmt.Sprintf("TypeAssert[%v](%v)", TypeFor[T](), val.Type()), func(b *testing.B) {
+               for b.Loop() {
+                       TypeAssert[T](val)
+               }
+       })
+}
index 6e062a56d1c7ac2674ffa6908a4f2d44910064be..05ecfa1a5b09abc1f8f726f84586e8537875dbe4 100644 (file)
@@ -1219,15 +1219,7 @@ func (v Value) Elem() Value {
        k := v.kind()
        switch k {
        case Interface:
-               var eface any
-               if v.typ().NumMethod() == 0 {
-                       eface = *(*any)(v.ptr)
-               } else {
-                       eface = (any)(*(*interface {
-                               M()
-                       })(v.ptr))
-               }
-               x := unpackEface(eface)
+               x := unpackEface(packIfaceValueIntoEmptyIface(v))
                if x.flag != 0 {
                        x.flag |= v.flag.ro()
                }
@@ -1500,19 +1492,91 @@ func valueInterface(v Value, safe bool) any {
 
        if v.kind() == Interface {
                // Special case: return the element inside the interface.
-               // Empty interface has one layout, all interfaces with
-               // methods have a second layout.
-               if v.NumMethod() == 0 {
-                       return *(*any)(v.ptr)
-               }
-               return *(*interface {
-                       M()
-               })(v.ptr)
+               return packIfaceValueIntoEmptyIface(v)
        }
 
        return packEface(v)
 }
 
+// TypeAssert is semantically equivalent to:
+//
+//     v2, ok := v.Interface().(T)
+func TypeAssert[T any](v Value) (T, bool) {
+       if v.flag == 0 {
+               panic(&ValueError{"reflect.TypeAssert", Invalid})
+       }
+       if v.flag&flagRO != 0 {
+               // Do not allow access to unexported values via TypeAssert,
+               // because they might be pointers that should not be
+               // writable or methods or function that should not be callable.
+               panic("reflect.TypeAssert: cannot return value obtained from unexported field or method")
+       }
+
+       if v.flag&flagMethod != 0 {
+               v = makeMethodValue("TypeAssert", v)
+       }
+
+       typ := abi.TypeFor[T]()
+       if typ != v.typ() {
+               // We can't just return false here:
+               //
+               //      var zero T
+               //      return zero, false
+               //
+               // since this function should work in the same manner as v.Interface().(T) does.
+               // Thus we have to handle two cases specially.
+
+               // Return the element inside the interface.
+               //
+               // T is a concrete type and v is an interface. For example:
+               //
+               // var v any = int(1)
+               // val := ValueOf(&v).Elem()
+               // TypeAssert[int](val) == val.Interface().(int)
+               //
+               // T is a interface and v is an interface, but the iface types are different. For example:
+               //
+               // var v any = &someError{}
+               // val := ValueOf(&v).Elem()
+               // TypeAssert[error](val) == val.Interface().(error)
+               if v.kind() == Interface {
+                       v, ok := packIfaceValueIntoEmptyIface(v).(T)
+                       return v, ok
+               }
+
+               // T is an interface, v is a concrete type. For example:
+               //
+               // TypeAssert[any](ValueOf(1)) == ValueOf(1).Interface().(any)
+               // TypeAssert[error](ValueOf(&someError{})) == ValueOf(&someError{}).Interface().(error)
+               if typ.Kind() == abi.Interface {
+                       v, ok := packEface(v).(T)
+                       return v, ok
+               }
+
+               var zero T
+               return zero, false
+       }
+
+       if v.flag&flagIndir == 0 {
+               return *(*T)(unsafe.Pointer(&v.ptr)), true
+       }
+       return *(*T)(v.ptr), true
+}
+
+// packIfaceValueIntoEmptyIface converts an interface Value into an empty interface.
+//
+// Precondition: v.kind() == Interface
+func packIfaceValueIntoEmptyIface(v Value) any {
+       // Empty interface has one layout, all interfaces with
+       // methods have a second layout.
+       if v.NumMethod() == 0 {
+               return *(*any)(v.ptr)
+       }
+       return *(*interface {
+               M()
+       })(v.ptr)
+}
+
 // InterfaceData returns a pair of unspecified uintptr values.
 // It panics if v's Kind is not Interface.
 //