Help us understand the problem. What is going on with this article?

Goのinterfaceがわからない人へ

More than 1 year has passed since last update.

はじめに

Go言語を勉強していて、interfaceが絡んでくるとなかなか理解ができなくなってしまうという人に読んでほしいです。
特にTour of GoStringersあたりでわからなくなった場合を想定しています。
また、Javaと対比して理解を深めることを目的としているため、Javaの経験がある方には刺さる説明になるかと思います。

なぜわからなくなるのか

原因としては2つあると思います。

  • そもそもインターフェースの概念がうまく理解できてない
  • 明示的な実装(implement)が前提になっていると勘違いしている

前者の方は、全く種類の違う型(クラス)がインターフェース型として、一部同じものとしてに扱うことができるということを知らない。
後者の方は、Javaの明示的なインターフェースの実装ありきで考えすぎて、Goの書き方に困惑してしまっている。
ということなのだと思います。

interfaceの実装例(お題:食べるということとは?)

eating.go
package main  

import "fmt" 

// 食べるためのインターフェース
type Eater interface{
    PutIn() // 口に入れる
    Chew() // 噛む
    Swallow() // 飲み込む
}
// 人間の構造体
type Human struct{
    Height int // 身長
}
// カメの構造体
type Turtle struct{
    Kind string // 種類
}

// 人間用のインターフェースの実装
func (h Human) PutIn(){
    fmt.Println("道具を使って丁寧に口に運ぶ")
}
func (h Human) Chew(){
    fmt.Println("歯でしっかり噛む")
}
func (h Human) Swallow(){
    fmt.Println("よく噛んだら飲み込む")
}

// カメ用のインターフェースの実装
func (h Turtle) PutIn(){
    fmt.Println("獲物を見つけたら首をすばやく伸ばして噛む")
}
func (h Turtle) Chew(){
    fmt.Println("クチバシで噛み砕く")
}
func (h Turtle) Swallow(){
    fmt.Println("小さく砕いたら飲み込む")
}

// インターフェースが引数になる、食べるメソッド
func EatAll(e Eater){
    e.PutIn() // インターフェースから呼び出す
    e.Chew()
    e.Swallow()
}

func main() {
    var man Human = Human{Height: 300} // 人間用の構造体を作成
    var cheloniaMydas = Turtle{Kind: "アオウミガメ"} // カメ用の構造体を作成
    var eat Eater // インターフェースEater型の変数を用意
    fmt.Println("<人間が食べる>")
    eat = man // Human型がインターフェースであるEater型に変換される
    EatAll(eat) // インターフェースを引数に関数を呼ぶ
    fmt.Println("<カメが食べる>")
    eat = cheloniaMydas // Turtle型がインターフェースであるEater型に変換される
    EatAll(eat)
}
Console
<人間が食べる>
道具を使って丁寧に口に運ぶ
歯でしっかり噛む
よく噛んだら飲み込む
<カメが食べる>
獲物を見つけたら首をすばやく伸ばして噛む
クチバシで噛み砕く
小さく砕いたら飲み込む

コードがずいぶんと長くなってしまいましたが、きちんと説明するのでついてきてください。
それでは、次の4つのフェーズに分けて紹介していきます。

  • インターフェースの定義
  • インターフェースを使った関数
  • インターフェースの実装
  • インターフェースの使い方

インターフェースの定義

// 食べるためのインターフェース
type Eater interface{
    PutIn() // 口に入れる
    Chew() // 噛む
    Swallow() // 飲み込む
}
// 人間の構造体
type Human struct{
    Height int // 身長
}
// カメの構造体
type Turtle struct{
    Kind string // 種類
}

まず「食べる」ということをプログラムで表現することを考えましょう。
口に入れる、噛む、飲み込むといったフェーズにわけて、関数をインターフェースに定義してみました。
といっても答えは人それぞれですが、今回はこんな感じで作ってみます。

次に、構造体(Javaでいうところのクラスに相当)を定義しました。
人間の構造体とカメの構造体です。ポイントは「それぞれの構造体の中身がまるっきり違ってもいい」ということにあります。

インターフェースを使った関数

// インターフェースが引数になる、食べるメソッド
func EatAll(e Eater){
    e.PutIn() // インターフェースから呼び出す
    e.Chew()
    e.Swallow()
}

今回の最終ゴールは、「インターフェースを引数にとるEatAll関数を作ること」と考えるとわかりやすくなります。
まずその第一歩として、最初にインターフェースを定義したのです。

