Массивы, срезы, карты

В главе 3 мы изучили базовые типы Go. В этой главе мы рассмотрим еще три встроенных типа: массивы, срезы и карты.

Массивы

Массив — это нумерованная последовательность элементов одного типа с фиксированной длинной. В Go они выглядят так:

var x [5]int

x— это пример массива, состоящего из пяти элементов типаint. Запустим следующую программу:

package main

import "fmt"

func main() {
    var x [5]int
    x[4] = 100
    fmt.Println(x)
}

Вы должны увидеть следующее:

[0 0 0 0 100]

x[4] = 100должно читаться как «присвоить пятому элементу массива x значение 100». Может показаться странным то, чтоx[4]является пятым элементом массива, а не четвертым, но, как и строки, массивы нумеруются с нуля. Доступ к элементам массива выглядит так же, как у строк. Вместоfmt.Println(x)мы можем написатьfmt.Println(x[4])и в результате будет выведено100.

Пример программы, использующей массивы:

func main() {
    var x [5]float64
    x[0] = 98
    x[1] = 93
    x[2] = 77
    x[3] = 82
    x[4] = 83

    var total float64 = 0
    for i := 0; i < 5; i++ {
        total += x[i]
    }
    fmt.Println(total / 5)
}

Эта программа вычисляет среднюю оценку за экзамен. Если вы выполните её, то увидите86.6. Давайте рассмотрим её внимательнее:

  • сперва мы создаем массив длины 5 и заполняем его;
  • затем мы в цикле считаем общее количество баллов;
  • и в конце мы делим общую сумму баллов на количество элементов, чтобы узнать средний балл.

Эта программа работает, но её всё еще можно улучшить. Во-первых, бросается в глаза следующее:i < 5иtotal / 5. Если мы изменим количество оценок с 5 на 6, то придется переписывать код в этих двух местах. Будет лучше использовать длину массива:

var total float64 = 0
for i := 0; i < len(x); i++ {
    total += x[i]
}
fmt.Println(total / len(x))

Напишите этот кусок кода и запустите программу. Вы должны получить ошибку:

$ go run tmp.go
# command-line-arguments
.\tmp.go:19: invalid operation: total / len(x) (mismatched types float64 and int)

Проблема в том, чтоlen(x)иtotalимеют разный тип.totalимеет типfloat64, аlen(x)int. Так что, нам надо конвертироватьlen(x)вfloat64:

fmt.Println(total / float64(len(x)))

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

Другая вещь, которую мы можем изменить в нашей программе - это цикл:

var total float64 = 0
for i, value := range x {
    total += value
}
fmt.Println(total / float64(len(x)))

В этом циклеiпредставляет текущую позицию в массиве, аvalueбудет тем же самым что иx[i]. Мы использовали ключевое словоrangeперед переменной, по которой мы хотим пройтись циклом.

Выполнение этой программы вызовет другую ошибку:

$ go run tmp.go
# command-line-arguments
.\tmp.go:16: i declared and not used

Компилятор Go не позволяет вам создавать переменные, которые никогда не используются в коде. Поскольку мы не используемiвнутри нашего цикла, то надо изменить код следующим образом:

var total float64 = 0
for _, value := range x {
    total += value
}
fmt.Println(total / float64(len(x)))

Одиночный символ подчеркивания_используется, чтобы сказать компилятору, что переменная нам не нужна (в данном случае нам не нужна переменная итератора).

А еще в Go есть короткая запись для создания массивов:

x := [5]float64{ 98, 93, 77, 82, 83 }

Указывать тип не обязательно — Go сам может его выяснить по содержимому массива.

Иногда массивы могут оказаться слишком длинными для записи в одну строку, в этом случае Go позволяет записывать их в несколько строк:

x := [5]float64{ 
    98, 
    93, 
    77, 
    82, 
    83,
}

Обратите внимание на последнюю,после83. Она обязательна и позволяет легко удалить элемент из массива просто закомментировав строку:

x := [4]float64{ 
    98, 
    93, 
    77, 
    82, 
    // 83,
}

Срезы

