この記事は2019新卒 エンジニア Advent Calendar 2019の21日目の記事です。
今回は、爆速でGoに入門していくシリーズのPart2です(Part1はこちら【爆速】Go入門)。
#Methods
Goには、クラスのしくみはありませんが、型にメソッドを定義できます。
メソッドは、レシーバと呼ばれる特別な引数を伴う関数です。
レシーバはfunc キーワードとメソッド名の間に自身の引数リストとして書きます。
この例では、 Abs メソッドは v という名前の Vertex 型のレシーバを持つことを意味しています。
package main
import (
"fmt"
"math"
)
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())
}
$ go run main.go
5
以下の Abs は、先ほどの例から機能を変えずに通常の関数として記述しています。上の例との違いを確認しておきましょう。
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
// func (v Vertex) Abs() float64 {
// return math.Sqrt(v.X*v.X + v.Y*v.Y)
// }
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
// fmt.Println(v.Abs())
fmt.Println(Abs(v))
}
$ go run main.go
5
例で挙げたstructの型だけではなく、任意の型(type)にもメソッドを宣言できます。
また、レシーバを伴うメソッドの宣言は、レシーバ型が同じパッケージにある必要があります。 他のパッケージに定義している型に対して、レシーバを伴うメソッドを宣言できません。
package main
import (
"fmt"
)
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
func main() {
f := MyFloat(-46)
fmt.Println(f.Abs())
}
$ go run main.go
46
ポインタレシーバでメソッドを宣言できます。
例では *Vertex に Scale メソッドが定義されています。
ポインタレシーバを持つメソッド(ここでは Scale )は、レシーバが指す変数を変更できます。 レシーバ自身を更新することが多いため、変数レシーバよりもポインタレシーバの方が一般的です。
変数レシーバでは、 Scale メソッドの操作は元の Vertex 変数のコピーを操作します。 (これは関数の引数としての振るまいと同じです)。 つまり main 関数で宣言した Vertex 変数を変更するためには、Scale メソッドはポインタレシーバにする必要があるのです。
package main
import (
"fmt"
"math"
)
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())
}
$ go run main.go
50
ポインタレシーバを変数レシーバに変えて、違いを確認しておきましょう。
package main
import (
"fmt"
"math"
)
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 (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())
}
$ go run main.go
5
次に、先ほどのScaleメソッドを関数として書き直し(ScaleFunc)、呼び出し時の違いを見ていきましょう。
package main
import "fmt"
type Vertex struct {
X, Y float64
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func ScaleFunc(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
// ScaleFunc(v, 10) エラーになる
ScaleFunc(&v, 10)
v.Scale(2) // (&v).Scale(2)として解釈される
p := &Vertex{4, 3}
p.Scale(3)
ScaleFunc(p, 8)
fmt.Println(v, p)
}
ポインタを引数に取る ScaleFunc 関数は、ポインタを渡す必要があります。
v := Vertex{3, 4}
ScaleFunc(&v, 10) // OK
ScaleFunc(v, 10) // ERROR
メソッドがポインタレシーバである場合、呼び出し時に、変数、または、ポインタのいずれかのレシーバとして取ることができます。
v := Vertex{3, 4}
v.Scale(2) //OK
p := &Vertex{4, 3}
p.Scale(10) //OK
v.Scale(5) のステートメントでは、 v は変数であり、ポインタではありません。 メソッドでポインタレシーバが自動的に呼びだされます。 Scale メソッドはポインタレシーバを持つ場合、Goは利便性のため、 v.Scale(5) のステートメントを (&v).Scale(5) として解釈します。
$ go run main.go
{60 80} &{96 72}
ポインタレシーバを使う2つの理由があります。
ひとつは、メソッドがレシーバが指す先の変数を変更するためです。
ふたつに、メソッドの呼び出し毎に変数のコピーを避けるためです。 例えば、レシーバが大きな構造体である場合に効率的です。
下の例では、次の Abs メソッドはレシーバ自身を変更する必要はありませんが、 Scale と Abs は両方とも *Vertex 型のレシーバです。
一般的には、変数レシーバ、または、ポインタレシーバのどちらかですべてのメソッドを与え、混在させるべきではありません。
package main
import (
"fmt"
"math"
)
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())
}
$ go run main.go
50
#Interfaces
型にメソッドを実装していくことによって、インタフェースを実装(満た)します。
インタフェースを実装することを明示的に宣言する必要はありません( "implements" キーワードは必要ありません)。
package main
import (
"fmt"
)
type I interface {
M()
}
type T struct {
S string
}
// このメソッドは暗黙的にTがインターフェースIを実装していることを意味する
func (t T) M() {
fmt.Println(t.S)
}
func main() {
var i I = T{"hello gunsoo"}
i.M()
}
$ go run main.go
hello gunsoo
インターフェースの値は、値と具体的な型のタプルのように考えることができます : (value, type)
インターフェースの値は、特定の基底になる具体的な型の値を保持します。
インターフェースの値のメソッドを呼び出すと、その基底型の同じ名前のメソッドが実行されます。
関数describeを定義して確認してみましょう。
package main
import (
"fmt"
)
type I interface {
M()
}
type T struct {
S string
}
func (t T) M() {
fmt.Println(t.S)
}
func main() {
var i I = T{"hello gunsoo"}
describe(i)
i.M()
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
$ go run main.go
({hello gunsoo}, main.T)
hello gunsoo
インターフェース自体の中にある具体的な値が nil の場合、メソッドは nil をレシーバーとして呼び出されます。
いくつかの言語ではこれは null ポインター例外を引き起こしますが、Go では nil をレシーバーとして呼び出されても適切に処理するメソッドを記述するのが一般的です(この例では M メソッドのように)。
具体的な値として nil を保持するインターフェイスの値それ自体は非 nil であることに注意してください。
package main
import (
"fmt"
)
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()
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
$ go run main.go
(<nil>, *main.T)
<nil>
ゼロ個のメソッドを指定されたインターフェース型は、 空のインターフェース と呼ばれます : interface{}
空のインターフェースは、任意の型の値を保持できます。 (全ての型は、少なくともゼロ個のメソッドを実装しています。)
空のインターフェースは、未知の型の値を扱うコードで使用されます。 例えば、 fmt.Print は interface{} 型の任意の数の引数を受け取ります。
package main
import (
"fmt"
)
func main() {
var i interface{}
describe(i)
i = 46
describe(i)
i = "gunsoo"
describe(i)
}
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}
$ go run main.go
(<nil>, <nil>)
(46, int)
(gunsoo, string)
#Type assertions
型アサーション は、インターフェースの値の基になる具体的な値を利用する手段を提供します。
t := i.(T)
この文は、インターフェースの値 i が具体的な型 T を保持し、基になる T の値を変数 t に代入することを主張します。
i が T を保持していない場合、この文は panic を引き起こします。
インターフェースの値が特定の型を保持しているかどうかを テスト するために、型アサーションは2つの値(基になる値とアサーションが成功したかどうかを報告するブール値)を返すことができます。
t, ok := i.(T)
i が T を保持していれば、 t は基になる値になり、 ok は真(true)になります。
そうでなければ、 ok は偽(false)になり、 t は型 T のゼロ値になり panic は起きません。
package main
import "fmt"
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s)
s, ok := i.(string)
fmt.Println(s, ok)
f, ok := i.(float64)
fmt.Println(f, ok)
// f := i.(float64) // panic
// fmt.Println(f)
}
$ go run main.go
hello
hello true
0 false
型switch はいくつかの型アサーションを直列に使用できる構造です。
package main
import "fmt"
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(46)
do("hello")
do(true)
}
$ go run main.go
Twice 46 is 92
"hello" is 5 bytes long
I don't know about type bool!
#Errors
Goのプログラムは、エラーの状態を error 値で表現します。
error 型は fmt.Stringer に似た組み込みのインタフェースです:
type error interface {
Error() string
}
よく、関数は error 変数を返します。そして、呼び出し元はエラーが nil かどうかを確認することでエラーをハンドル(取り扱い)します。
nil の error は成功したことを示し、 nilではない error は失敗したことを示します。
package main
import (
"fmt"
"strconv"
)
func main() {
i, err := strconv.Atoi("46")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
}
$ go run main.go
Converted integer: 46
#まとめ
主にメソッドとインターフェースを学んだGo入門第2回目でした。
次回へと続きます。
(参考)
https://go-tour-jp.appspot.com/list
(Go入門1)
【爆速】Go入門