🏆 151 курс за 1 подписку: хватит выбирать — бери все и сразу!

Один клик — 151 возможность. Подпишись на OTUS сейчас!
Техномир мчится вперед, а вместе с ними растут и требования к специалистам. OTUS придумал крутую штуку — подписку на 151 курс по всем ключевым направлениям IT!
-
Почему подписка OTUS меняет правила игры:
- Доступ к 151 курсу от практикующих экспертов
- В 3 раза выгоднее, чем покупать каждый курс отдельно
- До 3 курсов одновременно без дополнительных затрат
- Свобода выбора направления — меняй треки когда угодно
Изучай новое, развивайся в своем темпе, меняй направления — подпишись на OTUS и прокачивай скилы по полной!
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576. Erid 2VtzqupFnNL
В статье я хотел бы рассмотреть массивы, слайсы и строки в Golang и их особенности. Я стараюсь приводить примеры вопросов и заданий, которые могут встретиться вам на собеседовании на должность backend-разработчика, где предполагается знание языка Go. Практически все это вы сможете найти в других источниках, но в статье я постарался собрать в одном месте и отсеять то, что, на мой взгляд, является второстепенным, чтобы уменьшить количество материала и обратить внимание читателя на более основные и важные моменты. Для более детального изучения вы сможете воспользоваться ссылками на дополнительные материалы, приведенные в статье.
Массивы (arrays)
Сразу хочу сказать, что вопросы по массивам встречаются на интервью редко. Однако их понимание необходимо, потому что они являются базой для слайсов и строк. Массив в Go не отличается по своей сути от массивов в общепринятом смысле – это структура данных, которая содержит от 0 до N элементов определенного (заданного) типа. Количество элементов массива указывается при его создании.
Особенности массива:
- память под массив выделяется в процессе создания.
- размер массива поменять невозможно.
- два массива разной размерности, но с элементами одного типа – это два разных массива (разных типа). Следовательно, их невозможно сравнить между собой с помощью операторов
==
и!=
- по умолчанию все элементы массива инициализируются нулевыми значениями заданного типа.
Примеры:
var nonInitedArray [5]int
var initedArrayWithLen [5]int = [5]int{5, 4, 3, 2, 1}
initedArrayWithoutLen := [...]int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
package main
import "fmt"
func foo(a [5]int) {
a[3] = 10
}
func bar(a *[5]int) {
a[3] = 10
}
func main() {
a := [...]int{1, 2, 3, 4, 5}
fmt.Printf("%#v\n", a)
foo(a)
fmt.Printf("%#v\n", a) // что выведет?
bar(&a)
fmt.Printf("%#v\n", a) // что выведет?
}
Срезы (slices)
Срез можно рассматривать как динамический массив. Это значит, что вы можете изменять его размер.
Срез представляет собой структуру, в которой содержится указатель на начало области памяти (массива), длина слайса (length) и объем области памяти (capacity)
В коде Golang slice определен как структура с указателем на массив, длиной и емкостью: https://github.com/golang/go/blob/master/src/runtime/slice.go#L15
Важной особенностью такого представления является то, что на одну и ту же область памяти с данными (массив) может ссылаться несколько слайсов. Например, как показано на рисунке:

