ゆるりのこと。

文系営業マンから新米データサイエンティストしています。

A Tour of Goのざっくりまとめ⑥ <Methods and Interfaces編>

前回までの続きです。

mattsun-plapla.hatenablog.com
mattsun-plapla.hatenablog.com
mattsun-plapla.hatenablog.com
mattsun-plapla.hatenablog.com
mattsun-plapla.hatenablog.com


Methods

Goにクラスは存在しないため、代わりにメソッドが定義できる。
メソッドを定義する際、レシーバを引数に取る。
下記の例では、add関数は引数にVertex型のレシーバを受け取る。
このvからadd関数を呼び出せるため、オブジェクト指向的に見える。

import "fmt"

type Vertex struct {
	X, Y int
}

// 通常の記法
func add(v Vertex) int{
	return v.X + v.Y
}

// メソッド
func (v Vertex) add() int {
	return v.X + v.Y
}

func main() {
	v := Vertex{30, 40}
	fmt.Println(v.add()) // 70
}

ポインタレシーバ

レシーバがポインタの場合、それはポインタレシーバである。
レシーバ自身のもつ値を変更する際はこちらを使うため、通常の変数レシーバよりも使われることが多いらしい。
下記の例では、Scale関数はVertexのポインタを引数にとるポインタレシーバである。
もしアスタリスクを無くして、変数レシーバにした場合、元のレシーバが指す変数の値自体は変わらない。

type Vertex struct {
	X, Y int
}

/* ポインタを引数にとるポインタレシーバ */
func (v *Vertex) Scale(f int) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{30, 40}
	v.Scale(2)
	fmt.Println(v) //{60 80}
}

ここで、通常ポインタを受け取るためには、アドレスを渡さないといけなかった。
上の例だと、vはただの変数であってアドレスではない。
なのに自動的にポインタレシーバが呼び出され、ポインタのもつ実体も変わった。

理由として、ポインタレシーバを持つ場合、Goは利便性を考慮して、自動的にv.Scale(2)を(&v).Scale(2)と解釈してくれるから。

したがって、わざわざ下記のように書かなくてもいいというわけである。

p := &Vertex{50, 50}
	p.Scale(2)
	fmt.Println(*p) //{100 100}

ポインタレシーバを使う理由

  1. レシーバが指す実体の値を変更するため
  2. メソッドの呼び出しごとに変数を複製することを避けるため

2つ目は、大きな構造体などを扱う場合にメリットがあるらしい。

Interfaces

メソッドの集まりをinterface型の変数へ持たせることができる。
...とはいえよくわかりません。原文の解説を見てみます。

An interface is a collection of method signatures that a Type can implement (using methods). Hence interface defines (not declares) the behavior of the object (of the type Type).

...やっぱりよくわかりません。

インターフェース上ではメソッドシグネチャを定義します。(※メソッドシグネチャについては下記リンクが参考になりました。)
インターフェースは、メソッド名と引数、返り値から構成されるメソッドシグネチャを提供するものです。


import (
	"fmt"
	"math"
)

type geometry interface {
	area() float64
	perim() float64
}

type rect struct {
	height, width float64
}

type circle struct {
	radius float64
}

func (r rect) area() float64 {
	return r.width * r.height
}

func (r rect) perim() float64 {
	return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
	return math.Pi * c.radius * c.radius
}

func (c circle) perim() float64 {
	return 2 * math.Pi * c.radius
}

func measure(g geometry) {
	fmt.Println(g)
	fmt.Println(g.area())
	fmt.Println(g.perim())
}

func main() {
	r := rect{4, 5}
	c := circle{8}

	measure(r)
	measure(c)
}

まず、areaとperimのメソッドをもつインターフェースgeometryを定義する。
続いて、構造体rectとcircleを定義する。
これらを引数とするarea, perimメソッドはインターフェース上に定義されている。
変数がインターフェース型をもつ場合、インターフェース上の各メソッドを呼び出せる。
インターフェース上で宣言したメソッドはすべて実装しないとエラーになる。

空のインターフェース

空のインターフェースは、任意の型の値を保持できます。 (全ての型は、少なくともゼロ個のメソッドを実装しています。)

空のインターフェースは、未知の型の値を扱うコードで使用されます。 例えば、 fmt.Print は interface{} 型の任意の数の引数を受け取ります。
https://go-tour-jp.appspot.com/methods/14

インターフェースがメソッドを持っていない場合、空のインターフェースと呼ばれる。
これを使えば、異なる型のデータを呼び出してプリントすることもできる。

type MyString string

type Rect struct {
	width, height float64
}

func explain(i interface{}) {
	fmt.Printf("value given to explain this function is of type %T with %v\n", i, i)
}

func main() {
	ms := MyString("My massage is here!")
	rect := Rect{30, 40}

	explain(ms) // value given to explain this function is of type main.MyString with My massage is here! 
	explain(rect) // value given to explain this function is of type main.Rect with {30 40}
}

explain関数は空のインターフェースを引数に取るため、string型のMyStringやstruct型のRectも受け取ることができる。

Type Assertion

func main() {
	var i interface{} = "Hello world"

	s := i.(string)
	fmt.Printf("type is %T value is %v\n", s, s) // type is string value is Hello world

	s2, ok := i.(string)
	fmt.Println(s2, ok) // Hello world true

	s3, ok := i.(int)
	fmt.Println(s3, ok) // 0 false

	s4 := i.(float64)
	fmt.Println(s4) // panic: interface conversion: interface {} is string, not float64
}

アサーションにより、インターフェースのある値を取り出すことができる。

t := i.(T)

インターフェースiにある具体的な型Tを保持する。
そしてTの値をtに代入している。
iがTを保持していれば、tはその値が代入されるが、保持していない場合、返り値にokを加えてテストをしないとpanic errorとなる。

Type switches

複数の型アサーションができる構文。

import "fmt"

func do(i interface{}) {
	switch v := i.(type) {
	case string:
		fmt.Printf("Expected String type is %T / value is %v\n", v, v)
	case int:
		fmt.Printf("Expected Int type is %T / value is %v\n", v, v)
	default:
		fmt.Printf("type is %T / value is %v\n", v, v)
	}
}

func main() {
	do(1) // Expected Int type is int / value is 1
	do("OK") // Expected String type is string / value is OK
	do(true) // type is bool / value is true
}
/* */