Go言語入門:言語仕様 -Vol.23-

スポンサーリンク
Go言語入門:言語仕様 -Vol.23- 用語解説
Go言語入門:言語仕様 -Vol.23-
この記事は約17分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語の言語仕様について解説しています。

スポンサーリンク

背景

Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。

言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。

そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。

言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!

Slice types(スライス型)

スライスは、基礎となる配列の連続したセグメントの記述子であり、その配列からの番号付き要素のシーケンスへのアクセスを提供します。スライス型は、その要素型の配列のすべてのスライスの集合を示します。要素数はスライスの長さと呼ばれ、決して負になりません。初期化されていないスライスの値はnilです。

SliceType = "[" "]" ElementType .

スライスsの長さは、組み込み関数lenによって調べることができます。配列とは異なり、実行中に変化する可能性があります。要素は、0からlen(s)-1までの整数インデックスでアドレス指定できます。指定された要素のスライスインデックスは、基礎となる配列内の同じ要素のインデックスよりも小さい場合があります。

スライスは、一度初期化されると、常にその要素を保持する基礎となる配列に関連付けられます。したがって、スライスはその配列および同じ配列の他のスライスとストレージを共有します。対照的に、異なる配列は常に異なるストレージを表します。

スライスの基礎となる配列は、スライスの終わりを超えて拡張される場合があります。容量はその範囲の尺度です。これはスライスの長さとスライスを超えた配列の長さの合計です。その容量までの長さのスライスは、元のスライスから新しいスライスをスライシングすることによって作成できます。スライスaの容量は、組み込み関数cap(a)を使用して調べることができます。

指定された要素型Tの新しい初期化されたスライス値は、組み込み関数makeを使用して作成でき、これはスライス型と長さを指定するパラメータ、およびオプションで容量を指定するパラメータを受け取ります。makeで作成されたスライスは、常に新しい隠された配列を割り当て、返されるスライス値はそれを参照します。つまり、次を実行すると:

make([]T, length, capacity)

配列を割り当ててスライスするのと同じスライスが生成されるため、これら2つの式は同等です:

make([]int, 50, 100)
new([100]int)[0:50]

配列と同様に、スライスは常に一次元ですが、高次元のオブジェクトを構築するために組み合わせることができます。配列の配列では、内部の配列は構造上常に同じ長さです。ただし、スライスのスライス(またはスライスの配列)では、内部の長さは動的に変化する可能性があります。さらに、内部のスライスは個別に初期化する必要があります。


解説

スライスとは何か?

スライス(slice) は、配列の一部を参照する「窓」のようなものです。配列と違って長さが可変で、実行時に要素を追加・削除できます。

たとえ話: スライスは「可変長のビューワー」です。本棚(配列)の一部を見る窓のようなもので、窓のサイズ(長さ)は自由に変えられます。窓を通して本を見たり、本を追加したりできますが、実際の本は本棚(配列)に保存されています。

package main

import "fmt"

func main() {
    // スライスの宣言(配列と違って長さを指定しない)
    var numbers []int
    
    fmt.Println(numbers)        // []
    fmt.Println(numbers == nil) // true
    
    // 要素を追加
    numbers = append(numbers, 10)
    numbers = append(numbers, 20)
    numbers = append(numbers, 30)
    
    fmt.Println(numbers) // [10 20 30]
}

1. スライスの基本

スライスの宣言と初期化

package main

import "fmt"

func main() {
    // 方法1: nilスライス
    var s1 []int
    fmt.Println(s1 == nil) // true
    
    // 方法2: 空のスライス(nilではない)
    s2 := []int{}
    fmt.Println(s2 == nil) // false
    
    // 方法3: 初期値付き
    s3 := []int{1, 2, 3, 4, 5}
    
    // 方法4: makeを使う
    s4 := make([]int, 5)       // 長さ5
    s5 := make([]int, 5, 10)   // 長さ5、容量10
    
    fmt.Println(s1, s2, s3, s4, s5)
}

長さと容量

スライスには長さ(length)容量(capacity) があります。

package main

import "fmt"

func main() {
    s := make([]int, 5, 10)
    
    // len(): スライスの長さ(現在の要素数)
    fmt.Println("長さ:", len(s))  // 5
    
    // cap(): スライスの容量(基礎配列のサイズ)
    fmt.Println("容量:", cap(s))  // 10
    
    // 長さ5、容量10なので、あと5個追加できる
    s = append(s, 1, 2, 3, 4, 5)
    fmt.Println("長さ:", len(s))  // 10
    fmt.Println("容量:", cap(s))  // 10
}

たとえ話:

  • 長さ: 現在使っている席の数
  • 容量: 会議室の全席数(基礎配列のサイズ)

2. スライシング操作

既存のスライスや配列から、新しいスライスを作成できます。

基本的なスライシング

package main

import "fmt"

func main() {
    s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    
    // s[開始:終了] - 開始は含む、終了は含まない
    sub1 := s[2:5]   // [2 3 4]
    sub2 := s[:5]    // [0 1 2 3 4] (最初から5番目の手前まで)
    sub3 := s[5:]    // [5 6 7 8 9] (5番目から最後まで)
    sub4 := s[:]     // [0 1 2 3 4 5 6 7 8 9] (全体)
    
    fmt.Println(sub1, sub2, sub3, sub4)
}

容量を指定したスライシング

package main

import "fmt"

func main() {
    s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    
    // s[開始:終了:容量の終端]
    sub := s[2:5:7]
    
    fmt.Println("値:", sub)       // [2 3 4]
    fmt.Println("長さ:", len(sub)) // 3 (5-2)
    fmt.Println("容量:", cap(sub)) // 5 (7-2)
}

3. 基礎配列の共有

スライスは基礎配列を共有します。これは重要な概念です。

共有による影響

package main

import "fmt"

func main() {
    original := []int{1, 2, 3, 4, 5}
    
    // スライスを作成(基礎配列を共有)
    sub := original[1:4]
    
    fmt.Println("original:", original) // [1 2 3 4 5]
    fmt.Println("sub:", sub)           // [2 3 4]
    
    // subを変更すると...
    sub[0] = 100
    
    // originalも変わる!(同じ基礎配列を参照)
    fmt.Println("original:", original) // [1 100 3 4 5]
    fmt.Println("sub:", sub)           // [100 3 4]
}

たとえ話: 同じ本棚を2つの窓から見ているようなもの。一方の窓から本を動かすと、もう一方の窓からも変化が見えます。

コピーで独立させる

package main

import "fmt"

func main() {
    original := []int{1, 2, 3, 4, 5}
    
    // copy()で完全に独立したスライスを作成
    copied := make([]int, len(original))
    copy(copied, original)
    
    // copiedを変更
    copied[0] = 100
    
    // originalは変わらない
    fmt.Println("original:", original) // [1 2 3 4 5]
    fmt.Println("copied:", copied)     // [100 2 3 4 5]
}

4. appendによる要素の追加

append()は、スライスに要素を追加する組み込み関数です。

基本的な使い方

package main

import "fmt"

func main() {
    var s []int
    
    // 1つずつ追加
    s = append(s, 1)
    s = append(s, 2)
    s = append(s, 3)
    
    fmt.Println(s) // [1 2 3]
    
    // 複数を一度に追加
    s = append(s, 4, 5, 6)
    fmt.Println(s) // [1 2 3 4 5 6]
    
    // 別のスライスを展開して追加
    more := []int{7, 8, 9}
    s = append(s, more...)
    fmt.Println(s) // [1 2 3 4 5 6 7 8 9]
}

容量の自動拡張

package main

import "fmt"

func main() {
    s := make([]int, 0, 2)
    fmt.Printf("長さ:%d, 容量:%d\n", len(s), cap(s)) // 0, 2
    
    s = append(s, 1)
    fmt.Printf("長さ:%d, 容量:%d\n", len(s), cap(s)) // 1, 2
    
    s = append(s, 2)
    fmt.Printf("長さ:%d, 容量:%d\n", len(s), cap(s)) // 2, 2
    
    // 容量を超えると自動的に拡張される(通常は2倍)
    s = append(s, 3)
    fmt.Printf("長さ:%d, 容量:%d\n", len(s), cap(s)) // 3, 4
}

5. make()によるスライスの作成

make()は、指定したサイズのスライスを作成します。

長さだけ指定

package main

import "fmt"

func main() {
    // 長さ5のスライス(容量も5)
    s := make([]int, 5)
    
    fmt.Println(s)           // [0 0 0 0 0]
    fmt.Println(len(s))      // 5
    fmt.Println(cap(s))      // 5
}

長さと容量を指定

package main

import "fmt"

func main() {
    // 長さ5、容量10のスライス
    s := make([]int, 5, 10)
    
    fmt.Println(s)           // [0 0 0 0 0]
    fmt.Println(len(s))      // 5
    fmt.Println(cap(s))      // 10
    
    // あと5個追加できる
    s = append(s, 1, 2, 3, 4, 5)
    fmt.Println(len(s))      // 10
    fmt.Println(cap(s))      // 10
}

makeと配列割り当ての同等性

package main

import "fmt"

func main() {
    // これらは同等
    s1 := make([]int, 50, 100)
    s2 := new([100]int)[0:50]
    
    fmt.Println(len(s1), cap(s1)) // 50 100
    fmt.Println(len(s2), cap(s2)) // 50 100
}

6. 多次元スライス

スライスのスライスを作ることで、多次元構造を表現できます。

2次元スライス

package main

import "fmt"

