はじめに
12/1から3日連続で、Go初心者向けのやさしい記事を公開しています。
今日のテーマはインターフェースです。
Go初心者にとって理解が難しいインターフェースをなるべく簡潔に分かりやすくまとめました。
ある程度の規模のシステム開発やそれなりに有名なライブラリ、組み込みパッケージであれば、至る所でインターフェースが活用されていますので、めちゃめちゃ重要です。ぜひマスターしましょう!
インターフェースの基本
インターフェースの定義
interface
を使って定義します。
interface内ではシグネチャと呼ばれる「メソッド名」と「引数の型」と「返り値の型」だけを宣言し、中身の実装はしません。
type <interface名> interface {
<メソッド名>(引数の型, ...) (返り値の型, ...)
}
また、メソッドが1つだけの場合は、インターフェース名は メソッド名 + er
とすることが多いみたいです。
具体例としては組み込みのStringer。
type Stringer interface {
String() string
}
インターフェースの実装
GoではJavaのように implements
は用意されていません。
代わりにダックタイピングでインターフェースを実装します。
ダックタイピングとは、動的型付けのオブジェクト指向言語でよく使われる型付け方法です。Goは静的言語なのですが、このダックタイピングを実現できます。
出典: フリー百科事典『ウィキペディア(Wikipedia)』
オブジェクトがあるインタフェースのすべてのメソッドを持っているならば、たとえそのクラスがそのインタフェースを宣言的に実装していなくとも、オブジェクトはそのインタフェースを実行時に実装しているとみなせる、ということである。
もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルである。
Goにはクラスが無いため構造体に置き換えると、「ある構造体があるインターフェースの持つ全てのメソッドを実装していれば、その構造体はインターフェースを満たしていると言える。」といったところでしょうか。
インターフェースがあると何が嬉しいか
インターフェースがあると何がいいのでしょうか。
それは、型の柔軟性の恩恵を受けられることです。
ポリモーフィズム
ポリモーフィズムとは、抽象化により汎用的な実装を可能にする方法です。
これにより、例えば1つのメソッド名で異なる処理をもたせることができます。
具体例を見ていきましょう!
AnimalインターフェースでRunメソッドとCryメソッドのシグネチャを宣言しています。
構造体のCatとHumanがそれぞれRunメソッドとCryメソッドの実体を実装しています。
package main
import (
"fmt"
"strconv"
)
type Animal interface {
Run(int) string
Cry() string
}
type Cat struct {
Name string
}
type Human struct {
Name string
Job string
}
func (c *Cat) Cry() string {
return fmt.Sprintf(c.Name + "ちゃんはお腹が減っているので泣きます。")
}
func (h *Human) Cry() string {
return fmt.Sprintf(h.Name + "さんは" + h.Job + "の仕事が辛いので泣きます。")
}
func (c *Cat) Run(speed int) string {
return fmt.Sprintf(c.Name + "ちゃんは獲物を追う時に時速" + strconv.Itoa(speed) + "kmで走ります。")
}
func (h *Human) Run(speed int) string {
return fmt.Sprintf(h.Name + "さんはマラソンで時速" + strconv.Itoa(speed) + "kmで走ります。")
}
func main() {
// 初期化
cat := &Cat{Name: "クー"}
human := &Human{Name: "田中", Job: "ソフトウェアエンジニア"}
// メソッド呼び出し
fmt.Println(cat.Cry()) // クーちゃんはお腹が減っているので泣きます。
fmt.Println(human.Cry()) // 田中さんはソフトウェアエンジニアの仕事が辛いので泣きます。
fmt.Println(cat.Run(30)) // クーちゃんは獲物を追う時に時速30kmで走ります。
fmt.Println(human.Run(10)) // 田中さんはマラソンで時速10kmで走ります。
}
上述の実行結果から分かる通り、RunメソッドとCryメソッドはそれぞれが同一のメソッド名でありながら、レシーバによって動作が変わっています。つまり、ダックタイピングでインターフェースを実装し、ポリモーフィズムの実現できることが分かります。
また、異なる構造体同士を同じインターフェース型として扱えます。
上の例を引き続き使います。
下記の通り、構造体のCatとHumanはどちらもインターフェースAnimalを満たすため、同じインターフェースのAnimal型としてまとめることができます。
つまり、全く異なる構造体を1つの同じインターフェース型として扱うことができるのです。
これはコードの見通しを良くするのに役立ちます。
func main() {
// 初期化
cat := &Cat{Name: "クー"}
human := &Human{Name: "田中", Job: "ソフトウェアエンジニア"}
// 異なる構造体をインターフェース単位でまとめられる
animals := []Animal {
cat,
human,
}
for _, a := range animals {
fmt.Println(a.Cry())
}
}
また、関数の引数をインターフェースで定義すると、そのインターフェースさえ満たしていればどんな構造体も引数として渡すことができます。
このテクニックは、汎用的な関数を定義するうえでかなり役立つテクニックです。
例えば、httptestパッケージに良い例があります。
http.NewServer関数はhttp.Handlerインターフェースを引数に持ちます。
http.HandlerインターフェースはServeHTTPメソッドのシグネチャを持っており、構造体ServeMuxはこれを実装しているため、http.Handlerインターフェースを満たしてます。
したがって、NewServer関数には構造体ServeMuxを渡すことができます。
func NewServer(handler http.Handler) *Server {
ts := NewUnstartedServer(handler)
ts.Start()
return ts
}
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
hosts bool // whether any patterns contain hostnames
}
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
これは、ポリモーフィズムによる恩恵です。
何でも受け入れる型
もしメソッドを持たない空のインターフェースがあるとしたら、何の型をそのインターフェース型として扱えるのでしょうか?
答えは全ての型です!
何もメソッドを持っていないということは、何でもこのinterfaceを満たすことができるからです。
具体例を見ていきましょう!
interface{}型の変数animalを宣言します。
この変数animalにはstring型も入りますし、int型も入ります。
ある変数に色々な型の値を代入できるようになります。
型に厳密である静的言語のGoに、型の柔軟性を持たせることができるのです。
func main() {
var animal interface{} // 空のインターフェース
animal = "cat"
fmt.Println(animal) // cat
fmt.Printf("%T", animal) // string
fmt.Println()
animal = 1
fmt.Println(animal) // 1
fmt.Printf("%T", animal) // int
}
型アサーション
型アサーションとは、変数の型をチェックして、値を取り出す機能です。
interface{}型の変数に格納した元の値の型をチェックできます。
10はint型なのでintの型アサーションには成功しますが、float32の型アサーションには失敗します。
func main() {
var a interface{} = 10
i := a.(int)
fmt.Println(i) // 10
f := a.(float32) // panic: interface conversion: interface {} is int, not float32
fmt.Println(f)
}
上記の書き方では、panicエラーの可能性があるため、そうならないために、2つの変数に代入する記法があります。
1つ目の変数には値、2つ目の値にはboolで型アサーションの結果が格納されます。
func main() {
var a interface{} = 10
i, ok := a.(int)
fmt.Println(i, ok) // 10 true
f, ok := a.(float32)
fmt.Println(f, ok) // 0 false
}
型アサーションは何が嬉しいのか?
では、この型アサーションは何に使えるのでしょうか?
それは、**型に応じた分岐処理(型スイッチ)**の実現です。
事前に型情報が不明なものに、実行時に型に応じた処理をさせることができます。
↓はswitchで型ごとにcase文を書いて分岐させる例です。
func main() {
var a interface{} = 10
switch a.(type) {
case int:
fmt.Println("This is int.")
case float32:
fmt.Println("This is float.")
case string:
fmt.Println("This is string.")
default:
fmt.Println("This is unknown type.")
}
}
This is int.
ただし、扱う値の型が完全不明で、全ての型に柔軟に対応するというのはなかなか難しい部分もあります。
case文の数が膨大になるのもそうですが、mapの場合は、完全な型でなければcase文に書けないからです。
こういう場合は、reflectパッケージを使うことで解決できますが、それは別の話ということで。
また、型を柔軟に扱えるようにする分、処理速度が落ちることは覚悟しましょう。
(reflectを使うと一般的により遅くなります。)
まとめ
- インターフェースで何が嬉しいの?
- ポリモーフィズムを実現できることで柔軟な実装を実現できるよー
- 型に柔軟性を持たせることができるよー
参考資料
最後に
明日は、【Go初心者向けのやさしい記事】埋め込みによる委譲を5分で学ぼうという記事です。
お楽しみに!