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

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

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

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

スポンサーリンク

背景

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

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

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

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

Array types(配列型)

配列は、要素型と呼ばれる単一の型の要素の番号付きシーケンスです。要素数は配列の長さと呼ばれ、決して負になりません。

ArrayType   = "[" ArrayLength "]" ElementType .
ArrayLength = Expression .
ElementType = Type .

長さは配列の型の一部です。これは、int型の値で表現可能な非負の定数として評価される必要があります。配列aの長さは、組み込み関数lenを使用して調べることができます。要素は、0からlen(a)-1までの整数インデックスでアドレス指定できます。配列型は常に一次元ですが、多次元型を形成するために組み合わせることができます。

[32]byte
[2*N] struct { x, y int32 }
[1000]*float64
[3][5]int
[2][2][2]float64  // [2]([2]([2]float64))と同じ

配列型Tは、それらの含む型が配列または構造体型のみである場合、直接的または間接的に、型Tの要素、またはTをコンポーネントとして含む型の要素を持つことはできません。

// 無効な配列型
type (
	T1 [10]T1                 // T1の要素型がT1
	T2 [10]struct{ f T2 }     // T2が構造体のコンポーネントとしてT2を含む
	T3 [10]T4                 // T3がT4内の構造体のコンポーネントとしてT3を含む
	T4 struct{ f T3 }         // T4が構造体内の配列T3のコンポーネントとしてT4を含む
)

// 有効な配列型
type (
	T5 [10]*T5                // T5がポインタのコンポーネントとしてT5を含む
	T6 [10]func() T6          // T6が関数型のコンポーネントとしてT6を含む
	T7 [10]struct{ f []T7 }   // T7が構造体内のスライスのコンポーネントとしてT7を含む
)

解説

配列とは何か?

配列(array) は、同じ型の要素固定個数並べたデータ構造です。

たとえ話: 配列は「ロッカー」のようなもので、同じサイズの箱が固定個数並んでいます。各箱には番号(インデックス)が付いていて、0番から始まります。一度作ったロッカーのサイズ(要素数)は変更できません。

package main

import "fmt"

func main() {
    // 5個の整数を格納する配列
    var numbers [5]int
    
    numbers[0] = 10
    numbers[1] = 20
    numbers[2] = 30
    numbers[3] = 40
    numbers[4] = 50
    
    fmt.Println(numbers)  // [10 20 30 40 50]
}

1. 配列の基本

配列の宣言

package main

import "fmt"

func main() {
    // 基本的な宣言
    var arr1 [5]int  // ゼロ値で初期化: [0 0 0 0 0]
    
    // 初期値を指定
    var arr2 [3]string = [3]string{"apple", "banana", "cherry"}
    
    // 短縮構文
    arr3 := [4]int{1, 2, 3, 4}
    
    // 長さを自動計算
    arr4 := [...]int{10, 20, 30}  // 長さ3の配列
    
    fmt.Println(arr1, arr2, arr3, arr4)
}

配列の長さ

package main

import "fmt"

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    
    // len()で長さを取得
    length := len(arr)
    fmt.Println("長さ:", length)  // 5
    
    // 長さは型の一部
    var arr2 [5]int  // arr と同じ型
    var arr3 [10]int // arr とは異なる型
    
    // これは代入できる
    arr2 = arr  // OK
    
    // これはエラー(型が違う)
    // arr3 = arr  // cannot use arr (type [5]int) as type [10]int
    
    _ = arr2
    _ = arr3
}

2. インデックスによるアクセス

配列の要素は、0から始まるインデックスでアクセスできます。

package main

import "fmt"

func main() {
    arr := [5]int{10, 20, 30, 40, 50}
    
    // 読み取り
    fmt.Println(arr[0])  // 10
    fmt.Println(arr[2])  // 30
    fmt.Println(arr[4])  // 50
    
    // 書き込み
    arr[1] = 25
    arr[3] = 45
    
    fmt.Println(arr)  // [10 25 30 45 50]
    
    // 範囲外アクセスはコンパイルエラー(定数の場合)
    // fmt.Println(arr[5])  // invalid array index 5 (out of bounds for 5-element array)
    
    // 実行時の範囲外アクセスはパニック
    // i := 5
    // fmt.Println(arr[i])  // panic: runtime error: index out of range [5] with length 5
}

3. 配列の初期化

部分的な初期化

package main

import "fmt"

func main() {
    // 最初の3つだけ初期化、残りはゼロ値
    arr1 := [5]int{1, 2, 3}
    fmt.Println(arr1)  // [1 2 3 0 0]
    
    // インデックスを指定して初期化
    arr2 := [5]int{0: 10, 2: 30, 4: 50}
    fmt.Println(arr2)  // [10 0 30 0 50]
    
    // 混在も可能
    arr3 := [5]int{1, 2, 4: 50}
    fmt.Println(arr3)  // [1 2 0 0 50]
}

