Goの基本のキ Part3
Tour of Goの内容メモ
前回の記事
ここからは、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
があることを意味する。
例として、*Vertex
にScale
メソッドを定義する。
ポインタレシーバをもつメソッド(ここでは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)
i
がT
を保持していない場合、panicを引き起こす。
Pythonのアサーションと違って、 アサーションの通りに実装しなければならない…と解釈。
型アサーションは2つの値(もとになる値とアサーションが成功したかどうかを報告するbool値)を返すので、インターフェースの値が特定の型を保持しているかどうかをテストを行うことができる。
t, ok := i.(T)
i
がT
を保持していれば、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