新しいことにはウェルカム

技術 | 電子工作 | ガジェット | ゲーム のメモ書き

Go言語でハマったことメモ(インターフェース)

Golangを始めました。

GolangはC言語のように、シンプルな文法・データ構造でできているのですが、同時に、生産性を高めるための、高度な概念も取り入られています。

そしてそのために、Golangには若干トリッキーな構文がいくつかあります。

しかし、それらを知らずに、他の言語での先入観や勝手な思い込みで判断してしまって、ハマることがちょいちょいありました。

ここでは、Golangを始めてみて、個人的にハマったことや、勘違いしたことを、トピック別に備忘録としてメモしていこうと思います。

今回は、「インターフェース(interface)」とは何ぞやについてのメモです。

インターフェースとは?

Golangの変数は、値だけを持っていて、型情報は持っていません。Golangには、変数の値とその変数の型情報をセットにして保持する、言語組み込みの「特殊な構造体」があり、その「構造体」をインターフェースと呼びます。

インターフェースには任意の型の変数を格納できるので、インターフェースはいわゆる汎用の変数容器とも言えます。

インターフェースは何に使うのか?

Golangがインターフェースを導入した目的の一つに、汎用的な関数を書けるようにしたかったというのがあると思います。

Golangは静的型付け言語のため、変数を使う時には、その変数の型が決まっていないといけません。

しかし、汎用的な関数を書こうとした時、それでは未知の型に対しての処理が書けません。

そこで、任意の型の変数を格納できる型、つまりインターフェースを用意して、そのインターフェースに対して処理を書くことにより、汎用的な関数が書けるようにしています。

インターフェースの使い方(空のインターフェース)

インターフェースは汎用容器なので、任意の型の変数を代入できます。このような何でも格納できるインターフェースは、空のインターフェースと呼ばれています。

例えば、下記のインターフェースには「int」と「bool」の両方の型の値を代入できます。

var i interface{}

a := 123
i = a
fmt.Println(i)  // 123

b := true
i = b
fmt.Println(i)  // true

ただし、インターフェースは構造体で、その構造体の中に値が格納されているので、値を使うには、インターフェースから値を取り出す必要があります。

インターフェースから値を取り出すには<インターフェース>.(型)で指定し、これを型アサーションと呼びます。

var i1, i2 interface{}

i1 = 1
i2 = 2
fmt.Println(i1 + i2)  // これはできない

fmt.Println(i1.(int) + i2.(int)) // 3

インターフェースの仕組み

インターフェースは下記のような、型情報へのポインターと値情報へのポインターからなる、16バイトの構造体です。

type emptyInterface struct {
    typ  *rtype
    word unsafe.Pointer
}

インターフェースに値を代入すると、値を格納する領域が割り当てられ、その領域に値が代入され、インターフェース構造体の値領域へのポインターにアドレスがセットされます。

例えば、インターフェースに代入した値を、直接取得することも可能です。

package main

import (
    "fmt"
    "unsafe"
)

type emptyInterface struct {
    typ  unsafe.Pointer
    word unsafe.Pointer  // ここに代入された値が格納されている
}

func main() {

    var i interface{}

    a := "TEST"
    i = a

    p := (*string)(((*emptyInterface)(unsafe.Pointer(&i))).word)
    fmt.Println(*p) // TEST
}

インターフェースの注意点

初めてインターフェースを見た時は、汎用ポインターみたいなものかと思ったのですが、違いました。

インターフェースに代入した時にインターフェースに格納されるのは、変数の代入「=」で行われるものと同じで、「値のコピー」です。

例えばint変数をインターフェースに代入すると、int変数の値が格納され、代入に使った変数が格納されるわけではありません。同様に、構造体を代入すると、構造体のコピーが格納されるといったように、通常の変数の代入で入るのと同じものが格納されます。

代入に使った変数を格納したいのであれば、変数のポインターを代入する必要があります。

var i interface{}

a := 123
i = a
i.(int) = 0 // これはできない。エラーになる

