Многомерные массивы в языке Go

Массивы в Go

Многомерный массив — это массив массивов. Представьте его кактаблицу данныхилиматрицу, где каждый элемент доступен по нескольким индексам.

Многомерные массивы в Go можно использовать для обработки структурированной информации, такой как табличные данные, матричные операции или игровые доски.

Основные концепции

  • Одномерный массив: как точки на прямой, чтобы найти конкретный элемент, нужна только одна координата (индекс)
  • Двумерный массив: как таблица, требующая двух координат: строки и столбца
  • Трёхмерный массив: как куб, требующий трёх координат: длина, ширина и высота
  • Более высокие размерности: теоретически могут иметь много измерений, но на практике чаще всего используются двумерные и трёхмерные массивы

Это можно сравнить с нашими повседневными парковочными местами:

  • Одномерный массив= ряд парковочных мест (нужен только номер парковочного места)
  • Двумерный массив= многоэтажная парковка (нужны этаж и номер парковочного места)
  • Трёхмерный массив= несколько многоэтажных парковок (нужны номер парковки, этаж и номер парковочного места)

Go поддерживает многомерные массивы. Ниже приведены распространённые способы объявления многомерных массивов:

var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type

Объявление и инициализация:

// Объявление двумерного массива var arrayName [rows][cols]elementType // Объявление и инициализация var arrayName [rows][cols]elementType = [rows][cols]elementType{initialValues}

В следующем примере объявляется трёхмерный целочисленный массив:

var threedim [5][10][4]int

Двумерный массив

Двумерный массив — это простейший многомерный массив. Двумерный массив по сути состоит из одномерных массивов. Способ определения двумерного массива следующий:

var arrayName [ x ][ y ] variable_type

variable_type — это тип данных в Go, arrayName — имя массива. Двумерный массив можно рассматривать как таблицу, где x — строки, а y — столбцы. На следующей диаграмме показан двумерный массив a с тремя строками и четырьмя столбцами:

К элементам двумерного массива можно получить доступ черезa[ i ][ j ].

Пример

package main

import "fmt"

func main() {
    // Шаг 1: Создать массив
    values := [][]int{}

    // Шаг 2: Использовать функцию append() для добавления двух строк одномерных массивов в пустой двумерный массив
    row1 := []int{1, 2, 3}
    row2 := []int{4, 5, 6}
    values = append(values, row1)
    values = append(values, row2)

    // Шаг 3: Отобразить две строки данных
    fmt.Println("Row 1")
    fmt.Println(values[0])
    fmt.Println("Строка 2")
    fmt.Println(values[1])

    // Шаг 4: Доступ к первому элементу
    fmt.Println("Первый элемент:")
    fmt.Println(values[0][0])
}

Вывод приведенного выше примера:

Row 1
[1 2 3]
Row 2
[4 5 6]
第一个元素为:
1

Инициализация двумерного массива

Многомерные массивы можно инициализировать с помощью фигурных скобок. В следующем примере представлен двумерный массив с 3 строками и 4 столбцами:

a := [3][4]int{  
 {0, 1, 2, 3} ,   /*  第一行索引为 0 */
 {4, 5, 6, 7} ,   /*  第二行索引为 1 */
 {8, 9, 10, 11},   /* 第三行索引为 2 */
}
Примечание:В приведённом выше коде предпоследняя строка}должна иметь запятую, потому что последняя строка}Не обязательно должна быть на одной строке; её также можно записать так:
a := [3][4]int{  
 {0, 1, 2, 3} ,   /*  第一行索引为 0 */
 {4, 5, 6, 7} ,   /*  第二行索引为 1 */
 {8, 9, 10, 11}}   /* 第三行索引为 2 */

В следующем примере инициализируется двумерный массив с 2 строками и 2 столбцами:

Пример

package main

import "fmt"

func main() {
    // Создаём двумерный массив
    sites := [2][2]string{}

    // Добавляем элементы в двумерный массив
    sites[0][0] = "Google"
    sites[0][1] = "Example"
    sites[1][0] = "Taobao"
    sites[1][1] = "Weibo"

    // Вывести результат
    fmt.Println(sites)
}

Вывод приведенного выше примера:

[[Google Example] [Taobao Weibo]]

Доступ к двумерному массиву

Доступ к двумерному массиву осуществляется путем указания координат, таких как индекс строки и индекс столбца в массиве. Например:

val := a[2][3]
или 
var value int = a[2][3]

В приведенном выше примере был осуществлен доступ к четвертому элементу в третьей строке двумерного массива val.

Для вывода элементов двумерного массива можно использовать вложенные циклы:

Пример

package main

import "fmt"

func main() {
   /* Массив - 5 строк 2 столбца*/
   var a = [5][2]int{ {0,0}, {1,2}, {2,4}, {3,6},{4,8}}
   var i, j int

   /* вывести элементы массива */
   for  i = 0; i < 5; i++ {
      for j = 0; j < 2; j++ {
         fmt.Printf("a[%d][%d] = %d\n", i,j, a[i][j] )
      }
   }
}

Вывод приведенного выше примера:

a[0][0] = 0
a[0][1] = 0
a[1][0] = 1
a[1][1] = 2
a[2][0] = 2
a[2][1] = 4
a[3][0] = 3
a[3][1] = 6
a[4][0] = 4
a[4][1] = 8

В следующем примере создается многомерный массив, в котором количество элементов в каждом измерении неодинаково:

Пример

package main

import "fmt"

func main() {
    // Создать пустой двумерный массив
    animals := [][]string{}

    // Создайте три одномерных массива, каждый с разной длиной
    row1 := []string{"рыба", "акула", "угорь"}
    row2 := []string{"птица"}
    row3 := []string{"ящерица", "саламандра"}

    // Используйте функцию append() для добавления одномерного массива в двумерный массив
    animals = append(animals, row1)
    animals = append(animals, row2)
    animals = append(animals, row3)

    // Цикл для вывода
    for i := range animals {
        fmt.Printf("Строка: %v\n", i)
        fmt.Println(animals[i])
    }
}

Вывод приведенного выше примера:

Row: 0
[fish shark eel]
Row: 1
[bird]
Row: 2
[lizard salamander]

Доступ к элементам массива и их изменение

Доступ к элементам

Пример

package main

import "fmt"

func main() {
    // Создать матрицу 3x3
    matrix := [3][3]int{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
    }
   
    // Доступ к одному элементу
    fmt.Println("Строка 1, Столбец 2:", matrix[0][1])  // Вывод: 2
    fmt.Println("Строка 3, Столбец 3:", matrix[2][2]) // Вывод: 9
   
    // Доступ ко всей строке
    fmt.Println("Строка 2:", matrix[1])  // Вывод: [4 5 6]
   
    // Обход всех элементов
    fmt.Println("\nОбход всех элементов:")
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            fmt.Printf("matrix[%d][%d] = %d\n", i, j, matrix[i][j])
        }
    }
}

изменение элементов

Пример

package main

import "fmt"

func main() {
    // Создать нулевую матрицу 2x2
    var grid [2][2]int
   
    fmt.Println("Матрица до изменения:", grid)
   
    // Изменить элемент в определенной позиции
    grid[0][0] = 10
    grid[0][1] = 20
    grid[1][0] = 30
    grid[1][1] = 40
   
    fmt.Println("Матрица после изменения:", grid)
   
    // Пакетно изменить строку
    grid[0] = [2]int{100, 200}
    fmt.Println("После изменения первой строки:", grid)
}

Трехмерные и многомерные массивы

Пример трехмерного массива

Пример

package main

import "fmt"

