Eugene Mikhalev 09 апреля 2023

🏃 Готовимся к интервью по Golang: массивы, слайсы и строки

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

В статье я хотел бы рассмотреть массивы, слайсы и строки в 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}
    

Ссылка на playground.

Пример задачи
        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) // что выведет?
}

    

Ссылка на playground.

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека Go разработчика»

Срезы (slices)

Срез можно рассматривать как динамический массив. Это значит, что вы можете изменять его размер.

Срез представляет собой структуру, в которой содержится указатель на начало области памяти (массива), длина слайса (length) и объем области памяти (capacity)

В коде Golang slice определен как структура с указателем на массив, длиной и емкостью: https://github.com/golang/go/blob/master/src/runtime/slice.go#L15

Важной особенностью такого представления является то, что на одну и ту же область памяти с данными (массив) может ссылаться несколько слайсов. Например, как показано на рисунке:

Два среза (Drinks и Menu) указывают на перекрывающиеся области памяти
Два среза (Drinks и Menu) указывают на перекрывающиеся области памяти

Такое может получиться в результате операции 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)
    

Ссылка на playground.

  • Для добавления элемента(-ов) в слайс используется функция append:
Пример использования 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)
    

Ссылка на playground.

  • И можно итерировать по слайсу с помощью range.
Пример итерирования по элементами слайса
        menu := []string{"Pizza", "Cheese", "Tea", "Water", "Milk", "Burger", "Salad", "Pasta"}

for idx, dishTitle := range menu {
	fmt.Printf("%d. %s, ", idx, dishTitle)
}
    

Ссылка на playground.

Примеры задач

Задачи на понимание внутреннего устройства слайса
        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) // печатает весь слайс, что здесь выведет?
}
    

Ссылка на playground.

Задачи на понимание особенностей работы append и reslicing
        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) // что выведет?
}

    

Ссылка на playground.

Дополнительные материалы

Строки

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

Особенности строк:

  • строка содержит неизменяемые байты, ее длина и сами байты не могут быть изменены после объявления.
  • доступ по индексу – это доступ к байту, а не к символу (так как символ может занимать более одного байта).
  • исходный код в 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()

}

    

Ссылка на playground.

Примеры работы с рунами с помощью unicode/utf8
        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
	}
}
    

Ссылка на playground.

Примеры задач

Пример задачи на строки
        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)
}
    

Пример на playground.

Дополнительные материалы

***

Материалы по теме

Расскажите, какие вопросы вам задавали на интервью на Go-разработчика?

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