Go でメソッドを使用する
Go のメソッドは特殊な関数であり、関数名の前に追加のパラメーターを含める必要があるという単純な違いがあります。 この追加のパラメーターは、"レシーバー" として知られています。
メソッドは、関数をグループ化してカスタム型に関連付ける場合に便利です。 Go のこの方法は、他のプログラミング言語でクラスを作成する場合と似ています。これは、オブジェクト指向プログラミング (OOP) モデルの特定の機能 (埋め込み、オーバーロード、カプセル化など) を実装できるためです。
Go のメソッドが重要である理由を理解するために、まずはその宣言方法から説明します。
メソッドを宣言する
これまで、Go で作成できる別のカスタム型として、構造体のみを使用してきました。 このモジュールでは、メソッドを追加することで、作成した構造体に動作を追加できるということを学びます。
メソッドを宣言する構文は次のようになります。
func (variable type) MethodName(parameters ...) {
// method functionality
}
ただし、メソッドを宣言する前に、構造体を作成する必要があります。 たとえば、ジオメトリ パッケージを作成し、そのパッケージの一部として、triangle
という三角形の構造体を作成するとします。 次に、メソッドを使用して、その三角形の外周を計算します。 Go では、これを次のように表すことができます。
type triangle struct {
size int
}
func (t triangle) perimeter() int {
return t.size * 3
}
通常の構造体のように見えますが、perimeter()
関数には関数名の前に triangle
型の追加パラメーターがあります。 このレシーバーは、構造体を使用すると、次のような関数を呼び出せることを意味します。
func main() {
t := triangle{3}
fmt.Println("Perimeter:", t.perimeter())
}
いつもと同じように perimeter()
関数を呼び出そうとすると、実行されません。これは、関数のシグネチャによってレシーバーが必要であると示されているためです。 そのメソッドを呼び出す唯一の方法は、最初に構造体を宣言することです。これにより、メソッドにアクセスできるようになります。 別の構造体に属していれば、メソッドに同じ名前を付けることもできます。 たとえば、次のように perimeter()
関数を使用して square
構造体を宣言できます。
package main
import "fmt"
type triangle struct {
size int
}
type square struct {
size int
}
func (t triangle) perimeter() int {
return t.size * 3
}
func (s square) perimeter() int {
return s.size * 4
}
func main() {
t := triangle{3}
s := square{4}
fmt.Println("Perimeter (triangle):", t.perimeter())
fmt.Println("Perimeter (square):", s.perimeter())
}
前述のコードを実行すると、エラーが発生せず、次の出力が表示されます。
Perimeter (triangle): 9
Perimeter (square): 16
perimeter()
関数は 2 回呼び出されますが、コンパイラではレシーバーの型に基づいて呼び出す関数が決定されます。 この動作は、複数のパッケージにおいて関数の一貫性と短い名前を保持するのに役立ち、パッケージ名がプレフィックスとして含まれることを回避できます。 次のユニットでインターフェイスについて説明するときに、なぜこの動作が重要になるのかについて説明します。
メソッド内のポインター
メソッドで変数を更新する必要がある場合があります。 または、メソッドの引数が大きすぎる場合は、コピーを避ける必要があることがあります。 このような場合、ポインターを使用して、変数のアドレスを渡す必要があります。 前のモジュールでポインターについて説明した際には、Go で関数を呼び出すと、そのたびに各引数値のコピーが作成され使用されるということを述べました。
メソッドでレシーバーの変数を更新する必要がある場合も、同じ動作が実行されます。 たとえば、三角形のサイズを 2 倍にする新しいメソッドを作成するとします。 次のように、レシーバーの変数でポインターを使用する必要があります。
func (t *triangle) doubleSize() {
t.size *= 2
}
次のように、メソッドが動作することを実証できます。
func main() {
t := triangle{3}
t.doubleSize()
fmt.Println("Size:", t.size)
fmt.Println("Perimeter:", t.perimeter())
}
前述のコードを実行すると、次の出力が表示されます。
Size: 6
Perimeter: 18
メソッドがレシーバーの情報にアクセスするだけの場合、レシーバーの変数にポインターは必要ありません。 しかし、Go の規則では、構造体のいずれかのメソッドにポインター レシーバーがある場合、その構造体のすべてのメソッドにポインター レシーバーが必要であると定められています。 構造体のメソッドで必要がない場合でも同様です。
他の型のメソッドを宣言する
メソッドの重要な側面の 1 つに、構造体などのカスタム型にだけでなく、任意の型に対しても定義するという点があります。 ただし、別のパッケージに属する型から構造体を定義することはできません。 そのため、string
などの基本型に対してメソッドを作成することはできません。
ただし、ハッキングを使用して基本型からカスタム型を作成し、基本型であるかのように使用することができます。 たとえば、文字列を小文字から大文字に変換するメソッドを作成するとします。 次のように記述します。
package main
import (
"fmt"
"strings"
)
type upperstring string
func (s upperstring) Upper() string {
return strings.ToUpper(string(s))
}
func main() {
s := upperstring("Learning Go!")
fmt.Println(s)
fmt.Println(s.Upper())
}
前述のコードを実行すると、次の出力が表示されます。
Learning Go!
LEARNING GO!
新しいオブジェクト s
は、最初にその値を出力するときに文字列であるかのように使用できます。 次に、Upper
メソッドを呼び出すと、s
によってその文字列がすべて大文字で出力されます。
メソッドを埋め込む
前のモジュールでは、1 つの構造体でプロパティを使用し、同じプロパティを別の構造体に埋め込むことができることを学習しました。 つまり、1 つの構造体のプロパティを再利用して繰り返しを回避し、コード ベースで一貫性を維持することができます。 同様のアイデアがメソッドにも適用されます。 レシーバーが異なる場合でも、埋め込まれた構造体のメソッドを呼び出すことができます。
たとえば、色を含めるロジックを持つ新しい三角形の構造体を作成するとします。 また、前に宣言した三角形の構造体を引き続き使用するとします。 したがって、色付きの三角形の構造体は次のようになります。
type coloredTriangle struct {
triangle
color string
}
次に、以下のように coloredTriangle
構造体を初期化して、triangle
構造体から perimeter()
メソッドを呼び出し、そのフィールドにアクセスすることもできます。
func main() {
t := coloredTriangle{triangle{3}, "blue"}
fmt.Println("Size:", t.size)
fmt.Println("Perimeter", t.perimeter())
}
プログラムに前述の変更を追加し、埋め込みがどのように機能しているのかを確認します。 前述のような main()
メソッドを含むプログラムを実行すると、次の出力が表示されます。
Size: 3
Perimeter 9
Java や C++ などの OOP 言語を使い慣れている場合は、triangle
構造体が基底クラスのように見え、coloredTriangle
がサブクラス (継承など) であると思われるかもしれませんが、これは正しくありません。 実際には、次に示すように、ラッパー メソッドを作成することによって、Go のコンパイラにより perimeter()
メソッドが昇格します。
func (t coloredTriangle) perimeter() int {
return t.triangle.perimeter()
}
レシーバーが coloredTriangle
であることに注意してください。これにより、triangle フィールドから perimeter()
メソッドが呼び出されます。 メリットは、前述のメソッドを作成する必要がないという点です。 それも可能ですが、Go によって自動的に行われます。 前述の例は、学習目的でのみ示しました。
メソッドをオーバーロードする
前に説明した triangle
の例に戻りましょう。 coloredTriangle
構造体の perimeter()
メソッドの実装を変更する場合、どうしたらよいでしょうか。 同じ名前を持つ 2 つの関数を使用することはできません。 ただし、メソッドには追加のパラメーター (レシーバー) が必要であるため、使用するレシーバーに固有なものである限り、同じ名前のメソッドを使用することができます。 この違いを利用することが、メソッドをオーバーロードする方法です。
つまり、その動作を変更する場合は、説明したラッパー メソッドを記述できます。 色付きの三角形の外周が通常の三角形の外周の 2 倍である場合、コードは次のようになります。
func (t coloredTriangle) perimeter() int {
return t.size * 3 * 2
}
前に記述した main()
メソッドで他の変更は行いません。次のようになります。
func main() {
t := coloredTriangle{triangle{3}, "blue"}
fmt.Println("Size:", t.size)
fmt.Println("Perimeter", t.perimeter())
}
これを実行すると、異なる出力が表示されます。
Size: 3
Perimeter 18
ただし、triangle
構造体から perimeter()
メソッドを呼び出す必要がある場合は、次のように明示的にアクセスすることで実行できます。
func main() {
t := coloredTriangle{triangle{3}, "blue"}
fmt.Println("Size:", t.size)
fmt.Println("Perimeter (colored)", t.perimeter())
fmt.Println("Perimeter (normal)", t.triangle.perimeter())
}
このコードを実行すると、次の出力が表示されます。
Size: 3
Perimeter (colored) 18
Perimeter (normal) 9
お気づきのように、Go ではメソッドを "オーバーライド" しても、必要に応じて "元の" ものにアクセスすることができます。
メソッドでのカプセル化
カプセル化すると、メソッドがオブジェクトの呼び出し元 (クライアント) にアクセスできなくなります。 通常、他のプログラミング言語では、private
または public
のキーワードをメソッド名の前に配置します。 Go では、メソッドを公開にするには大文字の識別子だけを、メソッドを非公開にするには子文字の識別子を使用する必要があります。
Go のカプセル化は、パッケージ間でのみ有効になります。 つまり、パッケージ自体ではなく、別のパッケージから実装の詳細のみを非表示にすることができます。
試してみるには、次のように新しいパッケージ geometry
を作成し、三角形の構造体をそこに移動します。
package geometry
type Triangle struct {
size int
}
func (t *Triangle) doubleSize() {
t.size *= 2
}
func (t *Triangle) SetSize(size int) {
t.size = size
}
func (t *Triangle) Perimeter() int {
t.doubleSize()
return t.size * 3
}
次のように、前述のパッケージを使用できます。
func main() {
t := geometry.Triangle{}
t.SetSize(3)
fmt.Println("Perimeter", t.Perimeter())
}
次の出力が表示されます。
Perimeter 18
次のように、main()
関数から size
フィールドまたは doubleSize()
メソッドを呼び出そうとすると、プログラムは動作しません。
func main() {
t := geometry.Triangle{}
t.SetSize(3)
fmt.Println("Size", t.size)
fmt.Println("Perimeter", t.Perimeter())
}
前述のコードを実行すると、次のエラーが表示されます。
./main.go:12:23: t.size undefined (cannot refer to unexported field or method size)