配列のコピー

配列は値型なので、代入するとコピーされます。

package main

import "fmt"

func main() {
    arr1 := [3]int{1, 2, 3}
    
    // arr2にコピー
    arr2 := arr1
    
    // arr2を変更
    arr2[0] = 10
    
    // arr1は変わらない
    fmt.Println("arr1:", arr1)  // [1 2 3]
    fmt.Println("arr2:", arr2)  // [10 2 3]
}

4. 多次元配列

配列を組み合わせて、多次元配列を作れます。

2次元配列

package main

import "fmt"

func main() {
    // 3行5列の配列
    var matrix [3][5]int
    
    // 初期化
    matrix[0][0] = 1
    matrix[0][1] = 2
    matrix[1][0] = 3
    
    fmt.Println(matrix)
    // [[1 2 0 0 0] [3 0 0 0 0] [0 0 0 0 0]]
    
    // 初期値を指定
    matrix2 := [2][3]int{
        {1, 2, 3},
        {4, 5, 6},
    }
    
    fmt.Println(matrix2)
    // [[1 2 3] [4 5 6]]
}

3次元配列

package main

import "fmt"

func main() {
    // 2×2×2の配列
    var cube [2][2][2]float64
    
    cube[0][0][0] = 1.0
    cube[1][1][1] = 8.0
    
    fmt.Println(cube)
    
    // 初期化
    cube2 := [2][2][2]int{
        {
            {1, 2},
            {3, 4},
        },
        {
            {5, 6},
            {7, 8},
        },
    }
    
    fmt.Println(cube2)
    // [[[1 2] [3 4]] [[5 6] [7 8]]]
}

5. 配列の走査

for文での走査

package main

import "fmt"

func main() {
    arr := [5]int{10, 20, 30, 40, 50}
    
    // 方法1: インデックスを使う
    for i := 0; i < len(arr); i++ {
        fmt.Printf("%d: %d\n", i, arr[i])
    }
    
    // 方法2: for-range
    for i, v := range arr {
        fmt.Printf("%d: %d\n", i, v)
    }
    
    // インデックスが不要な場合
    for _, v := range arr {
        fmt.Println(v)
    }
}

多次元配列の走査

package main

import "fmt"

func main() {
    matrix := [2][3]int{
        {1, 2, 3},
        {4, 5, 6},
    }
    
    // ネストしたループ
    for i := 0; i < len(matrix); i++ {
        for j := 0; j < len(matrix[i]); j++ {
            fmt.Printf("matrix[%d][%d] = %d\n", i, j, matrix[i][j])
        }
    }
    
    // for-range版
    for i, row := range matrix {
        for j, val := range row {
            fmt.Printf("matrix[%d][%d] = %d\n", i, j, val)
        }
    }
}

6. 配列と関数

配列を引数として渡す

配列は値型なので、コピーが渡されます

package main

import "fmt"

func modify(arr [3]int) {
    arr[0] = 100  // コピーを変更
    fmt.Println("関数内:", arr)
}

func main() {
    arr := [3]int{1, 2, 3}
    
    modify(arr)
    // 関数内: [100 2 3]
    
    // 元の配列は変わらない
    fmt.Println("main:", arr)
    // main: [1 2 3]
}

ポインタを使って配列を変更

package main

import "fmt"

func modifyByPointer(arr *[3]int) {
    arr[0] = 100  // 元の配列を変更
    fmt.Println("関数内:", *arr)
}

func main() {
    arr := [3]int{1, 2, 3}
    
    modifyByPointer(&arr)
    // 関数内: [100 2 3]
    
    // 元の配列が変わる
    fmt.Println("main:", arr)
    // main: [100 2 3]
}

7. 配列 vs スライス

配列とスライスは似ていますが、重要な違いがあります。

特徴配列スライス
長さ固定(型の一部)可変
宣言[5]int[]int
値型/参照型値型参照型
サイズ変更不可可能(append)
package main

import "fmt"

func main() {
    // 配列(長さ固定)
    arr := [3]int{1, 2, 3}
    fmt.Printf("配列: %v, 型: %T\n", arr, arr)
    
    // スライス(長さ可変)
    slice := []int{1, 2, 3}
    fmt.Printf("スライス: %v, 型: %T\n", slice, slice)
    
    // スライスは要素を追加できる
    slice = append(slice, 4, 5)
    fmt.Println("追加後:", slice)  // [1 2 3 4 5]
    
    // 配列は追加できない
    // arr = append(arr, 4)  // エラー!
}

8. 再帰的な配列型の制限

配列型は、自分自身を要素として直接または間接的に含むことができません(配列・構造体経由の場合)。