func main() {
    // Объявить трехмерный массив 2x3x4
    // Можно понимать как: 2 плоскости, каждая с 3 строками и 4 столбцами
    var cube [2][3][4]int
   
    // Инициализация 3D
    cube = [2][3][4]int{
        { // Первая плоскость
            {1, 2, 3, 4},
            {5, 6, 7, 8},
            {9, 10, 11, 12},
        },
        { // Вторая плоскость
            {13, 14, 15, 16},
            {17, 18, 19, 20},
            {21, 22, 23, 24},
        },
    }
   
    // Доступ к элементам 3D-массива
    fmt.Println("cube[0][1][2] =", cube[0][1][2])  // Вывод: 7
    fmt.Println("cube[1][2][3] =", cube[1][2][3])  // Вывод: 24
   
    // Обход 3D-массива
    fmt.Println("\nСодержимое трёхмерного массива:)
    for i := 0; i < 2; i++ {
        fmt.Printf("Плоскость %d:\n", i)
        for j := 0; j < 3; j++ {
            for k := 0; k < 4; k++ {
                fmt.Printf("%3d ", cube[i][j][k])
            }
            fmt.Println()
        }
        fmt.Println()
    }
}

см. рисунок ниже:


Общие операции с многомерными массивами

1. Итерация с range

Пример

package main

import "fmt"

func main() {
    // Создать двумерный массив
    scores := [3][4]int{
        {85, 90, 78, 92},
        {88, 76, 95, 89},
        {92, 85, 88, 90},
    }
   
    fmt.Println("Таблица оценок студентов:")
   
    // Использовать range для перебора двумерного массива
    for i, row := range scores {
        fmt.Printf("Оценки студента %d: ", i+1)
        for j, score := range row {
            fmt.Printf("%d ", score)
            // Если нужны и индекс, и значение
            _ = j // Избежать предупреждения о неиспользуемой переменной
        }
        fmt.Println()
    }
   
    // Нужно только значение, а не индекс
    total := 0
    count := 0
    for _, row := range scores {
        for _, score := range row {
            total += score
            count++
        }
    }
    fmt.Printf("\nСредний балл: %.2f\n", float64(total)/float64(count))
}

2. Получение длины массива

Пример

package main

import "fmt"

func main() {
    // Создание нерегулярного многомерного массива
    jagged := [3][3]int{
        {1, 2, 3},
        {4, 5},
        {6, 7, 8, 9}, // Примечание: это вызовет ошибку компиляции, потому что каждая строка должна быть одинаковой длины
    }
   
    // Правильный пример: получение размеров массива
    matrix := [4][5]int{}
   
    // Получение количества строк
    rows := len(matrix)
    fmt.Println("Строк:", rows)  // Вывод: 4
   
    // Получить количество столбцов в первой строке (все строки имеют одинаковую длину)
    cols := len(matrix[0])
    fmt.Println("Столбцов:", cols)  // Вывод: 5
   
    // Получить общее количество элементов
    totalElements := rows * cols
    fmt.Println("Всего элементов:", totalElements)  // Вывод: 20
}

3. Сравнение массивов

Пример

package main

import "fmt"

func main() {
    // Создать два идентичных двумерных массива
    a := [2][2]int{{1, 2}, {3, 4}}
    b := [2][2]int{{1, 2}, {3, 4}}
    c := [2][2]int{{1, 2}, {3, 5}}
   
    // Массивы можно сравнивать напрямую (только когда размерности точно совпадают)
    fmt.Println("a == b:", a == b)  // Вывод: true
    fmt.Println("a == c:", a == c)  // Вывод: false
   
    // Примечание: массивы разных размерностей нельзя сравнивать
    // d := [2][3]int{{1, 2, 3}, {4, 5, 6}}
    // fmt.Println(a == d) // Ошибка компиляции: несоответствие типов
}

Практические сценарии применения

Сценарий 1: Игровое поле (Крестики-нолики)

Пример

package main

import "fmt"

func main() {
    // Инициализировать доску 3x3 для крестиков-ноликов
    var board [3][3]string
   
    // Инициализировать как пустое
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            board[i][j] = " "
        }
    }
   
    // Имитировать ходы
    board[0][0] = "X"
    board[1][1] = "O"
    board[2][2] = "X"
   
    // Вывести доску
    fmt.Println("Доска для крестиков-ноликов:")
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            fmt.Printf(" %s ", board[i][j])
            if j < 2 {
                fmt.Printf("|")
            }
        }
        fmt.Println()
        if i < 2 {
            fmt.Println("---+---+---")
        }
    }
}

Сценарий 2: Система управления оценками студентов

Пример

package main

import "fmt"

func main() {
    // Определение: 3 студента, у каждого студента по 4 курса
    var grades [3][4]float64
   
    // Ввод баллов студентов
    grades = [3][4]float64{
        {85.5, 90.0, 78.5, 92.0},  // Баллы студента 1
        {88.0, 76.5, 95.0, 89.5},  // Баллы студента 2
        {92.5, 85.0, 88.5, 90.0},  // Баллы студента 3
    }
   
    // Вычислить средний балл каждого студента
    fmt.Println("Статистика баллов студентов:")
    for i, studentGrades := range grades {
        sum := 0.0
        for _, grade := range studentGrades {
            sum += grade
        }
        average := sum / float64(len(studentGrades))
        fmt.Printf("Студент %d: средний балл = %.2f\n", i+1, average)
    }
   
    // Вычислить средний балл по каждому курсу
    fmt.Println("\nСредний балл по курсу:")
    for j := 0; j < 4; j++ {
        sum := 0.0
        for i := 0; i < 3; i++ {
            sum += grades[i][j]
        }
        average := sum / 3.0
        fmt.Printf("Курс %d: средний балл = %.2f\n", j+1, average)
    }
}

Сценарий 3: Обработка пикселей изображения

Пример

package main

import "fmt"

func main() {
    // Симулировать простое изображение в оттенках серого 3x3
    // Диапазон значений каждого пикселя: 0(чёрный) ~ 255(белый)
    var image [3][3]int
   
    // Инициализируем изображение (простой градиент)
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            image[i][j] = (i + j) * 50
        }
    }
   
    // Отобразить исходное изображение
    fmt.Println("Исходное изображение:")
    displayImage(image)
   
    // Обработка изображения: увеличение яркости
    fmt.Println("\nИзображение после увеличения яркости:")
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            // Увеличить яркость, но не превышать 255
            newValue := image[i][j] + 50
            if newValue > 255 {
                newValue = 255
            }
            image[i][j] = newValue
        }
    }
    displayImage(image)
}

func displayImage(img [3][3]int) {
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            fmt.Printf("%3d ", img[i][j])
        }
        fmt.Println()
    }
}

Заметки и лучшие практики

1. Длина массива — часть типа

Пример

package main

import "fmt"

func main() {
    // Эти два — разные типы!
    var a [2][3]int
    var b [3][2]int
   
    // Следующий код вызовет ошибку компиляции
    // a = b // ошибка: несоответствие типов
   
    fmt.Printf("тип a: %T\n", a)  // [2][3]int
    fmt.Printf("тип b: %T\n", b)  // [3][2]int
}

2. Тип-значение vs ссылочный тип

Пример

package main

import "fmt"

func main() {
    // Массивы — это типы-значения
    original := [2][2]int{{1, 2}, {3, 4}}
   
    // Присваивание создаёт копию
    copy := original
   
    // Изменение копии не влияет на исходный массив
    copy[0][0] = 100
   
    fmt.Println("Исходный массив:", original)  // [[1 2] [3 4]]
    fmt.Println("Копия массива:", copy)      // [[100 2] [3 4]]
   
    // Если вам нужна ссылочная семантика, можно использовать срезы (описаны в следующих статьях)
}

3. Вопросы производительности

  • Непрерывная память: многомерные массивы хранятся в памяти непрерывно, обеспечивая быстрый доступ
  • Фиксированный размер: длина массива определяется во время компиляции и не может изменяться динамически
  • Подходящие сценарии: когда размер данных известен и фиксирован, массивы — лучший выбор

Часто задаваемые вопросы

Вопрос 1: В чём разница между многомерным массивом и вложенным срезом?

Особенности Многомерные массивы Вложенные срезы
Size Фиксированный, определяется во время компиляции Динамический, изменяемый во время выполнения
Память Непрерывное выделение Возможно, несмежное
Производительность Высокая скорость доступа Немного медленнее, имеет дополнительные накладные расходы
Сценарий использования Размер данных известен Размер данных переменный

Вопрос 2: Как создать зубчатый двумерный массив?

Массивы Go требуют, чтобы каждая строка имела одинаковую длину. Если вам нужна зубчатая структура, следует использовать срезы:

Пример

// Используйте срезы для создания нерегулярных структур
irregular := [][]int{
    {1, 2, 3},
    {4, 5},          // В этой строке только 2 элемента
    {6, 7, 8, 9},    // Эта строка содержит 4 элемента
}

Q3: Можно ли использовать многомерный массив в качестве параметра функции?

Да, но обратите внимание, что массивы являются типами значений, и передача больших массивов приводит к затратам производительности:

Пример

func processMatrix(matrix [3][3]int) [3][3]int {
    // Обработка матрицы...
    matrix[0][0] = 100
    return matrix
}

// Лучший способ — использовать указатели или срезы
func processMatrixPtr(matrix *[3][3]int) {
    matrix[0][0] = 100
}

Массивы в Go

Другие расширения