🕸️

PythonからGoへ:参照渡しと値渡しを整理してスムーズな移行をする

2023/08/11に公開

背景

いざGoを書くにあたり参照渡しと値渡しが曖昧だったので再整理しました。

本文

例えば、次のGoのコードを見てみましょう。
showName 関数の引数は①user Userとするのか、それとも②user *Userとするのか、どちらが適切なのでしょうか?

package main

import (
    "fmt"
)

type User struct {
    Name string
    Age  int
}

func showName(user *User) {
    fmt.Println(user.Name)
}

func main() {
    user := User{
        Name: "Bob",
        Age:  18,
    }
    showName(&user)
}

結論から言うと、showName 関数が値の変更を伴わない場合、どちらの方法でも問題ありません。
しかし、userの値を変更する場合、例えばPythonから移行してきた方[1]は、①の方法で引数を渡しして変更しても、その後userを参照しても値は変わらないので、②を選択するべきです。
Goでは、関数の引数はデフォルトで値渡しとなります。しかし、ポインタを使用することで、擬似的にPythonのような振る舞いを実現することができるためです。

それでは、単に構造体の値を参照して表示するだけの場合、どちらの方法が適しているでしょうか?
結論として、シンプルな構造体であれば、①の方法で引数を渡すことが良いと考えます。もし userが巨大な構造体である場合、②を選択しても良いです。
①と②の選択にはそれぞれメリットとデメリットが存在します。

①:

  • メリット: user自体の値は変更されないことが明確
  • デメリット: 引数で渡したときにコピーが作成され、オーバーヘッドが発生

②:

  • メリット: ポインタを渡すことで、構造体のコピーを避け、効率的に処理可能
  • デメリット: user 自体の値が変更される可能性があり、予期しない動作が発生する可能性有

デメリットを比べると、予期しない動作 > コピーによる小さなオーバヘッド なので①を選ぶべきというのが無難と思います。
なお、今回のような構造体だけでなく、プリミティブ型(intやstringなど)を渡す場合でも、同様に値渡しによるコピーが作成されますが、一般的にはコピーのオーバーヘッドが小さく、問題ありません。

ありがとうございました!

参考

https://go.dev/doc/faq#Pointers
https://dodotechno.com/python-call-by-value/#:~:text=誤解が生じる理由は,内容が反映されます。

脚注
  1. ちなみにPython=参照渡しではないです。あくまで関数の引数にオブジェクトをそのまま渡しても、値を変えることができる挙動という意図。
    https://docs.python.org/ja/3/tutorial/controlflow.html?#id2 ↩︎

Discussion