Срез это часть массива. Как и массивы, срезы индексируются и имеют длину. В отличии от массивов их длину можно изменить. Вот пример среза:

var x []float64

Единственное отличие объявления среза от объявления массива — отсутствие указания длины в квадратных скобках. В нашем случаеxбудет иметь длину 0.

Срез создается встроенной функциейmake:

x := make([]float64, 5)

Этот код создаст срез, который связан с массивом типаfloat64, длиной5. Срезы всегда связаны с каким-нибудь массивом. Они не могут стать больше чем массив, а вот меньше — пожалуйста. Функцияmakeпринимает и третий параметр:

x := make([]float64, 5, 10)

10— это длина массива, на который указывает срез:

Другой способ создать срез — использовать выражение[low : high]:

arr := [5]float64{1,2,3,4,5}
x := arr[0:5]

low- это позиция, с которой будет начинаться срез, аhigh- это позиция, где он закончится. Например:arr[0:5]вернет[1,2,3,4,5],arr[1:4]вернет[2,3,4].

Для удобства мы также можем опуститьlow,highили и то, и другое.arr[0:]это то же самое чтоarr[0:len(arr)],arr[:5]то же самое чтоarr[0:5]иarr[:]то же самое чтоarr[0:len(arr)].

Функции срезов

В Go есть две встроенные функции для срезов:appendиcopy. Вот пример работы функцииappend:

func main() {
    slice1 := []int{1,2,3}
    slice2 := append(slice1, 4, 5)
    fmt.Println(slice1, slice2)
}

После выполнения программыslice1будет содержать[1,2,3], аslice2[1,2,3,4,5].append создает новый срез из уже существующего (первый аргумент) и добавляет к нему все следующие аргументы.

Пример работыcopy:

func main() {
    slice1 := []int{1,2,3}
    slice2 := make([]int, 2)
    copy(slice2, slice1)
    fmt.Println(slice1, slice2)
}

После выполнения этой программыslice1будет содержать[1,2,3], аslice2[1,2]. Содержимоеslice1копируется вslice2, но поскольку вslice2есть место только для двух элементов, то только два первых элементаslice1будут скопированы.

Карта

Карта (также известна как ассоциативный массив или словарь) — это неупорядоченная коллекция пар вида ключ-значение. Пример:

var x map[string]int

Карта представляется в связке с ключевым словомmap, следующим за ним типом ключа в скобках и типом значения после скобок. Читается это следующим образом: «x— это картаstring-ов дляint-ов».

Подобно массивам и срезам, к элементам карт можно обратиться с помощью скобок. Запустим следующую программу:

var x map[string]int
x["key"] = 10
fmt.Println(x)

Вы должны увидеть ошибку, похожую на эту:

panic: runtime error: assignment to entry in nil map

goroutine 1 [running]:
main.main()
  main.go:7 +0x4d

goroutine 2 [syscall]:
created by runtime.main
        C:/Users/ADMINI~1/AppData/Local/Temp/2/bindi
t269497170/go/src/pkg/runtime/proc.c:221
exit status 2

До этого момента мы имели дело только с ошибками во время компиляции. Сейчас мы видим ошибку исполнения.

Проблема нашей программы в том, что карта должна быть инициализирована перед тем, как будет использована. Надо написать так:

x := make(map[string]int)
x["key"] = 10
fmt.Println(x["key"])

Если выполнить эту программу, то вы должны увидеть10. Выражениеx["key"] = 10похоже на те, что использовались при работе с массивами, но ключ тут не число, а строка (потому что в карте указан тип ключаstring). Мы также можем создать карты с ключом типаint:

x := make(map[int]int)
x[1] = 10
fmt.Println(x[1])

Это выглядит очень похоже на массив, но существует несколько различий. Во-первых, длина карты (которую мы можем найти так:len(x)) может измениться, когда мы добавим в нее новый элемент. В самом начале при создании длина0, послеx[1] = 10она станет равна1. Во-вторых, карта не является последовательностью. В нашем примере у нас есть элементx[1], в случае массива должен быть и первый элементx[0], но в картах это не так.

