Goのmake関数を復習してみませんか?
はじめに
組み込み関数のmake
関数を何気なく使用している方は多いのではないでしょうか?
(自分はかなり雰囲気で使ってました)
make
関数の使い方や内部動作を理解することで、コードの効率化やバグの防止につながります
本記事を機に、make
関数について一緒に復習してみましょう!
make
関数とは
概要
make
関数はGoの組み込み関数の一つで、以下のデータ構造を初期化するために使用します
- スライス(slice)
- マップ(map)
- チャネル(channel)
これらのデータ構造は内部に複雑な情報を持つため、単なるメモリ割り当てだけでなく、初期化が必要になります
make
関数はこれのデータ構造を使用可能な状態に初期化し、戻り値としてその型の値を返します
基本的な使い方
以下はmake
関数を使用した各データ構造の初期化例です
// スライスの作成
s := make([]int, length, capacity)
// マップの作成
m := make(map[string]int, hint)
// チャネルの作成
ch := make(chan int, buffer)
length
、capacity
、hint
、buffer
は初期サイズや容量(capacity)を指定するオプションの整数値です
内部で起きていること
1. スライス
sliceは、以下の情報を持つ構造体のようなものです
sliceについて理解を深めるにはこちらのdocsがおすすめです
- ポインタ:基になる配列を指す
- 長さ(len):スライスの要素数
- 容量(cap):スライスが参照できる基になる配列の最大要素数
内部処理
-
基になる配列の割り当て:指定された容量(
capacity
)に応じて配列を確保 - スライスヘッダーの構築:ポインタ、長さ、容量を設定したスライスを作成
例
s := make([]int, 5, 10)
長さ5、容量10のスライスを作成します。基になる配列は容量(10)分のメモリを確保します。
2. マップ(map)
mapは、ハッシュテーブルを使ってキーと値を関連付けるデータ構造です
内部処理
-
ハッシュテーブルの作成:指定された容量(
hint
)に基づいて、ハッシュテーブルの内部構造を初期化します。Goのmapは必要に応じて拡張されますが、容量を指定することで性能を最適化できます
例
m := make(map[string]int, 100)
容量100のマップを作成します。これにより、最初の100個の要素追加時に再配置(リハッシュ)が発生しにくくなります
要素を追加する際、容量が不足すると新たな容量のハッシュテーブルが作成され、既存の要素がリハッシュされます。このリハッシュは計算コストが高く、パフォーマンスに影響を与える可能性があるので避けたほうがよいです
3. チャネル(channel)
チャネルは、goroutine間でデータをやり取りするためのデータ構造です
内部処理:
-
チャネルの割り当て:指定されたバッファサイズ(
buffer
)に応じて、チャネルの内部バッファを確保します - 同期オブジェクトの初期化:送受信を管理するためのロックや条件変数を初期化します
例
ch := make(chan int, 5)
バッファサイズ5のチャネルを作成します。最初の5つの値は受信側が受け取る前に送信側が送信できます
よくある誤用と注意点
make
関数の利用において、よくある誤用や注意点を見ていきます
make
と new
の混同
1. new
関数は任意の型のポインタを取得するために利用します
具体的には、new(T)
は T
型のポインタを返します(引数で指定した型のゼロ値へのポインタです)
誤用の例
// `make`はスライス、マップ、チャネルの初期化に使います。これはエラーになる
p := make(int)
正しい使用法
p := new(int) // *int型のポインタを取得
ポイント
-
make
はスライス、マップ、チャネル専用です。他の型のポインタや値を初期化したい場合は、new
またはリテラルを使用します
2. mapの未初期化によるpanic
未初期化のマップ(nil
マップ)に値を設定しようとすると、ランタイムエラーが発生します
誤用の例
var m map[string]int
m["key"] = 1 // panicになる
正しい使用法
m := make(map[string]int)
m["key"] = 1 // 問題なく動作する
3. sliceの容量指定忘れによる非効率なメモリ使用
大量のデータをスライスに追加する際、容量を適切に指定しないと内部での配列再割り当てが頻発し、性能が低下します
よくある非効率なコード例
s := make([]int, 0)
for i := 0; i < 1000000; i++ {
s = append(s, i)
}
改善例
s := make([]int, 0, 1000000) // capを指定しておく
for i := 0; i < 1000000; i++ {
s = append(s, i)
}
容量を設定することで、メモリ再割り当ての回数を減らし性能を向上させます
4. チャネルのバッファサイズに関する注意
バッファサイズを指定しない(デフォルトの0)場合、チャネルはUnbuffered Channelとなり、送信と受信が同期的になります
初手、書きがちなコード
ch := make(chan int)
go func() {
ch <- 1
}()
Unbuffered Channel ch
に対して値1
を送信しようとしています
しかし、上記のコードには受信側が存在しないため、ch <- 1
の送信操作は永遠に完了せず、ゴルーチンはブロックされたままになります
結果、プログラム全体がデッドロックに陥ります
正しい使用法
ch := make(chan int, 1) // バッファサイズ1を指定
ch <- 1 // バッファに格納され、即座にゴルーチンは進行
バッファサイズを適切に設定し、デッドロックを防止しましょう
まとめ
make
関数の説明,使い方、よくある誤用,注意点をみてきました
make
関数は、Go言語において重要なデータ構造であるスライス、マップ、チャネルを正しく初期化するための欠かせない関数です
正しく理解し使いこなすことでGoを用いた開発が効率的かつ安全になります
今回の記事が参考になれば幸いです🙇
以上、Applibot Advent Calendar 2024 の13日目の記事でした
Discussion