🐛

Goのmake関数を復習してみませんか?

2024/12/22に公開

はじめに

https://pkg.go.dev/builtin#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)

lengthcapacityhintbuffer は初期サイズや容量(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関数の利用において、よくある誤用や注意点を見ていきます

1. makenewの混同

new関数は任意の型のポインタを取得するために利用します
具体的には、new(T)T型のポインタを返します(引数で指定した型のゼロ値へのポインタです)
https://pkg.go.dev/builtin#new

誤用の例

// `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