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)
}
Copy
が Write
メソッドを呼び出す場所が見つかるまで、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 アプリケーションの作成に関する投稿を参照してください。