🚶‍♀️

Goの基本のキ Part3

2022/11/19に公開約8,100字

Tour of Goの内容メモ

前回の記事
https://zenn.dev/tamanegi/articles/c5974179ceb89b

ここからは、Methods and interfaces のチュートリアルを行っていく。
正直使ってみないと実感できない部分があるので、理解できなくてもとりあえず進めていく。

Methods | メソッド

Goには、クラス(class)の仕組みがない(!)が、型にメソッドを定義することはできる。
メソッドは、レシーバ引数を関数に取る。 言い方を変えるとレシーバと呼ばれる引数を伴う関数。
レシーバは、funcとメソッド名の間に引数リストで表現される。

この例では、Absメソッドはvという名前のVertex型レシーバを持つことを意味する。

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
}

> 5

structだけでなく、任意の型にもメソッドを宣言できる。

例として、Absメソッドをもつ数値型のMyFloat

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

func main() {
	f := MyFloat( -math.Sqrt2)
	fmt.Plentln(f.Abs())
}

レシーバを伴うメソッドの宣言は、レシーバ型が同じパッケージにある必要がある。
他のパッケージに定義している型に対して、レシーバを伴うメソッドを宣言することはできない。

ポインタのレシーバでメソッドを宣言することもできる。
レシーバの型が、ある型Tへの構文*Tがあることを意味する。

例として、*VertexScaleメソッドを定義する。
ポインタレシーバをもつメソッド(ここではScale)は、レシーバが指す変数を変更することができる。レシーバ自身を更新することが多いため、変数レシーバよりもポインタレシーバの方が一般的。

変数レシーバは、元のVertex変数のコピーを操作する。
main関数で宣言したVertex変数を変更するなら、Scaleメソッドはポインタレシーバにする必要がある。

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(10)
	fmt.Println(v.Abs())
}

> 50
type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(10)
	fmt.Println(v.Abs())
}


> 5

ポインタレシーバを使う理由は2つ。
一つ目は、メソッドがレシーバが指す変数を操作するため。
二つ目は、メソッドの呼び出し毎に変数のコピーを避けるため。 例えばレシーバが大きな構造体である場合などに便利。
一般的には、レシーバは変数レシーバもしくはポインタレシーバのどちらかで統一させるべきで、混在させないほうが望ましい。

Interfaces | インターフェース

インターフェース型は、メソッドの集まりで定義される。

型にメソッドを実装していくことは、すなわちインターフェースを実装しているので、明示的に宣言しなくてもよい。(一つのメソッドだけだったらという意味かな)

type I interface {
	M()
}

type T struct {
	S string
}

// This method means type T implements the interface I,
// but we don't need to explicitly declare that it does so.
func (t T) M() {
	fmt.Println(t.S)
}

func main() {
	var i I = T{"hello"}
	i.M()
}

インターフェースの値は、値と具体的な型のタプルのように考えられる。

(value, type)

インターフェースの値は、特定の基底になる具体的な型の値を保持し、インターフェースの値のメソッドを呼び出すとその基底型の同じ名前のメソッドが実行される。

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	fmt.Println(t.S)
}

type F float64

func (f F) M() {
	fmt.Println(f)
}

func main() {
	var i I

	i = &T{"Hello"}
	describe(i)
	i.M()

	i = F(math.Pi)
	describe(i)
	i.M()
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}

> (&{Hello}, *main.T)
> Hello
> (3.141592653589793, main.F)
> 3.141592653589793

インターフェースの中にある値がnilの場合、メソッドのレシーバもnilとして呼び出される。
Goではnilをレシーバーとして呼び出されてもnullポインターの例外を出さずに適切に処理してくれる。

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	if t == nil {
		fmt.Println("<nil>")
		return
	}
	fmt.Println(t.S)
}

func main() {
	var i I

	var t *T
	i = t
	describe(i)
	i.M()

	i = &T{"hello"}
	describe(i)
	i.M()
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}

> (<nil>, *main.T)
> <nil>
> (&{hello}, *main.T)
> hello

nilインターフェースの値は、値も具体的な型も保持しない。
呼び出すメソッドの示す型がインターフェースのタプル内に存在しないので、 nilインターフェースのメソッドを呼び出すと、ランタイムエラーを引き起こす。

type I interface {
	M()
}

func main() {
	var i I
	describe(i)
	i.M()
}

func describe(i I) {
	fmt.Printf("(%v, %T)\n", i, i)
}

> (<nil>, <nil>)
> panic: runtime error: invalid memory address or nil pointer dereference
> [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x482161]
> 
> goroutine 1 [running]:
> main.main()
> 	/tmp/sandbox539928408/prog.go:12 +0x61

ゼロ個のメソッドを指定されたインターフェース型(メソッドを持たないインタフェース)は、 空のインターフェースと呼ばれる。

interface{}

空のインターフェースは、任意の型の値を保持することができ、未知の型の値を扱うコードで使用される。
例えば、 fmt.Print は interface{} 型の任意の数の引数を受け取る。

func main() {
	var i interface{}
	describe(i)

	i = 42
	describe(i)

	i = "hello"
	describe(i)
}

func describe(i interface{}) {
	fmt.Printf("(%v, %T)\n", i, i)
}

