こんにちは。Tomoyuki(@tomoyuki65)です。
私が初めてGo言語(Golang)を学んだ際、大学時代に学んでいたC言語に似ているなと思って懐かしい気持ちになりましたが、その理由はポインタが使えることです。
そんなポインタ(ポインタ変数)というのは、変数が格納されているメモリのアドレスを保持するための特殊な変数を指し、よく初心者が躓きやすいポイントでもあります。
この記事では、そんなポインタや使いどころについてまとめます。
Go言語(Golang)のポインタと使いどころまとめ
まずポインタ(ポインタ変数)とは、変数が格納されているメモリのアドレスを保持するための特殊な変数を指しますが、基本的な使い方としては以下のようになります。
package main
import (
"fmt"
)
func main() {
// int型の変数「num」を定義
var num int
// int型のポインタ変数「pNum」を定義
var pNum *int
// 変数「num」のアドレスをポインタ変数「pNum」に格納
pNum = &num
// pNumをログ出力して確認
fmt.Printf("ポインタ変数「pNum」に格納したアドレス: %p\n", pNum)
}
※ポインタを扱う際は、「*」や「&」を使います。
実行結果の例
ポインタ変数「pNum」に格納したアドレス: 0xc000010070
※アドレスの値は環境により変わります
値渡しと参照渡し
そしてポインタの理解を深めるには、「値渡し」と「参照渡し」を理解するといいです。
例えば以下のように、変数の値を関数のパラメータに渡した場合、基本的には「値渡し」になるため、関数内で値を変更しても元の変数には影響がありません。
package main
import (
"fmt"
)
// パラメータに渡した値に+1する関数
func addOne(num int) {
num = num + 1
}
func main() {
// int型の変数「num」を定義
var num int
// 変数に値を格納
num = 1
// addOne関数を実行
addOne(num)
// 変数「num」の値をログ出力
fmt.Printf("変数「num」の値: %d\n", num)
}
実行結果
変数「num」の値: 1
次にポインタを使って「参照渡し」をした場合、関数内で元の値を変更できるようになります。
package main
import (
"fmt"
)
// パラメータに渡した値に+1する関数
// 型を「*int」に変更してポインタを渡せるようにする
func addOne(num *int) {
// ポインタの値を使うには「*」を使う
*num = *num + 1
}
func main() {
// int型の変数「num」を定義
var num int
// 変数に値を格納
num = 1
// addOne関数を実行
// numのポインタを渡す
addOne(&num)
// 変数「num」の値をログ出力
fmt.Printf("変数「num」の値: %d\n", num)
}
実行結果
変数「num」の値: 2
このように関数に変数を渡す際は「値渡し」と「参照渡し」があり、ポインタを使って「参照渡し」をすることで、関数内で元の変数の値を変更することができるようになります。
参照渡しの注意点
「参照渡し」については、以前書いた記事「Go言語(Golang)の配列(array)・スライス(slice)・マップ(map)の使い方や違いについて」でもご紹介していますが、関数内で変数の値を上書きした際に意図せず呼び出し元の変数の値が変わってしまってバグになるということもよくある話なので、ポインタやマップで参照渡しを使う際はその点に注意しましょう。
値渡しのデメリット
「参照渡し」を使うと思わぬバグが生まれる可能性があるため、基本的には「値渡し」を使った方が安全です。
ただし、「値渡し」は変数の値をコピーし、その分余計にメモリを消費することになるため、例えば大量データを扱うような場面においては、必要に応じてポインタで「参照渡し」を使った方がいい可能性もあるので注意して下さい。
ポインタの使いどころの判断について
ポインタを使うかどうかの判断については、基本的には以下の条件に一つでもあてはまれば使うことを検討しましょう。
・渡すデータがすごく大きい
・データが存在しない(nil)という状態を表現したい
構造体のインスタンス生成用関数の戻り値では基本的にはポインタを使う
package main
import (
"fmt"
)
// ユーザーモデルの構造体
type User struct {
Id int
Name string
Email string
}
// インスタンス生成用関数
func NewUser(name, email string) *User {
return &User{
Id: 0,
Name: name,
Email: email,
}
}
// Emailを変更するメソッド
func (u *User) changeEmail(email string) {
u.Email = email
}
func main() {
// ユーザーの定義
user := NewUser("田中太郎", "t.tanaka@example.com")
// 構造体の値をデフォルトの書式で出力
fmt.Printf("user: %v\n", user)
// フィールド名付きで値を出力
fmt.Printf("user: %+v\n", user)
// Goの構文に沿ったリテラル形式で出力
fmt.Printf("user: %#v\n", user)
// Emailを「t2.tanaka2@example.com」に変更
user.changeEmail("t2.tanaka2@example.com")
// 再度ユーザーをログ出力
fmt.Println("Email変更後")
fmt.Printf("user: %v\n", user)
fmt.Printf("user: %+v\n", user)
fmt.Printf("user: %#v\n", user)
}
user: &{0 田中太郎 t.tanaka@example.com}
user: &{Id:0 Name:田中太郎 Email:t.tanaka@example.com}
user: &main.User{Id:0, Name:"田中太郎", Email:"t.tanaka@example.com"}
Email変更後
user: &{0 田中太郎 t2.tanaka2@example.com}
user: &{Id:0 Name:田中太郎 Email:t2.tanaka2@example.com}
user: &main.User{Id:0, Name:"田中太郎", Email:"t2.tanaka2@example.com"}
尚、ドメイン駆動設計によるAPI開発方法については以下の記事を参考にどうぞ。


C言語のポインタと比べてGo言語のポインタでできないこと
また、Go言語ではC言語にあるポインタ演算(ポインタ変数に整数を加算・減算して、隣接するメモリアドレスに移動させる)はできないため、もし普段C言語をお使いの方がいたらご注意下さい。
最後に
今回はGo言語(Golang)のポインタや使いどころについてまとめました。
ポインタはよく初心者が躓きやすいポイントではありますが、適切に使うことでパフォーマンス改善に繋げられる可能性もあるため、Go言語を極めたい方はぜひ参考にしてみて下さい。
コメント