初心者に送りたいinterfaceの使い方[Golang]
# Intro
私はプログラミング言語について右も左もわからない時にGolangを始めました。それから一年間ぐらいGolangを書き、その後pythonをひたすら描いたり、アルゴリズムについて学んだりしました。
Golangのことを完全に忘れていた頃、久しぶりにTour of Goをやってみると、前まで理解できなかったことがするすると理解できるようになったのです(海外の記事みたいな見出しになったw)
特に最初の頃理解の乏しかったinterfaceについて書いてみたいと思います。特にTour of Goでmapぐらいまではちゃんと理解できたのに、interfaceに入ったとたんに理解できなくなった人に見て欲しいです。これは、初心者向けなので、ちゃんとした理論よりざっくりとした概念を学びます。ちゃんと理解したい人はqiitaで探せばたくさん出てくるので、そちらをお勧めします。
Sliceとかメソッドとかにもまとめたのでこちらの記事もどうぞ。
# Overview
interfaceは大きく分けて2つの使い方があると考えてください(結局どちらも同じ)。
1つめは何でも入る型としての使い方で、2つめは関数を集めたものです。1つめは簡単で、2つめが少し複雑です。
1. 何でもはいる型としてのinterface
既知の通りGolangは型に厳しいです。型に厳しいおかげで、コンパイラが色々教えてくれるのですが、時にはゆるーく型を扱いたい時があります。そのような時にinterfaceを使います。
package mainimport "fmt"func main() {
var i interface{}
i = 4
fmt.Println(i) //4
i = 4.5
fmt.Println(i) //4.5
i = "文字列だってはいるんだ"
fmt.Println(i) //文字列だってはいるんだ
}
重要なのは、interfaceで定義したものを元の型に戻す時です。例えば、引数を文字列で取る関数はinterfaceを引数として渡せないので、interfaceから文字列に直す必要があります。その場合は下のようにします。
str, ok := interface.(string) //stringに戻す場合
書き方は (返り値), (変換可能か) := (interfaceの変数).(型名)
という感じです。もしinterfaceにstring以外の型が入っていたらokは false
、stringならばokは true
が入ります。
型変換と聞くと、私は以下のようなやつと混同する時があります。
// float64 から intの変換
i := 4.5
j := int(i)
この書き方の方が他の言語と似てるので、interfaceもこのやり方で型を変換できると勘違いする時があるのですが、interfaceは(interfaceの変数).(型名)
のやり方でないとダメです。注意しましょう(私だけか笑)。
あと、switch — caseを使って、柔軟に型によって条件分岐させることができます。
go run interface.go
私はintです
私はfloat64です
私はstringです
私はそれ以外です
詳しくはTour of Go の Type switchesを見てください。
2. 関数をまとめたもの
interfaceは関数をまとめたものとして定義できます。書き方は以下のように書きます。
type <名前> interface{
func1()(string)
func2()(int)
func3()(float64)
...
}
このように定義したinterfaceは新たな型のように扱うことができ、この関数群をメソッドにもつ構造体を代入することができます。
例えば以下のようなinterface,structを考えましょう。 MyInterface
は adder(int,int)(int)
というメソッドをもつものを表したiterfaceです。また、構造体 Struct1
, Struct2
を定義し、 Struct1
はadder(int,int)(int)
というメソッドを持っています。よって、Struct1
だけがMyInterface
を満たしていることになります。
type MyInterface interface{
adder(x int)(int)
}type Struct1 struct{
...
}type Struct2 struct{
...
}func(s1 Struct1)adder(x int)(int){
...
}
以上のことからMyInterface
の型をもつ変数にStruct1
は代入できますが、Struct2
はエラーが出ます。
var i MyInterface
i = Struct1{..} // エラーなし
i = Struct2{..} // エラーあり
// cannot use Struct2 literal (type Struct2) as type MyInterface in assignment:
// Struct2 does not implement MyInterface (missing adder method)
これがinterfaceの2つめの説明です。
….
で、これ何に使うねん?
って私は最初に思いました。ここまではTour of Goを読んでいたら理解できると思いますが、実際どんな場面で使うのか全くわからなかったです。
まだざっくりとした理解ですが、interfaceを使っていいことは具体的な実装を気にすることなく1つのまとまった塊として定義できることで、他の関数から扱いやすくなるといったところでしょうか?抽象的なことをいってもわからないので、具体例をあげます。
## 具体例
あなたは学校向けにサービスを作っているとします。そこでメールアドレスを生成し、メールの本文を作る機能を作ることになりました。
学校には生徒と先生がいて、それぞれ構造体で定義しましょう。この時生徒のみ学年(Grade
)、学籍番号(Number
)を示す値があります(構造体の属性に Email
という要素がないのは無視で)。
type Student struct {
Name string
Number int
Grade int
}type Teacher struct {
Name string
}
メールを送る時にName
を元にしたメールアドレスが必要ですが、先生と生徒で末尾のメールアドレスが異なります。よって Student struct
,Teacher struct
のそれぞれにfunc get_email() string
というメソッドをつけて、そのメソッドを実行するとメールアドレスを返すようにしましょう
func (s Student) getEmail() string {
return s.Name + "@student.ed.jp"
}func (t Teacher) getEmail() string {
return t.Name + "@teacher.ed.jp"
}
あとはメール全体の文章をstringで返す関数を追加するだけです!
メール全体のstringには、メールアドレスを記載する必要があります。よって、引数に生徒・先生を渡す構造体を渡す関数を作成し、、その関数内でfunc get_email() string
を呼び出し、メールアドレスを取得しましょう。本当は引数にメールアドレスをとったり、構造体の生成時に Email
という要素に代入した方がいいですが、今回は便宜上引数に構造体をとります。
func sendEmailOfStuend(s Student)(context string)
func sendEmailOfTeacher(t Teacher)(context string)
したがって上の2つの関数を実装します。2つに分けたのは、Golangは引数の型に厳しいため、型を明示する必要があり、引数の型が異なるものを同じ関数にすることはできないからです。
全体はこんな感じです。
go run interface_exp1.go 送信元 : Yamada@student.ed.jpこれはテスト用のメールです。
よろしくお願いします。送信元 : Tsubomi@teacher.ed.jp
これはテスト用のメールです。
よろしくお願いします。
これで実装できましたが、もっと綺麗にしましょう。ほとんど内容が同じなので、以下の2つはDRYしたいですね。
func sendEmailOfStuend(s Student)(context string)
func sendEmailOfTeacher(t Teacher)(context string)
願わくばStudent
とTeacher
は同じメソッド( func getEmail()
)を持っているから、同じものとして扱いたいですね。同じものとして扱ったら上の関数も
func sendEmail(StudentとTeacherみたいな奴)(context string)
のような感じで纏めれますよね?つまり私が前に言ったみたいに具体的な実装を気にすることなく1つのまとまった塊として定義できることで、他の関数から扱いやすくなりますよね?(強引)
そこでinterfaceの登場です。interfaceは関数の集合体なので、今回はfunc getEmail()
を持っているものを Person
というinterfaceで表しましょう。イメージとしては、Student
もTeacher
は型は違えど、getEmail()
を持っているからひとつのPerson
という塊で扱えるよね、って感じです。
type Person interface {
getEmail() string
}
Student
とTeacher
はfunc getEmail()
をメソッドに持っているので、どちらも Person
を満たします。ゆえにどちらもPerson
型の変数に代入することができます。また、func sendEmail
は以下のようにPerson
型を引数にとるように書き直しましょう。
func sendEmail(p Persion)(context string)
以上を踏まえてコードは以下のように書き直せます。
interfaceを使うことで func sendEmail
がスッキリしました。
このようにiterfaceを使うことで全体の見渡しがしやすくなり、コードが綺麗にかけます。今回は短いプログラムなのでinterfaceを使わなくても書けなくはないですが、大規模なプログラムになってくると、他のファイルからStudent
もTeacher
の違いを意識したくないですよね?このような時にinterfaceに纏めておくことで、他ファイルから使うときもStudent
もTeacher
どちらか意識することなく、 Person
という塊で扱うことができます。
他のプログラミング言語を見ると、pythonはinterfaceはない代わりに抽象クラスというものがあります。個人的にはinterfaceという呼び方よりも抽象クラスと読んだ方が直感的で好きです(Javaはinterfaceも抽象クラスもあるが…)。デザインパターンを見ると、よりinterface(抽象メソッド)の必要性がわかると思います。
ところで、interfaceは関数をまとめたものと言いました。それでは、何も関数をまとめていないiterfaceはどんな型でしょうか?
type EmptyInter interface{
(何もない)
}
何も関数に持っていないということは、どんな構造体でも、stringでも、intでも、何でもかんでもこのinterfaceを満たしますよね?だからこのinterfaceは全ての値を代入できるでのです。
実はこれが一番最初に示した「1. 何でもはいる型としてのinterface」の正体なのです。空のinterfaceだから、どんな型でも代入できるのです(参考)。
# Conclusion
interfaceを簡単にまとめて見ました。Golangは言語機能は単純ですが、interfaceはとっつきにくいですよね。interfaceを利用するとコードの再利用化・部品化がグッと上がるので積極的に使っていきましょう。
# ref
- 「A Tour of Go」https://tour.golang.org/welcome/1
- 「インタフェースの実装パターン #golang — Qiita」https://qiita.com/tenntenn/items/eac962a49c56b2b15ee8