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


105

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は次のとおりです。

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

さて、これが私の質問です。これはきちんとしたトリックですか、それとも別の方法で行う必要がありますか?


3
「動作ではなくデータを定義するインターフェースを作成できます」。データを返す動作があると私は主張します。
jmaloney 2014

答えを書きます。あなたがそれを必要とし、その結果を知っていればいいと思いますが、結果があり、私はいつもそれをするわけではありません。
twotwotwo

@jmaloneyはっきりと見たいのであれば、私はあなたが正しいと思います。しかし、全体として、私が示したさまざまな部分により、セマンティクスは「この関数は、その構成に___を持つすべての構造体を受け入れます」になります。少なくとも、それは私が意図したことです。
Matt Mc

1
これは「答え」の資料ではありません。「struct property golangとしてのインターフェース」をグーグルすることであなたの質問に行きました。インターフェイスを実装する構造体を別の構造体のプロパティとして設定することで、同様のアプローチを見つけました。こちらが遊び場です。play.golang.org/ p / KLzREXk9xoアイデアをありがとうございます。
デール

1
振り返ってみると、Goを5年間使用してきた結果、上記が慣用的なGoではないことは明らかです。それはジェネリックへの負担です。もしあなたがこの種のことをやりたくなったら、システムのアーキテクチャを再考することをお勧めします。インターフェースを受け入れて構造体を返し、コミュニケーションによって共有し、喜びます。
Matt Mc

回答:


55

それは間違いなくきちんとしたトリックです。ただし、ポインタを公開してもデータに直接アクセスできるため、将来の変更に備えて柔軟性が制限されます。また、Goの規則では、データ属性の前に常に抽象概念を置く必要はありません。

これらをまとめると、私は特定のユースケースでどちらか一方の極端に近づく傾向があります:a)パブリック属性を作成し(該当する場合は埋め込みを使用)、具体的な型を渡すか、b)データを公開するように見える場合後で問題を引き起こし、より堅牢な抽象化のためにゲッター/セッターを公開します。

これを属性ごとに比較します。たとえば、一部のデータが実装固有のものであるか、他の何らかの理由で表現を変更する必要がある場合は、属性を直接公開する必要はないかもしれませんが、他のデータ属性は十分に安定しているため、それらを公開することは正味の勝利です。


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

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

その柔軟性は、型が複雑な場合(およびコードベースが大きい場合)により適切です。あなたが持っている場合PersonCollection、それは内部的に裏付けされるかもしれないsql.Rows[]*Person[]uintデータベースID、または任意の。適切なインターフェースを使用して、発信者がそれがどういうものであるかを気にすることからio.Readerネットワーク接続とファイルを同じように見せる方法を節約できます。

具体的にinterfaceは、Goのsには、それを定義するパッケージをインポートせずに実装できる固有のプロパティがあります。循環インポート回避するのに役立ちます。インターフェイスが*Person文字列だけではなくを返す場合、すべてが定義されてPersonProvidersいるパッケージをインポートする必要がありますPerson。それは問題ないかもしれませんし、避けられないかもしれません。知っておくべき結果です。


ただし、繰り返しになりますが、Goコミュニティには、型のパブリックAPIでデータメンバーを公開することに対して強い規則はありません。特定のケースでAPIの一部として属性へのパブリックアクセスを使用することが合理的かどうかは、後で実装の変更を複雑化または防止する可能性があるため公開を妨げるのではなく、あなたの判断に任されています。

したがって、たとえば、stdlibは、構成を使用http.Serverしてを初期化し、ゼロbytes.Bufferが使用可能であることを約束するなどのことを行います。そのような独自のことを行うことは問題ありません。実際、私は、より具体的なデータ公開バージョンが機能する可能性が高いと思われる場合は、先制的に抽象化すべきではないと思います。それはトレードオフを意識することです。


もう1つ、埋め込みアプローチは継承に少し似ていますよね?埋め込まれた構造体が持つフィールドとメソッドを取得し、そのインターフェイスを使用して、インターフェイスのセットを再実装することなく、任意のスーパー構造体が修飾されるようにすることができます。
Matt Mc

ええ-他の言語の仮想継承によく似ています。埋め込みを使用して、ゲッターとセッターのどちらで定義されていても、データへのポインターで定義されていても、インターフェースを実装できます(または、小さな構造体への読み取り専用アクセスの3番目のオプション、構造体のコピー)。
twotwotwo 2014

私は言わなければならない、これは私に1999年までのフラッシュバックを与え、Javaで定型的なゲッターとセッターの連を書くことを学ぶ。
トム

Go独自の標準ライブラリがこれを常に実行するとは限りません。ユニットテストのためにos.Processの呼び出しをモックアウトしようとしている最中です。Pidメンバー変数は直接アクセスされ、Goインターフェースはメンバー変数をサポートしていないため、インターフェースでプロセスオブジェクトをラップすることはできません。
Alex Jansen

1
@トムそれは本当だ。ゲッター/セッターはポインターを公開するよりも柔軟性が増すと思いますが、誰がすべてをゲッター/セッター化する必要があるとは思いません(または、典型的なGoスタイルと一致します)。私は以前、いくつかの言葉を身振りで示していましたが、それをより強調するために最初と最後を修正しました。
twotwotwo

2

私が正しく理解している場合は、1つの構造体フィールドを別の構造体フィールドに入力する必要があります。拡張にインターフェースを使用しないという私の意見。次のアプローチで簡単にできます。

package main

import (
    "fmt"
)

type Person struct {
    Name        string
    Age         int
    Citizenship string
}

type Bob struct {
    SSN string
    Person
}

func main() {
    bob := &Bob{}

    bob.Name = "Bob"
    bob.Age = 15
    bob.Citizenship = "US"

    bob.SSN = "BobSecret"

    fmt.Printf("%+v", bob)
}

https://play.golang.org/p/aBJ5fq3uXtt

宣言に注意PersonしてくださいBob。これにより、含まれている構造体フィールドがBob、構文糖質を使用して直接構造体で使用できるようになります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.