Golang 入門

Go new 関数

Go言語において、new 関数はメモリ割り当て(Memory Allocation)を行うための基礎的なツールであり、特にポインタ(Pointer)を扱う際に重要となります。

CやC++などの言語とは異なり、Goには mallocfree のように手動でメモリの割り当てと解放を管理する関数はありません。これは、Goが強力なガベージコレクション(Garbage Collection)機能を備えており、メモリを自動的に管理しているためです。

しかし、変数にメモリを割り当て、その変数をポインタ経由で参照したい場合には、new 関数が極めて重要な役割を果たします。new 関数を深く理解することは、ポインタを効率的に使いこなし、連結リスト(Linked List)やツリー(Tree)などの複雑なデータ構造を構築するための必須科目と言えます。

1. new 関数は何を行うのか?

new 関数は、データ型(Type)という一つの引数のみを受け取ります。
その動作フローは非常に明確です:

  1. システムに対して、その型(Type)の値を格納するのに十分なサイズのメモリ領域を要求する。
  2. そのメモリ領域を「ゼロクリア」する。つまり、その型のゼロ値(Zero Value)で初期化する。
  3. その新しいメモリ領域を指すポインタを返す。

一般的な構文は以下の通りです:

ptr := new(Type)

ここでの Type は、intstring、構造体(Struct)、あるいは独自に定義した型など、あらゆる有効なGoの型を指定できます。実行後、ptr はクリーンでゼロ値状態の新しい変数を指すポインタとなります。

1.1 ゼロ値(Zero Value)の振り返り

new は割り当てたメモリを自動的にゼロ値で初期化することを常に覚えておいてください。主要な型のゼロ値は以下の通りです:

  • int (整数): 0
  • float64 (浮点数): 0.0
  • bool (論理値): false
  • string (文字列): "" (空文字列)
  • Pointers (ポインタ): nil

2. new 関数の基礎的な例

2.1 整数型 (int) へのメモリ割り当て

package main

import "fmt"

func main() {
	// 整数型のメモリを割り当て、それを指すポインタを取得
	ptr := new(int)
	
	// ポインタ自身のアドレスと、そのアドレスが指すメモリ内の値を表示
	fmt.Println("ポインタのメモリドレス:", ptr)   // 出力例: ポインタのメモリドレス: 0xc00001a0b8
	fmt.Println("アドレスに格納されている値:", *ptr)  // 出力: アドレスに格納されている値: 0 (int のゼロ値)
	
	// デリファレンス(参照解決)を通じて、そのメモリ位置の値を変更
	*ptr = 42
	
	// 更新後の値を表示
	fmt.Println("更新後の値:", *ptr)     // 出力: 更新後の値: 42
}

この例では、new(int) によって整数を格納できるメモリ領域が確保されます。変数 ptr はそのアドレスを受け取ります。初期状態では 0 が格納されていますが、*ptr を使ってアドレスを辿り、データを 42 に書き換えています。

2.2 文字列型 (string) へのメモリ割り当て

package main

import "fmt"

func main() {
	// 文字列型のメモリを割り当て、ポインタを取得
	strPtr := new(string)
	
	fmt.Println("文字列ポインタのアドレス:", strPtr)       // 出力例: 0xc00001a0e0
	fmt.Println("アドレス内の初期文字列:", *strPtr)  // 出力: (空文字列 "" のため空行が表示される)
	
	// 新しい文字列を代入
	*strPtr = "Hello, Go!"
	
	fmt.Println("更新後の文字列:", *strPtr)      // 出力: 更新後の文字列: Hello, Go!
}

3. 核心的な比較:new vs. & (アドレス演算子)

初心者は new& を混同しがちです。どちらも最終的にはポインタを返しますが、その背後にある動作は全く異なります。

  • new(Type): まったく新しいものを作成します。ヒープ(Heap)メモリ上に新しい領域を確保し、ゼロ値で埋め、そのアドレスを返します。
  • &variable: 既存のものを辿ります。既に存在する変数に対してポインタを作成します。新しいメモリ領域を確保することはありません。

コードでの比較:

package main

import "fmt"

func main() {
	// new を使用
	ptr1 := new(int) // 何もないところから新しいメモリを作成
	*ptr1 = 10
	fmt.Println("new を使用:", ptr1, *ptr1) // 出力例: new を使用: 0xc00001a0b8 10
	
	// & を使用
	x := 20          // x は既にメモリが割り当てられた実在する変数
	ptr2 := &x       // x の既存のアドレスを抽出
	fmt.Println("& を使用:", ptr2, *ptr2)   // 出力例: & を使用: 0xc000012088 20
}

4. 構造体(Structs)における new の活用

実際の開発では、new は新しい構造体をインスタンス化(Instantiate)し、そのポインタを直接返す際によく使われます。

package main

import "fmt"

// Person 構造体を定義
type Person struct {
	Name string
	Age  int
}

func main() {
	// new を使って Person 構造体にメモリを割り当て
	personPtr := new(Person)
	
	// すべてのフィールドは対応するゼロ値で初期化される
	fmt.Println("初期状態の Person:", *personPtr) // 出力: 初期状態の Person: { 0}
	
	// ポインタ経由でフィールドにアクセスして変更
	// 💡 注意:Goの親切な設計により、自動的にデリファレンスされるため (*personPtr).Name と書く必要はない
	personPtr.Name = "Alice"
	personPtr.Age = 30
	
	fmt.Println("更新後の Person:", *personPtr) // 出力: 更新後の Person: {Alice 30}
}

ここで注目すべきは、personPtr がポインタであるにもかかわらず、フィールドへの代入時に personPtr.Name と直接記述している点です。これはGo言語が提供するシンタックスシュガー(Syntax Sugar)であり、低レイヤーで自動的にデリファレンスを行ってくれます。

5. どのような時に new を使うべきか?

new は便利ですが、Go言語における唯一の、あるいは最優先のメモリ割り当て方法ではありません。以下のガイドラインに従ってください。

  • new を使うべきシーン: 明示的にポインタが必要で、かつそのメモリ内のデータがクリーン(すべてゼロ値)であることを望む場合。これは、初期化関数(Initialization Function)にポインタを渡す必要がある際によく見られます。
  • コンポジットリテラル(Composite Literals)を優先するシーン: 構造体を作成すると同時に、特定の初期値を割り当てたい場合。この書き方は可読性が高く、Goコミュニティで最も推奨される手法です。
  • 直接変数を宣言するシーン: ほとんどの通常のビジネスロジックでは、var x intx := 10 による宣言で十分です。Goのガベージコレクションが適切に処理するため、常に new で強制的にポインタを割り当てる必要はありません。

new とコンポジットリテラルの直感的な比較:

package main

import "fmt"

type Point struct {
	X, Y int
}

func main() {
	// 方法 1:new を使用(まずゼロ値を割り当て、後から一つずつ代入)
	pointPtr := new(Point)
	pointPtr.X = 10
	pointPtr.Y = 20
	fmt.Println("new で作成した Point:", *pointPtr) // 出力: {10 20}
	
	// 方法 2:コンポジットリテラルを使用(作成と同時に代入、最も一般的)
	point := Point{X: 30, Y: 40}
	fmt.Println("リテラルで作成した Point:", point)       // 出力: {30 40}
	
	// 方法 3:リテラル + アドレス演算子 &(一ステップで初期化済みのポインタを取得、非常に推奨)
	point2 := &Point{X: 50, Y: 60}
	fmt.Println("リテラルで作成して直接アドレス取得:", *point2) // 出力: {50 60}
}

6. 実践演習:ポインタを通じた値の変更

new を使用する典型的なシナリオは、変数を関数に渡し、関数内部での変更を外部に確実に反映させたい場合です。これにはポインタを渡す必要があります。

package main

import "fmt"

// increment は整数のポインタを受け取り、実データを 1 加算する
func increment(ptr *int) {
	*ptr++ // デリファレンスしてインクリメント
}

func main() {
	// new で整数のメモリを確保し、ポインタを取得
	numPtr := new(int)
	
	// 初期値 5 を代入
	*numPtr = 5
	fmt.Println("インクリメント前の値:", *numPtr) // 出力: インクリメント前の値: 5
	
	// ポインタを関数に渡す
	increment(numPtr)
	
	fmt.Println("インクリメント後の値:", *numPtr) // 出力: インクリメント後の値: 6
}

numPtr(メモリ番地)を渡しているため、関数がそのメモリを直接操作し、外部の値も正常に 6 へと変化します。