46

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

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)保持私有?

4

5 回答 5

62

一种方法可以使用反射读取未导出的成员(在 Go < 1.7 中)

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

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

简而言之:未导出的字段应该出于某种原因取消导出,如果您需要更改它们,请将需要更改的内容放在同一个包中,或者公开/导出一些安全的方式来更改它。

也就是说,为了完全回答问题,您可以这样做(并且必须在 Go >= 1.7 中这样做)

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 中字段的顺序,更改它们的类型或大小,或者在预先存在的字段之前添加新字段,这个函数会很高兴地改变随机乱码数据的新表示,无需告诉您。我也认为它可能会破坏这个块的垃圾收集。

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

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

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

于 2013-07-31T23:26:14.090 回答
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
}

请注意,这些Get和的想法Set是限制对字段的读和/或写访问,而直接导出它们总是自动赋予它们读+写访问权限。如果真正的目标是只读取私有字段而不对它们进行操作(包内部会以自己的方式做),那么这是一个微妙的区别,但值得考虑

最后,如果您不习惯为包中的所有私有字段添加这些类型的包装器,那么您可以将它们写入该包内的新文件中,并使用构建标签在您的常规构建中忽略它,并包含它在您的测试版本中(无论您在哪里/如何触发您的测试)。

// +build whitebox

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

常规构建会忽略与它们一起构建测试文件,因此这些不会出现在您的最终二进制文件中。如果这个包在完全不同的生态系统中的其他地方使用,那么这个文件将不会因为构建标签的限制而被构建。

于 2020-06-05T07:34:45.567 回答
0

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

于 2020-11-15T08:42:08.070 回答
0

内部字段原则上不从包中导出,这允许包的作者在不破坏任何其他包的情况下自由修改其内部。使用reflector解决这个问题unsafe不是一个好主意。

该语言故意不帮助您实现您想要的,因此您将不得不自己做。最简单的一种是简单地合并这两个包——这是 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 实现的私有信息。

于 2020-06-08T13:43:36.840 回答
0

我刚开始使用 C++ -> Go 移植,我遇到了一对彼此友好的类。我很确定他们是否是同一个包的一部分,默认情况下他们是朋友,有效。

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

使用反射,即使它是 Go stdlib,也是您应该始终仔细考虑的事情。它增加了很多运行时开销。如果您想成为朋友,则解决方案基本上是复制和粘贴,它们必须在同一个文件夹中。否则你必须导出它们。(我个人认为,关于导出敏感数据的“风险”的说法被夸大了,尽管如果你正在编写一个没有可执行文件的单独库,也许这有一些意义,因为库的用户不会看到这些GoDoc 中的字段,因此认为它们不能依赖于它们的存在)。

于 2018-06-17T12:55:42.013 回答