0

I'm trying to make a general purpose debug printer for complex data types because %v has a tendency to just print pointer values rather than what they point at. I've got it working with everything up until I have to deal with structs containing reflect.Value fields.

The following demo code runs without error: (https://play.golang.org/p/qvdRKc40R8k)

package main

import (
    "fmt"
    "reflect"
)

type MyStruct struct {
    i int
    R reflect.Value
}

func printContents(value interface{}) {
    // Omitted: check if value is actually a struct
    rv := reflect.ValueOf(value)
    for i := 0; i < rv.NumField(); i++ {
        fmt.Printf("%v: ", rv.Type().Field(i).Name)
        field := rv.Field(i)
        switch field.Kind() {
        case reflect.Int:
            fmt.Printf("%v", field.Int())
        case reflect.Struct:
            // Omitted: check if field is actually a reflect.Value to an int
            fmt.Printf("reflect.Value(%v)", field.Interface().(reflect.Value).Int())
        }
        fmt.Printf("\n")
    }
}

func main() {
    printContents(MyStruct{123, reflect.ValueOf(456)})
}

This prints:

i: 123
R: reflect.Value(456)

However, if I change MyStruct's R field name to r, it fails:

panic: reflect.Value.Interface: cannot return value obtained from unexported field or method

Of course, it's rightly failing because this would otherwise be a way to get an unexported field into proper goland, which is a no-no.

But this leaves me in a quandry: How can I gain access to whatever the unexported reflect.Value refers to without using Interface() so that I can walk its contents and print? I've looked through the reflect documentation and haven't found anything that looks helpful...

Karl
  • 14,434
  • 9
  • 44
  • 61
  • Have you looked at any of the many existing implementations of this sort of functionality, e.g. https://github.com/davecgh/go-spew? – Adrian Feb 18 '20 at 18:19
  • You can do something like this: https://play.golang.org/p/MWnjy1iIrJQ Note that I'm passing in a pointer to the original struct so that the fields are addressable. More importantly however, to make this work you need to use the `unsafe` package which should not be used without due consideration. – mkopriva Feb 18 '20 at 18:39
  • Possible duplicate of [Access unexported fields in golang/reflect?](https://stackoverflow.com/questions/42664837/access-unexported-fields-in-golang-reflect/42666934) – icza Feb 18 '20 at 20:04

1 Answers1

1

After some digging, I've found a solution:

The only way to get at the inner reflect.Value is to call Interface() and type assert it, but this will panic if called on an unexported field. The only way around this is to use the unsafe package to clear the read-only flag so that the Interface() method will think it's exported when it's not (basically, we subvert the type system):

type flag uintptr // reflect/value.go:flag

type flagROTester struct {
    A   int
    a   int // reflect/value.go:flagStickyRO
    int     // reflect/value.go:flagEmbedRO
    // Note: flagRO = flagStickyRO | flagEmbedRO
}

var flagOffset uintptr
var maskFlagRO flag
var hasExpectedReflectStruct bool

func initUnsafe() {
    if field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag"); ok {
        flagOffset = field.Offset
    } else {
        log.Println("go-describe: exposeInterface() is disabled because the " +
            "reflect.Value struct no longer has a flag field. Please open an " +
            "issue at https://github.com/kstenerud/go-describe/issues")
        hasExpectedReflectStruct = false
        return
    }

    rv := reflect.ValueOf(flagROTester{})
    getFlag := func(v reflect.Value, name string) flag {
        return flag(reflect.ValueOf(v.FieldByName(name)).FieldByName("flag").Uint())
    }
    flagRO := (getFlag(rv, "a") | getFlag(rv, "int")) ^ getFlag(rv, "A")
    maskFlagRO = ^flagRO

    if flagRO == 0 {
        log.Println("go-describe: exposeInterface() is disabled because the " +
            "reflect flag type no longer has a flagEmbedRO or flagStickyRO bit. " +
            "Please open an issue at https://github.com/kstenerud/go-describe/issues")
        hasExpectedReflectStruct = false
        return
    }

    hasExpectedReflectStruct = true
}

func canExposeInterface() bool {
    return hasExpectedReflectStruct
}

func exposeInterface(v reflect.Value) interface{} {
    pFlag := (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + flagOffset))
    *pFlag &= maskFlagRO
    return v.Interface()
}

There are caveats, in that unsafe isn't allowed or desirable in all environments, not to mention that subverting the type system is rarely the right thing to do. It's recommended that you make such code conditional on build tags, and include a safe alternative.

Karl
  • 14,434
  • 9
  • 44
  • 61