組み込み
Goは典型的な型駆動のサブクラス化の概念を提供しませんが、構造体やインターフェース内に型を組み込むことによって実装の一部を「借用」する機能があります。
インターフェースの組み込みは非常にシンプルです。これまでにio.Reader
とio.Writer
インターフェースについて言及しました。これらの定義は次のとおりです。
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
io
パッケージは、そのようなメソッドのいくつかを実装できるオブジェクトを指定する他のインターフェースもエクスポートしています。たとえば、Read
とWrite
の両方を含むio.ReadWriter
があります。2つのメソッドを明示的にリストしてio.ReadWriter
を指定することもできますが、2つのインターフェースを組み込んで新しいものを形成する方が簡単で、より示唆的です:
// ReadWriterは、ReaderとWriterインターフェースを組み合わせるインターフェースです。
type ReadWriter interface {
Reader
Writer
}
これは見た目通りの意味です:ReadWriter
はReader
ができることとWriter
ができることの両方ができます。組み込まれたインターフェースの結合です。インターフェース内に組み込めるのはインターフェースのみです。
同じ基本的なアイデアが構造体に適用されますが、より広範囲にわたる影響があります。bufio
パッケージには2つの構造体型、bufio.Reader
とbufio.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.ReadWriter
はbufio.Reader
とbufio.Writer
のメソッドを持つだけでなく、3つのインターフェース:io.Reader
、io.Writer
、io.ReadWriter
も満たします。
組み込みがサブクラス化と異なる重要な方法があります。型を組み込むと、その型のメソッドが外部型のメソッドになりますが、呼び出されるときのレシーバーは内部型であり、外部型ではありません。この例では、bufio.ReadWriter
のRead
メソッドが呼び出されたとき、上で書いた転送メソッドとまったく同じ効果があります。レシーバーはReadWriter
自身ではなく、ReadWriter
のreader
フィールドです。
組み込みは単純な便利さでもあります。この例は、通常の名前付きフィールドと並んで組み込まれたフィールドを示しています。
type Job struct {
Command string
*log.Logger
}
Job
型は今度は*log.Logger
のPrint
、Printf
、Println
およびその他のメソッドを持ちます。Logger
にフィールド名を与えることもできますが、そうする必要はありません。そして今、初期化後、Job
にログを記録できます:
job.Println("starting now...")
Logger
はJob
構造体の通常のフィールドなので、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.Logger
がCommand
と呼ばれるフィールドまたはメソッドを含んでいた場合、Job
のCommand
フィールドがそれを支配します。
次に、同じネストレベルで同じ名前が現れる場合、通常はエラーです。Job
構造体がLogger
という別のフィールドまたはメソッドを含んでいた場合、log.Logger
を組み込むのは誤りです。ただし、重複する名前が型定義の外側でプログラム内で言及されない場合は問題ありません。この資格は、外部から組み込まれた型に対して行われた変更に対する保護を提供します。他のサブタイプの別のフィールドと競合するフィールドが追加されても、どちらのフィールドも使用されなければ問題ありません。