Goではとても繊細なinterfaceと呼ぶべき設計があります。これはオブジェクト指向と内容構成にとって非常に便利です。この章を終わった時にはあなたはinterfaceの巧妙な設計に感服することでしょう。
簡単にいえば、interfaceはmethodの組み合わせです。interfaceを通してオブジェクトの振る舞いを定義することができます。
前の章の最後の例でStudentとEmployeeはどちらもSayHiを持っていました。彼らの内部処理は異なりますが、それは重要ではありません。重要なのは彼らがどちらもsay hi
と言えることです。
続けてさらに拡張していきましょう。StudentとEmployeeで他のメソッドSing
を実現します。その後StudentはBorrowMoneyメソッドを追加してEmployeeはSpendSalaryを追加しましょう。
Studentには3つのメソッドがあることになります:SayHi、Sing、BorrowMoneyです。EmployeeはSayHi、Sing、SpendSalaryです。
上のような組み合わせをinterface(オブジェクトStudentとEmployeeに追加されます)と言います。例えばStudentとEmployeeでどちらもinterfaceであるSayHiとSingを実装します。この2つのオブジェクトはこのinterface型です。EmployeeはこのinterfaceであるSayHi、SingとBorrowMoneyは実装しません。EmployeeはBorrowMoneyメソッドを実装しないからです。
interface型ではメソッドのセットを定義します。もしあるオブジェクトがインターフェースとなるすべてのメソッドを実装するとしたら、このオブジェクトはこのインターフェースを実装することになります。細かい文法は下の例を参考にしてください。
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名フィールドHuman
school string
loan float32
}
type Employee struct {
Human //匿名フィールドHuman
company string
money float32
}
//HumanオブジェクトにSayHiメソッドを実装します。
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
// HumanオブジェクトにSingメソッドを実装します。
func (h *Human) Sing(lyrics string) {
fmt.Println("La la, la la la, la la la la la...", lyrics)
}
//HumanメソッドにGuzzleメソッドを実装します。
func (h *Human) Guzzle(beerStein string) {
fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}
// EmployeeはHumanのSayHiメソッドをオーバーロードします。
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //この行は複数に渡ってもかまいません。
}
//StudentはBorrowMoneyメソッドを実装します。
func (s *Student) BorrowMoney(amount float32) {
s.loan += amount // (again and again and...)
}
//EmployeeはSpendSalaryメソッドを実装します。
func (e *Employee) SpendSalary(amount float32) {
e.money -= amount // More vodka please!!! Get me through the day!
}
// interfaceを定義します。
type Men interface {
SayHi()
Sing(lyrics string)
Guzzle(beerStein string)
}
type YoungChap interface {
SayHi()
Sing(song string)
BorrowMoney(amount float32)
}
type ElderlyGent interface {
SayHi()
Sing(song string)
SpendSalary(amount float32)
}
上のコードを通して、interfaceは任意のオブジェクトで実装できることがわかるかと思います。上のMen interfaceはHuman、Student及びEmployeeによって実装されます。例えばStudentはMenとYoungChapの2つのinterfaceを実装することになります。
最後に、任意の型は空のinterface(ここではinterface{}と定義しましょう)を実装しています。これには0個のメソッドが含まれるinterfaceです。
では、interfaceの中には一体どのような値が存在しているのでしょうか。もし我々がinterfaceの変数を定義すると、この変数にはこのinterfaceの任意の型のオブジェクトを保存することができます。上の例でいえば、我々はMen interface型の変数mを定義しました。このmにはHuman、StudentまたはEmployeeの値を保存できます。
mは3つの型を持つことのできるオブジェクトなので、Men型の要素を含むsliceを定義することができます。このsliceはMenインターフェースの任意の構造のオブジェクトを代入することができます。このsliceともともとのsliceにはいくつか違いがあります。
次の例を見てみましょう。
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名フィールド
school string
loan float32
}
type Employee struct {
Human //匿名フィールド
company string
money float32
}
//HumanにSayHiメソッドを実装します。
func (h Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//HumanにSingメソッドを実装します。
func (h Human) Sing(lyrics string) {
fmt.Println("La la la la...", lyrics)
}
//EmployeeはHumanのSayHiメソッドをオーバーロードします。
func (e Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone)
}
// Interface MenはHuman,StudentおよびEmployeeに実装されます。
// この3つの型はこの2つのメソッドを実装するからです。
type Men interface {
SayHi()
Sing(lyrics string)
}
func main() {
mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000}
//Men型の変数iを定義します。
var i Men
//iにはStudentを保存できます。
i = mike
fmt.Println("This is Mike, a Student:")
i.SayHi()
i.Sing("November rain")
//iにはEmployeeを保存することもできます。
i = tom
fmt.Println("This is Tom, an Employee:")
i.SayHi()
i.Sing("Born to be wild")
//sliceのMenを定義します。
fmt.Println("Let's use a slice of Men and see what happens")
x := make([]Men, 3)
//この3つはどれも異なる要素ですが、同じインターフェースを実装しています。
x[0], x[1], x[2] = paul, sam, mike
for _, value := range x{
value.SayHi()
}
}
上のコードで、interfaceはメソッドの集合を抽象化したものだとお分かりいただけるとおもいます。他のinterfaceでない型によって実装されなければならず、自分自身では実装することができません。Goはinterfaceを通してduck-typingを実現できます。すなわち、"鳥の走る様子も泳ぐ様子も鳴く声もカモのようであれば、この鳥をカモであると呼ぶことができる"わけです。
空のinterface(interface{})にはなんのメソッドも含まれていません。この通り、すべての型は空のinterfaceを実装しています。空のinterfaceはそれ自体はなんの意味もありません(何のメソッドも含まれていませんから)が、任意の型の数値を保存する際にはかなり役にたちます。これはあらゆる型の数値を保存することができるため、C言語のvoid*型に似ています。
// aを空のインターフェースとして定義
var a interface{}
var i int = 5
s := "Hello world"
// aは任意の型の数値を保存できます。
a = i
a = s
ある関数がinterface{}を引数にとると、任意の型の値を引数にとることができます。もし関数がinterface{}を返せば、任意の型の値を返すことができるのです。とても便利ですね!
interfaceの変数はこのinterface型のオブジェクトを持つことができます。これにより、関数(メソッドを含む)を書く場合思いもよらない思考を与えてくれます。interface引数を定義することで、関数にあらゆる型の引数を受けさせることができるです。
例をあげましょう:fmt.Printlnは私達がもっともよく使う関数です。ですが、任意の型のデータを受けることができる点に気づきましたか。fmtのソースファイルを開くとこのような定義が書かれています:
type Stringer interface {
String() string
}
つまり、Stringメソッドを持つ全ての型がfmt.Printlnによってコールされることができるのです。ためしてみましょう。
package main
import (
"fmt"
"strconv"
)
type Human struct {
name string
age int
phone string
}
// このメソッドを使ってHumanにfmt.Stringerを実装します。
func (h Human) String() string {
return "❰"+h.name+" - "+strconv.Itoa(h.age)+" years - ✆ " +h.phone+"❱"
}
func main() {
Bob := Human{"Bob", 39, "000-7777-XXX"}
fmt.Println("This Human is : ", Bob)
}
前のBoxの例を思い出してみましょう。Color構造体もメソッドを一つ定義しています:String。実はこれもfmt.Stringerというinterfaceを実装しているのです。つまり、もしある型をfmtパッケージで特殊な形式で出力させようとした場合Stringerインターフェースを実装する必要があります。もしこのインターフェースを実装していなければ、fmtはデフォルトの方法で出力を行います。
//同じ機能を実装します。
fmt.Println("The biggest one is", boxes.BiggestsColor().String())
fmt.Println("The biggest one is", boxes.BiggestsColor())
注:errorインターフェースのオブジェクト(Error() stringのオブジェクトを実装)を実装します。fmtを使って出力を行う場合、Error()メソッドがコールされます。そのため、String()メソッドを再定義する必要はありません。
interfaceの変数の中にはあらゆる型の数値を保存できることを学びました(この型はinterfaceを実装しています)。では、この変数に実際に保存されているのはどの型のオブジェクトであるかどのように逆に知ることができるのでしょうか?現在二種類の方法があります:
-
Comma-okアサーション
Go言語の文法では、ある変数がどの型か直接判断する方法があります: value, ok = element.(T), ここでvalueは変数の値を指しています。okはbool型です。elementはinterface変数です。Tはアサーションの型です。
もしelementにT型の数値が存在していれば、okにはtrueが返されます。さもなければfalseが返ります。
例を見ながら詳しく理解していきましょう。
package main import ( "fmt" "strconv" ) type Element interface{} type List [] Element type Person struct { name string age int } //Stringメソッドを定義します。fmt.Stringerを実装します。 func (p Person) String() string { return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)" } func main() { list := make(List, 3) list[0] = 1 // an int list[1] = "Hello" // a string list[2] = Person{"Dennis", 70} for index, element := range list { if value, ok := element.(int); ok { fmt.Printf("list[%d] is an int and its value is %d\n", index, value) } else if value, ok := element.(string); ok { fmt.Printf("list[%d] is a string and its value is %s\n", index, value) } else if value, ok := element.(Person); ok { fmt.Printf("list[%d] is a Person and its value is %s\n", index, value) } else { fmt.Println("list[%d] is of a different type", index) } } }
とても簡単ですね。前のフローの項目の際にご紹介したとおり、いくつもifの中で変数の初期化が許されているのにお気づきでしょうか。
また、アサーションの型が増えれば増えるほど、ifelseの数も増えるのにお気づきかもしれません。下ではswitchをご紹介します。
-
switchテスト
コードの例をお見せしたほうが早いでしょう。上の実装をもう一度書きなおしてみます。
package main import ( "fmt" "strconv" ) type Element interface{} type List [] Element type Person struct { name string age int } //プリント func (p Person) String() string { return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)" } func main() { list := make(List, 3) list[0] = 1 //an int list[1] = "Hello" //a string list[2] = Person{"Dennis", 70} for index, element := range list{ switch value := element.(type) { case int: fmt.Printf("list[%d] is an int and its value is %d\n", index, value) case string: fmt.Printf("list[%d] is a string and its value is %s\n", index, value) case Person: fmt.Printf("list[%d] is a Person and its value is %s\n", index, value) default: fmt.Println("list[%d] is of a different type", index) } } }
ここで強調したいのは、
element.(type)
という文法はswitchの外のロジックで使用できないということです。もしswitchの外で型を判断したい場合はcomma-ok
を使ってください。
Goが本当に魅力的なのはビルトインのロジック文法です。Structを学んだ際の匿名フィールドはあんなにもエレガントでした。では同じようなロジックをinterfaceに導入すればより完璧になります。もしinterface1がinterface2の組み込みフィールドであれば、interface2は暗黙的にinterface1のメソッドを含むことになります。
ソースパッケージのcontainer/heapの中にこのような定義があるのを確認できると思います。
type Interface interface {
sort.Interface //組み込みフィールドsort.Interface
Push(x interface{}) //a Push method to push elements into the heap
Pop() interface{} //a Pop elements that pops elements from the heap
}
sort.Interfaceは実は組み込みフィールドです。sort.Interfaceのすべてのメソッドを暗黙的に含んでいます。つまり以下の3つのメソッドです。
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less returns whether the element with index i should sort
// before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}
もう一つの例はioパッケージの中にある io.ReadWriterです。この中にはioパッケージのReaderとWriterの2つのinterfaceを含んでいます:
// io.ReadWriter
type ReadWriter interface {
Reader
Writer
}
Goはリフレクションを実装しています。リフレクションはプログラムの実行時の状態を検査することができます。私達が一般的に使用しているパッケージはreflectパッケージです。どのようにreflectパッケージを使うかはオフィシャルのドキュメントに詳細な原理が説明されています。laws of reflection
reflectを使うには3つのステップに分けられます。下で簡単にご説明します:リフレクションは型の値(これらの値はすべて空のインターフェースを実装しています。)。まずこれをreflectオブジェクトに変換する必要があります(reflect.Typeまたはreflect.Valueです。異なる状況によって異なる関数をコールします。)この2つを取得する方法は:
t := reflect.TypeOf(i) //元データを取得します。tを通して型定義の中のすべての要素を取得することができます。
v := reflect.ValueOf(i) //実際の値を取得します。vを通して保存されている中の値を取得することができます。値を変更することもできます。
reflectオブジェクトに変換した後、何かしらの操作を行うことができます。つまり、reflectオブジェクトを対応する値に変換するのです。例えば
tag := t.Elem().Field(0).Tag //structの中で定義されているタグを取得する。
name := v.Elem().Field(0).String() //はじめのフィールドに保存されている値を取得する。
reflectの値を取得することで対応する型と数値を返すことができます。
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
最後にリフレクションを行ったフィールドは修正できる必要があります。前で学んだ値渡しと参照渡しも同じ道理です。リフレクションのフィールドが必ず読み書きできるということは、以下のように書いた場合、エラーが発生するということです。
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)
もし対応する値を変更したい場合、このように書かなければなりません。
var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)
上はリフレクションに対する簡単なご説明ではありますが、より深い理解には実際のプログラミングで実践していく他ありません。