Skip to content

proposal: reflect: add Value.As[T any] for unpacking Values into Go types #78007

@mateusz834

Description

@mateusz834

Proposal Details

In the reflect package we have two ways to convert a Value into a Go type, i.e. v.Interface.(T) and reflect.TypeAssert[T](v).

Both of these work on interface-basis, and are not easy to reason about (especially the TypeAssert, since the interface is "hidden" through generics), see the comments in the implementation, for all the corner-cases:

go/src/reflect/value.go

Lines 1535 to 1553 in b9545da

// If v is an interface, 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 a non-nil interface value. For example:
//
// var v any = &someError{}
// val := ValueOf(&v).Elem()
// TypeAssert[error](val) == val.Interface().(error)
//
// T is a interface and v is a nil interface value. For example:
//
// var v error = nil
// val := ValueOf(&v).Elem()
// TypeAssert[error](val) == val.Interface().(error)

go/src/reflect/value.go

Lines 1559 to 1562 in b9545da

// If T is an interface and v is a concrete type. For example:
//
// TypeAssert[any](ValueOf(1)) == ValueOf(1).Interface().(any)
// TypeAssert[error](ValueOf(&someError{})) == ValueOf(&someError{}).Interface().(error)

What we are missing is a way to convert a Value directly into a Go type without all these interface unpacking/packing logic.

I propose we add:

// As attempts to unpack v into type T.
// It returns the value and true if v.Type() == reflect.TypeFor[T](),
// otherwise the zero value of T and false.
//
// Panics if the Value was obtained by accessing
// unexported struct fields.
func (v Value) As[T any]() (val T, ok bool)

Implementation-wise, it would be identical to TypeAssert, but without the special cases

go/src/reflect/value.go

Lines 1535 to 1576 in b9545da

// If v is an interface, 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 a non-nil interface value. For example:
//
// var v any = &someError{}
// val := ValueOf(&v).Elem()
// TypeAssert[error](val) == val.Interface().(error)
//
// T is a interface and v is a nil interface value. For example:
//
// var v error = nil
// val := ValueOf(&v).Elem()
// TypeAssert[error](val) == val.Interface().(error)
if v.kind() == Interface {
v, ok := packIfaceValueIntoEmptyIface(v).(T)
return v, ok
}
// If T is an interface and 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 {
// To avoid allocating memory, in case the type assertion fails,
// first do the type assertion with a nil Data pointer.
iface := *(*any)(unsafe.Pointer(&abi.EmptyInterface{Type: v.typ(), Data: nil}))
if out, ok := iface.(T); ok {
// Now populate the Data field properly, we update the Data ptr
// directly to avoid an additional type asertion. We can re-use the
// itab we already got from the runtime (through the previous type assertion).
(*abi.CommonInterface)(unsafe.Pointer(&out)).Data = packEfaceData(v)
return out, true
}
var zero T
return zero, false
}

Assumes #77273

Metadata

Metadata

Assignees

No one assigned

    Labels

    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a toolProposal

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions