Go言語における上級データ型:配列、スライス、マップ、関数

この記事は、「Golang入門」シリーズの前回の記事を受け継いでおり、Goの構文の複雑さに深く触れ、特にGo言語の基本的なデータ型に焦点を当てたいます。この記事では、Go言語での上級データ型の世界に踏み入れます。ここでは、様々な形のデータをモデル化し、保存し、アクセスするために私たちが使えるツールの多様性を探究します。

配列

Goの配列は、同じタイプの項目の順序付けられた集まりであり、作成時にメモリ内に固定サイズで割り当てられます。多くのプログラミング言語の慣例に従って、配列のインデックスは0から始まり、カウントが1ではなく0から始まることを意味します。対照的に、Goのスライスは配列に代わるより動的な代替品です。スライスは予め決められた長さがなく、需要に応じて拡張または縮小します。

配列の固定された性質(例えば、長さ3の配列が作成され、3つ以上の項目を収容することはできません)は、多くのアプリケーションに必要な適応性に欠けています。例を挙げると、4つ目の項目を追加するには、より大きなサイズの新しい配列を作成し、既存の項目をコピーしてから新しい項目を追加する必要があります。そのため、スライスはGoでより一般的な選択肢となり、直接的に配列を扱うことはめったにありません。

Goでの配列宣言の構文は以下の通りです:

var fiveNames [5]string

fiveNames = [5]string{"John", "Jean", "Joe", "Jim", "Jane"}

// 関数内部でのみ := のショートハンドを使うことを覚えておいてください
firstName := fiveNames[0] // 最初の項目、Johnを取得する
secondName := fiveNames[1] // 2番目の項目、Jeanを取得する

lastName := fiveNames[len(fiveNames)-1] // Janeを返す

len() 関数は、Goに組み込まれている関数で、配列操作において重要な役割を果たし、配列の長さを返します。

Goの配列を特徴づけるいくつかの重要な点があります:

  1. 定数サイズ: Goの配列のサイズは定数でなければならず、コンパイル時に決定可能でなければなりません。

  2. サイズによるタイプの差別化: 配列のサイズはそのタイプにとって不可欠です。例えば、[3]int[4]int は異なるタイプです。

  3. サイズ推論のためのシンタックスシュガー: Goには、要素数に基づいて配列のサイズを推論するためのシンタックスシュガーがあります。例えば:

    // これにより、タイプ [5]string となります
    fiveNames := [...]string{"John", "Jean", "Joe", "Jim", "Jane"}
    
  4. 関数引数におけるコピー動作: 配列を関数の引数として渡すとき、関数は配列のコピーを受け取ります。したがって、関数内での変更や操作は、元の配列には影響しません。

配列のこれらのニュアンスにより、Goにおける配列は堅牢かつ静的に型付けされた性質を持っており、配列に基づく操作において予測可能性と型の安全性を保証しています。配列が本質的に不柔軟であることを理解することが重要であり、そのためGoでは日常的なアプリケーションで使用されることはめったにありませんが、より動的で汎用性に富むデータ構造であるスライスがGoプログラミングにおける主要な選択肢となっています。

スライスは、アプリケーションの要件に基づいてサイズが適応する、配列に代わる柔軟な代替手段を提供します。この適応性により、配列に比べて幅広い用途に適し、より便利で使いやすくなっています。

スライス

シンプルに言えば、スライスは固定サイズがない配列です。他の一般的なプログラミング言語のListの概念に似ています。内部的には、スライスは配列に基づいて構築されており、配列へのポインタとして機能します。配列とは異なり、関数へ引数としてスライスを渡すとき、関数はスライスのリファレンスまたはエイリアスを取得します。これは、変更が関数内で行われた際の影響を元のコピーにも及ぼすことを意味しており、理にかなっています。なぜなら、スライスは常に別のデータ構造へのポインタであるため、その基盤となる構造への変更は、それを参照または指しているすべてに影響するべきだからです。後でポインタと参照の詳細について詳しく触れます。

スライスを作成することは、固定サイズを指定せずに配列を宣言するのと同様に簡単です:

// []が空であることに注目してください。これはこのスライスがスライスであることを示しています
var sliceofNames []string

sliceOfNumbers := []int{0,1,2,3,4,5}

スライスでよく行われる操作は以下の通りです:

// インデックスから項目を取得する場合、0から始まるインデックスであることを覚えておいてください

zero := sliceOfNumbers[0]
one := sliceOfNumbers[1]

// これにより、2、3、4、5の項目が含まれる新しいスライスが作成されます。
// 操作は s[i:j] の形式で行われ、i が開始インデックス、j が終了インデックスです。
// end 演算子は終了インデックスの要素を含まないため、この結果のスライスでは j-1 要素までしか取得しません。
// そのために、例では6を使って6-1インデックスつまり数値5のインデックスを取得します。
twoTo5 := sliceOfNumbers[2:6]

