メソッドとは
メソッドと聞くとオブジェクト指向プログラミングを想像する人が多いと思います。Goにおいてのメソッドやオブジェクトについては、プログラミング言語 Goでは以下のように書かれています。
Goにおいてのオブジェクトはメソッドを持つ単なる変数であり、メソッドは特定の型に関連付けられた関数です。
オブジェクト指向プログラミングは、クライアントがオブジェクト内部表現に直接アクセスする必要がないようにするために、個々のデータ構造の特性と操作を表現するのにメソッドを使ってきた。
これでなるほど!となった人もいれば、わからん!となる人も居ると思います。わからなくても、以下に書いていくサンプルソースで多分意味がわかります。
まずはメソッドを書いてみよう
// 構造体(Calcという型)
type Calc struct {atai1, atai2 int}
// 普通の関数
func Add(q Calc) int {
return q.atai1 + q.atai2
}
// メソッド
func (p Calc) Add() int {
return p.atai1 + p.atai2
}
func main() {
q := Calc{8, 6} // 8 + 6 = 14
fmt.Println(Add(q)) // 14
p := Calc{3, 2} // 3 + 2 = 5
fmt.Println(p.Add()) // 5
}
Goにおいてメソッドは普通の関数宣言の変形で記述されます。ちなみに普通に実行すれば、結果はもちろん同じです。何が違うか?
先ほど書きましたが、変形で記述すれば普通の関数から変わります。
// 関数
func Add(q Calc) int {
return q.atai1 + q.atai2
}
func 関数名(引数)戻り値の型 {
// 処理
return
}
// メソッド
func (p Calc) Add() int {
return p.atai1 + p.atai2
}
func (レシーバ 型) 関数名(引数)戻り値の型 {
// 処理
return
}
大きな違いはレシーバの存在です。Calc型のメソッドであるCalc.Addという形で宣言したです。ちなみにメソッドにアクセスする時は、p.Add(レシーバ.メソッド)です。このような指定をセレクタと呼びます。
そして、気づいた人もいるかもしれませんが、同じ関数名で宣言してもエラーになりません。それは名前空間が違うからです。では、メソッドの色々な例を以下に書きます。
type Calc struct {atai1, atai2 int}
func (p Calc) Add() int {
return p.atai1 + p.atai2
}
func (p Calc) Sub() int {
return p.atai1 - p.atai2
}
func (p Calc) Multi() int {
return p.atai1 * p.atai2
}
func (p Calc) Div() int {
if p.atai2 == 0 {
return 0
}
return p.atai1 / p.atai2
}
func main() {
p := Calc{5, 2} // 5 ? 2
fmt.Println(p.Add()) // 7
fmt.Println(p.Sub()) // 3
fmt.Println(p.Multi()) // 10
fmt.Println(p.Div()) // 2
// もちろん型が同じであれば、引数のレシーバ名は自由です。
q := Calc{5, 2} // 5 ? 2
fmt.Println(q.Add()) // 7
}
この例に関して必要な説明は特にないと思います。ではクラスの説明〜と言いたいところなんですが、golangにはクラスという概念がありません。なので、メソッドは型に対して紐づくものまででそれ以上はありません。さすがgolangシンプル!!!
違う型のメソッドを使う(継承までいかないけど)
え?不便じゃね?となりますよね。実はgolang特有の特徴はちゃんとあります。それはどんな型であってもメソッドを定義することが出来る点です。また、以下のような書き方もできます。
type Calc struct{ atai1, atai2 int }
type Sums []Calc
// 足し算メソッド
func (p Calc) Add() int {
return p.atai1 + p.atai2
}
// 総和を求めるメソッド
func (s Sums) Adds() int {
ans := 0
for _, s := range s {
ans += s.Add()
}
return ans
}
func main() {
sums := Sums{
{1, 3},
{2, 4},
{3, 5},
}
fmt.Println(sums.Adds()) // 18
}
違う型のメソッドであっても、このように中でうまいこと使えば他のメソッドと組み合わせることができます。
関数ではなく、メソッドを使う恩恵
今回の例のような「Add」を例えば同じ加算でもちょっと使い道が違うという場合があるでしょう。他にもAPIヘノリクエストで同じrequestでも種類が違う!など。そういう場合、関数などでは「xxxRequest」「○○○Request」のように名前を分けなければいけませんが、メソッドでは、型が前につくので、「○○○.Request」というシンプルな形で済みますし、無駄にメソッド名が長くなることを防げます。
また、コード量がどんどん増えた際にも、値に紐づくメソッドは、変更時の影響範囲が分かりやすかったりもします。
ポインタレシーバ
レシーバはもちろんポインタで渡すことが出来ます。ここで大事なのはなぜポインタで渡す必要があるのかを理解するということです。goは通常のデータの値の受け渡しは、値渡しの形になるので、データのコピーを取って〜渡して〜という形になるので、メモリ上に同じデータサイズが2つあることになります。
func (p *Calc) Sub() int {
return p.atai1 - p.atai2
}
func main() {
calc := &Calc{1, 2}
fmt.Println(calc.Add()) // 3
}
書き方は特に難しいことはありません。単純にポインタ指定すればいいだけです。これによるメリットを理解するには、しっかりと値渡しと参照渡しを理解する必要がありますので、値渡しと参照渡しの意味が分からない人はぐぐって調べてみてください!
ここまでの説明で分かることですが、ポインタにしないと、レシーバの値を影響させることが出来ません。
これは慣習の話になりますが、ある型でどれかのメソッドでポインタレシーバを使う場合は全部のメソッドでポインタ指定することが望まれるらしいです。
カプセル化
オブジェクトの変数やメソッドはクライアントからアクセス不可能な状態をカプセル化と言います。いまからそれがどういった状態なのか、以下にソースで例を示します。
package main
import "./calc"
func main() {
calc := &calc.Values{}
calc.Atai1 = 1
calc.Atai2 = 2
calc.Add() // 3
calc.Ans() // print 3
calc.ans = 1 // アクセスできない
}
package calc
import "fmt"
type Values struct {
Atai1 int
Atai2 int
ans int // カプセル化されている
}
func (p *Values) Add() {
p.ans = p.Atai1 + p.Atai2
}
func (p *Values) Sub() {
p.ans = p.Atai1 + p.Atai2
}
func (p *Values) Ans() {
fmt.Println(p.ans)
}
golangにおいて、公開範囲を決めるのは名前の最初を大文字にするかしないかの差だけです。ちなみに小文字である「ans」はアクセスが出来ません。このアクセス範囲への理解があまり出来ていない人が居ましたら、こちらを参考にしてください。カプセル化のメリットについては、詳しく書くと長くなるので、他の記事でまとめてみたいと思います。