Search code examples
go

Cast to interface for implementation with pointer receiver


I want to check whether an received item of type any implements encoding.TextUnmarshaler, so I can invoke UnmarshalText on it. The problem is, it's method must be a pointer receiver, and this seems to be the source of quite some trouble.

import (
    "encoding"
    "github.com/stretchr/testify/assert"
    "reflect"
    "testing"
)

type MyStruct struct{ value string }

var _ encoding.TextMarshaler = (*MyStruct)(nil)

func (id MyStruct) MarshalText() (text []byte, err error) {
    return []byte(id.value), nil
}

var _ encoding.TextUnmarshaler = (*MyStruct)(nil)

func (id *MyStruct) UnmarshalText(data []byte) error {
    *id = MyStruct{value: string(data)}
    return nil
}

func TestIfICouldInvokeUnmarshalText(t *testing.T) {
    var ms any = MyStruct{value: "hello"}
    _, ok := (ms).(encoding.TextMarshaler)
    assert.True(t, ok)
    _, ok = (ms).(encoding.TextUnmarshaler)
    //assert.True(t, ok) // this is the first attempt, which will fail
    v := reflect.ValueOf(ms)
    if assert.True(t, v.Type().NumMethod() > 0 && v.CanInterface()) {
        if v.CanAddr() {
            _, ok := v.Addr().Interface().(encoding.TextUnmarshaler)
            assert.True(t, ok)
        } else {
            t.FailNow() // this is where I end up
        }
    }
}

Is there any (idiomatic) way to achieve that?


Solution

  • Firstly,

    var _ encoding.TextUnmarshaler = (*MyStruct)(nil)
    

    guarantees that *MyStruct implements encoding.TextUnmarshaler, so why would you test for that?

    Second, MyStruct does not implement encoding.TextUnmarshaler, and in extension:

    var ms any = MyStruct{value: "hello"}
    

    doesn't, so everything is correct there.

    The reason that MyStruct doesn't implement encoding.TextUnmarshaler is that any method of it can not modify the original structure, since it's working on a copy, so any changes would be lost.

    You've already got a copy in your interface, so the link to the original structure is lost anyway (Go Playground):

    package main
    
    import (
        "fmt"
    )
    
    type MyValue struct{ Value string }
    
    func (m MyValue) SetValue(value string) {
        m.Value = value
    }
    
    func main() {
        m := MyValue{"hello"}
        a := any(m)
    
        m.SetValue("world")
        a.(MyValue).SetValue("goodbye")
        fmt.Println(m, a)
    
        m.Value = "world"
        fmt.Println(m, a)
    }
    

    prints

    {hello} {hello}
    {world} {hello}
    

    you have different values already. As a side note: a.(MyValue).Value = ... does not compile, since a.(MyValue).Value is not addressable.

    *MyStruct does implement encoding.TextUnmarshaler, so try

    var ms any = &MyStruct{value: "hello"}
    

    Edit:

    If you would try any trickery, you are in for a surprise (Go Playground):

    package main
    
    import (
        "encoding"
        "reflect"
        "testing"
    
        "github.com/stretchr/testify/assert"
    )
    
    type MyStruct struct{ value string }
    
    var _ encoding.TextMarshaler = (*MyStruct)(nil)
    
    func (id MyStruct) MarshalText() (text []byte, err error) {
        return []byte(id.value), nil
    }
    
    var _ encoding.TextUnmarshaler = (*MyStruct)(nil)
    
    func (id *MyStruct) UnmarshalText(data []byte) error {
        *id = MyStruct{value: string(data)}
    
        return nil
    }
    
    func TestIfICouldInvokeUnmarshalText(t *testing.T) {
        var ms any = MyStruct{value: "hello"}
        _, ok := (ms).(encoding.TextMarshaler)
        if !ok {
            t.FailNow()
        }
        _, ok = (ms).(encoding.TextUnmarshaler)
        assert.False(t, ok)
    
        v := reflect.Indirect(reflect.New(reflect.TypeOf(ms)))
        v.Set(reflect.ValueOf(ms)) // makes an addressable copy
        sp := v.Addr().Interface()
    
        u, ok := (sp).(encoding.TextUnmarshaler)
        if !ok {
            t.FailNow()
        }
    
        _ = u.UnmarshalText([]byte("world"))
    
        m, _ := ms.(MyStruct)
        assert.Equal(t, "world", m.value)
    }
    

    Yes, you could call UnmarshalText on a copy of your structure, but the results are lost and your original variable still has “hello” instead of “world”.