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

huangapple go评论82阅读模式
英文:

Is there any way to access private fields of a struct from another package?

问题

我有一个在一个包中的结构体,它有私有字段:

package foo

type Foo struct {
    x int
    y *Foo
}

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

package bar

import "../foo"

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

有没有一种方法可以声明bar为一种“友好”包或者其他任何方式,以便能够从bar访问foo.Foo的私有成员,但仍然使它们对所有其他包保持私有(也许在unsafe中有一些方法)?

英文:

I have a struct in one package that has private fields:

package foo

type Foo struct {
    x int
    y *Foo
}

And another package (for example, a white-box testing package) needs access to them:

package bar

import "../foo"

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

Is there a way to declare bar to be a sort of "friend" package or any other way to be able to access foo.Foo's private members from bar, but still keep them private for all other packages (perhaps something in unsafe)?

答案1

得分: 73

使用反射(reflect)可以读取未导出的成员(在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) {
    // 由于结构体在内存中是按顺序组织的,我们可以通过字段大小逐步推进指针,直到到达所需的成员。
    // 对于y,我们推进8个字节,因为在64位机器上int的大小是8个字节,而int "x"是Foo的表示中的第一个字段。
    //
    // 如果你想修改x,你根本不需要推进指针,只需要将ptrTof转换为(*int)类型即可。
    ptrTof := unsafe.Pointer(f)
    ptrTof = unsafe.Pointer(uintptr(ptrTof) + uintptr(8)) // 如果是32位机器,则为4

    ptrToy := (**Foo)(ptrTof)
    *ptrToy = nil // 或者 *ptrToy = &Foo{} 或者其他你想要的值

}

这是一个非常糟糕的想法。它不具备可移植性,如果int的大小发生变化,它将失败;如果你重新排列Foo中的字段顺序、更改它们的类型或大小,或在已有字段之前添加新字段,这个函数将会毫不知情地将新的表示更改为随机的无意义数据。我还认为它可能会破坏垃圾回收。

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

然而,如果你需要同时对两个包进行白盒测试,我认为你可能会陷入困境,需要重新思考你的设计。

英文:

There is a way to read unexported members using reflect (in Go < 1.7)

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

However, trying to use y.Set, or otherwise set the field with reflect will result in the code panicking that you're trying to set an unexported field outside the package.

In short: unexported fields should be unexported for a reason, if you need to alter them either put the thing that needs to alter it in the same package, or expose/export some safe way to alter it.

That said, in the interest of fully answering the question, you can do this (and have to do it this way in 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&#39;re at the desired member. For y, we advance by 8
    // since it&#39;s the size of an int on a 64-bit machine and the int &quot;x&quot; is first
    // in the representation of Foo.
    //
    // If you wanted to alter x, you wouldn&#39;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 = &amp;Foo{} or whatever you want

}

This is a really, really bad idea. It's not portable, if int ever changes in size it will fail, if you ever rearrange the order of the fields in Foo, change their types, or their sizes, or add new fields before the pre-existing ones this function will merrily change the new representation to random gibberish data without telling you. I also think it might break garbage collection for this block.

Please, if you need to alter a field from outside the package either write the functionality to change it from within the package or export it.

Edit2: Since you mention White Box testing, note that if you name a file in your directory &lt;whatever&gt;_test.go it won't compile unless you use go test, so if you want to do white box testing, at the top declare package &lt;yourpackage&gt; which will give you access to unexported fields, and if you want to do black box testing then you use package &lt;yourpackage&gt;_test.

If you need to white box test two packages at the same time, however, I think you may be stuck and may need to rethink your design.

答案2

得分: 4

我假设你正在测试的是一个改变该包对象状态的功能,但你想要验证改变后的内部状态是否正确。可能有助于编写私有字段的GetSet函数,以便可以在包范围之外访问它们。

注意,这些GetSet的目的是限制对字段的读取和/或写入访问,而直接导出它们会自动给予读写访问权限。这是一个微妙的区别,但如果真正的目标是仅读取私有字段而不对其进行操作(包内部会以自己的方式进行操作),这是值得考虑的。

最后,如果你不愿意为包中的所有私有字段添加这些包装器,那么你可以在该包的一个新文件中编写它们,并使用构建标签在常规构建中忽略它,并在测试构建中包含它(无论何时/如何触发测试)。

// +build whitebox

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

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

英文:

I assume what you're testing is a package functionality that changes the state of that package's object, but you want to verify the internals post that change to affirm the new state is correct.

What might help would be writing Get and Set function for the private fields, so they can be accessed beyond the package scope.

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
}

Note that the idea of these Get and Set is to limit read and/or write access to the fields, while directly exporting them gives them read+write access automatically always. A subtle difference but worth consideration if the true goal is to only read the private fields and not operate on them (which the package internals would do in there own way)

Finally, if you're not comfortable with adding these type of wrappers for all the private fields in your package, then you can write them in a new file within that package and use build tags to ignore it in your regular builds, and include it in your test builds (wherever/however you trigger your testing).

// +build whitebox

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

Regular builds ignore building test files along with them, so these wont come in your final binary. If this package is used elsewhere in entirely different ecosystem, then this file wouldn't be built still because of the build tags constraint.

答案3

得分: 1

我刚开始学习C++ -> Go的移植,并遇到了一对彼此友好的类。我相当确定,如果它们是同一个包的一部分,它们默认就是友好的。

标识符的首字母大写在包内是有限制的。因此,它们可以在不同的文件中,只要它们在同一个目录中,并且可以看到彼此的未导出字段。

使用反射,即使是Go标准库,你应该仔细考虑。它会增加很多运行时开销。解决方案基本上是复制粘贴,如果你想让两个结构类型成为友好的,它们必须在同一个文件夹中。否则,你必须导出它们。(就个人而言,我认为关于导出敏感数据的“风险”的吹嘘有些夸张,尽管如果你正在编写一个没有可执行文件的独立库,也许这样做有些道理,因为库的用户在GoDoc中看不到这些字段,因此不会认为它们可以依赖它们的存在)。

英文:

I am just starting out with C++ -> Go porting and I ran across a pair of classes that were friends with each other. I am pretty sure if they are part of the same package they are friends by default, effectively.

The upper case first letter for an identifier is bound within the package. Thus they can be in separate files so long as they are in the same directory, and will have the ability to see each other's unexported fields.

Using reflect, even if it is Go stdlib, is something you should think always carefully about. It adds a lot of runtime overhead. The solution would be basically copy&paste if the two struct types you want to be friends, they simply must be in the same folder. Otherwise you have to export them. (Personally I think the woo woo about the 'risk' of exporting sensitive data is quite overblown, although if you are writing a solitary library that has no executable, maybe there can be some sense to this since users of the library will not see these fields in the GoDoc and thus not think they can depend on their existence).

答案4

得分: 1

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

语言故意不帮助你实现你想要的,所以你需要自己做。最简单的方法是简单地合并两个包 - 这是Go单元测试通常做的事情。

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

// SetYPrivate设置y的值。这个函数对foo和bar是私有的,其他包不应该使用它。它可能在未来的版本中消失。
func (foo *Foo) SetYPrivate(y int) {
    foo.y = y
}

这种技术的一个例子是标准库中的runtime.MemStats函数,它返回了一堆GC实现的私有信息。

英文:

Internal fields are in principle not exported from a package, which allows the author of a package to freely modify its internals without breaking any other package. Working around this using reflect or unsafe is not a good idea.

The language does deliberately not help you achieve what you want, so you're going to have to do it yourself. The simplest one is to simply merge the two packages — this is what Go unit tests typically do.

The alternative is to create an extra accessor that is only used by the bar package:

// 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
}

An example of this technique is the runtime.MemStats function in the standard library, which returns a bunch of privates of the GC implementation.

答案5

得分: 0

有多种方法可以实现这个目标。一种方法是使用go:linkname。另一种解决方案是使用公共的setter并检查堆栈跟踪。

英文:

There are multiple "hacks" to get there. One is using go:linkname. Another solution would be to have a public setter and inspect the stack trace.

huangapple
  • 本文由 发表于 2013年8月1日 05:54:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/17981651.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定