一尘不染

有什么方法可以从另一个包访问结构的私有字段吗?

go

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

package foo

type Foo struct {
    x int
    y *Foo
}

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

package bar

import "../foo"

func change_foo(f *Foo) {
    f.y = nil
}

是否有一种方法可以声明bar是某种“朋友”软件包,或者可以通过其他任何方式访问foo.Foo的私有成员bar,但仍然对所有其他软件包保持私有(也许在其中unsafe)?


阅读 258

收藏
2020-07-02

共1个答案

一尘不染

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

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

但是,尝试使用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

}

这是一个非常非常糟糕的主意。它不是可移植的,如果进行大小更改,它将失败;如果您重新排列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

}

这样比较安全,因为它总是会找到正确的命名字段,但它仍然不友好,可能很慢,而且我不确定它是否与垃圾回收弄乱了。它也将无法警告你,如果你正在做一些奇怪的(你可以把这个代码
稍微 添加一些检查比较安全,但是我不会理会,这得到要点跨越不够好)。

Also keep in mind that FieldByName is susceptible to the package developer
changing the name of the variable. As a package developer, I can tell you that
I have absolutely no qualms about changing the names of things users should be
unaware of. You could use Field, but then you’re susceptible to the developer
changing the order of the fields with no warning, which is something I also
have no qualms about doing. Keep in mind that this combination of reflect and
unsafe is… unsafe, unlike normal name changes this won’t give you a compile
time error. Instead, the program will just suddenly panic or do something
weird and undefined because it got the wrong field, meaning even if YOU are
the package developer that did the name change, you still may not remember
everywhere you did this trick and spend a while tracking down why your tests
suddenly broke because the compiler doesn’t complain. Did I mention that this
is a bad idea?

Edit2:既然您提到了白盒测试,请注意,如果您在目录中命名文件,<whatever>_test.go除非您使用go test,否则它将不会编译,因此,如果您要进行白盒测试,请在顶部声明package <yourpackage>,这将使您可以访问未导出的字段,如果您要做黑匣子测试,请使用package <yourpackage>_test

但是,如果您需要同时对两个软件包进行白盒测试,我认为您可能会陷入困境,可能需要重新考虑您的设计。

2020-07-02