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

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