Go/メソッドとインターフェース
Goはオブジェクト指向言語?
編集Goはオブジェクト指向の機能を持っているものの、従来のオブジェクト指向言語とは異なるアプローチを取っています。
まず、Goにはクラスという概念はありません。代わりに、構造体(struct
)を用いて型を定義します。Goでは、型の定義にメソッドを追加することで、オブジェクト指向プログラミングに必要な基本的な機能を提供しています。これにより、オブジェクト指向の考え方を実現することができます。
Goのメソッドは、クラスベースのオブジェクト指向言語におけるメソッドとは異なり、Kotlinの拡張関数に似た性質を持っています。
また、Goはインターフェース(interface
)をサポートしています。インターフェースは、実装すべきメソッドを定義する抽象的な型であり、実装の詳細を指定する必要はありません。インターフェースは複数の型によって実装可能で、ポリモーフィズムを実現します。
さらに、Goにはクラスの継承機能はありません。代わりに「埋め込み(embedding)」という仕組みを利用します。埋め込みは、ある構造体に別の構造体を埋め込むことで、継承に似た機能を提供します。
このように、Goは従来のオブジェクト指向言語とは異なるアプローチを取りますが、オブジェクト指向プログラミングの基本的な要素を備えています。
メソッド
編集メソッド呼び出し
編集Goでは、メソッド呼び出しにドット記法が使用されます。例えば、以下のような形式で呼び出します:
オブジェクト.メソッド(引数)
メソッド定義
編集Goでは、構造体やその他の型にメソッドを定義できます。メソッドは、通常の関数と同様にパラメータを取りますが、最初のパラメータには、メソッドが呼び出される型のインスタンスを指定する必要があります。このパラメータは「レシーバー」と呼ばれ、通常、最初の文字を小文字にした名前を付けます。
例:メソッドの例
package main import "fmt" type Number int func (n Number) pow() Number { return n * n } func main() { n := Number(7) m := n.pow() fmt.Printf("%v(%T)\n", m, m) }
- 実行結果:
49(main.Number)
解説:
type Number int
:Goの組み込み型にはメソッドを定義できないため、型定義を行います(この例ではNumber
という新しい型を定義しています)。- メソッド定義:
func (n Number) pow() Number { return n * n }
ここで、(n Number)
はレシーバーで、pow
がメソッド名です。レシーバーがあることで、これは関数とは異なることがわかります。
- メソッド呼び出し:
m := n.pow()
n.pow()
がメソッドコールであり、n.
は関数呼び出しにはない、メソッド特有のレシーバーを示しています。
fmt.Printf
での表示:fmt.Printf("%v(%T)\n", m, m)
ここでは、C言語のprintf
にはない%v
(値の表示)と%T
(型名の表示)を使っています。この出力結果から、Goのfmt
パッケージがどのように動作するか、また型をどのように表示するかについて理解できます。
Goの標準ライブラリであるreflect
パッケージのTypeOf
関数を使用して型情報を取得しているため、型名の表示が可能です。
インターフェース
編集まず次のコードを観てみましょう。
- 例
package main import "fmt" type GeoCoord struct { longitude, latitude float64 } func main() { sites := map[string]GeoCoord{ "東京駅": {139.7673068, 35.6809591}, "シドニー・オペラハウス": {151.215278, -33.856778}, "グリニッジ天文台": {-0.0014, 51.4778}, } for name, gc := range sites { fmt.Printf("%v: %v\n", name, gc) } }
- 実行例
東京駅: {139.7673068 35.6809591} シドニー・オペラハウス: {151.215278 -33.856778} グリニッジ天文台: {-0.0014 51.4778}
fmt.Printf の "%v は便利ですね。ユーザー定義の型の変数を渡しても良きに計らってくれます。
fmt.Stringer
編集ユーザー定義の型の変数を、応用に即した形式で文字列化したいケースは多々あります。
- Stringメソッドの例
package main import "fmt" type GeoCoord struct { longitude, latitude float64 } func (gc GeoCoord) String() string { ew, ns := "東経", "北緯" long, lat := gc.longitude, gc.latitude if long < 0.0 { ew = "西経" long = -long } if lat < 0.0 { ns = "南緯" lat = -lat } return fmt.Sprintf("(%s: %f, %s: %f)", ew, long, ns, lat) } func main() { sites := map[string]GeoCoord{ "東京駅": {139.7673068, 35.6809591}, "シドニー・オペラハウス": {151.215278, -33.856778}, "グリニッジ天文台": {-0.0014, 51.4778}, } for name, gc := range sites { fmt.Printf("%v: %v\n", name, gc) } }
- 実行結果
東京駅: (東経: 139.767307, 北緯: 35.680959) シドニー・オペラハウス: (東経: 151.215278, 南緯: 33.856778) グリニッジ天文台: (西経: 0.001400, 北緯: 51.477800)
- GeoCoord 型にメソッド String() を定義しました。
- 実行結果をご覧になると判る通り String() が %v で使われています。
これは、どういうことでしょうか?Printfが定義されてるfmtパッケージの働きです。具体的には、
- fmt.Stringer
type Stringer interface { String() string }
String()メソッド1つが定義されたインターフェース型の型fmt.Stringerが fmt パッケージの中で参照されているため、型にString()メソッドが定義されていれば、ディフォルトの構造体文字列化に変わってString()メソッドが使われるからです。
大圏距離を求めるメソッドを追加
編集- 大圏距離を求めるメソッドdistanceを追加
package main import ( "fmt" "math" ) type GeoCoord struct { longitude, latitude float64 } func (gc GeoCoord) String() string { ew, ns := "東経", "北緯" long, lat := gc.longitude, gc.latitude if long < 0.0 { ew = "西経" long = -long } if lat < 0.0 { ns = "南緯" lat = -lat } return fmt.Sprintf("(%s: %f, %s: %f)", ew, long, ns, lat) } func (gc GeoCoord) distance(other GeoCoord) float64 { i := math.Pi / 180 r := 6371.008 return math.Acos(math.Sin(gc.latitude*i)*math.Sin(other.latitude*i)+ math.Cos(gc.latitude*i)*math.Cos(other.latitude*i)*math.Cos(gc.longitude*i-other.longitude*i)) * r } func main() { sites := map[string]GeoCoord{ "東京駅": {139.7673068, 35.6809591}, "シドニー・オペラハウス": {151.215278, -33.856778}, "グリニッジ天文台": {-0.0014, 51.4778}, } for name, gc := range sites { fmt.Printf("%v: %v\n", name, gc) } ks := []string{} for k, _ := range sites { ks = append(ks, k) } for i := 0; i < len(sites); i++ { ksi, ksx := ks[i], ks[(i+1)%len(sites)] fmt.Printf("%v - %v: %v [km]\n", ksi, ksx, sites[ksi].distance(sites[ksx])) } }
- 実行結果
東京駅: (東経: 139.767307, 北緯: 35.680959) シドニー・オペラハウス: (東経: 151.215278, 南緯: 33.856778) グリニッジ天文台: (西経: 0.001400, 北緯: 51.477800) 東京駅 - シドニー・オペラハウス: 7823.269299386704 [km] シドニー・オペラハウス - グリニッジ天文台: 16987.2708377249 [km] グリニッジ天文台 - 東京駅: 9560.546566490015 [km]
- 2点間の距離 func (gc GeoCoord) distance(other GeoCoord) float64 を定義しました。
interface{}
編集interface{} は、JavaScript の変数や VB の Variant 型の様に全てのインスタンスを参照できる型として振る舞います。
- interface{}
package main import ( "fmt" ) func main() { var i interface{} fmt.Println(i) i = 0 fmt.Println(i) i = "abc" fmt.Println(i) i = []float64{1, 2, 3, 4, 5} fmt.Println(i) i = struct { name string age int }{"joe", 14} fmt.Println(i) }
- 実行例
<nil> 0 abc [1 2 3 4 5] {joe 14}
型アサーション
編集interfaceから値を取り出す方法が型アサーションです。
- 型アサーション
package main import "fmt" func main() { i := interface{}("String") s := i.(string) fmt.Println(s) // n := i.(int) ⇒ panic: interface conversion: interface {} is string, not int n, ok := i.(int) fmt.Println(n, ok) // 0 false x, ok := i.(string) fmt.Println(x, ok) // hello true }
- 実行例
String 0 false String true
interfaceと型スイッチ
編集- interfaceと型スイッチ
package main import "fmt" func main() { ts(42) ts("Sting") ts(nil) } func ts(i interface{}) { switch v := i.(type) { case int: fmt.Printf("Power of %v is %v\n", v, v*v) case string: fmt.Printf("%q is %v bytes long\n", v, len(v)) default: fmt.Printf("I don't know about type %T!\n", v) } }
- 実行例
Power of 42 is 1764 "Sting" is 5 bytes long I don't know about type <nil>!
- 型スイッチでは、switch の式の部分に インスタンス.(type) のように型アサーションの構文を使います。
- 型スイッチでは、case は式ではなく型を取ります。