インターフェースを引数にとるとどんなことがうれしいのでしょうか。
例えば、こうすることで、「飲む」ということをプログラムで表現するときに、さきほどのインターフェースを使い回すことができそうですよね。液体なので噛む必要はなくなります。

// インターフェースが引数になる、飲むメソッド
func DrinkAll(e Eater){
    e.PutIn() // インターフェースから呼び出す
    e.Swallow()
}

プログラムで書くとこんな感じです。

インターフェースの実装

私が一番混乱したのはここの部分です。
Javaであれば、

class Human implements Eater{
    private int height;
    public void PutIn(){
        System.out.println("道具を使って・・・");
    }
    // ...
}

こうなるはずです。
決定的にJavaと違う点は、implementsがないことです。ここに注意してください。
Goでは、interfaceの中にある関数名と同一の関数が全て実装されている構造体に自動的に実装されると思ってください。

// 人間用のインターフェースの実装
func (h Human) PutIn(){
    fmt.Println("道具を使って丁寧に口に運ぶ")
}
func (h Human) Chew(){
    fmt.Println("歯でしっかり噛む")
}
func (h Human) Swallow(){
    fmt.Println("よく噛んだら飲み込む")
}

// カメ用のインターフェースの実装
func (h Turtle) PutIn(){
    fmt.Println("獲物を見つけたら首をすばやく伸ばして噛む")
}
func (h Turtle) Chew(){
    fmt.Println("クチバシで噛み砕く")
}
func (h Turtle) Swallow(){
    fmt.Println("小さく砕いたら飲み込む")
}

この部分では、レシーバーを使って、HumanTurtle両方にそれぞれの3つのメソッドを定義しています。
試しに、eating.goからHumanSwallow()を抜くとどうなるでしょうか。

source_file.go:56: cannot use man (type Human) as type Eater in assignment:
    Human does not implement Eater (missing Swallow method)

こうなります。
まだmain関数の中身を説明していないので、わかりづらいかもしれません。
これは、Human型がインターフェースのEater型として割り当てることには使えないと言っています。
つまり、Human型にはインターフェースのEater型が実装されていないということです。
裏を返せば、インターフェースの中にある同じ名前のメソッドを全て実装するだけで自動的にインターフェースが実装されているということになります。

インターフェースの使い方

最後に、main関数の中身を紹介します。

func main() {
    var man Human = Human{Height: 300} // 人間用の構造体を作成
    var cheloniaMydas = Turtle{Kind: "アオウミガメ"} // カメ用の構造体を作成
    var eat Eater // インターフェースEater型の変数を用意
    fmt.Println("<人間が食べる>")
    eat = man // Human型がインターフェースであるEater型に変換される
    EatAll(eat) // インターフェースを引数に関数を呼ぶ
    fmt.Println("<カメが食べる>")
    eat = cheloniaMydas // Turtle型がインターフェースであるEater型に変換される
    EatAll(eat)
}

最初の2行は、構造体を定義してるだけです。
3行目は、インターフェースEater型の変数を定義しています。ここが、ポイントです。あくまでもこの変数eatEater型です。もう一度いいますが、インターフェースですからね。
そして5行目でHuman型の構造体の実体をインターフェース型の変数に入れています。この時点で、Eater型に変換されていると思ってください。
そして、6行目では先程説明したEatAll関数にインターフェースであるeatを渡しています。
これを同様にカメの場合でやります。
すると、結果が人間の場合とカメの場合で変わってることがわかるかと思います。

これをStringerで考える

Tour of Goに出てくるものをそのまま持ってきました。
このチュートリアルが言っていることというのは、

type Stringer interface {
    String() string
}
  • そもそもfmt.Stringerというインターフェースは上記で定義されている。
  • fmt.Stringerfmtパッケージの中にある関数の引数としてこのインターフェースを使っている。

fmtパッケージの中にある関数として代表的なものがfmt.Printlnです。この引数にもfmt.Stringerが使えるということです。

それでは、実際に実装してみましょう。
あれ、実装ってどうやりましたっけ?implementsのようなものはありませんし。
そうです。インターフェースにある関数を全て実装したい型に実装してしまえばそれでおしまいです。

type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

今回はPersonに実装したいので・・・
レシーバーにPersonを、そして、Stringerの中にある関数と同じ名前の関数を作り、引数の型、返り値の型も合わせました!
これで実装が完了というわけです。

流れはわかったでしょうか?

もし、次のExerciseがわからない方は、この記事を御覧ください。ものすごく納得がいく説明でわかりやすいです。

rtok
色々やってます
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした