如何访问未导出的结构体字段

31

在Go 1.8中是否有使用反射(reflect)访问未公开字段(unexported fields)的方法?以下链接不再适用:https://dev59.com/wGMl5IYBdhLWcg3w966L#17982725

请注意,reflect.DeepEqual可以正常工作(也就是说,它可以访问未公开的字段),但我无法理解该函数。这个go playarea演示了它的使用。以下是源代码:

import (
"fmt"
"reflect"
)

type Foo struct {
  private string
}

func main() {
    x := Foo{"hello"}
    y := Foo{"goodbye"}
    z := Foo{"hello"}

    fmt.Println(reflect.DeepEqual(x,y)) //false
    fmt.Println(reflect.DeepEqual(x,z)) //true
}
4个回答

57

如果结构体是可寻址的,您可以使用 unsafe.Pointer 来访问该字段(读取或写入),如下所示:

rs := reflect.ValueOf(&MyStruct).Elem()
rf := rs.Field(n)
// rf can't be read or set.
rf = reflect.NewAt(rf.Type(), unsafe.Pointer(rf.UnsafeAddr())).Elem()
// Now rf can be read and set.

在playground上查看完整示例。

根据文档,使用unsafe.Pointer是有效的,并且运行go vet不会返回错误。

如果结构体不可寻址,则此技巧将无法工作,但您可以创建一个可寻址的副本,如下所示:

rs = reflect.ValueOf(MyStruct)
rs2 := reflect.New(rs.Type()).Elem()
rs2.Set(rs)
rf = rs2.Field(0)
rf = reflect.NewAt(rf.Type(), unsafe.Pointer(rf.UnsafeAddr())).Elem()
// Now rf can be read.  Setting will succeed but only affects the temporary copy.

点击此处在 playground 中查看完整示例。


1
太棒了,谢谢。结合FieldByName使用,这非常强大,虽然有点不规范。 - user1112789
“struct可寻址”是什么意思? - user12211419
3
@rakim: 它的意思是你可以获取它的地址 - 例如,它可以是本地变量或在堆上分配的内存,而不是函数的返回值等临时性数值。您可以在此处阅读更多信息:https://utcc.utoronto.ca/~cks/space/blog/programming/GoAddressableValues - cpcallen

24

基于 cpcallen 的工作:

import (
    "reflect"
    "unsafe"
)

func GetUnexportedField(field reflect.Value) interface{} {
    return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface()
}

func SetUnexportedField(field reflect.Value, value interface{}) {
    reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).
        Elem().
        Set(reflect.ValueOf(value))
}


reflect.NewAt可能一开始会让人感到困惑。它返回一个reflect.Value,该值表示指向指定field.Type()的值的指针,使用unsafe.Pointer(field.UnsafeAddr())作为该指针。在这种上下文中,reflect.NewAtreflect.New不同,后者将返回指向新初始化值的指针。

示例:

type Foo struct {
    unexportedField string
}

GetUnexportedField(reflect.ValueOf(&Foo{}).Elem().FieldByName("unexportedField"))

https://play.golang.org/p/IgjlQPYdKFR


这对我来说非常完美。代码示例比cpcallen的答案更有结构性。 - Pokulo

6

reflect.DeepEqual()可以做到这一点,因为它可以访问reflect包的未导出特性,例如valueInterface()函数,该函数需要一个safe参数,如果safe=true,则通过Value.Interface()方法拒绝访问未导出字段值。reflect.DeepEqual()将(可能)调用传递safe=false的函数。

您仍然可以这样做,但不能使用Value.Interface()来访问未导出的字段。相反,您必须使用类型特定的方法,例如对于string,使用Value.String(),对于浮点数,使用Value.Float(),对于整数,使用Value.Int()等。这些方法将返回值的副本(足以检查它),但不允许修改字段的值(如果Value.Interface()可以工作并且字段类型是指针类型,则可能“部分”可行)。

如果字段恰好是接口类型,则可以使用Value.Elem()来获取由接口值包含/包装的值。

示例:

