在Go中,有没有办法从另一个包访问结构的私有字段?

Mat*_*att 25 private friend package go

我在一个包含私有字段的结构中有一个结构:

package foo

type Foo struct {
    x int
    y *Foo
}
Run Code Online (Sandbox Code Playgroud)

另一个包(例如,白盒测试包)需要访问它们:

package bar

import "../foo"

func change_foo(f *Foo) {
    f.y = nil
}
Run Code Online (Sandbox Code Playgroud)

有没有办法声明bar是一种"朋友"包或任何其他方式,以便能够访问其foo.Foo私人成员bar,但仍然保持私有所有其他包(可能是某些东西unsafe)?

Lin*_*ope 44

一种方法来读取使用反映不导出成员

func read_foo(f *Foo) {
    v := reflect.ValueOf(*f)
    y := v.FieldByName("y")
    fmt.Println(y.Interface())
}
Run Code Online (Sandbox Code Playgroud)

但是,尝试使用y.Set或以反射方式设置字段将导致代码恐慌,您尝试在包外部设置未导出的字段.

简而言之:出于某种原因,未导出的字段应该是未导出的,如果您需要更改它们,或者将需要更改它的东西放在同一个包中,或者公开/导出一些安全的方法来改变它.

也就是说,为了完全回答这个问题,你可以做到这一点

func change_foo(f *Foo) {
    // Since structs are organized in memory order, we can advance the pointer
    // by field size until we're at the desired member. For y, we advance by 8
    // since it's the size of an int on a 64-bit machine and the int "x" is first
    // in the representation of Foo.
    //
    // If you wanted to alter x, you wouldn't advance the pointer at all, and simply
    // would need to convert ptrTof to the type (*int)
    ptrTof := unsafe.Pointer(f)
    ptrTof = unsafe.Pointer(uintptr(ptrTof) + uintptr(8)) // Or 4, if this is 32-bit

    ptrToy := (**Foo)(ptrTof)
    *ptrToy = nil // or *ptrToy = &Foo{} or whatever you want

}
Run Code Online (Sandbox Code Playgroud)

这是一个非常非常糟糕的主意.它不是可移植的,如果int的大小发生变化就会失败,如果你重新排列Foo中字段的顺序,改变它们的类型或它们的大小,或者在预先存在的字段之前添加新字段,这个函数会快速地改变它在没有告诉你的情况下对随机乱码数据的新表示.我也认为它可能会破坏这个块的垃圾收集.

如果您需要从包外部更改字段,请编写功能以从包中更改它或将其导出.

编辑:这是一个更安全的方法:

func change_foo(f *Foo) {
    // Note, simply doing reflect.ValueOf(*f) won't work, need to do this
    pointerVal := reflect.ValueOf(f)
    val := reflect.Indirect(pointerVal)

    member := val.FieldByName("y")
    ptrToY := unsafe.Pointer(member.UnsafeAddr())
    realPtrToY := (**Foo)(ptrToY)
    *realPtrToY = nil // or &Foo{} or whatever

}
Run Code Online (Sandbox Code Playgroud)

这是更安全的,因为它总能找到正确的命名字段,但它仍然不友好,可能很慢,我不确定它是否与垃圾收集混淆.如果你做了一些奇怪的事情,你也不会警告你(你可以通过添加一些检查来使这些代码更安全一点,但我不会打扰,这足以让你的要点得到足够的好处).

还要记住,FieldByName容易受到包开发人员更改变量名称的影响.作为一个软件包开发人员,我可以告诉你,我对改变用户应该不知道的东西的名字毫无疑问.你可以使用Field,但是你很容易让开发人员在没有警告的情况下更改字段的顺序,这也是我对此没有任何疑虑.请记住,这种反射和不安全的组合是......不安全的,与普通名称更改不同,这不会给您带来编译时错误.相反,该程序会突然发生恐慌或做一些奇怪和未定义的事情,因为它得到了错误的字段,这意味着即使你是改变名称的软件包开发人员,你仍然可能不记得你做过这个技巧的所有地方并花一些时间跟踪为什么你的测试突然爆发,因为编译器没有抱怨.我提到这是个坏主意吗?

Edit2:既然你提到了White Box测试,请注意,如果你在目录中命名一个文件,<whatever>_test.go除非你使用它go test,否则它将无法编译,因此如果你想进行白盒测试,可以在顶部声明package <yourpackage>,这样你就可以访问未导出的字段,如果你想做黑盒测试,那么你使用package <yourpackage>_test.

但是,如果您需要同时对两个包进行白盒测试,我认为您可能会遇到困难,可能需要重新考虑您的设计.

  • 通过将*_test.go文件放在与测试对象相同的包中,可以轻松完成白盒测试,因此您可以访问未导出的字段.Go工具正确支持这个用例,除了运行`go test`时,不会用你的包代码编译你的测试. (4认同)
  • 您还可以通过执行 [`wv := reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem( )`](https://play.golang.org/p/5rWZGUFlZp),之后 `wv.Set(newValue)` 不会恐慌。这使用了 `unsafe` 但不应该影响 GC。 (4认同)
  • 这些似乎都不再适用于1.8 (3认同)
  • 很好的答案,并且在“[...] 毫不犹豫地更改用户应该不知道的事物的名称”上 +1000。它没有出口是有原因的。 (2认同)
  • 需要说明的是,所有这些包都是我自己的,所以如果我更改一个字段的名称,我就会知道它。我有两个潜在的用途:白盒测试,您的解决方案肯定适用,但也是一个解析器,它将字符串转换为另一个包中的对象,其效率将受益于绕过结构的常用构造函数,但您的解决方案会破坏那个目的。当然,我可以将解析器放在同一个包中,但我想将程序的各个部分分开(也许这不太像 Go?)。 (2认同)
  • 您的第一个建议(不再使用?已通过Go 1.7测试)起作用了,而是使用panic惊恐了:reflect.Value.Interface:无法返回从未导出的字段或方法获得的值。 (2认同)