Go/メソッドとインターフェース

< Go

メソッド編集

Goには、C++Javaのようなクラスベースオブジェクト指向言語にあるような class はありません。 またGoには、JavaScript, LuaSelfのようなプロタイプベースオブジェクト指向言語にあるようなプロトタイプチェーンもありません。

では、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
組込み型にはメソッドは定義できず、型定義が必要です(この場合は Number)。
メソッド定義
func (n Number) pow() Number {
    return n * n
}
(n Number) がレシーバーで、pow がメソッド名です。
レシーバーがあることが関数との違いで、それは呼び出し方法にもあります。
メソッドの呼び出し
	m := n.pow()
短い変数宣言の構文です。
n.pow() がメソッドコールで、関数コールにはない n. がメソッド独特なレシーバーです。
func (n Number) pow() Number { return n * n } の戻り値の型も Number なので、m の型は Number と型推論されます。
fmt.Printfで表示
	fmt.Printf("%v(%T)\n", m, m)
C言語のprintf()にはない型指定子 %v(値を表示)と %T(型名を表示)を使っています。
「なぜユーザー定義型の型名を知っているのか?」
というのが最もな疑問だと思います。
ライブラリーのソースコード go/src/fmt/print.go を読むのが最適なのですが、結論から言うと reflect パッケージの TypeOf 関数を呼び出しています。
reflect.TypeOfgo/src/reflect/type.go で定義されています。
この様に、GoのパッケージはGo自身で書かれていますし、go(コンパイラーやパッケージマネージャーのフロントエンド)もGoで書かれているので、コーディング例の宝庫と言えます。

インターフェース編集

まず次のコードを観てみましょう。

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 は式ではなく型を取ります。

脚註編集