Также мы можем удалить элементы из карты используя встроенную функциюdelete:

delete(x, 1)

Давайте посмотрим на пример программы, использующей карты:

package main

import "fmt"

func main() {
    elements := make(map[string]string)
    elements["H"] = "Hydrogen"
    elements["He"] = "Helium"
    elements["Li"] = "Lithium"
    elements["Be"] = "Beryllium"
    elements["B"] = "Boron"
    elements["C"] = "Carbon"
    elements["N"] = "Nitrogen"
    elements["O"] = "Oxygen"
    elements["F"] = "Fluorine"
    elements["Ne"] = "Neon"

    fmt.Println(elements["Li"])
}

В данном примереelements- это карта, которая представляет 10 первых химических элементов, индексируемых символами. Это очень частый способ использования карт — в качестве словаря или таблицы. Предположим, мы пытаемся обратиться к несуществующему элементу:

fmt.Println(elements["Un"])

Если вы выполните это, то ничего не увидите. Технически карта вернет нулевое значение хранящегося типа (для строк это пустая строка). Несмотря на то, что мы можем проверить нулевое значение с помощью условия (elements["Un"] == ""), в Go есть лучший способ сделать это:

name, ok := elements["Un"]
fmt.Println(name, ok)

Доступ к элементу карты может вернуть два значения вместо одного. Первое значение это результат запроса, второе говорит, был ли запрос успешен. В Go часто встречается такой код:

if name, ok := elements["Un"]; ok {    
    fmt.Println(name, ok)
}

Сперва мы пробуем получить значение из карты, а затем, если это удалось, мы выполняем код внутри блока.

Объявления карт можно записывать сокращенно - так же, как массивы:

elements := map[string]string{
    "H": "Hydrogen",
    "He": "Helium",
    "Li": "Lithium",
    "Be": "Beryllium",
    "B": "Boron",
    "C": "Carbon",
    "N": "Nitrogen",
    "O": "Oxygen",
    "F": "Fluorine",
    "Ne": "Neon",
}

Карты часто используются для хранения общей информации. Давайте изменим нашу программу так, чтобы вместо имени элемента хранить какую-нибудь дополнительную информацию о нем. Например его агрегатное состояние:

func main() {
    elements := map[string]map[string]string{
            "H": map[string]string{
            "name":"Hydrogen", 
            "state":"gas",
        },
        "He": map[string]string{
            "name":"Helium", 
            "state":"gas",
        },
        "Li": map[string]string{
            "name":"Lithium", 
            "state":"solid",
        },
        "Be": map[string]string{
            "name":"Beryllium", 
            "state":"solid",
        },
        "B":  map[string]string{
            "name":"Boron",
            "state":"solid",
        },
        "C":  map[string]string{
            "name":"Carbon",
            "state":"solid",
        },
        "N":  map[string]string{
            "name":"Nitrogen",
            "state":"gas",
        },
        "O":  map[string]string{
            "name":"Oxygen",
            "state":"gas",
        },
        "F":  map[string]string{
            "name":"Fluorine",
            "state":"gas",
        },
        "Ne":  map[string]string{
            "name":"Neon",
            "state":"gas",
        },
    }

    if el, ok := elements["Li"]; ok {    
        fmt.Println(el["name"], el["state"])
    }
}

Заметим, что тип нашей карты теперьmap[string]map[string]string. Мы получили карту строк для карты строк. Внешняя карта используется как поиск по символу химического элемента, а внутренняя — для хранения информации об элементе. Не смотря на то, что карты часто используется таким образом, в главе 9 мы узнаем лучший способ хранения данных.

Задачи

  • Как обратиться к четвертому элементу массива или среза?

  • Чему равна длина среза, созданного таким способом:make([]int, 3, 9)?

  • Дан массив:

    x := [6]string{"a","b","c","d","e","f"}
    

    что вернет вамx[2:5]?

  • Напишите программу, которая находит самый наименьший элемент в этом списке:

    x := []int{
        48,96,86,68,
        57,82,63,70,
        37,34,83,27,
        19,97, 9,17,
    }
    

results matching ""

    No results matching ""