【Go言語入門】7.ポインタについてさくっと学ぶ

ここまでの記事でようやく役者が揃ったので、今回はエンジニアなら誰もが??となったことといっても過言ではないポインタをキャッチアップしていきます。

前回の記事

ポインタの概念が存在するのはコンパイラ型言語ですが、その中でC/C++のポインタに関する記事は圧倒的に多いと思います。ここでは概念自体の理解をゴールとしているので、応用やどう作用しあっているのかについて掘り下げて学びたいという方は是非調べてみてください。言語は違えどかなり詳しく書かれている記事もあってコンピュータサイエンスの勉強の学習にもなります。

ポインタとは?

ポインタは一言でいうと値のメモリアドレスのことを指します。

メモリアドレス(英: memory address)は、コンピュータの主記憶装置にアクセスするためにソフトウェアおよびハードウェアによって様々なレベルで使用されるデータ概念である。 通常、メモリアドレスは、符号なし整数として表示・処理される固定長の数字の列である。

wikipedia

といっても最初はピンとこないと思いますが、例えばa := 100と定義した時に実際に値100が保存されている場所のことです。

これまで様々な変数や関数を定義しては、変更、削除などをしてきましたが、これらの値はどこかで保存されています

まあなかったらそもそも処理自体ができないので当たり前ではあるのですが、Goではその保存されている値の実体を扱うことができます

PythonやRubyなどのインタプリタ型言語には扱う手段がないのでここが大きな違いとなってきます。

定義

ひとまず使い方から。

package main

import "fmt"

func main() {
  var p int = 100
  var q *int = &p
  var s *string
  fmt.Println(p, q, &p, *q, s)
}

// >>> 100 0xc0000a8010 0xc0000a8010 100 <nil>

ポインタやアドレスを引き出すオペレータはそれぞれ*&です。

  • &変数(○型)→ 値が格納されているアドレス
  • *変数(○型のポインタ)→ アドレスに格納された値

このように確認することができます。

試しに適当な変数を定義して&オペレータを付けてPrintlnしてみてください。

アドレスが出力されるはずです。

ポインタは今まで使ってきたfloat64型bool型などの前に*を付けるとそれぞれfloat64型のポインタbool型のポインタという風に扱うことが出来るようになります。

ここで勘違いしてはいけないのはint ≠ *intということです。定義する際や、演算(通常の値の演算のことです。Goにはポインタ演算がありません)する際など、ポインタも混ざった処理をするときは気をつけましょう(といってもエラーがでますが)

値渡しとポインタ渡し

後の記事で例を挙げてますが、構造体を扱う際、その中のフィールドの値を変更する関数を定義したならば、その引数にはポインタを渡さければ値が変更できません

関数は引数が渡された時点でコピーを行うので、値渡しだと変更の対象がコピーになり、実体ではなくなるからです

これはポインタを使用しなければいけないケースなのでしっかり頭に入れておきましょう。

結局なにがいいの?

じゃあそれらのアドレスを扱うことで他にどのような恩恵が受けられるの?ということですが、

  • メモリの節約
  • 処理の高速化

などがメリットとして挙げられます。

PCを使っていて色んなソフトを同時に動かしていると固まった、ということは過去にあったと思いますが、あれはメモリ容量が足りずに処理が追いついていないせいです

使い手には固まって見えるものの内部でコンピュータは必死です。

つまりメモリは有限であるため、極力小さい消費量でいろんな処理を片付けないといけません。

ポインタをうまく扱うことはそのあたりのパフォーマンスに直結します。

逆に下手に扱うと、メモリリークやクラッシュを引き起こす原因にもなります。

・・・と、よくあるテンプレートを述べてはみましたが、実際のところ僕もまだキャッチアップ段階なので多くを語ることはできません。

構造体のサイズが大きければ、速度に直接影響があるということなので、試してみました。以下が例です。

package main

type Test struct {
  a uint8
  b uint16
  c uint32
  d uint64
  e int8
  f int16
  g int32
  h int64
  i float32
  j float64
  k complex64
  l complex128
  m bool
  n string
  o []uint8
  p []uint16
  q []uint32
  r []uint64
  s []int8
  t []int16
  u []int32
  v []int64
  w []float32
  x []float64
  y []complex64
  z []complex128
  A []bool
  B []string
}

func passValue(n Test) Test {
  return n
}

func passPointer(n *Test) Test {
  return *n
}

func main() {
  t := new(Test)
  for i := 0; i < 100000000; i++ {
    passValue(*t)
    passPointer(t)
  }
}

関数passValueと関数passPointerで値渡しとポインタ渡しに分けました。

それぞれの結果です。

値渡し

real    0m0.939s
user    0m0.793s
sys     0m0.043s

ポインタ渡し

real    0m0.274s
user    0m0.125s
sys     0m0.027s

結構顕著に出ましたね。

フレームワークやライブラリを使っていくうちに更にありがたみがわかるようになるのかも知れません。