Такое может получиться в результате операции
re-slicing'а. Для этого используется запись:
newSlice = originalSlice[firstElementIndex:lastElementIndex]
.
Обратите внимание, что lasElementIndex
не
включается в новый слайс (т. е. в слайсе
будут элементы от firstElementIndex
до
lastElementIndex-1
включительно): пример на playground.
Если
не указывать firstElementIndex
, то он будет
равен первому элементу, если не указывать lastElementIndex
, то он будет равен длине слайса: пример на playground.
Особенности среза:
- нулевое значение слайса:
nil
и к нему можно применять функциюappend
для добавления элементов (пример на playground). - при создании с помощью
make
можно указать capacity (третьим аргументом). - особенности работы
append
: при достаточном capacity увеличивается length слайса, если места не хватает – происходит перевыделение памяти и копирование данных. Новый слайс указывает на новую область памяти с новой длиной (length) и обьемом (capacity). Обычно говорят, что capacity увеличивается в 2 раза (на 100%), но это верно пока количество элементов в слайсе менее 512. После этого увеличение размера плавно уменьшается до 25% (пример на playground). Логику работыappend
можно посмотреть в коде golang. - стоит учитывать, что слайс хотя и является структурой, но содержит внутри ссылку и поэтому при рейслайсинге или передаче слайса в функцию и изменении данных слайса в новом слайсе или внутри функции они будут изменены и в оригинальном слайсе, так как указывают на одну область памяти. Чтобы избежать такого поведения, можно воспользоваться функцией
copy
– она скопирует данные в новый слайс (пример на playground). - однако относительно предыдущего пункта стоит учитывать, что если функция
append
расширила область памяти (не хватило capacity и была выделена дополнительная память), то старый слайс будет указывать на старую область, а новый – на новую. Тогда изменения в одном из них не приведут к изменениям в другом (пример на playground). Но стоит помнить, что не всегда приappend
происходит перевыделение памяти. - слайсы (как и массивы) – одномерные. Для создания двумерного слайса нужно создать слайс слайсов (пример на playground).
- cлайсы можно сравнивать только с
nil
. В остальных случаях можно использоватьreflect.DeepEqual
(пример на playground).
Примеры работы со срезами
- Создать слайс можно разными способами (см. пример), при использование
make
можно указать capacity (обратите внимание на значения по умолчанию):
a := []string{"Pizza", "Cheese", "Tea", "Water", "Milk", "Burger", "Salad", "Pasta"}
b := []int{} // слайс из 0 элементов
var c []int // пусто слайс, значения nil по умолчанию
d := make([]int, 5, 20)
e := make([]int, 5)
- Для добавления элемента(-ов) в слайс используется функция
append
:
menu := []string{"Pizza", "Cheese", "Tea", "Water", "Milk", "Burger", "Salad", "Pasta"}
addMenu := []string{"Toast", "Boiled Eggs", "Omlet"}
fmt.Printf("Menu: %v\n", menu)
menu = append(menu, "Coffee")
fmt.Printf("Menu: %v\n", menu)
menu = append(menu, addMenu...)
fmt.Printf("Menu: %v\n", menu)
- И можно итерировать по слайсу с помощью
range
.
menu := []string{"Pizza", "Cheese", "Tea", "Water", "Milk", "Burger", "Salad", "Pasta"}
for idx, dishTitle := range menu {
fmt.Printf("%d. %s, ", idx, dishTitle)
}
Примеры задач
func bar(a []int) {
for i := 0; i < len(a); i += 2 {
a[i], a[i+1] = a[i+1], a[i]
}
}
func main() {
a := []int{1, 2, 3, 4, 5, 6}
fmt.Printf("a[1]=%d\n", a[1])
foo(a)
fmt.Printf("a[1]=%d\n", a[1]) // что выведет?
bar(a)
fmt.Printf("a=%v\n", a) // печатает весь слайс, что здесь выведет?
}
func foo(a []int) {
a = append(a, 7)
a[1] = 7
}
func bar(a *[]int) {
*a = append(*a, 7)
}
func main() {
a := []int{1, 2, 3, 4, 5, 6}
fmt.Printf("a[1]=%d\n", a[1])
b := a[1:3]
b[0] = 10
fmt.Printf("1. a[1]=%d\n", a[1]) // что выведет?
b = append(b, a...)
b[0] = 100
fmt.Printf("2. a[1]=%d\n", a[1]) // что выведет?
foo(a)
fmt.Printf("3. a[1]=%d\n", a[1]) // что выведет?
bar(&a)
fmt.Printf("4. a=%v\n", a) // что выведет?
}
Дополнительные материалы
- A tour of Go: Slices
- A Comprehensive Guide of Arrays and Slices in Golang (and their differences) (или перевод на habr)
- SliceTricks
Строки
Строка представляет собой слайс байтов и является неизменяемой. Это значит, что вы не можете поменять отдельный байт в строке после ее объявления. Однако стоит сказать, что строка может быть представлена в различной кодировке и один символ не обязательно соответствует одному байту (это зависит от символа и используемой кодировки).
Особенности строк:
- строка содержит неизменяемые байты, ее длина и сами байты не могут быть изменены после объявления.
- доступ по индексу – это доступ к байту, а не к символу (так как символ может занимать более одного байта).
- исходный код в Go использует кодировку UTF-8, поэтому строки обычно представлены в этой кодировке (если значения строк заданы в тексте программы).
rune
– специальный тип в Go, который представляет символ в формате UTF-8.- для итерации по
runes
(рунам) можно использовать операторrange
- для работы с UTF-8 можно использовать пакет
unicode/utf8
из стандартной библиотеки.
Примеры работы со строками
func main() {
s1 := "hello, world!"
s2 := `Hello, "World"!`
s3 := `Long string
Next line`
s4 := "Привет, Мир!"
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(s3)
for idx, ch := range s4 {
fmt.Printf("%d=%c ", idx, ch)
}
fmt.Println()
}
func main() {
s := "Привет, Мир!"
cnt := utf8.RuneCountInString(s)
runeIdx := 0
for i := 0; i < cnt; i++ {
r, siz := utf8.DecodeRuneInString(s[runeIdx:])
fmt.Printf("%c", r)
runeIdx += siz
}
}
Примеры задач
func main() {
s1 := "Hello, World!"
s2 := "Привет, Мир!"
// Что выведет?
for i := 0; i < len(s1); i++ {
fmt.Printf("%c", s1[i])
}
fmt.Println()
// Что выведет?
for i := 0; i < len(s2); i++ {
fmt.Printf("%c", s2[i])
}
fmt.Println()
// Что выведет?
/*s1[len(s1)-1] = '.'
fmt.Println(s1)*/
// Что выведет?
s1 = s1[0:5]
fmt.Println(s1)
// Что выведет?
s2 = s2[0:6]
fmt.Println(s2)
}
Дополнительные материалы
- Strings, bytes, runes and characters in Go
- Go Walkthrough: bytes + strings packages (или перевод на habr)
Расскажите, какие вопросы вам задавали на интервью на Go-разработчика?