執筆の経緯
最近の静的型付言語ブームに乗っかって、Goの勉強を始めました。
絶対詰まるだろうなと思っていたポインタでやっぱり詰まったので、20億番煎じくらいの内容ですが備忘録として残します。
保険をかける1わけではないですが、初心者の備忘録なので誤っている箇所も多数あると思います。
是非ご指摘いただけると大変嬉しいです。
なお各コードの冒頭にThe Go Playgroundのリンクを付けてあります。
是非実際の動作を試したり、ちょっとイジって実験してみたりしてください。
筆者について
普段Vue.js / TypeScriptを業務で書いているフロントエンドエンジニアです。
趣味でReactやLaravelを触ったりはしてますが、Goのような静的型付言語は初めてです。(Javaはほんのちょっと触ったことがある程度)
値渡しと参照渡しについて
package main
import "fmt"
func main() {
var age = 10
fmt.Println(age) // 10
}
- まず値渡しについて再確認しましょう。
例えば上記のようにage
という値が10の変数を宣言しました。
この10という値は宣言した時点でメモリのどこかに格納されます。
age
を呼び出すと、メモリから10という値を取り出して出力します。
package main
import "fmt"
func main() {
var age = 10
var age2 = age
age = 20
fmt.Println(age2) // 10
}
- 今度は
age2
という変数にage
を代入しました。
この時、age2
にage
の中身である10という値が代入され、メモリにage2
が10という情報が格納されます。 - そのため
age
に20を代入したとしても、age2
は10と出力されます。
これがいわゆる値渡しという概念です。
age
がint型というプリミティブな型だったために値渡しになっています。
age2 = age
としていますが、age
の中身をコピーしてage2
が新しく生まれているイメージです。
package main
import "fmt"
func main() {
var subjects = []string{"japanese", "math", "english"}
var subjects2 = subjects
subjects[2] = "history"
fmt.Println(subjects2[2]) //history
}
- 次に参照渡しです。
適当な文字列型のスライスであるsubjects
を宣言し、それをsubjects2
に代入しました。
この時、subjects2
にはsubjects
の中身の値それ自体ではなく、subjects
の値が格納されているメモリの場所(アドレス)が格納されます。-
【2022/4/3追記】
コメント欄でご指摘をいただきました。ありがとうございます!
スライスの正体は構造体であり、プロパティにポインタを持っています。
そのためアドレスが代入されるわけではなく、subjects2
には構造体が代入されていて、同じアドレスへのポインタを持っている状態になっているようです。- この話を理解するにはそもそもスライスとはある配列へのポインタを持っている構造体であることを理解しないといけません。スライス自体は配列の中身の値を持っていなくて、あくまで配列へのポインタを持っているのみです。配列から切り出しているからスライスという命名なのかもですね。詳細は公式の言語仕様やスライスのソースコードを見ていただくと分かりやすいです。
-
【2022/4/3追記】
-
【2022/4/5追記】
コメント欄でご指摘をいただきました。ありがとうございます!
Go言語は全てが値渡しのため、参照渡しという言葉を用いるのは誤っているようです。
実態としてはポインタのコピーが渡されているようです。
分かりやすさ重視で表現はそのままにしております。
- そのため
subjects
の値を変更すると、subjects2
も同様に変わります。
subjects = subjects2
という見た目どおり、subjects2
はsubjects
そのものになっているイメージです。
これがいわゆる参照渡しという概念です。
こちらはsubjects
がスライスという凝った型だったためにそうなっています。-
【2022/4/3追記】
こちらも前述の追記のとおりそのものになっているという表現は微妙です。とりあえず効率良く学習するために一旦そういうイメージでも良いかも知れませんが、正確ではないという認識は持っておいた方が良いでしょう。
-
【2022/4/3追記】
package main
import "fmt"
func main() {
var subjects = [3]string{"japanese", "math", "english"}
var subjects2 = subjects
subjects[2] = "history"
fmt.Println(subjects2[2]) //english
fmt.Println("なんでやねん")
}
- 大体の言語で「プリミティブな型は値渡し、配列とかオブジェクトみたいな凝った型は参照渡し」と考えて大きく外さないと思うのですが、Golangでは2つ罠があります。
まずスライスではなく配列の場合、まさかの値渡しになります。
※一応ですが、スライスは可変長配列、配列は固定長配列と考えておけばOKです。-
【2022/4/3追記】
こちらも前述の追記のとおりスライスは配列へのポインタを持った構造体が正体です。
-
【2022/4/3追記】
package main
import "fmt"
type Person struct {
name string
}
func main() {
var Hogeyama = Person{"Hogeyama"}
var Fugayama = Hogeyama
Hogeyama.name = "Hogeyama2"
fmt.Println(Fugayama) // {Hogeyama}
}
- 罠の2つ目は構造体です。
クラスみたいなものなのですが、こちらもまさかの値渡しになります。-
【2022/4/3追記】
今よく考えてみれば結局参照渡しになるかどうかは参照したいものへのポインタを持っているかどうかが肝で、今回だとPerson
という構造体は何らポインタを持っていないので、値渡しになるのは当たり前の話だなと思いました。
-
【2022/4/3追記】
アドレス演算子とポインタ型について
package main
import "fmt"
func main() {
var age = 10
var age2 = &age // ageの値が格納されているアドレスを渡す。
fmt.Println(age2) // カッコイイ文字列が表示されるはず。
age = 20
fmt.Println(*age2) // 20
}
- 前述のとおりint型は値渡しになりますが、参照を渡すようにGolangに指示することができます。
var age2 = &age
のように&
(アドレス演算子)を付けると参照しているメモリの場所(アドレス)を渡すことができます。
この時、age2
の型はポインタ型と呼ばれ、int型のポイント型なので*int
と表現します。
age2
を出力するとカッコイイ文字列が見えると思いますが、これがアドレスです。
さらに*age2
のように*
を付けると、そのアドレスに格納されている値そのものが取得できます。 - アドレスは住所、値は建物と考えるとイメージしやすいかも知れません。
構造体とレシーバについて
package main
import "fmt"
type Person struct {
name string
}
func (person Person) sayName() {
fmt.Println(person.name)
}
func (person Person) setName(name string) {
person.name = name
}
func main() {
var Hogeyama = Person{"Hogeyama"}
Hogeyama.sayName() // Hogeyama
Hogeyama.setName("Fugayama")
Hogeyama.sayName() // Hogeyama
}
- 構造体はレシーバというものがセットで使われます。
func (person Person)
のように書くことで、どの構造体のレシーバかを示すことができます。
レシーバはクラスが持っているメソッドみたいなものと捉えると良いと思います。
インスタンス2からメソッドを呼び出すことができます。
ここでHogeyama.setName("Fugayama")
以降を見てほしいのですが、setName
を実行したにも関わらず、sayName
の結果はHogeyama
のままです。
これはsetName
の中にあるperson
とはそれを呼び出したインスタンスのコピーであるためです。
package main
import "fmt"
type Person struct {
name string
}
func (person Person) sayName() {
fmt.Println(person.name)
}
func (person *Person) setName(name string) {
person.name = name
}
func main() {
var Hogeyama = Person{"Hogeyama"}
Hogeyama.sayName() // Hogeyama
Hogeyama.setName("Fugayama")
Hogeyama.sayName() // Fugayama
}
- 期待する結果にするには
func (person *Person)
のようにポインタ型にします。
そうすればperson
にはインスタンスのアドレスが渡ってくる、すなわち参照渡しになります。
それならvar Hogeyama = &Person{"Hogeyama"}
と定義しておかないとおかしいのでは?、と思いますが、コンパイラがよしなに処理してくれているようです。 - なお
sayName
のようなインスタンスのプロパティにアクセスするだけのメソッドの場合はポインタ型にする必要がないように見えますが、前述のとおりインスタンスのコピーをわざわざ作り出している(すなわちメモリの無駄)ため、ポインタ型の方が良いようです。
ちなみに値渡しになっているレシーバを値レシーバ、参照渡しはポインタレシーバと言います。そのまんまですね。
ポインタ型のインスタンスについて
package main
import "fmt"
type Person struct {
name string
}
func (person Person) sayName() {
fmt.Println(person.name)
}
func (person *Person) setName(name string) {
person.name = name
}
func main() {
var Hogeyama = &Person{"Hogeyama"}
fmt.Println(Hogeyama) // &{Hogeyama}
fmt.Println(Hogeyama.name) // Hogeyama
Hogeyama.sayName() // Hogeyama
Hogeyama.setName("Fugayama")
Hogeyama.sayName() //Fugayama
}
- よくインスタンス化する時点でアドレス演算子を付けているのを見かけると思います。
前述のとおりレシーバはポインタレシーバにするべきなので、それに合わせてインスタンスをポインタ型にしているのかと考えています。
Hogeyama
はポインタ型なので、出力も&{Hogeyama}
と構造体のポインタ型になっています。
プリミティブな型のポインタ型はアドレスが記載されていましたが、構造体のアドレスをそのまま記載すると長くなるので、こういう記述で省略しているのだと思っています。 - そして
Hogeyama.setName()
という記述はとても自然に見えます。
先ほどと記述は変わりませんが、先ほどはコンパイラがよしなに解釈してくれていたわけですから。
ですが今後はHogeyama.name
という表記が奇妙に見えます。
Hogeyamaインスタンスはポインタ型なので、*Hogeyama.name
とするべきに見えますが、こちらもコンパイラがよしなにやってくれているようです。 - ちなみに
age
の時のようなカッコイイ文字列でないのは、おそらく本当にアドレスを返すととても長い文字列になるので、&
で省略しているのだろうと考えています。
ちなみに実際に詰まったところ
package main
import (
"fmt"
"net/url"
)
func main() {
url := &url.URL{}
url.Scheme = "https"
url.Host = "google.com"
query := url.Query()
query.Set("q", "Golang")
url.RawQuery = query.Encode()
fmt.Println(url)
}
- 冒頭で「ポインタでやっぱり詰まった」と書きましたが、実際に詰まったのは上記のコードです。
ちなみに参考文献にあるUdemyで説明されていたものになります。
まずURL
の定義を見てみましょう。
type URL struct {
Scheme string
Opaque string // encoded opaque data
User *Userinfo // username and password information
Host string // host or host:port
Path string // path (relative paths may omit leading slash)
RawPath string // encoded path hint (see EscapedPath method)
ForceQuery bool // append a query ('?') even if RawQuery is empty
RawQuery string // encoded query values, without '?'
Fragment string // fragment for references, without '#'
RawFragment string // encoded fragment hint (see EscapedFragment method)
}
- インスタンス化されていることからも分かる通り、URLは構造体ということが分かります。
url.Scheme = "https"
とurl.Host = "google.com"
は問題ないと思います。
インスタンスのプロパティに値を代入しているだけです。
Query
メソッドはインスタンスから呼ばれていることから分かる通り、レシーバです。
色々処理が入ってますが、url
のRawQuery
プロパティに値を代入して出力しています。 - ここでインスタンス化する時にアドレス演算子がついているので、出力すると
&{https}
みたいな構造体のポインタ型で出力されるかと思いきや・・・。
https://google.com?q=Golang
なんでやねん!
- 公式の例を見てもポインタ型でインスタンス化されていて、出力の例にもURL3全体が書かれています。
型をfmt.Printf("%T\n", url)
で確認してみたところ、*url.URL
と出力されたので、ポインタ型であることをGolangは理解しているようです。
標準パッケージ故にGolangがよしなにしてくれている挙動だと思われます。
っていう話を社内で共有してみたところ
色々と議論が進み、以下のことが分かりました。
-
Println(url)
はfmt.Printf("%v\n", url)
と同じ意味っぽい。-
Printf
の第一引数はverb
と呼ばれ、出力形式を指定するもの。Printf
はフォーマットを指定して出力するというメソッド。 -
%v
はデフォルトのフォーマットで出力しなさい、\n
は改行しなさい、という意味。
-
- 構造体の場合はデフォルトは
{http}
みたいな出力だが、Stringメソッドをレシーバとして持っている場合はそこに定義されているものがデフォルトフォーマットになる様子。
func (u *URL) String() string {
var buf strings.Builder
// 省略
buf.WriteString("hogehoge") // これを足してみた。
return buf.String()
}
- urlパッケージの中身をのぞくと構造体URLのレシーバとしてStringメソッドが定義されていました。
出力時にhogehogeが足されるようにしてみました。
package main
import (
"fmt"
"net/url"
)
func main() {
url := &url.URL{}
url.Scheme = "https"
url.Host = "google.com"
query := url.Query()
query.Set("q", "Golang")
url.RawQuery = query.Encode()
fmt.Println(url) // https://google.com?q=Golanghogehoge
}
- hogehogeが末尾につきましたね。
やっぱりStringメソッドでデフォルトフォーマットを決めているようです。
終わりに
ポインタで詰まったというよりはPrintln
の挙動で詰まっていたみたいですね。
流行っているからというミーハーな理由で始めましたが、非常に楽しく学習できています。
今後もGoの記事を出していこうと思ってますので、よろしくお願いします!
参考文献
- Goで学ぶポインタとアドレス
- 【Goのやさしい記事】Goのポインタとか値渡しとか参照渡しを5分で学ぼう
- 【Go入門】Golang基礎入門 + 各種ライブラリ + 簡単なTodoWebアプリケーション開発(Go言語)
- 書式 %v のカスタマイズ
- fmt.Printfなんかこわくない
- Go で値を出力する方法