Goのinterfaceを理解しよう!
こんにちは、最近競プロが少しアツくなっているlogicaです。
先人が少ないけどGoで競プロをやるぞ!って言って、AtCoderの典型90問の簡単めなやつをやりながら、I/Oや二分探索、Union-Find木などをUtility化しています。
さて、Goを学んでいる人の中でかなりの人がつまずく部分、一番の代表はinterfaceだと思っています。
struct / sliceあたりまでは割と直感的に動きますが、interfaceがいきなり機能・挙動共に直感的じゃなくなるんですよね。
それもそのはず、interfaceはそもそも「間接的に型を扱う」ためのものなのですから、直感的に理解できるはずはないのです。
今回は、僕の所属するtraPというサークルで行われた「Webエンジニアになろう講習会」という講習会の中で出た、「interfaceとは何か」という質問に対する僕の回答をブログ化しました。
質問をしてくれた後輩には好評をいただきましたので、他の人の助けにもなれば幸いです。
はじめに
今回の対象読者は、A Tour of GoのBasicsまで終わったGoの学習者で、Methods and interfacesがよく理解できないという人です。
(interface以外の型や関数に関しては理解し、コードが読める事を前提とします)
説明をわかりやすくするために厳密な定義を用いていない部分がありますので、プロGopherの方々はどうか温かい目でご覧下さい。
型
「変数の形式」だというのはもう学ばれたことかと思います。
今回覚えてほしいことは、「型はメソッドを持つことができる」ということです。
メソッド
メソッドは、型が持つ関数のことで、{型}.{メソッド名}()
という形で呼び出すことができる特殊な関数です。
メソッドが欲しい状況
例えば、以下のようなstructの型があるとします。
type Blog struct {
title string
content string
}
タイトルと中身の文章を持っただけの、ブログ記事を表す簡単な型です。
今、この記事を表示するために「Blog
型を受け取って、中身のtitle
とcontent
を区切り線を介して繋げた文字列を出力する」関数(GetFullArticle()
)が欲しいとします。
この関数は、普通の関数として以下のように書けますね。
func GetFullArticle(b Blog) string {
return b.title + "\n" + "------------" + "\n" + b.content
}
ですが、これを単体の関数として定義するよりは、「Blog
型自体が、中身のtitle
とcontent
を区切り線を介して繋げた文字列を出力する機能を持つ」としたいと思いませんか?
メソッドを使う
この時に使えるのがメソッドです。次のように書きます。
func (b Blog) GetFullArticle() string {
return b.title + "\n" + "------------" + "\n" + b.content
}
このように定義すると、Blog
型のb
という変数があるとき、GetFullArticle(b)
(だけ)ではなくb.GetFullArticle()
という形でメソッドを呼び出すことができます。
「型に付いた機能として関数を定義する」、これがメソッドの本質だと言っていいと思います(人により様々な解釈があります)。
interface
ようやく本題、interfaceの説明に入りましょう。
interfaceは、「同じメソッド(機能)を持つ複数の型を、ひとくくりにして扱うための仕組み」です。
interfaceが欲しい状況
さあ、先ほどの例を使いましょう。先程のBlog
型と、その機能であるGetFullArticle()
メソッドを再掲します。
type Blog struct {
title string
content string
}
func (b Blog) GetFullArticle() string {
return b.title + "\n" + "------------" + "\n" + b.content
}
今、このBlog
型の亜種としてBlog2
型を定義します。
type Blog2 struct {
title string
paragraph []string
}
記事の中身を、段落のまとまりとして持つような型としました。
このBlog2
型に、先ほどのBlog
型と同じく、「中身のtitle
とcontent(ここでは記事の中身)を区切り線を介して繋げた文字列を出力する」という機能を持たせたいと思います。
func (b Blog2) GetFullArticle() string {
article := b.title + "\n" + "------------" + "\n"
for _, paragraph := range b.paragraph {
article += paragraph + "\n\n" // 段落の間は2重に改行する
}
return article
}
さてここで、Blog
型とBlog2
型について、その型の変数を受け取ってGetFullArticle()
で出力された文字列をfmt.Println()
で出力する関数をそれぞれ定義したいとします。
func DisplayBlog(b Blog) {
fmt.Println(b.GetFullArticle())
}
func DisplayBlog2(b Blog2) {
fmt.Println(b.GetFullArticle())
}
中身が全く同じであることに気づいたでしょうか。
そうなんです。同じ機能を持つ複数の型について、その機能を使う中身が全く同じ関数がいっぱいできることがあるのです。
これではコードがいっぱいになって読みにくいし、同じ機能を持つ型がまた増えた時、関数をその都度定義しなきゃいけなくなります。
どうにかして、これらの「同じ機能を持つ型」をひとくくりにして扱えないかな...
interfaceを使う
そこで、満を持してinterfaceの出番です!
interfaceは、「これらのメソッド(機能)を持っている型ならなんでも入っていいよ!」という特殊な型です。
type BlogInterface interface {
GetFullArticle() string
}
このように定義することで、BlogInterface
という型を指定した部分(引数や構造体のフィールドなど)は「GetFullArticle()
という、引数を取らずstring
型を出力するメソッドがある型は、何でも入っていいよ」という状態になります。
同じメソッドを持つ型を、ひとくくりにして扱えるわけです。
このinterfaceを使って、先ほどのDisplay~~
関数をスッキリさせてみましょう。
func DisplayBlog(b BlogInterface) {
fmt.Println(b.GetFullArticle())
}
ドーン。終了です。
これでBlog
型のb
という変数も、Blog2
型のb2
という変数も
DisplayBlog(b)
DisplayBlog(b2)
という風にDisplayBlog()
関数に渡すことができます。
interfaceが、同じメソッドを持つ複数の型をひとくくりにして扱えるという意味が分かったでしょうか?
interfaceの注意点
1つ気をつけなければいけないことは、interface型の変数を使う時メソッドしか使えなくなり、そこに当てはまる構造体のフィールドなどは使えなくなります。
例えば、DisplayBlog()
関数で、title
が無い記事は出力したくないと思ったとき、
func DisplayBlog(b BlogInterface) {
if b.title == "" { // b.titleはアウト
return
}
fmt.Println(b.GetFullArticle())
}
という書き方はできません。
interfaceは「ここに入る型は、このメソッドを持っているよ」ということしか保証してくれないからです。
別の言い方をすると、interfaceは単体としてみた時、メソッドしか持たない型であると言えます。
もしこのようにtitle
を使いたい場合、GetTitle()
のようなタイトルを取得するだけのメソッドをBlogInterface
の定義に追加して、BlogInterface
でひとくくりにされた型全てにこのメソッドを用意する必要があります。
余談
なぜ「interface」と呼ばれるのか
interfaceの言葉の意味を少し掘り下げてみましょう。
(以下はあくまで僕の理解です)
interfaceの和訳は「接触面」、大まかに言うと「間に挟まるもの」という意味の言葉です。
Goでは「interfaceでない型」と「それを使うもの(構造体・関数)」の間に挟まるのでそのような呼び方をされます。
(Go以前のプログラミング言語からある概念なので、厳密ではないです)
type Type1 struct {~}
type Type2 int
type Type3 []string // 全て、下記のinterfaceで指定されたメソッドを持つ
↓ ひとまとめにする
type Inter interface {~}
↓ 以下のように使える
func FuncX (i Inter) {~}
type TypeX struct {
i Inter
}
上の図のように、「間に挟まっている」イメージが持てると思います。
ちなみにこのブログのアイコンが「🥪」なのは、この「間に挟まっている」イメージから選んでいます。
interfaceのメリット
interfaceを使うと
- 複数の型に対する関数の定義を1回で済ませられる
- 構造体のフィールドを入れ替えられる
などの様々なメリットがありますが、これは学習を進めていくにつれて追々感じていくでしょう。
io.Reader
最後に、interfaceの代表例と言えるio.Reader
型を紹介したいと思います。
標準パッケージのioには、io.Reader
という型があります。
以下のように定義されます。
type Reader interface {
Read(p []byte) (n int, err error)
}
Read()
という、「何かしらの内容を受け取ったp
という変数の中に格納し、格納したバイト数と起こったエラーを出力する」機能を持った型をひとくくりにするためのinterfaceです。
io.Reader
は、指定された機能の少なさから、様々な型をひとまとめにすることができます。例えば
-
os.File
- ファイルを扱う構造体
-
Read()
メソッドではファイルの中身を読みだす
-
net.Conn
- ネットワークのコネクションを扱う構造体
-
Read()
メソッドでは受信したデータを読みだす
-
zip.Reader
- zip圧縮するための構造体
-
Read()
メソッドでは持っているデータをzip圧縮してp
に格納する
これらの様々な型を、「とりあえず中身を読み出して何かに使う」ような関数で受け取るとき、io.Reader
としてひとくくりにできることがどれだけ有用かは想像できると思います。
例えばRead()
した中身を出力する関数であったり、その文字数をカウントするような関数であったりです。
io.Reader
に対する愛はこちらの記事に勝るものが無いので、io.Reader
に興味が湧いた方は読んでみると良いと思います。
おわりに
いかがでした?わかりやすかったですか?
Goをこれからバリバリ使っていきたい!という方々が、スムーズに学習を進める手助けとなれたなら幸いです。
traPで数年間にわたり開発が進められ、今僕がメンテナーをしている、traQというOSSのメッセージングサービスがあります。
その中身のGoコードでは多数のinterfaceが使用され、複数人で開発するのに適した構造のコードが組み上げられています。
もし「interfaceの実用例がもっと知りたい!」という方は、頑張って読解を進めてみると必ず力になると思います。
それでは夜も遅いので、キーボードから手を放そうと思います。
またどこかでお会いしましょう。
Discussion