新しいことにはウェルカム

技術 | 電子工作 | ガジェット | ゲーム のメモ書き

Go言語でハマったことメモ(ポインターのアロー演算子がない)

Golangを始めました。

始めてみて感じたのですが、Golangはできるだけ文法をシンプルにかつ、できるだけコードがシンプルになるように設計されています。

そしてそのために、Golangには若干トリッキーな構文がいくつかあります。

しかし、それらを知らずに、他の言語での先入観や勝手な思い込みで判断してしまって、ハマることがちょいちょいありました。

ここでは、Golangを始めてみて、個人的にハマったことや、勘違いしたことを、トピック別に備忘録としてメモしていこうと思います。

今回は、ポインターの演算子についてのメモです。

ポインターのアロー演算子がない

Golangにはポインターがあります。

C言語(C++言語。以下C言語と表記)同様、オブジェクトのアドレスを取得するには「&」演算子を使います。

また、ポインターからオブジェクトを参照するには、C言語と同様「*」演算子を使います。

しかし、C言語では、オブジェクトからオブジェクトのメンバ変数・関数にアクセスするには「.」演算子を使い、 オブジェクトのポインターから、オブジェクトのメンバ変数・関数にアクセスするには「->」(アロー)演算子を使うのですが、 Golangはどちらの場合でも、「.」演算子でアクセスします。

例)オブジェクト・ポインターどちらでも「.」でアクセスできる

package main

import "fmt"

type myStruct struct {
    x int
}

func (o myStruct) testObject() {
    fmt.Println("Object")
}

func (p *myStruct) testPointer() {
    fmt.Println("Pointer")
}

func main() {
    // オブジェクト
    var a myStruct
    a.testObject()   // Object
    a.testPointer()  // Pointer
    fmt.Println(a.x) // 0

    // ポインター
    b := &a
    b.testObject()   // Object
    b.testPointer()  // Pointer
    fmt.Println(b.x) // 0
}

Golangにおける「.」演算子は、演算子というより、メンバ変数・関数にアクセスしたいという意思表示に近いです。

「元の変数がオブジェクトであれポインターであれ、コンパイラが良しなに判断して、ええ感じにメンバ変数・関数にアクセスできるようにしてやるから、 細かいことは気にせんと、黙って「.」演算子使っとけばええ」

といった感じです。

オブジェクトとポインターでメンバ関数が重複したらどうなるのか?

オブジェクト・ポインターの変換は、コンパイラが自動でやってくれるのですが、メンバ関数が、呼び出し元がオブジェクトの場合とポインター場合の2パターンあった時は、 どちらのメンバ関数が呼び出されるのでしょうか?

結論から言うと、同じ名前の関数は定義できないので、重複することはありません。

例えば、下記のように、オブジェクトとポインターで、同じ名前のメンバ関数「test()」を作成しようとするとコンパイルエラーになります。

package main

import "fmt"

type myStruct struct {
    x int
}

func (o myStruct) test() {
    fmt.Println("Object")
}

func (p *myStruct) test() {
    fmt.Println("Pointer")
}
func main() {
    var a myStruct
    a.test()

    b := &a
    b.test()
}

つまり、やっぱり

「いらん心配はせんと、黙って「.」演算子使っとけばええ」

なのです。

オブジェクトのメンバ関数と、ポインターのメンバ関数の違い

構造体のメンバ関数の書き方は、オブジェクトに記述する方法と、ポインターに記述する方法があります。

ぱっと見同じように見えるのですが、オブジェクトの場合は、構造体が関数に渡された時に、構造体のコピーが作成され、そのコピーに対して操作が行われます。

一方、ポインターの場合は、関数には構造体のポインターが渡されるので、構造体の実態に対して操作が行われます。

結果として何が違うかと言うと、ポインターの場合は、関数から構造体の要素の値を変更できますが、オブジェクトの場合は、構造体のコピーの要素の値が変更されて、元の構造体の要素の値は元のままです。

package main

import "fmt"

type myStruct struct {
    x int
    y int
}

// xを変更したい
func (o myStruct) testX(v int) {
    // oは呼び出し元の構造体のコピー
    o.x = v
}

// yを変更したい
func (p *myStruct) testY(v int) {
    // pは呼び出し元の構造体のポインター
    p.y = v
}

func main() {
    a := myStruct{x:1, y:1}
    fmt.Printf("%+v\n", a)  // {x:1 y:1} 

    a.testX(2)  // aのxは変更されない
    a.testY(2)  // aのyは変更される
    fmt.Printf("%+v\n", a)  // {x:1 y:2} 
}

個人的な感想ですが、メンバ関数で自分の要素を変更させたくないケースはほとんどなく、むしろ、メンバ関数から変更できない仕様による勘違いミスが起きそうです。また、構造体のコピーコストもかかるので、主にポインターを使っていくことになるのかなぁと思います。

元構造体の値を変更させないイミュータブルな操作をしたいなら、明示的に引数にオブジェクトを渡す関数を別途用意した方がよさそうです。

良しなにしてくれるのはメンバ変数・関数だけ

オブジェクト・ポインターの自動変換をやってくれるのは、あくまでメンバ変数・関数の時だけで、普通の関数の時はやってくれません。

package main

import "fmt"

type myStruct struct {
   x int
}

func test(o myStruct) {
   fmt.Println("Object")
}

func main() {
   var a myStruct
   test(a) // Object

   b := &a
   test(b) // エラー
}

なので、これら一連の仕様は、オブジェクト・ポインターの自動変換というより、「.」はメンバアクセス演算子と解釈するのが分かりやすいのかなぁと思います。

感想など

この仕様のおかげでプログラムがスッキリします。

しかし、合理的と言えば合理的なのですが、状況によって演算子の挙動が異なるということなので、若干モヤッとしたものもあります。

まぁ、そのあたりは慣れの問題かも知れませんね。