はじめに
船井総研デジタルのoswです。業務でGo言語を使うことになったのでこれから学習していきます。その備忘録です。参考になる方がいらっしゃれば幸いです。
対象読者
- これからGo言語を学習する方
- 既に他の言語で基本構文を学習されている方
学習環境
学習環境は次のようになっています。この環境の構築メモは下記記事にまとめてあります。ご興味がある方はご参照ください。
- Windows 11 Home / 22H2
- VSCode / 1.72.2
- go version go1.19.2 windows/amd64
- git version 2.38.0.windows.1
前回までの学習
前回は 関数、無名関数、関数型、クロージャ を学習しました。
メソッド
一般にメソッドというとクラス内に定義された関数をイメージすると思いますが、Goにクラスはありません。しかし、Goでは型に関数を紐づけることができるようで、その型に関連付けられた関数を「メソッド」と呼ぶようです。
メソッドの宣言は構文をご覧になっていただくとし、紐づける型を宣言に組み込みます。紐付けられた変数をレシーバと呼びます。レシーバの型は同じパッケージ内に存在するものでなければならないようです。
メソッドの存在意義ですが、Goにはクラスがないため、データとそれに関連する処理を紐づけるためにこの方針になっているようです。通常の関数に引数として処理対象のデータを渡すのではなく、それに関連する処理ならばいっそ型に関連づけてしまえ、ということのようです。クラスの責務とかその辺の話と同じ感じなのだと思います。
// レシーバがポインタではないため、値渡し
func (レシーバ 型) メソッド名(引数 型, 略...) 戻り値の型 {
// レシーバが構造体ならフィールドへアクセス
レシーバ変数.フィールド1
レシーバ変数.フィールド2
return 戻り値
}
// レシーバがポインタのため、参照渡し
func (レシーバ *型) メソッド名(引数 型, 略...) 戻り値の型 {
// レシーバが構造体ならフィールドへアクセス
レシーバ変数.フィールド1
レシーバ変数.フィールド2
return 戻り値
}
メソッド呼び出しの際、レシーバに渡される値は値渡しです。そのため、変更を反映させる場合は構文にあるようにレシーバをポインタとして宣言することで実現可能です。(この時、ポインタとして宣言したレシーバをポインタレシーバ、通常の変数として宣言したものを変数レシーバ、というようです)
また、紐付けられた変数がポインタであろうがなかろうが、レシーバはよしなに変換して受け取ってくれます。つまり、変更を反映するならポインタレシーバを使う、ということのようです。
ただ、変数レシーバとポインタレシーバを混在させると混乱の元なのでポインタレシーバで統一することが多いとか。
package main
import "fmt"
type Person struct {
Name string
Age int
}
type MyInt int
func main() {
fmt.Println("値渡しの動作確認")
person := Person {
Name: "tanaka",
Age : 99,
}
fmt.Println(person)
person.methodPerson()
fmt.Println(person)
fmt.Println()
var myInt MyInt = 100
fmt.Println(myInt)
myInt.methodMyInt()
fmt.Println(myInt)
// 基底型はレシーバにできない
// var i int
// i.methodInt(100)
fmt.Println("\n参照渡しの動作確認")
fmt.Println(person)
// personはポインタではないが、よしなに変換してくれる
person.methodPointerPerson()
fmt.Println(person)
fmt.Println()
fmt.Println(myInt)
// myIntはポインタではないが、よしなに変換してくれる
myInt.methodPointerMyInt()
fmt.Println(myInt)
fmt.Println("\nポインタを変数レシーバに渡してみる")
pPerson := &Person {
Name: "Yoshida",
Age : 50,
}
fmt.Println(*pPerson)
// 変更は反映されないが、変数レシーバにもポインタを渡すことができる
pPerson.methodPerson()
fmt.Println(*pPerson)
}
// 構造体に紐づける
func (p Person) methodPerson() {
tmpName := p.Name
tmpAge := p.Age
p.Name = "suzuki"
p.Age = 1
fmt.Printf("Nameを%s -> %sに変更\n", tmpName, p.Name)
fmt.Printf("Ageを%d -> %dに変更\n" , tmpAge , p.Age)
}
// intを別の型として定義した型を紐づける
func (myInt MyInt) methodMyInt() {
tmpNum := myInt
myInt = 0
fmt.Printf("myIntを%d -> %dに変更\n" , tmpNum , myInt)
}
// 構造体に紐づける
func (p *Person) methodPointerPerson() {
tmpName := p.Name
tmpAge := p.Age
p.Name = "suzuki"
p.Age = 1
fmt.Printf("Nameを%s -> %sに変更\n", tmpName, p.Name)
fmt.Printf("Ageを%d -> %dに変更\n" , tmpAge , p.Age)
}
// intを別の型として定義した型を紐づける
func (myInt *MyInt) methodPointerMyInt() {
tmpNum := *myInt
*myInt = 0
fmt.Printf("myIntを%d -> %dに変更\n" , tmpNum , *myInt)
}
// 基底型に紐づける
// func (i int) methodInt(num int) {
// fmt.Println("i:", i)
// }
値渡しの動作確認
{tanaka 99}
Nameをtanaka -> suzukiに変更
Ageを99 -> 1に変更
{tanaka 99}
100
myIntを100 -> 0に変更
100
参照渡しの動作確認
{tanaka 99}
Nameをtanaka -> suzukiに変更
Ageを99 -> 1に変更
{suzuki 1}
100
myIntを100 -> 0に変更
0
ポインタを変数レシーバに渡してみる
{Yoshida 50}
NameをYoshida -> suzukiに変更
Ageを50 -> 1に変更
{Yoshida 50}
メソッド値
メソッドも実質関数なので下記で学習した関数型変数に格納ができる。その後、紐付けられた変数の値を変更しても、関数型変数に格納した時点の値で束縛されるようです。
これは変数に格納された時点でレシーバに値渡しされ、その結果、変更に影響されなくなるということなんだと思います。ただし、メソッドがポインタレシーバで定義されていると変更はメソッド側にも影響します。
ポインタでアクセスする以上、変数そのものにアクセスするため現在の値が見える、ということだと思います。
package main
import "fmt"
type Person struct {
Name string
Age int
}
func (p Person) notPointer() {
fmt.Println("Name:", p.Name)
fmt.Println("Age :", p.Age)
}
func (p *Person) pointer() {
fmt.Println("Name:", p.Name)
fmt.Println("Age :", p.Age)
}
func main() {
person := Person {
Name: "tanaka",
Age : 100,
}
// 変数に格納された時点の値で束縛される
notPointer := person.notPointer
notPointer()
// 値を変更してみる
person.Name = "suzuki"
person.Age = 99
// 変更は反映されない
notPointer()
fmt.Println()
// 変更済みの値で変数に登録
pointer := person.pointer
pointer()
// 値を変更してみる
person.Name = "takahashi"
person.Age = 1
// ポインタレシーバだと変更後の、現在の値を参照する
pointer()
}
Name: tanaka
Age : 100
Name: tanaka
Age : 100
Name: suzuki
Age : 99
Name: takahashi
Age : 1
おわりに
今回はここまでです。