33
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Goのポインタで詰まったので備忘録

Last updated at Posted at 2022-04-01

執筆の経緯

最近の静的型付言語ブームに乗っかって、Goの勉強を始めました。
絶対詰まるだろうなと思っていたポインタでやっぱり詰まったので、20億番煎じくらいの内容ですが備忘録として残します。
保険をかける1わけではないですが、初心者の備忘録なので誤っている箇所も多数あると思います。
是非ご指摘いただけると大変嬉しいです。
なお各コードの冒頭にThe Go Playgroundのリンクを付けてあります。
是非実際の動作を試したり、ちょっとイジって実験してみたりしてください。

筆者について

普段Vue.js / TypeScriptを業務で書いているフロントエンドエンジニアです。
趣味でReactやLaravelを触ったりはしてますが、Goのような静的型付言語は初めてです。(Javaはほんのちょっと触ったことがある程度)

値渡しと参照渡しについて

main.go
package main

import "fmt"

func main() {
	var age = 10
	fmt.Println(age) // 10
}

  • まず値渡しについて再確認しましょう。
    例えば上記のようにageという値が10の変数を宣言しました。
    この10という値は宣言した時点でメモリのどこかに格納されます。
    ageを呼び出すと、メモリから10という値を取り出して出力します。

main.go
package main

import "fmt"

func main() {
	var age = 10
    var age2 = age
    age = 20
	fmt.Println(age2) // 10
}

  • 今度はage2という変数にageを代入しました。
    この時、age2ageの中身である10という値が代入され、メモリにage2が10という情報が格納されます。
  • そのためageに20を代入したとしても、age2は10と出力されます。
    これがいわゆる値渡しという概念です。
    ageがint型というプリミティブな型だったために値渡しになっています。
    age2 = ageとしていますが、ageの中身をコピーしてage2新しく生まれているイメージです。

main.go
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/5追記】
    コメント欄でご指摘をいただきました。ありがとうございます!
    Go言語は全てが値渡しのため、参照渡しという言葉を用いるのは誤っているようです。
    実態としてはポインタのコピーが渡されているようです。
    分かりやすさ重視で表現はそのままにしております。

  • そのためsubjectsの値を変更すると、subjects2も同様に変わります。
    subjects = subjects2という見た目どおり、subjects2subjectsそのものになっているイメージです。
    これがいわゆる参照渡しという概念です。
    こちらはsubjectsがスライスという凝った型だったためにそうなっています。
    • 【2022/4/3追記】
      こちらも前述の追記のとおりそのものになっているという表現は微妙です。とりあえず効率良く学習するために一旦そういうイメージでも良いかも知れませんが、正確ではないという認識は持っておいた方が良いでしょう。

main.go
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追記】
      こちらも前述の追記のとおりスライスは配列へのポインタを持った構造体が正体です。

main.go
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という構造体は何らポインタを持っていないので、値渡しになるのは当たり前の話だなと思いました。

アドレス演算子とポインタ型について

main.go
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のように*を付けると、そのアドレスに格納されている値そのものが取得できます。
  • アドレスは住所、値は建物と考えるとイメージしやすいかも知れません。

構造体とレシーバについて

main.go
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とはそれを呼び出したインスタンスのコピーであるためです。

main.go
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のようなインスタンスのプロパティにアクセスするだけのメソッドの場合はポインタ型にする必要がないように見えますが、前述のとおりインスタンスのコピーをわざわざ作り出している(すなわちメモリの無駄)ため、ポインタ型の方が良いようです。
    ちなみに値渡しになっているレシーバを値レシーバ、参照渡しはポインタレシーバと言います。そのまんまですね。

ポインタ型のインスタンスについて

main.go
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の時のようなカッコイイ文字列でないのは、おそらく本当にアドレスを返すととても長い文字列になるので、&で省略しているのだろうと考えています。

ちなみに実際に詰まったところ

main.go
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の定義を見てみましょう。

url.go
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メソッドはインスタンスから呼ばれていることから分かる通り、レシーバです。
    色々処理が入ってますが、urlRawQueryプロパティに値を代入して出力しています。
  • ここでインスタンス化する時にアドレス演算子がついているので、出力すると&{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メソッドをレシーバとして持っている場合はそこに定義されているものがデフォルトフォーマットになる様子。

url.go
func (u *URL) String() string {
	var buf strings.Builder
    // 省略
	buf.WriteString("hogehoge") // これを足してみた。
	return buf.String()
}
  • urlパッケージの中身をのぞくと構造体URLのレシーバとしてStringメソッドが定義されていました。
    出力時にhogehogeが足されるようにしてみました。
main.go
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の記事を出していこうと思ってますので、よろしくお願いします!

参考文献

  1. って書いてる時大体保険かけてるよね。

  2. クラスではないので、インスタンスと呼んでいいか微妙ですが、分かりやすさ重視でそう呼びます。

  3. ここでのURLとは構造体ではなく、一般的なURLです。

33
26
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?