無効な例

// ❌ 無効な配列型

// T1は自分自身を要素に持つ
// type T1 [10]T1

// T2は構造体経由で自分自身を含む
// type T2 [10]struct{ f T2 }

// T3とT4が相互に参照
// type T3 [10]T4
// type T4 struct{ f T3 }

有効な例

ポインタ、関数、スライス経由なら再帰的な定義が可能です。

package main

import "fmt"

// ✅ 有効な配列型

// ポインタ経由
type T5 [10]*T5

// 関数経由
type T6 [10]func() T6

// スライス経由
type T7 [10]struct{ f []T7 }

func main() {
    var t5 T5
    var t6 T6
    var t7 T7
    
    fmt.Printf("%T %T %T\n", t5, t6, t7)
}

9. 実用例

例1: 固定サイズのバッファ

package main

import "fmt"

const BufferSize = 1024

type Buffer [BufferSize]byte

func main() {
    var buf Buffer
    
    // データを書き込む
    copy(buf[:], []byte("Hello, World!"))
    
    // 最初の13バイトを表示
    fmt.Printf("%s\n", buf[:13])
    // Hello, World!
}

例2: ルックアップテーブル

package main

import "fmt"

func main() {
    // 月の日数(平年)
    daysInMonth := [12]int{
        31, 28, 31, 30, 31, 30,
        31, 31, 30, 31, 30, 31,
    }
    
    month := 2  // 3月(0始まりなので2)
    fmt.Printf("3月の日数: %d日\n", daysInMonth[month])
}

例3: 行列計算

package main

import "fmt"

type Matrix [2][2]int

func AddMatrices(a, b Matrix) Matrix {
    var result Matrix
    for i := 0; i < 2; i++ {
        for j := 0; j < 2; j++ {
            result[i][j] = a[i][j] + b[i][j]
        }
    }
    return result
}

func main() {
    a := Matrix{{1, 2}, {3, 4}}
    b := Matrix{{5, 6}, {7, 8}}
    
    c := AddMatrices(a, b)
    fmt.Println(c)
    // [[6 8] [10 12]]
}

例4: 固定サイズのキュー

package main

import "fmt"

type Queue struct {
    data  [5]int
    front int
    rear  int
    size  int
}

func (q *Queue) Enqueue(val int) bool {
    if q.size == 5 {
        return false  // キューが満杯
    }
    q.data[q.rear] = val
    q.rear = (q.rear + 1) % 5
    q.size++
    return true
}

func (q *Queue) Dequeue() (int, bool) {
    if q.size == 0 {
        return 0, false  // キューが空
    }
    val := q.data[q.front]
    q.front = (q.front + 1) % 5
    q.size--
    return val, true
}

func main() {
    var q Queue
    
    q.Enqueue(1)
    q.Enqueue(2)
    q.Enqueue(3)
    
    val, ok := q.Dequeue()
    if ok {
        fmt.Println("取り出した値:", val)  // 1
    }
}

まとめ: 配列型で覚えておくべきこと

配列の基本

  1. 固定長: 長さは型の一部で変更不可
  2. 同じ型: すべての要素が同じ型
  3. 値型: 代入や引数渡しでコピーされる
  4. 0始まり: インデックスは0からlen(arr)-1まで

宣言と初期化

// 宣言
var arr1 [5]int

// 初期値付き
arr2 := [3]string{"a", "b", "c"}

// 長さ自動
arr3 := [...]int{1, 2, 3, 4}

// インデックス指定
arr4 := [5]int{0: 10, 4: 50}

配列 vs スライス

// 配列: 長さ固定
arr := [5]int{1, 2, 3, 4, 5}

// スライス: 長さ可変(推奨)
slice := []int{1, 2, 3, 4, 5}
slice = append(slice, 6)  // 要素を追加可能

実用的なアドバイス

package main

import "fmt"

func main() {
    // 1. 固定サイズが必要な場合のみ配列を使う
    var buffer [1024]byte
    
    // 2. 通常はスライスを使う方が柔軟
    slice := []int{1, 2, 3}
    slice = append(slice, 4)
    
    // 3. 多次元データには配列が便利
    matrix := [3][3]int{
        {1, 0, 0},
        {0, 1, 0},
        {0, 0, 1},
    }
    
    // 4. 大きな配列は関数にポインタで渡す
    largeArray := [1000000]int{}
    processArray(&largeArray)  // コピーを避ける
    
    fmt.Println(buffer[0], slice, matrix[0][0])
}

func processArray(arr *[1000000]int) {
    // 処理
}

配列は、固定サイズのデータ構造として重要ですが、Goではスライスの方が一般的に使われます。用途に応じて適切に使い分けましょう!

おわりに 

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

よっしー
よっしー

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

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

コメント

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