いろいろ備忘録日記

主に .NET とか Go とか Python絡みのメモを公開しています。

Goメモ-26 (インターフェース, Interface, Tour of Go)

概要

Tour of Go の - Interfaces についてのサンプル。

tour.golang.org

今回は、Goのインタフェースについて。

インターフェースって聞くと、身構える人もいるかもしれませんね。。

オブジェクト指向絡みの本とか情報見ると、ややこしく書いてるものが多いと思います。

本来の定義とか厳密さを考えるとややこしいかもしれませんが

基本的にインターフェースってのは、「約束事」とか「契約」ってイメージで捉えておけばいいと思っています。

「約束事が書いてあるから、これを守ってくれていれば、この型(インターフェース)を名乗っていいよ」

って感じ。

で、約束事を守るってのが、そのインターフェースに定義されている関数を実装しておくこと。

Goのインターフェースは、まんまこのイメージです。

Goには、クラスの概念は有りませんが、型に対してメソッドを定義できることと今回のインターフェースで

シンプルな多態性を実現しています。

インターフェースを定義するには以下のようにします。

type sumer interface {
    sum() int
}

メソッドの中身は書かずに、定義だけ書きます。これは他の言語でも同じ。C#だと

public interface Sumer
{
    int Sum();
}

Goが、他の言語とちょっと違うのは、約束事を守るために明示的に

「このインターフェースを実装しています!!」

って書かなくてもいい点。そのインターフェースに定義されているメソッドと同じ形で定義しておけば、そのインターフェースを実装していることになります。

例えば、上のインターフェースを実装する場合

type data struct {
    x, y int
}

func (d *data) sum() int {
    ...
}

でいいです。

他の言語(例えばC#)だと

public class Data : Sumer
{
    public int x;
    public int y;
    
    public int Sum()
    {
        ...
    }
}

となります。Javaだと implements って書きますね。

Goの方がダックタイピングな感じになります。どのインターフェースを実装しているかは、いちいち宣言していないけど、そのインターフェースが持つ約束事を全部守ってたら、そのインターフェースを実装できてるってことでオッケイとなります。

例えば、fmtパッケージにStringerというインターフェースがいます。

golang.org

見ると分かりますが、このインターフェース、String() string って関数が一つだけ定義されています。

先程のdata構造体に、このインターフェースを実装しようと思ったら

func (d *data) String() string {
    return fmt.Sprintf("%d,%d", d.x, d.y)
}

って書くだけで、data構造体は、fmt.Stringer インターフェースを名乗ってよしです。

名乗ってよしってことは、その型になることが出来るってことです。

ちなみに、Goではインターフェース名に暗黙の了解があるみたいで

インターフェース定義が関数一つだけとなっているものは、インターフェース名の後ろに er って付ける

となるみたいです。さっきの Stringer は関数一つだけなので、 String + er で Stringer って感じ。

インターフェースとnilの関係

Goのインターフェースを扱うときに、注意しないといけないのが nil 値の扱いです。

これは、Goの言語仕様がちょっと特殊なこともあって、最初は理解しづらいです。

(私は、今でも間違えますが・・w)

これについては、下記のサンプルの方で実際のコード見てもらったほうが分かりやすいと思います。

個人的にインターフェース扱う際に気をつけている点は、以下の点。

  • Goのインターフェースは、概念的に内部で (実際のオブジェクト、実際の型)というタプルを内部で持っているイメージ
  • 内部で持っている実際のオブジェクトと実際の型の両方が nil の場合だけ、インターフェースは nil となる
  • インターフェースメソッドを実装する場合は、レシーバーが nil の場合を考慮する

ですね。普通に扱う分には、別に問題にはなりません。

サンプル作るときとかで、ひねくれた使い方した時に理解しづらいだけです。

サンプル

package tutorial

import "fmt"

type (
    // サンプル用のインターフェース
    sumer interface {
        sum() int
    }

    // サンプル用インターフェースを実装する型
    sumImpl struct {
        x, y int
    }
)

// コンストラクタ関数
func newSumImpl(x, y int) *sumImpl {
    return &sumImpl{
        x: x,
        y: y,
    }
}

// sumer インターフェースを実装
func (s *sumImpl) sum() int {
    if s == nil {
        fmt.Println("[sumImpl] <nil>")
        return 0
    }

    return s.x + s.y
}

// fmt.Stringer インターフェースを実装
func (s *sumImpl) String() string {
    if s == nil {
        fmt.Println("[sumImpl] <nil>")
        return fmt.Sprintf("x:%v\ty:%v", 0, 0)
    }

    return fmt.Sprintf("x:%v\ty:%v", s.x, s.y)
}

// Interface は、 Tour of Go - Interfaces (https://tour.golang.org/methods/9) の サンプルです。
//noinspection GoNilness
func Interface() error {
    // ------------------------------------------------------------
    // Go言語のインターフェース
    // Go言語におけるインターフェースは、メソッドのシグネチャの集まりを定義しているもの.
    // Go言語には、クラスの概念は無いが、型に対するメソッドの定義とインターフェースで
    // シンプルなポリモーフィズムを実現している。
    //
    // 他の言語のインターフェースの概念と大きく異なるのは、Go言語ではインターフェースを
    // 明示的に implements しているという記載は必要なく、そのインターフェースに定義
    // されているメソッドと同じシグネチャを定義しておけばインターフェースを実装していること
    // になる点である.
    //
    // 例えば、fmtパッケージには、Stringerというインターフェースが定義されており
    // Stringerインターフェースは、一つだけメソッドを定義している
    //     String() string
    // 自身で定義した型でfmt.Stringerインターフェースを実装したいと思った場合は
    // 単純に String() string というメソッドを定義すれば良い。
    //
    // ちなみに、Go言語では メソッド を一つだけ持つインターフェースは
    // xxxerという名前で定義するのが暗黙のようである.
    //
    // このようにダックタイピング的に利用できて、便利なGo言語のインターフェースだが
    // 一点注意点があって、それは nil の扱い方について。
    //
    // Go言語のインターフェースは、概念的に(実際のオブジェクト, 実際の型) という
    // タプルを内部で持っているような構造になっている。
    //
    // 宣言しただけで、何も設定していないインターフェースの場合
    // そのインターフェースの値は nil である。 つまり内部の値も型も nil。
    // この場合、 インターフェース == nil は True となる。
    //
    // そこに、実際の型で、値がnilなデータをインターフェースに設定すると
    // インターフェースの内部データは、値がnilで、型が実オブジェクトの型という形になる。
    // この場合、インターフェース == nil は False となる。
    //
    // つまり、インターフェース == nil が True と判定されるのは
    // インターフェース内部の実オブジェクトの値と型の両方が nil な場合のときだけである。
    //
    // なので、インターフェース型でデータを扱っている場合、インターフェースとしては nil では
    // 無いけれども、内部のデータが nil ということはあり得る。そのため、インターフェースを
    // 実装している具象型のメソッドでは、レシーバが nil の場合を考慮する必要がある。
    // (インターフェース自体は nil では無いので、メソッドの呼び出しは可能であるため)
    //
    // 他のオブジェクト指向言語(C#やJava)などを経験している人からすると
    // このような場合は、NullPointerExceptionなどが発生するだろうと思っている
    // ところで、Go言語では普通にメソッドが呼べてしまう場合があるので注意が必要。
    //
    // nil な インターフェースは、具体的な値も型も保持していないので
    // nil インターフェースのメソッドを呼び出すと、ランタイムエラーとなる.
    //
    // 参考: http://bit.ly/2LsltPP, http://bit.ly/2LutdB2
    // ------------------------------------------------------------
    var (
        ifValueAndTypeBothNotNil sumer
        ifValueAndTypeBothNil    sumer
        ifValueNilAndTypeNotNil  sumer
        implNotNil               *sumImpl
        implNil                  *sumImpl
    )

    // 普通に具象データを作成して、それをインターフェースに設定して扱う
    implNotNil = newSumImpl(10, 20)
    ifValueAndTypeBothNotNil = implNotNil

    printSum(ifValueAndTypeBothNotNil)
    printSum(newSumImpl(30, 40))

    // 宣言だけして、何も設定していないインターフェースは、インターフェースの値自体が nil
    ifVal := ifValueAndTypeBothNil
    fmt.Printf("[ifValueAndTypeBothNil] value:%v\ttype:%T\tis nil?:%v\n", ifVal, ifVal, ifVal == nil)

    // printSum() には nil が引数で渡る
    printSum(ifVal)

    // 具象データのポインタを宣言しただけの場合、そのポインタが示す先がないので、そのデータは当然 nil となっている
    // このデータをインターフェースに設定すると、インターフェースの値部分は nil だが、型が埋まるので
    // インターフェース自体は nil ではなくなる
    ifValueNilAndTypeNotNil = implNil
    ifVal = ifValueNilAndTypeNotNil
    fmt.Printf("[ifValueNilAndTypeNotNil] value:%v\ttype:%T\tis nil?:%v\n", ifVal, ifVal, ifVal == nil)

    // インターフェース自体は nil ではないので、普通にメソッドの呼び出しが行える
    // 呼び出された側は、レシーバが nil の状態でメソッド呼び出しされる
    // なので、メソッドを実装する場合は レシーバ が nil の場合を考慮する必要がある.
    printSum(ifVal)

    return nil
}

func printSum(v sumer) {
    fmt.Println(v)

    if v != nil {
        fmt.Printf("[sum()] %v\n", v.sum())
    }
}

try-golang/tutorial_gotour_19_interface.go at master · devlights/try-golang · GitHub

実行すると以下な感じ。

[Name] "tutorial_gotour_interface"
x:10    y:20
[sum()] 30
x:30    y:40
[sum()] 70
[ifValueAndTypeBothNil] value:<nil>   type:<nil>    is nil?:true
<nil>
[sumImpl] <nil>
[ifValueNilAndTypeNotNil] value:x:0 y:0 type:*tutorial.sumImpl  is nil?:false
[sumImpl] <nil>
x:0 y:0
[sumImpl] <nil>
[sum()] 0

過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com