// 上記と同じ結果を得ることができます。
// 終了インデックスが省略されている場合はlen(s)がデフォルト値になるため、すべてが含まれる場合と同じです。
twoTo5 := sliceOfNumbers[2:]

// 開始インデックスが無視された場合は、デフォルトでインデックス0が使われます。
// つまり新しいスライスは最初のインデックスから始まります。
zeroToFour := sliceOfNumbers[:5]

Go言語でのスライスと他言語の==を使った比較では、スライスの各項目を手動で比較する必要があります。これにより、Go言語のスライスと他言語のリストとは大きく異なります。addpopinsertといった一般的なタスクについても、Go言語ではより手作業が必要となることが多いです。

スライスに項目を追加するタスクから始めましょう。Go言語では、この目的のためにappend関数を提供しています。append関数はスライスと可変長の引数を取り、1つ、2つ、無限の引数を柔軟に指定することができます。

newSlice := append(oldSlice, newItem)

// append関数は可変長のパラメータを持っています。つまりスライスのあとにどのような数の引数でも渡すことができます。
newSlice := append(oldSlice, newItem1, newItem2, newItem3, ...)

// または別のスライスをスライスに追加することもできます。
newSlice = append(slice1, slice2...)

スライスから項目を削除するのは、それより少し複雑です:

names := []string{"John", "Paul", "George", "Ringo"}

// 最初の要素を削除する
names = names[1:]

// 最後の要素を削除する
names = names[:len(names)-1]

// スライスの中のn番目の要素を削除する
// 匿名関数を作成して変数に割り当てることに注意してください
removeNth := func(s []string, i int) []string {
        // インデックスが範囲外でないか確認
        if i >= len(s) {
            return s
        }
        /// ...を使っていることに注意してください。私はこれをスプレッド演算子と呼んでいます。
        return append(s[:i], s[i+1:]...)
}

names = removeNth(names, 10)

スライスを反復処理するために、Goはrangeメソッドを提供しています。このメソッドはスライスを反復処理し、最初の項目から始めて各反復で次の項目を返します。

// rangeは2つの結果を返します、最初のものはインデックス、二番目のものは値です
// この例ではインデックスを使わないので、_ という構文で無視します
for _, name := range names {
        fmt.Println(name)
}

// もし好みであれば、以下の一般的な方法を使うことができます
for i := 0; i < len(names); i++ {
        fmt.Println(names[i])
}

マップ

Goでは、mapは本質的にハッシュテーブルへの参照です。ハッシュテーブルは、キーと値のペアが無順序でまとめられているデータ構造のタイプです。各値は一意のキーに関連付けられ、この関連付けは「ハッシュ」関数と呼ばれる関数を使って可能になります。重要なことは、ハッシュテーブル内のすべてのキーは一意であり、ハッシュ関数を使って生成されます。

ハッシュテーブルは重要で価値のあるデータ構造であり、深く理解することを強くお勧めします。ハッシュテーブルは、一意のキーを基にして効率的にデータを整理し取り出すことができるため、様々なアプリケーションやアルゴリズムにおける中核となっています。

Goのmapは、アイテムを特定するために私たちが決めるキーを使ってアイテムを取得する順序が決まっていないスライスだと考えることができます。ただし、キーは==構文で比較可能でなければなりません。マップは以下の構文を持ち、Kはキーのタイプ、Vは値のタイプです。キーは同じタイプでなければなりません。つまり、あるアイテムに対してstringキーを使ってからintキーを使うことはできません。値も同じタイプでなければなりません。

var newMap map[K]V

// := の構文を使う
mapOfAlphabetsToLanguages := map[string]string{
    "A": "Ada",
    "B": "BASIC",
    "C": "C++",
    "D": "Dart",
    "E": "Erlang",
}

// 組み込みの make 関数を使う
newMap := make(map[string]int)

make 関数を使う理由について疑問に思うかもしれません。既に便利な := 構文があるとしてもです。その理由は、マップのサイズを事前に知っている場合に慣れます。make関数を使ってこのように指定します:make(map, sizeOfMap)。これにより、最初から適切なサイズのマップを割り当て、リソースの使用を最適化することができます。これは、特に大規模なデータセットを扱う場合や、効率性とリソース管理が重要なシナリオで特に有益です。

以下は、マップで行える一般的なアクションのいくつかです:

mapOfAlphabetsToLanguages := map[string]string{
    "A": "Ada",
    "B": "BASIC",
    "C": "C++",
    "D": "Dart",
    "E": "Erlang",
}

// キーを渡すことで値を取得することができます
ada := mapOfAlphabetsToLanguages["A"] // これは大文字と小文字を区別します

// もしこれを実<br><br>こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。<br>[https://dev.to/intellect4all/advance-data-types-in-go-arrays-slices-maps-functions-36cb](https://dev.to/intellect4all/advance-data-types-in-go-arrays-slices-maps-functions-36cb)