🏃 Готовимся к интервью по Golang: массивы, слайсы и строки
Статья начинает серию материалов по подготовке к интервью на backend-разработчика на языке Go. В данном тексте рассматриваются особенности таких структур данных, как массивы, слайсы и строки, нюансы их использования и немного задач для самостоятельной тренировки.
В статье я хотел бы рассмотреть массивы, слайсы и строки в 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)