func main() {
    // 3行の2次元スライス
    matrix := make([][]int, 3)
    
    // 各行を個別に初期化(長さは可変)
    matrix[0] = []int{1, 2, 3}
    matrix[1] = []int{4, 5}
    matrix[2] = []int{6, 7, 8, 9}
    
    fmt.Println(matrix)
    // [[1 2 3] [4 5] [6 7 8 9]]
    
    // 各行の長さが違う
    for i, row := range matrix {
        fmt.Printf("行%d: 長さ=%d\n", i, len(row))
    }
}

固定サイズの2次元スライス

package main

import "fmt"

func main() {
    // 3行4列の2次元スライス
    rows, cols := 3, 4
    matrix := make([][]int, rows)
    
    for i := range matrix {
        matrix[i] = make([]int, cols)
    }
    
    // 値を設定
    matrix[0][0] = 1
    matrix[1][2] = 5
    matrix[2][3] = 9
    
    fmt.Println(matrix)
    // [[1 0 0 0] [0 0 5 0] [0 0 0 9]]
}

7. スライスの走査

for-range

package main

import "fmt"

func main() {
    fruits := []string{"apple", "banana", "cherry"}
    
    // インデックスと値の両方
    for i, fruit := range fruits {
        fmt.Printf("%d: %s\n", i, fruit)
    }
    
    // 値だけ
    for _, fruit := range fruits {
        fmt.Println(fruit)
    }
    
    // インデックスだけ
    for i := range fruits {
        fmt.Println(i)
    }
}

8. 実用例

例1: 動的配列

package main

import "fmt"

func main() {
    var numbers []int
    
    // ユーザー入力を受け付ける想定
    inputs := []int{10, 20, 30, 40, 50}
    
    for _, input := range inputs {
        numbers = append(numbers, input)
    }
    
    fmt.Println("入力された数:", numbers)
}

例2: フィルタリング

package main

import "fmt"

func Filter(numbers []int, predicate func(int) bool) []int {
    var result []int
    for _, n := range numbers {
        if predicate(n) {
            result = append(result, n)
        }
    }
    return result
}

func main() {
    numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    
    // 偶数だけ抽出
    evens := Filter(numbers, func(n int) bool {
        return n%2 == 0
    })
    
    fmt.Println(evens) // [2 4 6 8 10]
}

例3: スタック

package main

import "fmt"

type Stack []int

func (s *Stack) Push(value int) {
    *s = append(*s, value)
}

func (s *Stack) Pop() (int, bool) {
    if len(*s) == 0 {
        return 0, false
    }
    index := len(*s) - 1
    value := (*s)[index]
    *s = (*s)[:index]
    return value, true
}

func main() {
    var stack Stack
    
    stack.Push(1)
    stack.Push(2)
    stack.Push(3)
    
    for {
        if val, ok := stack.Pop(); ok {
            fmt.Println(val)
        } else {
            break
        }
    }
    // 出力: 3, 2, 1
}

例4: スライスの結合

package main

import "fmt"

func main() {
    s1 := []int{1, 2, 3}
    s2 := []int{4, 5, 6}
    s3 := []int{7, 8, 9}
    
    // 複数のスライスを結合
    combined := append(s1, s2...)
    combined = append(combined, s3...)
    
    fmt.Println(combined)
    // [1 2 3 4 5 6 7 8 9]
}

まとめ: スライス型で覚えておくべきこと

スライスの特徴

  1. 可変長: 要素を追加・削除できる
  2. 参照型: 基礎配列を共有
  3. 長さと容量: len()cap()で確認
  4. nil値: 未初期化のスライスはnil

宣言と初期化

// nilスライス
var s1 []int

// 空のスライス
s2 := []int{}

// 初期値付き
s3 := []int{1, 2, 3}

// makeで作成
s4 := make([]int, 5)      // 長さ5
s5 := make([]int, 5, 10)  // 長さ5、容量10

よく使う操作

// 追加
s = append(s, 1)
s = append(s, 2, 3, 4)
s = append(s, anotherSlice...)

// スライシング
sub := s[1:4]

// コピー
copied := make([]int, len(s))
copy(copied, s)

// 長さと容量
len(s)
cap(s)

実用的なアドバイス

package main

import "fmt"

func main() {
    // 1. 容量を事前に確保すると効率的
    s := make([]int, 0, 100) // 100個追加する予定
    
    // 2. 基礎配列の共有に注意
    original := []int{1, 2, 3}
    sub := original[1:2]
    sub[0] = 100  // originalも変わる!
    
    // 3. 独立させるにはcopy
    copied := make([]int, len(original))
    copy(copied, original)
    
    // 4. nilスライスと空スライスの違い
    var nilSlice []int      // nil
    emptySlice := []int{}   // not nil
    
    fmt.Println(s, sub, copied, nilSlice == nil, emptySlice == nil)
}

スライスは、Goで最もよく使われるデータ構造です。柔軟性と効率性を兼ね備えており、配列よりも一般的に使われます!

おわりに 

本日は、Go言語の言語仕様について解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました