Goの構造体とメソッドについて、よく分からなかったので、図を交えて理解してみました。
ポインタについて、理解があやふやな場合は、下記の記事を参照にしていただくとスムーズに理解できると思います。
構造体について
構造体とは、簡単にいうと変数を、ひとまとめにしたグループです。
構造体のイメージを図で表します。
コードで記述すると下記となります。
struct {
name string
age int
height int
weight int
}
構造体を利用するには、下記のように任意の名前をつける必要があります。
下記はtaroという名前をつけた例です。
var taro struct {
name string
age int
height int
weight int
}
goでは変数を宣言した場合、自動的に型に応じたゼロ値が初期値として格納されます。
package main
import "fmt"
func main() {
var n int
fmt.Println(n)
var b bool
fmt.Println(b)
}
0
false
構造体も同様に、それぞの変数に応じたゼロ値が初期値として格納されます。
package main
import "fmt"
func main() {
var taro struct {
name string
age int
height int
weight int
}
fmt.Println(taro)
}
{"" 0 0 0}
構造体に値を格納する。
構造体の中の変数に値を入れるには、下記の様に記載します。
構造体名.フィールド = 格納する値
実際にtaroに値を格納します。
package main
import "fmt"
func main() {
var taro struct {
name string
age int
height int
weight int
}
taro.name = "taro"
taro.age = 20
fmt.Println(taro)
}
{taro 20 0 0}
また、下記のように型と異なる値を格納した場合、コンパイルエラーとなりますが、
var n int
n = "1" //コンパイルエラー
fmt.Println(n)
構造体自体も構造体内の変数の型と異なる値を格納した場合も、コンパイルエラーとなります。
var taro struct {
name string
age int
height int
weight int
}
taro.name = "taro"
taro.age = "20" //コンパイルエラー
また、構造体の宣言と同時に初期値を入れるには下記のようにします。
var taro = struct {
name string
age int
height int
weight int
}{
name: "taro",
age: 20,
height: 170,
weight: 60,
}
複数の構造体を作ってみます。
//一人目
var taro = struct {
name string
age int
height int
weight int
}{
name: "taro",
age: 20,
height: 170,
weight: 60,
}
//二人目
var jiro = struct {
name string
age int
height int
weight int
}{
name: "jiro",
age: 30,
height: 175,
weight: 70,
}
//三人目
var saburo = struct {
name string
age int
height int
weight int
}{
name: "saburo",
age: 40,
height: 160,
weight: 80,
}
このままでは、同じ構造であるにもかかわらず毎回、構造体を定義しなければならず冗長です。
Goはtypeキーワードを使うことにより既存の型に別名をつけることが可能です。
例としてint型にsuujiという別名をつけます。
type suuji int
var n suuji = 1
構造体も、別名をつけ型として利用することができます。
type Human struct {
name string
age int
height int
weight int
}
型として名前をつけると以後は、型に応じた構造体を作成することが可能です。
var taro Human = Human{
name: "taro",
age: 20,
height: 170,
weight: 60,
}
var jiro Human = Human{
name: "jiro",
age: 30,
height: 175,
weight: 70,
}
各変数名を省略して、格納することもできます。(構造体が持つ変数の順序通りに記載する必要あり。)
var taro Human = Human{"taro", 20, 170, 60}
構造体にメソッドを追加する。
Goの構造体にメソッドを追加する方法は、他言語のオブジェクト指向プログラミングのクラスへのメソッド追加とイメージが異なります。
オブジェクト指向言語のクラスにメソッドを定義する場合のイメージ
Goでは構造体に対し、メソッドを後付けするイメージとなります。
Goで関数を定義する場合は、下記のようになります。
func sayHello(){
fmt.Println("hello")
}
構造体に、この関数をメソッドとして定義するためには、関数名の前にメソッドを追加したい対象の構造体を指定してあげます。指定する場所は関数名の前です。
func <<ここでメソッドを追加したい構造体を指定>> sayHello(){
fmt.Println("hello")
}
例えば先ほど作成したHumanに対し、メソッドを追加したい場合は下記のように記述します。
type Human struct {
name string
age int
height int
weight int
}
func (h Human) sayHello() {
fmt.Println("hello")
}
(h Human)のところでHuman型に対しsayHelloメソッドが定義されました。
Human型から作り出した構造体からsayHelloメソッドを呼び出してみます。
type Human struct {
name string
age int
height int
weight int
}
func (h Human) sayHello() {
fmt.Println("hello")
}
func main() {
var taro = Human{"taro", 20, 170, 60}
taro.sayHello()
}
hello
この構造体を指定する部分のhをレシーバ値、Humanの部分をレシーバ型と言います。
(レシーバー値 レシーバー型)
まさに、メソッドを構造体が受け取って実装するのですね。
これで、Human型から作成された構造体はsayHelloメソッドを持つようになりました。
type Human struct {
name string
age int
height int
weight int
}
func (h Human) sayHello() {
fmt.Println("hello")
}
func main() {
var taro = Human{"taro", 20, 170, 60}
var jiro = Human{"jiro", 30, 175, 70}
var saburo = Human{"saburo", 40, 160, 80}
taro.sayHello()
jiro.sayHello()
saburo.sayHello()
}
hello
hello
hello
Humanという型にsayHelloメソッドを追加するだけならレシーバー型だけで良さそうな気がします。
しかし、Humanは単なる型であるため、メソッド内で構造体の値を利用した実装はできません。
例えば、sayHelloメソッドが"hello + 構造体のname"を出力する振る舞いに変更された時、この「構造体のname」を取り出す必要があります。
このhの部分のレシーバー値は、実際にレシーバー型の型から作りだされた構造体自体が入ります。
↓イメージ
>> hellotaro
では次に構造体内部の変数の値を変更させてみようと思います。
package main
import "fmt"
type Human struct {
name string
age int
height int
weight int
}
func (h Human) changeName(familyName string) {
h.name = familyName + " " + h.name
}
func main() {
var taro = Human{"taro", 20, 170, 60}
taro.changeName("yamada")
fmt.Println(taro.name)
}
>> taro
意図としては、"yamada taro"と出力したかったのですが、できておりません。
もう一度レシーバー部分について、深堀りしてみます。
レシーバー部分は、実は 「メソッドの呼び出し元が格納される暗黙的な第1引数。」 です。
つまり、下記のように表せます。
//メソッド定義
func (h Human) changeName(familyName string) {
h.name = familyName + " " + h.name
}
//メソッド呼び出し
taro.changeName("yamada")
これは下記と同じ⬇️
func changeName(h Human, familyName string) {
h.name = familyName + " " + h.name
}
changeName(taro, "yamada")
つまり、こういう事ですね。⬇️
こう考えると、hとtaroは異なるメモリ上の値なので、いくらメソッド内でhの値を変えたところで、taroに影響しないのは当然です。
↑ について、曖昧な場合は、下記をみていただければ、なんとなく理解できるかもです。
では、どのようにすればtaroの内部の値を変更できるのか。
関数で考えると下記のようにすれば変更できそうです。
package main
import "fmt"
type Human struct {
name string
age int
height int
weight int
}
func changeName(h *Human, familyName string) {
(*h).name = familyName + " " + (*h).name
}
//hはポイント変数。ポイント変数に格納されたアドレスを持つメモリの値はHuman型
//*hでメモリの値を参照
//(*h).name = でメモリ上の実際の値が書き変わる。つまりtaroのnameが書き変わる。
func main() {
var taro = Human{"taro", 20, 170, 60}
changeName(&taro, "yamada")
//changeNameの第1引数にはtaroが格納されたメモリのアドレスを入れる。
fmt.Println(taro.name)
}
>> yamada taro
ということは、メソッドで記載すると下記のように定義すればできそうです。⬇️
>> yamada taro
これで、Human型から作られた構造体の内部の値を変更するメソッドが定義できました。
package main
import "fmt"
type Human struct {
name string
age int
height int
weight int
}
func (h *Human) changeName(familyName string) {
(*h).name = familyName + " " + (*h).name
}
func main() {
var taro = Human{"taro", 20, 170, 60}
var jiro = Human{"jiro", 30, 175, 70}
var saburo = Human{"saburo", 40, 160, 80}
(&taro).changeName("yamada")
(&jiro).changeName("tanaka")
(&saburo).changeName("suzuki")
fmt.Println(taro.name)
fmt.Println(jiro.name)
fmt.Println(saburo.name)
}
yamada taro
tanaka jiro
suzuki saburo
そして、メソッドの便利なところは、レシーバー型がポインタ型の場合は、呼び出し元がポイント型ではない場合(値型)でも自動的にポインタ型に変換(メモリのアドレス)して、暗黙的な第1引数に渡してくれる。 というところです。
よって、下記のように呼び出し元は、値かポイント型かを意識せずにメソッドを呼び出すことができます。
もちろん、逆にメソッドのレシーバー型が値型の場合は、ポインタ型で呼び出しても自動的に、値型が暗黙的に渡されます。
以上となります。
姉妹記事
参考にさせていただいた記事
とってもやさしいGo言語入門
https://zenn.dev/ak/articles/1fb628d82ed79b#%E6%A7%8B%E9%80%A0%E4%BD%93
#golang メソッド式とメソッド値
https://zenn.dev/spiegel/articles/20201212-method-value-and-expression