web-dev-qa-db-ja.com

別のパッケージから構造体のプライベートフィールドにアクセスする方法はありますか?

私はプライベートフィールドを持つ1つのパッケージに構造体を持っています:

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にあるもの)?

28
Matt

ありisへの方法readリフレクトを使用してエクスポートされていないメンバー

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

ただし、y.Setを使用するか、別の方法でリフレクトを使用してフィールドを設定しようとすると、パッケージの外部にエクスポートされていないフィールドを設定しようとするコードパニックが発生します。

簡単に言うと、エクスポートされていないフィールドは、理由からエクスポートされるべきではありません。変更する必要がある場合は、変更する必要があるものを同じパッケージに入れるか、安全な方法で変更してください。

とはいえ、質問に完全に答えるために、あなたはcanこれを行う

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

}

これは常に正しい名前付きフィールドを見つけるのでより安全ですが、それでもまだ友好的ではなく、おそらく遅いため、ガベージコレクションを混乱させるかどうかはわかりません。奇妙なことをしている場合にも警告が表示されません(このコードをいくつかのチェックを追加することでlittleより安全にすることができますが、気にしないでください。 )。

また、FieldByNameは、変数の名前を変更するパッケージ開発者の影響を受けやすいことにも注意してください。パッケージ開発者として、ユーザーが知らないはずの名前を変更することについて、私は絶対に何の不安もないことをあなたに言うことができます。 Fieldを使用することもできますが、開発者がフィールドの順序を警告なしで変更する可能性があります。これは、私がやる気がないのです。このリフレクトと安全でない組み合わせは安全ではないことに注意してください。通常の名前変更とは異なり、コンパイル時にエラーが発生することはありません。代わりに、プログラムは突然パニックになるか、奇妙で未定義の何かを実行します。これは、間違ったフィールドを取得したためです。つまり、名前を変更したパッケージ開発者があなたであっても、このトリックを行った場所を覚えていなくて、追跡にしばらく費やすことはできません。コンパイラーが文句を言わないためにテストが突然壊れた理由。これは悪い考えだと私は言いましたか?

Edit2:ホワイトボックステストについて言及しているので、ディレクトリ内のファイルに<whatever>_test.goという名前を付けた場合、go testを使用しないとコンパイルされないことに注意してください。ホワイトボックステストを実行する場合は、トップpackage <yourpackage>を宣言すると、エクスポートされていないフィールドにアクセスできます。ブラックボックステストを行う場合は、package <yourpackage>_testを使用します。

ただし、2つのパッケージを同時にホワイトボックステストする必要がある場合は、行き詰まってデザインを再考する必要があるかもしれません。

44
LinearZoetrope

私はC++から始めたばかりです->移植に行くと、お互いに友だちだった2つのクラスに出くわしました。彼らが同じパッケージの一部であるかどうかは、事実上、デフォルトで友達であると確信しています。

識別子の最初の大文字はパッケージ内にバインドされています。したがって、同じディレクトリにある限り、別のファイルに置くことができ、お互いのエクスポートされていないフィールドを表示することができます。

Go stdlibであっても、reflectを使用することは、常に慎重に検討する必要があります。ランタイムのオーバーヘッドが大幅に増加します。解決策は、友達になりたい2つの構造体タイプが同じフォルダーにある必要がある場合、基本的にコピー&ペーストです。それ以外の場合は、それらをエクスポートする必要があります。 (個人的には、機密データをエクスポートすることの「リスク」については大げさだと思いますが、実行可能ファイルがない単一のライブラリを作成している場合、ライブラリのユーザーにはこれらが表示されないため、これには意味があるかもしれませんGoDoc内のフィールドであり、したがって、それらはフィールドの存在に依存できるとは考えていません)。

0
Louki Sumirniy