メモリ

Go言語のnewとmakeの使い分けを解説

Go言語には、メモリ確保に使う組み込み関数newmakeがあります。

newはゼロ初期化されたオブジェクトのポインタを返し、makeはスライス、マップ、チャネル専用の初期化を行います。

本記事では、両者の違いと使いどころをシンプルに解説します。

newとmakeの基本

new関数の概要

ゼロ初期化とメモリ確保の仕組み

new関数は、型に応じたゼロ値で初期化された変数のメモリ領域を確保します。

例えば、数値型の場合は0、文字列の場合は空文字列、ポインタの場合はnilなどがセットされます。

確保されたメモリのアドレスを指すポインタを返すため、戻り値は常にポインタとなります。

この仕組みにより、必要最低限な初期化が自動的に行われるため、メモリ管理が簡単になります。

基本的な使用方法

new関数は単一の値をポインタで返すため、以下のように使用します。

package main
import "fmt"
func main() {
	// int型のメモリ領域を0で初期化し、そのアドレスを取得する
	numPointer := new(int)    // numPointerは *int 型
	*numPointer = 100         // ポインタを介して値を設定する
	fmt.Println(*numPointer)  // 出力: 100
}
100

make関数の概要

スライス、マップ、チャネルの初期化処理

make関数は、スライス、マップ、チャネルといった組み込みのデータ構造専用に初期化処理を行います。

返り値はこれらの型そのものになり、初期化時に内部の適切な状態が整えられます。

例えば、スライスの場合は長さとキャパシティを持つスライスが返り、マップの場合は空のマップが返されます。

各型に応じた初期化の特徴

  • スライスの場合

make([]T, length, capacity)で長さとオプションとしてキャパシティを指定でき、連続したメモリ領域が用意されます。

  • マップの場合

make(map[Key]Value, hint)と記述し、初期の要素数のヒントを与えることができ、効率的なメモリ割り当てが行われます。

  • チャネルの場合

make(chan T, capacity)と記述し、バッファサイズを指定できます。

これにより、非同期処理に役立つデータのやりとりが円滑に行われます。

newとmakeの違いと使い分け

メモリ割り当てのアプローチ

newによる値の生成方法

newは任意の型のメモリ領域を確保するために使用されます。

確保されたメモリ領域には型に対応したゼロ値が設定され、返り値はその変数へのポインタです。

このため、シンプルな値(例えば、整数や構造体)のポインタが必要な場合に適しています。

数式で表すと、ある型Tに対して

new(T)=ポインタ,(ゼロ値のT)

という動作を行います。

makeによるデータ構造の初期化方法

makeは、スライス、マップ、チャネルなどの組み込みのデータ構造専用に設計されています。

データ構造それぞれに適した内部構造がセットアップされ、直接利用可能な形で返されます。

例えば、スライスでは容量や長さ、マップではハッシュテーブルの初期設定が自動で行われるため、即座に使用できます。

この特性により、複雑な初期化手順を記述する必要がなく、コードが簡素化されます。

使用シーンの比較

newの適用ケース

  • 主に単一の値や構造体の初期化に使用します。
  • ポインタとして扱いたい場合、例えば関数間で値の参照を共有する際に有効です。
  • メモリ確保の際にゼロ初期化が必要なときに適切です。

makeの適用ケース

  • []Tmap[Key]Valuechan Tといった組み込み型を初期化する際に使用します。
  • 複雑なデータ構造を扱う場合、例えばデータの集約や同時処理が求められるシーンで役立ちます。
  • 初期設定が内部的に必要なため、手作業での初期化を避けることができます。

コード例で見る実践的な使い分け

new関数を用いた具体例

ポインタ生成の例と注意点

以下は、構造体のインスタンスをnew関数で生成するサンプルコードです。

構造体の各フィールドはゼロ値で初期化され、必要に応じて値を上書きすることができます。

package main
import "fmt"
// Person構造体を定義
type Person struct {
	Name string // 名前
	Age  int    // 年齢
}
func main() {
	// newを使ってPersonのポインタを生成する
	personPointer := new(Person) // Person構造体のゼロ初期化を実施
	// ゼロ初期化後、必要に応じてフィールドに値を設定
	personPointer.Name = "Taro"
	personPointer.Age = 25
	fmt.Printf("Name: %s, Age: %d\n", personPointer.Name, personPointer.Age)
}
Name: Taro, Age: 25

make関数を用いた具体例

スライス、マップ、チャネルの生成例

以下は、make関数を用いてスライス、マップ、チャネルを初期化するサンプルコードです。

各データ構造は直ちに利用可能な状態となり、それぞれの特徴を簡単に確認できます。

package main
import "fmt"
func main() {
	// スライスの初期化: 長さ3、キャパシティ5のint型スライスを生成
	intSlice := make([]int, 3, 5)
	intSlice[0] = 10
	intSlice[1] = 20
	intSlice[2] = 30
	// マップの初期化: stringをキー、intを値とするマップを生成
	strIntMap := make(map[string]int)
	strIntMap["apple"] = 100
	strIntMap["banana"] = 200
	// チャネルの初期化: int型を扱うバッファサイズ2のチャネルを生成
	intChannel := make(chan int, 2)
	intChannel <- 1
	intChannel <- 2
	// スライスの出力
	fmt.Println("Slice:", intSlice)
	// マップの出力
	fmt.Println("Map:", strIntMap)
	// チャネルの出力(バッファから順に読み出し)
	fmt.Println("Channel:", <-intChannel, <-intChannel)
}
Slice: [10 20 30]
Map: map[apple:100 banana:200]
Channel: 1 2

開発時における考慮事項

保守性とコードの可読性

適切な関数選定のポイント

  • 目的に応じた関数選定がとても重要です。
  • 単純なメモリ領域確保にはnew、複雑な内部設定が必要なデータ構造の初期化にはmakeを選びます。
  • この選択により、コードの意図が明確になり、読み手にとって理解しやすくなります。
  • コードレビューや保守、バグ修正の際にも、選択基準がはっきりしていれば対応がスムーズです。

パフォーマンス面の視点

メモリ効率と処理速度の違い

  • newは単純なメモリ確保に特化しているため、オーバーヘッドが少なく、サクッとメモリを確保できるメリットがあります。
  • 一方でmakeは内部の複雑な初期化が必要なため、場合によっては多少のオーバーヘッドが発生します。
  • 実際のアプリケーションでは、データ構造の特性に合わせた最適な関数を使用することで、メモリ効率と処理速度の両立が可能となります。
  • 特に、大量のデータを扱うシステムや並行処理を行う場合は、使用するデータ構造ごとの特性を考慮して最適化を進めることが大切です。

まとめ

この記事では、Go言語におけるnewとmakeの基本的な使い分けや具体例、開発時の留意点について分かりやすく解説しました。

新とmakeの違いや各初期化方法の特性が整理され、用途に応じた最適な選択が理解できます。

ぜひコードを実際に試して、自分のプロジェクトに合った方法を見つけてみてください。

関連記事

Back to top button
目次へ