Go のインターフェイスの使用

完了

Go のインターフェイスは、他の型の動作を表すために使用されるデータの一種です。 インターフェイスは、オブジェクトが満たす必要があるブループリントやコントラクトに似ています。 インターフェイスを使用すると、コード ベースの柔軟性と適応性が高まります。これは、特定の実装に関連付けられていないコードを記述するためです。 このため、プログラムの機能をすばやく拡張することができます。 このモジュールでは、その理由を理解できます。

他のプログラミング言語のインターフェイスとは異なり、Go のインターフェイスは暗黙的に満たされます。 Go では、インターフェイスを実装するためのキーワードは提供されません。 そのため、他のプログラミング言語でのインターフェイスに慣れているユーザーが Go を初めて使用する場合、この考え方は混乱を招くかもしれません。

このモジュールでは、複数の例を使用して Go のインターフェイスを確認し、それらを最大限に活用する方法を示します。

インターフェイスの選択

Go のインターフェイスはブループリントのようなものです。 具象型に含む、または具象型で実装する必要があるメソッドだけを含む抽象型です。

シェイプで実装する必要のあるメソッドを示すインターフェイスをジオメトリ パッケージに作成するとします。 インターフェイスは次のように定義できます。

type Shape interface {
    Perimeter() float64
    Area() float64
}

Shape インターフェイスでは、Shape を考慮するすべての型に、Perimeter()Area() の両方のメソッドが必要であることが示されています。 たとえば、Square 構造体を作成する場合、片方だけではなく、両方のメソッドを実装する必要があります。 また、インターフェイスには、これらのメソッドの実装の詳細が含まれていないことに注意してください (シェイプの外周と領域を計算する、など)。 これらは、単純なコントラクトです。 三角形、円、正方形などのシェイプでは、さまざまな方法で領域と外周を計算できます。

インターフェイスの実装

これまでに説明したように、Go にはインターフェイスを実装するためのキーワードがありません。 インターフェイスで必要なメソッドがすべて含まれている場合、Go のインターフェイスは型で暗黙的に満たされます。

次のコード例に示すように、Shape インターフェイスの両方のメソッドを含む Square 構造体を作成します。

type Square struct {
    size float64
}

func (s Square) Area() float64 {
    return s.size * s.size
}

func (s Square) Perimeter() float64 {
    return s.size * 4
}

Square 構造体のメソッドのシグネチャが Shape インターフェイスのシグネチャとどのように一致しているかに注目してください。 ただし、別の名前を持つ他のインターフェイスに、同じメソッドが含まれていることがあります。 Go では、具象型で実装されるインターフェイスはいつ、またはどのように把握されるのでしょうか。 Go では、実行時に使用される際に把握されます。

インターフェイスがどのように使用されるかを示すために、次のコードを記述できます。