> (<nil>, <nil>)
> (42, int)
> (hello, string)

Type assertions | 型アサーション

型アサーションは、インターフェースの値のもととなる値や型を伝える手段を提供する。

この例では、インターフェースの値iが、具体的な型Tを保持し、もとになるTの値を変数tに代入することを伝えている。

t  := i.(T)

iTを保持していない場合、panicを引き起こす。
Pythonのアサーションと違って、 アサーションの通りに実装しなければならない…と解釈。

型アサーションは2つの値(もとになる値とアサーションが成功したかどうかを報告するbool値)を返すので、インターフェースの値が特定の型を保持しているかどうかをテストを行うことができる。

t, ok := i.(T)

iTを保持していれば、tはもとになる値になり、okはtrueになる。
異なる場合、okはfalse、tは型Tのゼロ値になる。panicは起きない。

func main() {
	var i interface{} = "hello"

	s := i.(string)
	fmt.Println(s)

	s, ok := i.(string)
	fmt.Println(s, ok)

	f, ok := i.(float64) // panicは起きない
	fmt.Println(f, ok)

	f = i.(float64) // panic
	fmt.Println(f)
}

> hello
> hello true
> 0 false
> panic: interface conversion: interface {} is string, not float64
> 
> goroutine 1 [running]:
> main.main()

Type Switches | 型Switch

型switchは、複数の型アサーションを直列に使用できる構造のこと。

型switchはswitch文と似ているが、型switchのcaseは型を指定し、指定れたインタフェースの値が保持する値の型と比較される。

switch v := i.(type) {
	case T:
		// here v has type T
	case S:
		// here v has type S
	default:
		// no match; here v has the same type as i
}

型switchの宣言は、型アサーションi.(T)と同じ構文ではあるが、特定の型T部分は、typeに置き換えられる。

このswitch文は、インタフェースの値の方がTなのかSなのかをテストする。各caseにおいて変数vはそれぞれの型TもしくはSとして扱われる。
defaultの場合は、変数vは同じインタフェース型で値はiとなる。

func do(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	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)
	}
}

func main() {
	do(21)
	do("hello")
	do(true)
}

> Twice 21 is 42
> "hello" is 5 bytes long
> I don't know about type bool!

もっともよく使われているinterfaceの一つにfmtパッケージに定義されているStringerがある。

type Stringer interface {
    String() string
}

Stringerインタフェースは、stringとして表現することができる型。
fmtパッケージや多くのパッケージでは、変数を文字列で出力するためにこのインタフェースがあることを確認している。

type Person struct {
	Name string
	Age  int
}

func (p Person) String() string {
	return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
	a := Person{"Arthur Dent", 42}
	z := Person{"Zaphod Beeblebrox", 9001}
	fmt.Println(a, z)
}

> Arthur Dent (42 years) Zaphod Beeblebrox (9001 years)

練習

IPAddr型のfmt.Stringerインタフェースの実装練習。

type IPAddr [4]byte

// TODO: Add a "String() string" method to IPAddr.
func (ip IPAddr) String() string {
	return fmt.Sprintf("%v.%v.%v.%v", ip[0], ip[1], ip[2], ip[3])
}


func main() {
	hosts := map[string]IPAddr{
		"loopback":  {127, 0, 0, 1},
		"googleDNS": {8, 8, 8, 8},
	}
	for name, ip := range hosts {
		fmt.Printf("%v: %v\n", name, ip)
	}
}

> loopback: 127.0.0.1
> googleDNS: 8.8.8.8

Errors | エラー

Goのプログラムは、エラーの常態をerror値で表現する。

error型は、fmt.Stringerに似た組み込みのインターフェース。

type error interface {
	Error() string
}

関数はerror変数を返し、呼び出し元はエラーがnilかどうかを確認することでエラーハンドリングすることができる。

i, err := strconv.atoi("42")
if err != nil {
	fmt.Printf("couldn't convert number: %v\n", err)
	return
}
fmt.Println("Converted integer:", i)

nilのerrorは成功したことを示し、nilではないerrorは失敗を示す。

package main

import (
	"fmt"
	"time"
)

type MyError struct {
	When time.Time
	What string
}

func (e *MyError) Error() string {
	return fmt.Sprintf("at %v, %s",
		e.When, e.What)
}

func run() error {
	return &MyError{
		time.Now(),
		"it didn't work",
	}
}

func main() {
	if err := run(); err != nil {
		fmt.Println(err)
	}
}

> at 2009-11-10 23:00:00 +0000 UTC m=+0.000000001, it didn't work

練習

前回の練習で実装したSqrt関数に負の数が渡されたときの例外処理を実装するもの。

type ErrNegativeSqrt float64

func (e ErrNegativeSqrt) Error() string {
	return fmt.Sprintf("cannot Sqrt negative number: %v", float64(e))
}

func Sqrt(x float64) (float64, error) {
	if x < 0 {
		return 0, ErrNegativeSqrt(x)
	}
	
	z := 1.0
	for i := 0; i < 10; i++ {
	z -= (z * z - x ) / (2 * z)
	}
	return z, nil
}

func main() {
	fmt.Println(Sqrt(2))
	fmt.Println(Sqrt(-2))
}

Discussion

ログインするとコメントできます