自分がインタフェースについて理解したことをまとめます。僕と同じ初学者の理解を手助けできればうれしいです。Gopher道場が提供してくれている動画やスライド、書籍「プログラミング言語Go」を参考にしています。
最初にこの記事の結論
以下のようになりました。タイトルの通りちょっとしかわかりませんでした。
インタフェースの使い方はちょっと分かったけど、メリットを実感できる使い方まではわからなかった。
はじめに用語を整理する
最初に用語を整理します。現時点は「へえ」くらいの温度感でいいと思うのでざっくり意味を理解することが大切だと思います。
メソッド | レシーバと紐づけられた関数 |
---|---|
インタフェース | 抽象化を実現するメソッドの集まり |
前段階にメソッドの理解を深める
本題はインタフェースの理解を深めることですが、前段階としてメソッドの理解を深める必要があると感じています。以下はメソッドを使用した簡単なコードです。一緒に見ていきましょう。
package main
import (
"fmt"
)
type Hoge string
func (h Hoge) String() string {
return string(h)
}
func main() {
var hoge Hoge = "Hello World"
fmt.Println(hoge.String())
}
https://go.dev/play/p/_Gs0R88968T
ざっくり解説すると、 Hoge
型の変数 hoge
に”Hello World”を代入してレシーバ h
が”Hello World”を受け取り、メソッド String()
を呼び出し戻り値の String(h)
を Println
の引数に渡しています。
とりあえず動くコードを書きましたが、メソッドの戻り値が以下でないことに疑問を持ちます。「 h
は文字列の”Hello World”を受け取るからstring型では?文字列をstring型でキャストするって無意味では?」と思いました。
return h
https://go.dev/play/p/j8u6Rk2zE0s
しかし実行するとエラーになります。cannot use h (type Hoge) as type string in return argument
とメッセージがでて、h
はstring型ではなく Hoge
型ですと注意されます。「なるほど、だから h
はキャストして string(h)
とする必要があるのか」と理解できました。
本題のインタフェースを理解していく
続いて本題のインタフェースに触れていきます。先ほどと同様にコードを一緒に見ていきましょう。
package main
import "fmt"
type Hoger interface {
String() string
}
type Hoge string
func (h Hoge) String() string {
return string(h)
}
func main() {
var hoge Hoger = Hoge("Hello World")
fmt.Println(hoge.String())
}
https://go.dev/play/p/M6rAuEbm4rb
このインタフェースを使用したコードは、型 Hoge
はインタフェース Hoger
を実装していると言えます。様々な場面でこの表現を耳にします。しかし僕はこの言葉がすんなり入ってきませんでした。僕と同じような感覚の方は多いのかな?と思っています。僕が思うにここで詰まる原因はインタフェースの概念について理解が足りてないのかなと思います。なので一緒に一つずつ理解していきましょう。
☆少し脱線 | 少し概念を理解する抽象的な話
ここからは概念について抽象的な話をしていきます。以下はメソッドのイメージ図を書いてみました。通常は関係者同士でやり取りをします。
しかしインタフェースを利用すると少し異なります。以下fig2はインタフェースを使用したイメージ図です。直接やり取りをせずインタフェースを介して必要な情報を渡します。これにより抽象化を実現できます。抽象化を実現することがインタフェースの役割と理解しています。
インタフェースを介することにどのようなメリットがあるかというと、①結果が欲しい人は②結果を返す人を意識する必要がないことです。僕のイメージ的には**インタフェースは「上手い感じに取り次いでくれる優しい人」**です。これにより①結果が欲しい人は、「良しなにお願いしますね。」とできます。これがインタフェースを利用する目的であり、インタフェースが必要な理由だと理解しています。
☆話を戻します
話を戻して先ほどのコードを一緒に見ていきましょう。インタフェースを実装することで追加した箇所、変更した箇所に注目していきます。
https://go.dev/play/p/M6rAuEbm4rb
type Hoger interface {
String() string
}
ここでは、インタフェースはメソッドの集まりと認識することが大切だと感じます。**『型 Hoge
はインタフェース Hoger
を実装している』**は以下fig3に注目すると理解が進む気がします。ここで定義されているメソッドのレシーバ h
は Hoge
型です。
ただしインタフェースを実装することで恩恵を受ける(言い換えるとメリットがある)のは①結果が欲しい人となります。ここがモヤっとする箇所な気がします。
次はレシーバ h
で”Hello World”を受け取る箇所の書き方が異なります。
var hoge Hoger = Hoge("Hello World")
ぱっと見、Hoge
メソッドに文字列を渡していると感じませんか?僕が勘違いしていた。。。正確には”Hello World”を Hoge
型にキャストしています。(そもそも Hoge
メソッドなんてありませんよね。)しかしここで疑問が生じます。「 Hoge
型でキャストするってどうゆうこと?」と思いました。なのでキャストを解除してみました。
var hoge Hoger = "Hello World"
cannot use "Hello World" (type string) as type Hoger in assignment:
とエラーメッセージが出ます。”Hello World”は Hoger
型ではありませんと注意されます。わかりましたと。いまれた通り Hoger
でキャストしてあげます。しかし次は別のエラーが出ます。
var hoge Hoger = Hoger("Hello World")
cannot convert "Hello World" (type string) to type Hoger:
とメッセージは、 Hoger
型にキャストできませんと言われます。は?注意された通り直したよね?となりましたが、実はスルーしていたエラーメッセージの2行目を見てみます。string does not implement Hoger (missing String method)
と出ています。DeepLの翻訳をそのまま書くと、「string が Hoger を実装していない(String メソッドがない)。」となります。
少し立ち返ると、ここではレシーバ h
に”Hello World”を渡したいのです。なのでレシーバの型である Hoge
でキャストしてあげる必要がありました。**『型 Hoge
はインタフェース Hoger
を実装している』**ので Hoger
型を Hoge
型にキャストすることが可能です。
なんとなくわかったけど結局、、、うーんわからん
ここまで来てまたひとつ疑問が生じました。「結局①結果が欲しい人のメリットは何?」と思いました。コードを色々いじってみます。
変数 hoge
で複数のメソッドを呼ぶことができる
package main
import "fmt"
type Hoger interface {
String() string
Fuga() string
}
type Hoge string
func (h Hoge) String() string {
return string(h)
}
func (h Hoge) Fuga() string {
return string(h)
}
func main() {
var hoge Hoger = Hoge("Hello World")
fmt.Println(hoge.String())
fmt.Println(hoge.Fuga())
}
https://go.dev/play/p/fYVTr1A_sf3
このようにインタフェースにメソッド Fuga()
を追加することで変数 hoge
でメソッドを呼ぶことができます。でも正直、便利なのか?という感覚になりました。感覚的にはインタフェースの役割が伝書鳩(ただ取り次ぐだけで価値を提供しない人)に感じます。
構造体(コンポジット型)をレシーバ h
に受け渡す
package main
import "fmt"
type Hoger interface {
String() string
Fuga() string
Piyo() int
}
type Hoge struct {
A string
B string
C int
}
func (h Hoge) String() string {
return string(h.A)
}
func (h Hoge) Fuga() string {
return string(h.B)
}
func (h Hoge) Piyo() int {
return int(h.C)
}
func main() {
var hoge Hoger = Hoge{A: "Hello World", B: "ennyu", C: 123}
fmt.Println(hoge.String())
fmt.Println(hoge.Fuga())
fmt.Println(hoge.Piyo())
}
https://go.dev/play/p/Bc1BgXXroEd
これは少し便利かな?と思いましたが、関数で良くない?となりました。
package main
import "fmt"
type Hoge struct {
A string
B string
C int
}
func String(ret Hoge) string {
return string(ret.A)
}
func main() {
var hoge Hoge = Hoge{A: "Hello World", B: "ennyu", C: 123}
fmt.Println(String(hoge))
}
https://go.dev/play/p/MsnpqEyUUFQ
結論
インタフェースの使い方はちょっと分かったけど、メリットを実感できる使い方まではわからなかった。
まとめ
チョットだけ理解できたと思いますが、インタフェースを使いこなすまでの理解はできませんでした。ただコードを読むための力はついたはずなので、標準パッケージを読み込むなどで理解度を向上させて使いこなしたいです。
余談
わからない構文は自分で書いてみて試行錯誤するのが大切だなと感じました。