func main() {
    var s Shape = Square{3}
    fmt.Printf("%T\n", s)
    fmt.Println("Area: ", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
}

前述のプログラムを実行すると、次の出力が表示されます。

main.Square
Area:  9
Perimeter: 12

この時点では、インターフェイスを使用するかどうかにかかわらず、違いはありません。 Circle などの別の型を作成し、インターフェイスが役に立つ理由を見てみましょう。 Circle 構造体のコードは次のとおりです。

type Circle struct {
    radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.radius
}

次に、main() 関数をリファクターし、受け取ったオブジェクトの型と、その領域および境界を出力できる関数を次のように作成しましょう。

func printInformation(s Shape) {
    fmt.Printf("%T\n", s)
    fmt.Println("Area: ", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
    fmt.Println()
}

printInformation 関数に Shape がパラメーターとして含まれていることに注意してください。 この関数には Square または Circle オブジェクトを送信でき、その出力は異なりますが、動作します。 main() 関数は次のようになります。

func main() {
    var s Shape = Square{3}
    printInformation(s)

    c := Circle{6}
    printInformation(c)
}

c オブジェクトでは、それが Shape オブジェクトであると指定していないことに注意してください。 ただし、printInformation 関数は、Shape インターフェイスで定義されているメソッドを実装するオブジェクトを想定しています。

このプログラムを実行すると、次の出力が表示されます。

main.Square
Area:  9
Perimeter: 12

main.Circle
Area:  113.09733552923255
Perimeter: 37.69911184307752

エラーが発生していないことに注意してください。出力は、受け取ったオブジェクトの型によって異なります。 また、出力内のオブジェクト型では、Shape インターフェイスについては示されていないことがわかります。

インターフェイスを使用する利点は、Shape の新しい型または実装ごとに、printInformation 関数を変更する必要がないことです。 前述のとおり、インターフェイスを使用すると、コードの柔軟性が向上し、より簡単に拡張できるようになります。

Stringer インターフェイスの実装

既存の機能を簡単に拡張する方法の例として、Stringer の使用があります。これは、次に示すように、String() メソッドを含むインターフェイスです。

type Stringer interface {
    String() string
}

fmt.Printf 関数では、このインターフェイスを使用して値を出力します。つまり、次のようにカスタムの String() メソッドを記述してカスタム文字列を出力できます。

package main

import "fmt"

type Person struct {
    Name, Country string
}

func (p Person) String() string {
    return fmt.Sprintf("%v is from %v", p.Name, p.Country)
}
func main() {
    rs := Person{"John Doe", "USA"}
    ab := Person{"Mark Collins", "United Kingdom"}
    fmt.Printf("%s\n%s\n", rs, ab)
}

前述のプログラムを実行すると、次の出力が表示されます。

John Doe is from USA
Mark Collins is from United Kingdom

ご覧のように、カスタム型 (構造体) を使用して、String() メソッドのカスタム バージョンを記述しました。 この手法は、Go においてインターフェイスを実装する一般的な方法であり、これから確認しようとしているように、多くのプログラムにおいてその例を見ることができます。

既存の実装の拡張

たとえば、次のコードがあり、一部のデータを操作する Writer メソッドのカスタム実装を記述して、その機能を拡張するとします。

次のコードを使用すると、GitHub API を使用して Microsoft から 3 つのリポジトリを取得するプログラムを作成できます。

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=3")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    io.Copy(os.Stdout, resp.Body)
}

前述のコードを実行すると、次のような出力が表示されます (読みやすくするために一部省略しています)。

[{"id":276496384,"node_id":"MDEwOlJlcG9zaXRvcnkyNzY0OTYzODQ=","name":"-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","full_name":"microsoft/-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","private":false,"owner":{"login":"microsoft","id":6154722,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYxNTQ3MjI=","avatar_url":"https://avatars2.githubusercontent.com/u/6154722?v=4","gravatar_id":"","url":"https://api.github.com/users/microsoft","html_url":"https://github.com/micro
....

io.Copy(os.Stdout, resp.Body) の呼び出しが、GitHub API の呼び出しから取得した内容をターミナルに出力するものであることに注意してください。 ターミナルに表示される内容を短くするために、独自の実装を記述するとします。 io.Copy 関数のソースを確認すると、次のように示されています。

func Copy(dst Writer, src Reader) (written int64, err error)

最初のパラメーター dst Writer の詳細をよく見ると、Writerインターフェイスであることがわかります。

type Writer interface {
    Write(p []byte) (n int, err error)
}

CopyWrite メソッドを呼び出す場所が見つかるまで、io パッケージのソース コードの確認を続けることはできますが、ここではこの確認はそのままにしておきます。

Writer はインターフェイスであり、Copy 関数が想定しているオブジェクトであるため、Write メソッドのカスタム実装を記述することができます。 このため、ターミナルに出力する内容をカスタマイズできます。

インターフェイスを実装するには、最初にカスタム型を作成する必要があります。 この場合は、カスタム Write メソッドを記述するだけで済むため、次のように空の構造体を作成できます。

type customWriter struct{}

これで、カスタムの Write 関数を記述する準備が整いました。 また、JSON 形式の API 応答を Golang オブジェクトに解析する構造体を記述する必要もあります。 JSON から Go に変換できるサイトを使用して、JSON ペイロードから構造体を作成できます。 このため、Write メソッドは次のようになります。

type GitHubResponse []struct {
    FullName string `json:"full_name"`
}

func (w customWriter) Write(p []byte) (n int, err error) {
    var resp GitHubResponse
    json.Unmarshal(p, &resp)
    for _, r := range resp {
        fmt.Println(r.FullName)
    }
    return len(p), nil
}

最後に、カスタム オブジェクトを使用するように、main() 関数を次のように変更する必要があります。

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    writer := customWriter{}
    io.Copy(writer, resp.Body)
}

このプログラムを実行すると、次の出力が表示されます。

microsoft/aed-blockchain-learn-content
microsoft/aed-content-nasa-su20
microsoft/aed-external-learn-template
microsoft/aed-go-learn-content
microsoft/aed-learn-template

作成したカスタム Write メソッドにより、出力が見やすくなります。 プログラムの最終バージョンは次のとおりです。

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

type GitHubResponse []struct {
    FullName string `json:"full_name"`
}

type customWriter struct{}

func (w customWriter) Write(p []byte) (n int, err error) {
    var resp GitHubResponse
    json.Unmarshal(p, &resp)
    for _, r := range resp {
        fmt.Println(r.FullName)
    }
    return len(p), nil
}

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    writer := customWriter{}
    io.Copy(writer, resp.Body)
}

カスタム サーバー API の作成

最後に、サーバー API を作成する場合に役立ちそうな、インターフェイスの別のユースケースを見てみましょう。 一般に、Web サーバーを作成するには、次に示すように、net/http パッケージの http.Handler インターフェイスを使用します (このコードを記述する必要はありません)。

package http

type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h Handler) error

ListenAndServe 関数が http://localhost:8000 などのサーバー アドレス、および呼び出しからの応答をサーバー アドレスにディスパッチする Handler のインスタンスを求めていることに注意してください。

次のプログラムを作成して、確認してみましょう。

package main

import (
    "fmt"
    "log"
    "net/http"
)

type dollars float32

func (d dollars) String() string {
    return fmt.Sprintf("$%.2f", d)
}

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

func main() {
    db := database{"Go T-Shirt": 25, "Go Jacket": 55}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

前述のコードを調べる前に、次のように実行してみましょう。

go run main.go

出力が得られないのは、よい兆候です。 次に、新しいブラウザー ウィンドウで http://localhost:8000 を開き、ターミナルで次のコマンドを実行します。

curl http://localhost:8000

次のような出力が表示されます。

Go T-Shirt: $25.00
Go Jacket: $55.00

前述のコードを詳細に確認して、何が実行されているかを理解し、Go のインターフェイスの機能を確認します。 まず、後で使用する String() メソッドのカスタム実装を記述することを念頭に、float32 型のカスタム型を作成します。

type dollars float32

func (d dollars) String() string {
    return fmt.Sprintf("$%.2f", d)
}

次に、http.Handler で使用できる ServeHTTP メソッドの実装を記述しました。 カスタム型をもう一度作成したことに注目してください。ただし、ここでは構造体ではなくマップです。 次に、database 型をレシーバーとして使用して ServeHTTP メソッドを記述しました。 このメソッドの実装では、レシーバーのデータを使用してループを実行し、各項目を出力します。

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

最後に、main() 関数で database 型をインスタンス化し、いくつかの値を使用して初期化しました。 http.ListenAndServe 関数を使用して HTTP サーバーを開始しました。ここでは、使用するポート、および ServeHTTP メソッドのカスタム バージョンを実装する db オブジェクトを含む、サーバーのアドレスを定義しています。 プログラムを実行すると、Go ではそのメソッドの実装が使用されます。これは、サーバー API の中でインターフェイスを使用および実装する方法です。

func main() {
    db := database{"Go T-Shirt": 25, "Go Jacket": 55}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

http.Handle 関数を使用する場合は、サーバー API におけるインターフェイスの別のユースケースを確認できます。 詳細については、Go サイトで Web アプリケーションの作成に関する投稿を参照してください。