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

时间:2013-07-31 21:54:09

标签: go package private friend

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

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中的某些内容)?

5 个答案:

答案 0 :(得分:42)

使用

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

}

这是一个非常非常糟糕的主意。它不是可移植的,如果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

}

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

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

Edit2:由于你提到了White Box测试,请注意,如果你在目录<whatever>_test.go中命名一个文件,除非你使用go test,否则它将无法编译,所以如果你想进行白盒测试,在顶部声明package <yourpackage>,它将允许您访问未导出的字段,如果您想进行黑盒测试,则使用package <yourpackage>_test

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

答案 1 :(得分:1)

我假设您正在测试的是一种可更改该软件包对象状态的软件包功能,但是您想验证内部更改后的内容,以确认新状态正确。

为私有字段编写GetSet函数可能会有所帮助,以便可以在包范围之外进行访问。

package foo

type Foo struct {
    x int
    y *Foo
}

func (f *Foo) GetY() *Foo {
    return f.y
}

func (f *Foo) SetY(newY *Foo) {
    f.y = newY
}

请注意,这些GetSet的想法是限制对字段的读写访问,而直接导出它们则始终自动为它们提供读写访问权限。如果真正的目标是仅读取私有字段而不对其进行操作(包内部将以其自己的方式进行操作),则存在细微差异,但值得考虑。

最后,如果您不满意为包中的所有私有字段添加此类包装器,则可以将其写入该包中的新文件中,并使用build tags在您的包中忽略它常规版本,并将其包含在测试版本中(无论您触发测试的位置/方式如何)。

// +build whitebox

// Get() and Set() function
go test --tags=whitebox

常规构建会忽略与它们一起构建的测试文件,因此这些不会出现在最终的二进制文件中。如果此软件包在完全不同的生态系统中的其他地方使用,则由于构建标签的限制,该文件仍无法构建。

答案 2 :(得分:0)

我刚刚开始使用C ++ - &gt;去移植,我遇到了一对彼此成为朋友的课程。我很确定他们是否属于同一个包,他们默认是有效的。

标识符的大写首字母绑定在包中。因此,只要它们位于同一目录中,它们就可以位于单独的文件中,并且能够看到彼此未被移植的字段。

使用反射,即使它是Go stdlib,也应该经常仔细考虑。它增加了很多运行时开销。如果要成为朋友的两个结构类型,解决方案基本上是复制和粘贴,它们只需要在同一个文件夹中。否则你必须导出它们。 (就我个人而言,我认为关于导出敏感数据的风险并非如此夸张,尽管如果您正在编写一个没有可执行文件的独立库,可能会有一些意义,因为库将不会在GoDoc中看到这些字段,因此不认为它们可以依赖于它们的存在。)

答案 3 :(得分:0)

原则上不会从包中导出内部字段,这使包的作者可以自由修改其内部内容而不会破坏任何其他包。使用reflectunsafe解决此问题不是一个好主意。

该语言故意无法帮助您实现所需的目标,因此您必须自己完成。最简单的方法是简单地合并两个程序包,这是Go单元测试通常要做的。

另一种方法是创建一个仅由bar包使用的额外访问器:

// SetYPrivate sets the value of y.  This function is private to foo and bar,
// and should not be used by other packages.  It might go away in future releases.
func (foo *Foo) SetYPrivate(y int) {
    foo.y = y
}

此技术的一个示例是标准库中的runtime.MemStats函数,该函数返回一堆GC实现的私有函数。

答案 4 :(得分:0)

有多个“技巧”可以到达那里。一种是使用go:linkname。另一个解决方案是拥有一个公共设置器并检查堆栈跟踪。