i = &a
*i.(*int) = 0 // これはできる。aが書き換わる
fmt.Println(a) // 0

メソッド種類定義としてのインターフェース

以上が、汎用容器(空のインターフェース)としてのインターフェースの使い方です。

インターフェースにはもう一つ、型がどんなメソッドを持っているかを定義する型としての使い方があります。

インターフェースから直接、格納されている変数の型を特定せずに、変数のメソッドを呼び出すことができます。

それによって、汎用的な関数の中で、インターフェースのまま、変数のメソッドを呼び出すことができます。

ただし、そのためには、どういったメソッドを持ったインターフェースかを、インターフェースとして定義しておく必要があります。

例えば「A Tour of Go」にあるように、String()stringを返すメソッドを持つインターフェース「Stringer」を定義して、「Stringer」を使って文字列を出力する汎用的な関数を書いてみます。

type Stringer interface {
    String() string
}

func printString(i Stringer){
   fmt.Println(i.String())
}

任意の構造体を「Stringer」にするには、String()メソッドを定義します。

すると、「Stringer」インターフェースを使って書いた汎用的な関数printString()で呼び出されることができます。

package main

import (
    "fmt"
)

// 任意の型
type myStruct struct {
    str string
}

// 任意の型を Stringer インターフェースに対応する
func (o myStruct) String() string {
    return o.str
}

// Stringerインターフェース
type Stringer interface {
    String() string
}

// Stringerインターフェースを使った汎用関数
func printString(i Stringer) {
    fmt.Println(i.String())
}

func main() {
    a := myStruct{str: "TEST"}
    printString(a) // TEST
}

ここで面白いのは、ある型にそのインターフェースを持たせるには、型の定義でインターフェースを取り込む必要はなく、インターフェースで定義されているメソッドと同じものを定義するだけで、 そのインターフェースを持つとみなされる点です。

更に面白いのは、任意のインターフェースから、別のインターフェースを取り出すこともできます。

例えば、空のインターフェースから、前述の「Stringer」インターフェースを取り出すことができます。

func printString(i interface{}) {
    switch v := i.(type) {
    case Stringer:
        fmt.Println(v.String())
    default:
        fmt.Println("null")
    }
}

よくあるインターフェースの使われ方

ライブラリやパッケージなどで、インターフェースが公開されていて、そのインターフェースを満たす型を作れば、それに沿った処理をしてくれるといったような使い方です。

例えば前述の「Stringer」インターフェースだと、自分で作った構造体に

type Stringer interface {
    String() string
}

を満たすメソッドを定義すれば、fmt.Println()でその構造体を渡すと、String()で返す文字列を出力してくれます。

もう一つは、汎用容器としての使われ方です。

例えばJSONの解析で、どういった値が入っているか分からない場合は、事前に型を定義することができません。

そういった場合は、インターフェースを渡して、そこに値を格納してもらうといったことができます。

package main

import (
    "encoding/json"
    "fmt"
)

func main() {

    jsonStr := "[{\"id\": 123, \"name\": \"TEST\"}]"

    var i interface{}
    json.Unmarshal([]byte(jsonStr), &i)
    fmt.Println(i) // [map[ id:123 name:TEST]]
}

reflect

インターフェースを使って汎用的な関数を書く場合、インターフェースの値を読み書きすることになるのですが、それらを扱いやすくするパッケージが「pkg/reflect」です。

感想など

インターフェースには、汎用的容器としての役割と、メソッド定義としての役割があって、最初混乱しました。

インターフェースに値を代入することの意味がよく分かってなかったのですが、下記記事でスッキリしました。要は、変数の入れ物(インターフェース)に値を代入してたんですね。

それが分かると、関数の引数にインターフェースを持ってくることは、要は値をインターフェースに代入しているのと同じということが理解できました。

func test(i interface{}){
    fmt.Println(i)
}

test(123) // i = 123 している

また、インターフェースに変数を代入しても、元の変数の値はインターフェースからは変更できない理由も理解できるようになりました。

関連カテゴリー記事

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com