web-dev-qa-db-ja.com

Goインターフェースフィールド

Goでは、インターフェイスはデータではなく機能を定義するという事実に精通しています。一連のメソッドをインターフェイスに配置しますが、そのインターフェイスを実装するものに必要なフィールドを指定することはできません。

例えば:

// Interface
type Giver interface {
    Give() int64
}

// One implementation
type FiveGiver struct {}

func (fg *FiveGiver) Give() int64 {
    return 5
}

// Another implementation
type VarGiver struct {
    number int64
}

func (vg *VarGiver) Give() int64 {
    return vg.number
}

これで、インターフェイスとその実装を使用できます。

// A function that uses the interface
func GetSomething(aGiver Giver) {
    fmt.Println("The Giver gives: ", aGiver.Give())
}

// Bring it all together
func main() {
    fg := &FiveGiver{}
    vg := &VarGiver{3}
    GetSomething(fg)
    GetSomething(vg)
}

/*
Resulting output:
5
3
*/

さて、あなたができないは次のようなものです:

type Person interface {
    Name string
    Age int64
}

type Bob struct implements Person { // Not Go syntax!
    ...
}

func PrintName(aPerson Person) {
    fmt.Println("Person's name is: ", aPerson.Name)
}

func main() {
    b := &Bob{"Bob", 23}
    PrintName(b)
}

ただし、インターフェイスと埋め込み構造体をいじってみたところ、次のような方法でこれを行う方法を発見しました。

type PersonProvider interface {
    GetPerson() *Person
}

type Person struct {
    Name string
    Age  int64
}

func (p *Person) GetPerson() *Person {
    return p
}

type Bob struct {
    FavoriteNumber int64
    Person
}

構造体が埋め込まれているため、BobにはPersonのすべてが含まれています。また、PersonProviderインターフェースも実装しているため、そのインターフェースを使用するように設計された関数にBobを渡すことができます。

func DoBirthday(pp PersonProvider) {
    pers := pp.GetPerson()
    pers.Age += 1
}

func SayHi(pp PersonProvider) {
    fmt.Printf("Hello, %v!\r", pp.GetPerson().Name)
}

func main() {
    b := &Bob{
        5,
        Person{"Bob", 23},
    }
    DoBirthday(b)
    SayHi(b)
    fmt.Printf("You're %v years old now!", b.Age)
}

これはGo Playgroundです 上記のコードを示しています。

このメソッドを使用して、動作ではなくデータを定義し、そのデータを埋め込むだけで任意の構造体によって実装できるインターフェイスを作成できます。その埋め込みデータと明示的に対話し、外部構造の性質を認識しない関数を定義できます。そして、コンパイル時にすべてがチェックされます! (混乱する唯一の方法は、具体的なPersonProviderではなく、BobPersonインターフェースを埋め込むことです。コンパイルして失敗します。ランタイム。)

さて、ここに私の質問があります:これはきちんとしたトリックですか、それとも違う方法でやるべきですか?

78
Matt Mc

これは間違いなくきちんとしたトリックであり、APIの一部としてそれらのフィールドへのアクセスを許可することができれば機能します。私が検討する代替案は、埋め込み可能なstruct/interfaceセットアップを維持することですが、ゲッターとセッターの観点からインターフェースを定義します。

ゲッターとセッターの背後にあるプロパティを非表示にすることで、後方互換性のある変更を後で行うための柔軟性が追加されます。いつかPersonを変更して、単一の「名前」フィールドだけでなく、first/middle/last/prefixを保存するとします。メソッドName() stringおよびSetName(string)がある場合は、Personインターフェースの既存のユーザーを満足させ、新しいきめ細かいメソッドを追加できます。または、保存されていない変更がある場合、データベースでバックアップされたオブジェクトを「ダーティ」としてマークすることができます。データの更新がすべてSetFoo()メソッドを通過するときに行うことができます。

そのため、ゲッター/セッターを使用すると、互換性のあるAPIを維持しながら構造体フィールドを変更し、コードを介さずにp.Name = "bob"を実行することはできないため、プロパティの取得/設定に関するロジックを追加できます。

あなたのタイプがもっと複​​雑なことをするとき、その柔​​軟性はより適切です。 PersonCollectionがある場合、sql.Rows[]*Person[]uintのデータベースIDなどによって内部的にサポートされている可能性があります。適切なインターフェースを使用すると、io.Readerがネットワーク接続とファイルの外観を同じようにする方法で、呼び出し元を気にする必要がなくなります。

具体的には、Goのinterfacesには、それを定義するパッケージをインポートせずに実装できる独特のプロパティがあります。それはあなたを助けることができます 循環インポートを避ける 。インターフェースが文字列だけではなく*Personを返す場合、すべてのPersonProvidersPersonが定義されているパッケージをインポートする必要があります。それは問題ないか、避けられないことさえあります。知っておくだけの結果です。

とはいえ、すべてのデータを非表示にする必要のあるGoの規則はありません。 (これは、たとえばC++との歓迎すべき違いです。)stdlibは、設定でhttp.Serverを初期化し、ゼロbytes.Bufferが使用可能であることを約束するなどのことを行います。そのような独自の作業を行うのは問題ありません。実際、より具体的なデータ公開バージョンが機能する場合、時期尚早な抽象化を行う必要はないと思います。トレードオフを意識するだけです。

39
twotwotwo