В главе 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, }