Goを学んでいてメソッドが出てきたんですが、関数とメソッドの違いとかポインタレシーバとかembeddedとか、なんか整理できなくなってきたのでこちらでまとめたいと思います。
また、この記事ではメソッドと関数と言う言葉は明確に使い分けしています。
メソッドと関数の違い
とりあえず、下のサンプルコードを見たあとで説明をしていきます。
関数
package main
import (
"fmt"
)
type Vertex struct {
X, Y int
}
func Calc(v Vertex) int {
return v.X * v.Y
}
func main() {
v := Vertex{10, 5}
fmt.Println(Calc(v)) // => 50
}
メソッド
func(レシーバ引数 型) 関数名(引数: option) 返り値: option {
// コード
}
package main
import (
"fmt"
)
type Vertex struct {
X, Y int
}
func (v Vertex) Calc int {
return v.X * v.Y
}
func main() {
sample := Vertex{10, 5}
fmt.Println(sample.Calc()) // => 50
}
メソッドとは、レシーバ引数を伴う関数のこと。
func
と 関数名
の間にレシーバ引数をとります。
上記の例では(v Vertex)
がレシーバ引数で、v
という名前のVertex
(構造体型)のレシーバ引数を持っていると言えます。
こうすることにより、Calc
(関数)はVertex
(構造体型)に紐付けられて、sample.Calc()
と呼び出すことができるわけです。
ちなみに、この紐付けられた構造体の方をレシーバと呼ぶみたいです。
逆に、関数は構造体に紐付けられていないもの、つまりレシーバ引数を伴わないもののことをいいます。
値レシーバとポインタレシーバ
値レシーバ
上記のメソッドのコードのレシーバは値レシーバと呼びます。
func (v Vertex) Calc int {
return v.X * v.Y
}
(v Vertex)
は値レシーバ。
(v *Vertex)
だとポインタレシーバ。
ポインタレシーバ
package main
import (
"fmt"
)
type Vertex struct {
X, Y int
}
// 値レシーバ
func (v Vertex) Calc int {
return v.X * v.Y
}
// ポインタレシーバ
func (v *Vertex) Scale(i int) {
v.X = v.X * i
v.Y = v.Y * i
}
func main() {
sample := Vertex{10, 5}
fmt.Println(sample.Calc()) // => 50
sample.Scale(10)
fmt.Println(sample) // => {100 50}
fmt.Println(sample.Calc()) // 5000
}
上記のコードを見てみると、Scale
メソッドではレシーバが(v *Vertex)
とポインタ型になっています。
暗黙的に変換されている
値型(int型やstring型など)とポインタ型間の型変換をコンパイラが暗黙的に変換してくれるみたいです。
例えば、構造体型のsample
がレシーバの型がポインタ型のScale
メソッドを問題なく呼び出しています。
これはコンパイラが構造体型をポインタ型に暗黙的変換を行ってくれているからです。
またメソッドが(v *Vertex)
の様なポインタレシーバの場合は、v.X = ~
のv.X
部分は暗黙的に(*v).X
として扱われます。
なので、v.X = v.X * i
でも元の変数の値が変更されているわけです。
ここら辺はこちらの記事が非常に参考になりました!
値レシーバとポインタレシーバのまとめ
値レシーバは、メソッドで元の変数のコピーを操作していることになります。要するに、関数の引数と同じ振る舞いをすると言うことです。
なので、メソッド内で値を変更しても変更されるのはメソッド内でコピーされたものだけで、元の変数は変更されません。
しかし、ポインタレシーバの場合は、ポインタ型を渡しているのでメソッド内で値を変更したら、元の変数も変更されます。
(ポインタがよく分かっていないと言う方は【Go】ポインタについてまとめてみたを読んでみてください。)
インターフェース(interface)
以下のコードを使用してインターフェースを説明していきます。
package main
import (
"fmt"
)
type Human interface {
Greeting() string
}
type User struct {
Name string
}
func (user User) Greeting() string {
text := "私の名前は" + user.Name + "です。"
return text
}
func main() {
var hiroki Human = User{"宏樹"}
fmt.Println(hiroki.Greeting()) // => 私の名前は宏樹です。
}
定義方法
インターフェースは**type
とinterface
**を使用して定義します。
また、Interfaces - A Tour of Goでは
interface(インタフェース)型は、メソッドのシグニチャの集まりで定義します。
と記載されています。
(シグネチャは状況によって色々な意味を持つみたいですが、ここでの意味としては「メソッド名」「引数の型」「返り値の型」をシグネチャと呼ぶっぽいです。)
type interface名 interface {
メソッド名(引数の型: option) 返り値の型
}
type
を使用していることからも分かる通り、インタフェース(interface)型という型の1つです。
上のコードでは
type Human interface {
Greeting() string
}
という部分がインターフェースの定義部分にあたります。
使い方
このインターフェースは型の1つなので変数を宣言する時に使うことができます。
// Humanというインターフェース型
var hiroki Human = User{"宏樹"}
これで変数hiroki
はHuman
というインターフェース型の構造体となります。
このインターフェース型を使用した変数は、インターフェースで定義したメソッドを使用する必要があります。
つまりこの場合、変数hiroki
にはGreeting
メソッドが存在しなければならないので、このままだとエラーが表示されます。
なので、この構造体User
にGreeting
メソッドを紐付けてあげれば良いわけです。
func (user User) Greeting() string {
text := "私の名前は" + user.Name + "です。"
return text
}
上の「メソッドと関数の違い」の章で書いた様に、こうすることでGreeting
メソッドはUser
に紐付けられます。
これでエラーは表示されなくなり、hiroki.Greeting()
という風な呼び出し方が可能になります。
どんな型でも受け入れる
interfaceを使うとどんな型でも受け入れることができます。
例えば以下のコードはdo
という関数にinterface型
の引数と返り値を取っています。
package main
import (
"fmt"
)
// 引数と返り値の型はなんでもOK
func do(any interface{}) interface{} {
return any
}
func main() {
fmt.Println(do(100)) // => 100
fmt.Println(do("test")) // => test
fmt.Println(do(true)) // => true
}
型アサーション
どうやらinterface型
はどんな型でも受け入れることができるみたいですが、引数で受け取った値は元の型を失うみたいです。
なので、そのまま文字列を連結したり、数値を計算する場合はエラーが発生します。
そこで使用するのが型アサーションです。
型アサーションとは、失った元の型をチェックしてくれる仕組みです。
わかりにくいかもしれないので実際のコードを見てみましょう。
変数.(型)
package main
import (
"fmt"
)
// 型アサーション
func do(i interface{}) interface{} {
return "私の名前は" + i.(string) + "です。"
}
func main() {
fmt.Println(do("宏樹")) // => 私の名前は宏樹です。
}
i
はインターフェース型で元の型を失っていましたが、i.(string)
とすることによりstring型というチェックがされて正常に実行されます。
ちなみに、do(100)
などの他の型を入れるとpanicエラーを起こします。
型switch
上の例では、インターフェース型でどんな型でも受け取れたけど、結局string型の場合はstring型用の処理しかできませんでした。これだと普通に引数をstring型として受け取った方が良いと思います。
そこで使用するのが型switchというものです。
型switchを使用することにより、string型の場合はstring型用の処理、int型の場合はint型用の処理、という風に型によって処理を分岐することが可能になります。
switch 変数名 := インターフェース型の変数.(type) {
case 型:
// 処理
default:
// 処理
}
package main
import (
"fmt"
)
func do(i interface{}) interface{} {
// 型アサーションをしてvに代入
switch v := i.(type) {
case string:
return "私の名前は" + v + "です。"
case int:
return v * 100
default:
return "その他の型です。"
}
}
func main() {
fmt.Println(do("宏樹")) // => 私の名前は宏樹です。
fmt.Println(do(100)) // => 10000
fmt.Println(do(false)) // => その他の型です。
}
インターフェースのまとめ
- インターフェースは
type
とinterface
を使用することによって定義できる。 - 定義したインターフェース型を使用した構造体は、インターフェース内に存在するメソッドを持っていなければならない。(= 紐付いている必要がある。)
- どんな型でも受け入れることができる様になる
- 型アサーションを使って元の型の情報をチェックできる
- 型switchによって型ごとに処理を振り分けることができる
参考サイト
メソッド
Methods - A Tour of Go
関数とメソッドの違い【golang】
値レシーバとポインタレシーバ
Go 言語の値レシーバとポインタレシーバ
インターフェース
【Goのやさしい記事】Goのインターフェースを10分で学ぼう
インタフェースの実装パターン #golang
【Golang】Golangのinterfaceで知っておくとお得なTips
Signature (シグネチャ) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
Go言語 - 空インターフェースと型アサーション