組み込み

Goは典型的な型駆動のサブクラス化の概念を提供しませんが、構造体やインターフェース内に型を組み込むことによって実装の一部を「借用」する機能があります。

インターフェースの組み込みは非常にシンプルです。これまでにio.Readerio.Writerインターフェースについて言及しました。これらの定義は次のとおりです。

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

ioパッケージは、そのようなメソッドのいくつかを実装できるオブジェクトを指定する他のインターフェースもエクスポートしています。たとえば、ReadWriteの両方を含むio.ReadWriterがあります。2つのメソッドを明示的にリストしてio.ReadWriterを指定することもできますが、2つのインターフェースを組み込んで新しいものを形成する方が簡単で、より示唆的です:

// ReadWriterは、ReaderとWriterインターフェースを組み合わせるインターフェースです。
type ReadWriter interface {
    Reader
    Writer
}

これは見た目通りの意味です:ReadWriterReaderができることとWriterができることの両方ができます。組み込まれたインターフェースの結合です。インターフェース内に組み込めるのはインターフェースのみです。

同じ基本的なアイデアが構造体に適用されますが、より広範囲にわたる影響があります。bufioパッケージには2つの構造体型、bufio.Readerbufio.Writerがあり、それぞれが当然、パッケージioの類似のインターフェースを実装しています。そしてbufioは、組み込みを使用してリーダーとライターを1つの構造体に結合することで、バッファ付きリーダー/ライターも実装しています:構造体内に型をリストしますが、フィールド名は与えません。

// ReadWriterは、ReaderとWriterへのポインタを格納します。
// io.ReadWriterを実装します。
type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

組み込まれた要素は構造体へのポインタであり、使用前に有効な構造体を指すように初期化される必要があります。ReadWriter構造体は次のように書くこともできます:

type ReadWriter struct {
    reader *Reader
    writer *Writer
}

しかし、フィールドのメソッドを昇格させ、ioインターフェースを満たすためには、次のような転送メソッドも提供する必要があります:

func (rw *ReadWriter) Read(p []byte) (n int, err error) {
    return rw.reader.Read(p)
}

構造体を直接組み込むことで、この簿記を回避できます。組み込まれた型のメソッドは無料で付いてきます。つまり、bufio.ReadWriterbufio.Readerbufio.Writerのメソッドを持つだけでなく、3つのインターフェース:io.Readerio.Writerio.ReadWriterも満たします。

組み込みがサブクラス化と異なる重要な方法があります。型を組み込むと、その型のメソッドが外部型のメソッドになりますが、呼び出されるときのレシーバーは内部型であり、外部型ではありません。この例では、bufio.ReadWriterReadメソッドが呼び出されたとき、上で書いた転送メソッドとまったく同じ効果があります。レシーバーはReadWriter自身ではなく、ReadWriterreaderフィールドです。

組み込みは単純な便利さでもあります。この例は、通常の名前付きフィールドと並んで組み込まれたフィールドを示しています。

type Job struct {
    Command string
    *log.Logger
}

Job型は今度は*log.LoggerPrintPrintfPrintlnおよびその他のメソッドを持ちます。Loggerにフィールド名を与えることもできますが、そうする必要はありません。そして今、初期化後、Jobにログを記録できます:

job.Println("starting now...")

LoggerJob構造体の通常のフィールドなので、Jobのコンストラクタ内で通常の方法で初期化できます:

func NewJob(command string, logger *log.Logger) *Job {
    return &Job{command, logger}
}

または複合リテラルで:

job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}

組み込まれたフィールドを直接参照する必要がある場合、パッケージ修飾子を無視した、フィールドの型名がフィールド名として機能します。ReadWriter構造体のReadメソッドで行ったように。Job変数job*log.Loggerにアクセスする必要がある場合は、job.Loggerと書きます。これはLoggerのメソッドを改良したい場合に有用です。

func (job *Job) Printf(format string, args ...interface{}) {
    job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}

型を組み込むと名前の競合の問題が発生しますが、それを解決するルールは簡単です。まず、フィールドまたはメソッドXは、型のより深くネストされた部分の他の項目Xを隠します。log.LoggerCommandと呼ばれるフィールドまたはメソッドを含んでいた場合、JobCommandフィールドがそれを支配します。

次に、同じネストレベルで同じ名前が現れる場合、通常はエラーです。Job構造体がLoggerという別のフィールドまたはメソッドを含んでいた場合、log.Loggerを組み込むのは誤りです。ただし、重複する名前が型定義の外側でプログラム内で言及されない場合は問題ありません。この資格は、外部から組み込まれた型に対して行われた変更に対する保護を提供します。他のサブタイプの別のフィールドと競合するフィールドが追加されても、どちらのフィールドも使用されなければ問題ありません。