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

出典: フリー教科書『ウィキブックス(Wikibooks)』
< Go


Goはオブジェクト指向言語?[編集]

Goはオブジェクト指向の機能を持っていますが、従来のオブジェクト指向言語とは異なるアプローチをとっています。

まず、Goにはクラスという概念がありません。代わりに、構造体(struct)を含む型を使用します。 Goでは、型の定義の外でメソッドを定義することで、オブジェクト指向プログラミングの基本的な機能を提供しています。

Goのメソッドは、クラスベースのオブジェクト指向言語のメソッドよりも、Kotlinの拡張関数に似ています。

また、Goはインターフェース(interface)をサポートしています。インターフェースは、実装するメソッドを定義するだけで、実装の詳細を指定する必要がない抽象的な型です。インターフェースは、複数の型によって実装されることができ、ポリモーフィズムの実現に役立ちます。

さらに、Goにはクラスの継承機能がありません。代わりに、埋め込み(embedding)を使用します。埋め込みは、構造体に他の構造体を埋め込むことで、継承と同様の機能を提供します。

以上のように、Goは従来のオブジェクト指向言語とは異なるアプローチをとっていますが、オブジェクト指向プログラミングに必要な基本的な機能を提供しています。

メソッド[編集]

メソッド呼び出し[編集]

Goのメソッド呼び出しには、ドット記法が使用されます。具体的には、以下のような形式で呼び出します。

オブジェクト.メソッド(引数)

メソッド呼び出し[編集]

Goでは、構造体を含む型にメソッドを定義することができます。メソッドは、関数と同じようにパラメータを取ることができますが、最初のパラメータには、メソッドが呼び出される構造体や型の値を受け取る変数を指定する必要があります。この変数は、通常、1文字目を小文字にした名前をつけます。

メソッドの例
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 は式ではなく型を取ります。

脚註[編集]