type Foo struct {
    s string
    i int
    j interface{}
}

func main() {
    x := Foo{"hello", 2, 3.0}
    v := reflect.ValueOf(x)

    s := v.FieldByName("s")
    fmt.Printf("%T %v\n", s.String(), s.String())

    i := v.FieldByName("i")
    fmt.Printf("%T %v\n", i.Int(), i.Int())

    j := v.FieldByName("j").Elem()
    fmt.Printf("%T %v\n", j.Float(), j.Float())
}

输出结果(在Go Playground上测试):

string hello
int64 2
float64 3

好的,但是你怎么知道类型是什么,以便调用正确的方法? - U Avalos
例如,通过调用 Value.Type()(在字段的值上)来实现。 - icza
那不会导致一个非常繁琐的 switch 块吗?例如:case Uint、case Uint8、Uint16 等等... - U Avalos
@UAvalos 如果你想处理所有类型,那么不幸的是,是的。如果你只想打印它的值,你可以简单地使用 Value.String(),它会在内部完成这个操作。此外,请注意,例如 Value.Int() 处理所有有符号整数类型(例如 int8int16int32int64int),因此这些函数可以帮助你“缓解”痛苦。 - icza
Value.String() 只有在值为字符串时才返回该值。否则,它将返回该值类型的字符串。 - nhooyr
正如@icza所指出的那样,这种痛苦并不是痛苦。请参见https://play.golang.org/p/uB5KJYZzR5v,其中包含一个从[反射定律博客文章](https://blog.golang.org/laws-of-reflection)开始的示例,我在其中添加了一些用于私有字段的访问器。 - torek

0
package main

import (
    "fmt"
    "reflect"
    "strings"
    "unsafe"
)

type Person1 struct {
    W3ID string
    Name string
}

type Address1 struct {
    city    string
    country string
}

type User1 struct {
    name      string
    age       int
    address   Address1
    manager   Person1
    developer Person1
    tech      Person1
}

func showDetails(load, email interface{}) {
    if reflect.ValueOf(load).Kind() == reflect.Struct {
        typ := reflect.TypeOf(load)
        value := reflect.ValueOf(load)
        //#1 For struct, not addressable create a copy With Element.
        value2 := reflect.New(value.Type()).Elem() 
        //#2 Value2 is addressable and can be set
        value2.Set(value)     
        for i := 0; i < typ.NumField(); i++ {
            if value.Field(i).Kind() == reflect.Struct {
                rf := value2.Field(i)
                /* #nosec G103 */
                rf = reflect.NewAt(rf.Type(), unsafe.Pointer(rf.UnsafeAddr())).Elem()
                irf := rf.Interface()
                typrf := reflect.TypeOf(irf)
                nameP := typrf.String()
                if strings.Contains(nameP, "Person") {
                    //fmt.Println(nameP, "FOUND !!!!!!! ")
                    for j := 0; j < typrf.NumField(); j++ {
                        re := rf.Field(j)
                        nameW := typrf.Field(j).Name
                        if strings.Contains(nameW, "W3ID") {
                            valueW := re.Interface()
                            fetchEmail := valueW.(string)
                            if fetchEmail == email {
                                fmt.Println(fetchEmail, " MATCH!!!!")
                            }
                        }
                    }
                }
                showDetails(irf, email)
            } else {
                // fmt.Printf("%d.Type:%T || Value:%#v\n",
                //  (i + 1), value.Field(i), value.Field(i))
            }
        }
    }
}

func main() {
    iD := "tsumi@in.org.com"

    load := User1{
        name: "John Doe",
        age:  34,
        address: Address1{
            city:    "New York",
            country: "USA",
        },
        manager: Person1{
            W3ID: "jBult@in.org.com",
            Name: "Bualt",
        },
        developer: Person1{
            W3ID: "tsumi@in.org.com",
            Name: "Sumi",
        },
        tech: Person1{
            W3ID: "lPaul@in.org.com",
            Name: "Paul",
        },
    }

    showDetails(load, iD)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接