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?
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”.