メソッドは、関数名の前に特別なパラメータを置いて宣言されます。 そのパラメータは、関数にその型のパラメータを付与します。
私達の最初のメソッドを見ていきましょう。
https://github.com/adonovan/gopl.io/blob/master/ch6/geometry/geometry.go
func (p Point) Distance(q Point) float64
特別なパラメータであるp
は、メソッドのレシーバと呼ばれ、初期のオブジェクト志向言語からの遺物では、"オブジェクトにメッセージを送る(sending a message to an object)"と呼ばれます。
c.f. OCaml
Goでは、this
やself
のような特別な名前を使いません。私たちは、他のパラメータと同じようにレシーバの名前を選べます。
レシーバの名前は頻繁に使われるので、なるべく短く、かつ全てのメソッドで統一されているのが良いでしょう。一般的なものは、最初の文字を取るものです。例えば、Point
であれば、p
のように。
メソッド呼び出しにおいて、メソッド名の前にレシーバがおかれます。これは、レシーバがメソッド名の前に現れるという点で、メソッドの宣言と似ています。
p := Point{1, 2}
q := Point{4, 5}
fmt.Println(Distance(p, q))
fmt.Println(q.Distance(q))
上の2つに、コンフリクトはありません。1つ目は、パッケージレベルのgepmetry.Distance
という関数の呼び出しで、2つ目は、Point
という型のメソッドの呼び出しです。
p.Distance
という表現は、selector と呼ばれます、それはPoint
型のp
の為に、適切なDistance
というメソッドを選択しているからです。セレクターは、p.X
のような構造体のフィールドを選択することとしても使われます。メソッドとフィールドは、同じ名前空間に存在していて、Point
型の構造体のX
というメソッドを宣言することは、不明瞭でコンパイラがリジェクトします。
それぞれの型はメソッドの為のそれぞれの名前空間を持っており、他の型に属している限り、Distance
という名前を使うことが出来ます。
type Path []Point
// Distance returns the distance traveled along the path.
func (path Path) Distance() float64 {
sum := 0.0
for i := range path {
if i > 0 {
sum += path[i-1].Distance(path[i])
}
}
return sum
}
Path
はslice型で、Point
のような構造体ではない為、メソッドを定義することが出来ます。どんな型に対してもメソッドを紐付けれる点において、Goは他の多くのオブジェクト志向言語とは異なります。それは時々、numbers,strings,slices,maps
などのようなシンプルな型に対して、付加的な振る舞いを定義出来て便利です。メソッドは、同じパッケージ内で定義されているいかなる型にも宣言できます、基底型がポインターやインターフェイスであるものではない限り。
perim := Path{
{1, 1},
{5, 1},
{5, 4},
{1, 1},
}
fmt.Println(perim.Distance())
特定の型に対する全てのメソッドはユニークでないといけません、しかし、型が異なる場合、同じ名前のメソッドが使えます。曖昧さを避けるために、例えばPathDistance
のように修飾する必要はありません。ここに、通常の関数を使う以上にメソッドを使う一つ目の利益があります: メソッドの名前は短い。この利益は、パッケージの外を出ると更に拡大します、短い名前でかつ、パッケージ名を省略できます。
関数を呼び出すと各引数のコピーを作成するので、もし変数を更新する必要やコピーを出来るなら避けたいぐらい大きい引数の場合、ポインタを使って変数のアドレスを渡すべきです。レシーバの値を更新する必要のあるメソッドの場合も同様です。*Point
のようにポインタにメソッドをアタッチします。
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
このメソッドの名前は、(*Point).ScaleBy
です。
括弧()
は必要です。これがなければ、*(Point.ScaleBy)
と解釈されてしまうからです。
現実世界のプログラムでは、慣習的にPoint
のメソッドがポインタをレシーバとしているなら、全てのPoint
のメソッドをポインタをレシーバとすべきです、たとえ一つでも厳格にそれが必要ではないとしても。
(*Point).ScaleBy
メソッドは*Point
をレシーバとして以下のように、呼び出せます。
r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r)
p := Point{1, 2}
pptr := &p
pptr.ScaleBy(2)
fmt.Println(p)
p := Point{1, 2}
(&p).ScaleBy(2)
fmt.Println(p)
しかし、2,3は不格好である。幸運にもここでは言語がサポートしてくれているが。レシーバーのp
がPoint
という型の値だったとしても、以下の短い呼び出しで使うことが可能である。
p.ScaleBy(2)
そして、コンパイラは暗黙的に変数を&p
と解釈して振る舞う。
これは、p.X
のような構造体のフィールドやperim[0]
のような配列やスライスを含む、値のときのみ作用します。
私たちは*Point
のメソッドをレシーバーのPoint
をアドレス指定が出来ない場合に呼び出すことが出来ません、それは一時変数のアドレスを確保することが出来ないためです。
Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal
しかし、私たちはPoint.Distance
という形で*Point
をレシーバとするメソッドを呼び出し可能です、それは変数からアドレスを知る方法があるからです。
コンパイラは暗黙的に*
という修飾子を挿入してくれます。下記の2つの関数呼び出しは等価です。
pptr.Distance(q)
(*pptr).Distance(q)
もし全てのメソッドが、*T
ではなくT
という型をレシーバとしていた場合、その型のインスタンスのコピーを安全に行い、全てのメソッド呼び出しでコピーが必要となります。
例えば、time.Duration
は、大量にコピーされています、関数の引数として。
https://golang.org/src/time/time.go
しかし、もしポインタをレシーバとするメソッドがあるなら、T
というインスタンスをコピーすることを避けるべきです。それは内部の不変条件(internal invariants)に反するためです。
幾つかの関数は、、それらをレシーバとするメソッドのために、nilポインタを引数として認めています。特にnil
はmapsやslicesのゼロバリューを意味する場合です。
下の例では、nilは空のlistを表します。
// An IntList is linked list of intergers.
// A nil *IntList represents the empty list.
type IntList Struct {
Value int
Tail *IntList
}
func (list *IntList) Sum() int {
if list == nil {
return 0
}
return list.Value + list.Tail.Sum()
}
レシーバーの値としてnilを許可するメソッドをもつ型を定義したなら、上のように明示的にドキュメンテーションコメントとして書いておくべきです。
net/url
パッケージのValues
もこのように定義されています。
https://golang.org/src/net/url/url.go
https://github.com/adonovan/gopl.io/blob/master/ch6/urlvalues/main.go
type Point struct{ X, Y float64 }
type ColoredPoint struct {
Point
Color color.RGBA
}
ColoredPoint
にPoint
を埋め込むことで、X,
Y`を含む、3つのフィールを持つ構造体を定義できます。
Point
と明示的に書かずとも、Point
のフィールドを扱うことが出来ます。
var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X)
cp.Point.Y = 2
fmt.Println(cp.Y)
同じメカニズムはPoint
のメソッドに対しても適応出来ます。ColoredPoint
をレシーバとして、埋め込んだPoint
のもつメソッドを呼び出し可能です。
https://github.com/adonovan/gopl.io/blob/master/ch6/coloredpoint/main.go
オブジェクト志向言語に親しんだ読者の型なら、Point
をベースクラスとして、ColoredPoint
がサブクラス、派生クラスと考えたり、まるでColoredPoint
はPoint
であるかのように解釈するでしょう。しかしそれらは間違いです。上で、Distance
を呼び出した時に注目してください。Distance
はPoint
型をパラメータとしています、そして、q
はPoint
型ではなく、埋め込みとしてPoint
のフィールドを持っています、私たちは明示的にそれを指定しなければいけません。q
を渡すとエラーが発生します。
p.Distance(q) compile error: cannnot use q (ColoredPoint) as Point
ColoredPoint
はPoint
ではありませんが、Point
を持っています。Distance
とScaleBy
というPoint
から割り当てられた2つのメソッドを持ちます。もし、あなたが実装の観点から考えるなら、委譲によるフィールドはコンパイラに追加の下記と等価のラッパーメソッドを追加させるよう命令します。
func (p ColoredPoint) Distance(q Point) float64 {
return p.Point.Distance(q)
}
func (p *ColoredPoint) ScaleBy(factor float64) {
p.Point.ScaleBy(factor)
}
一つ目のラッパーメソッド内で、Point.Distance
が呼ばれたとき、レシーバーの値はP
ではなくp.Point
なので、Point
が埋め込まれたColoredPoint
にアクセスする術はありません。無名フィールドの型はポインタであるべきでしょう、このようなケースでフィールドやメソッドがオブジェクトを指し示すことで、直接参照できるようになるので。
共通の構造体を共有して、オブジェクト間を直接変更することも出来ます。
type ColoredPoint struct {
*Point
Color color.RGBA
}
p := ColoredPoint{Point{1, 1}, red}
q := ColoredPoint{Point{5, 4}, blue}
q.Point = p.Point // p and q now share the same Point
構造体は無名フィールドを一つ以上持つことも出来ます。
type ColoredPoint struct {
Point
color.RGBA
}
上の型は、Point
とRGBA
の全てのメソッドをもち、追加のメソッドもColoredPoint
から直接、宣言できます。
もし、同じランクから2つのメソッドが割り当てられた場合、セレクターが曖昧なので、コンパイラはエラーを返します。
埋め込みのおかげで、無名の構造体を使ってメソッドを実装することは時々便利です。 以下では、mapとそれをガードするmutexの良い例を示します。
var cache = struct {
sync.mutex
mapping map[string]string
} {
mapping: make(map[string]string),
}
func Lookup(key string) string {
cache.Lock()
v := cache.mapping[key]
cache.Unlock()
return v
}
p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance // method value
fmt.Println(distanceFromP(q)) // "5"
var origin Point {0, 0}
fmt.Println(distanceFromP(origin)) // "2.23606797749979", √5
メソッド変数は、パッケージAPIとして関数変数を呼び出す際に便利です、クライアントは特定のレシーバのメソッドを関数のように振る舞うことを期待しています。
type Rocket struct { /* .... */ }
func (r *Rocket) Launch() { /* ... */ }
r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() })
メソッドバリューシンタックスはもっと短い。
time.AfterFunc(10 * time.Second, r.Launch)
メソッド変数に関係しているものとして、メソッド式があります。メソッドを呼び出した際、通常の関数とは対象的に、セレクターのシンタックスを用いてレシーバーを指定する必要があります。
p := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance // method expression
fmt.Println(distance(p, q)) // "5"
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"
scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p) // "{2, 4}"
fmt.Printf("%T\n", scale) // "func(*Point, float64)"
同じ型に属している複数のメソッドの中から、メソッドを選択して変数として表す必要がある際に便利です。
https://github.com/adonovan/gopl.io/blob/master/ch6/intset/intset.go
もしクライアントからアクセス不可にしたい場合、オブジェクトの変数やメソッドは、カプセル化すべきだと言われます。カプセル化は、時々_情報隠ぺい_と言われ、オブジェクト志向の重要項目です。
Goはそのメカニズムを命名で制御します。:大文字で始まる識別子はパッケージ外に持ち出すことが出来、大文字で始まらないものはそうではない。同じメカニズムでパッケージ内の構造体のフィールドやメソッド等のメンバへのアクセスも制御しています。結果的にオブジェクトをカプセル化するために、構造体を作らなければならない。
このため、先程のIntSet
は一つのフィールドしか持たないが、構造体として定義されている。
type IntSet struct {
words []uint64
}
type IntSet []uint64
上記のように定義しても本質的には同じだが、これは他のパッケージのクライアントから、sliceを直接変更することを許可する。
ネームベースのメカニズムの違う側面として、カプセル化のまとまりはパッケージ単位で、他の多くの言語と違い型単位ではない。構造体のフィールドは同じパッケージ内からは見えている。
カプセル化には3つの利点がある。 一つ目は、クライアントが直接オブジェクトの値を変更できないので、変数の変更される可能性を理解出来少ないステートメントステートメントの検査で済む。 二つ目は、実装を隠蔽することで、クライアントが実装に依存してしまうことを防ぎ、デザイナーにAPIの互換性を壊すことなく内部の実装を進化させる自由をもたらす。 三つ目は、多くのケースで最も重要で、クライアントが任意でオブジェクト変数を設定することを防ぐ。オブジェクトの変数は同じパッケージ内の関数によって設定されるので、パッケージの作成者がオブジェクトの不変性をこれらの関数によって保持できる。
getterメソッドに命名する際、私たちは大抵Get
プレフィックスを省略する。これらの参照は簡潔で、全てのメソッドに拡張でき、フィールドへのアクセサーだけでなく、Fetch
, Find
やLookup
のような他の冗長なプレフィックスでもそうである。