Готовимся к интервью по Golang: массивы, слайсы и строки
После чтения данного урока вы сможете:
- В случае необходимости использовать срез массива;
- Упорядочить срезы в алфавитном порядке через стандартную библиотеку.
Содержание статьи
Планеты нашей Солнечной системы принято классифицировать на планеты земной группы, газовые гиганты и ледяные гиганты. Ниже дается схема. Пока что можем сфокусироваться на земной группе, отрезав первые четыре элементы массива planets через planets[0:4]. Срез не изменяет массив planets. Он просто создает своеобразное окно, или вид внутрь массива. Данный вид называют срезом.
Премиум ???? канал по Golang
Рекомендуем вам супер TELEGRAM канал по Golang где собраны все материалы для качественного изучения языка. Удивите всех своими знаниями на собеседовании! ????
Подписаться на канал
Уроки, статьи и Видео
Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.
Go в ВК ЧАТ в Telegram
Срез Солнечной системы
У вас есть какие-то коллекции? Они организованы в определенном порядке? К примеру, книги на полке могут быть рассортированы по фамилиям автора. Упорядочивание подобного рода помогает ориентироваться и быстро книги.
Таким же образом вы можете также использовать срезы к нулю как часть коллекции.
Срез массива в Golang
Срез выражается через наполовину открытый диапазон. В следующем примере planets[0:4] начинается с планеты с индексом 0, продолжается, но не включает планету с индексом 4.
planets := […]string{
«Меркурий»,
«Венера»,
«Земля»,
«Марс»,
«Юпитер»,
«Сатурн»,
«Уран»,
«Нептун»,
}
terrestrial := planets[0:4]
gasGiants := planets[4:6]
iceGiants := planets[6:8]
fmt.Println(terrestrial, gasGiants, iceGiants) // Выводит: [Меркурий Венера Земля Марс] [Юпитер Сатурн] [Уран Нептун]
terrestrial := planets[0:4]gasGiants := planets[4:6]iceGiants := planets[6:8]fmt.Println(terrestrial, gasGiants, iceGiants) // Выводит: [Меркурий Венера Земля Марс] [Юпитер Сатурн] [Уран Нептун] |
Хотя terrestrial, gasGiants и iceGiants являются срезами, вы по-прежнему можете получать значение по индексу внутри срезов, как и внутри массивов:
fmt.Println(gasGiants[0]) // Выводит: Юпитер
fmt.Println(gasGiants[0]) // Выводит: Юпитер |
Вы также можете разрезать массив, а потом полученный срез разрезать еще раз:
giants := planets[4:8]
gas := giants[0:2]
ice := giants[2:4]
fmt.Println(giants, gas, ice) // Выводит: [Юпитер Сатурн Уран Нептун] [Юпитер Сатурн] [Уран Нептун]
fmt.Println(giants, gas, ice) // Выводит: [Юпитер Сатурн Уран Нептун] [Юпитер Сатурн] [Уран Нептун] |
Срезы terrestrial, gasGiants, iceGiants, giants, gas и ice являются видами одного и того же массива planets. Присваивание нового значения к элементу среза меняет базовый массив planets. Изменение будет видно и в других срезах:
iceGiantsMarkII := iceGiants // Копирует срез iceGiants (вид массива planets)
iceGiants[1] = «Посейдон»
fmt.Println(planets) // Выводит: [Меркурий Венера Земля Марс Юпитер Сатурн Уран Посейдон]
fmt.Println(iceGiants, iceGiantsMarkII, ice) // Выводит: [Уран Посейдон] [Уран Посейдон] [Уран Посейдон]
iceGiantsMarkII := iceGiants // Копирует срез iceGiants (вид массива planets)iceGiants[1] = «Посейдон»fmt.Println(planets) // Выводит: [Меркурий Венера Земля Марс Юпитер Сатурн Уран Посейдон]fmt.Println(iceGiants, iceGiantsMarkII, ice) // Выводит: [Уран Посейдон] [Уран Посейдон] [Уран Посейдон] |
Вопросы для проверки:
- Что получается в результате разреза массива?
- При разрезе через planets[4:6] сколько элементов получится в конечном итоге?
Индексы для среза по умолчанию
При разрезе массива пропуск первого индекса равнозначен указанию начала массива, а пропуск последнего индекса — длине массива. Таким образом, разрез в Листинге 1 можно написать и так:
terrestrial := planets[:4]
gasGiants := planets[4:6]
iceGiants := planets[6:]
terrestrial := planets[:4]gasGiants := planets[4:6] |
Обратите внимание, что индексы среза не могут быть отрицательными.
К чему приводит пропуск обоих индексов? Переменная allPlanets является срезом, который теперь содержит все восемь планет:
Срез строк в Golang
Синтаксис среза массива также можно использовать для строк:
neptune := «Neptune»
tune := neptune[3:]
fmt.Println(tune) // Выводит: tune
fmt.Println(tune) // Выводит: tune |
Результатом среза строки является другая строка. Однако, присваивание нового значения neptune не изменит значения tune и наоборот:
neptune = «Poseidon»
fmt.Println(tune) // Выводит: tune
fmt.Println(tune) // Выводит: tune |
Имейте в виду, что индексы учитывают количество байтов, но не рун:
question := «¿Cómo estás?»
fmt.Println(question[:6]) // Выводит: ¿Cóm
question := «¿Cómo estás?»fmt.Println(question[:6]) // Выводит: ¿Cóm |
Вопрос для проверки:
Если бы Земля и Марс были единственными колонизированными планетами, как бы вы могли получить срез colonized от terrestrial?
colonized := terrestrial[2:]
Композитные литералы для срезов
Многие функции Go лучше оперируют со срезами, чем с массивами. Если вам нужен срез, что показывает каждый элемент базового массива, можно объявить массив, а затем сделать срез через [:]. Это делается следующим образом:
dwarfArray := […]string{«Церера», «Плутон», «Хаумеа», «Макемаке», «Эрида»}
dwarfSlice := dwarfArray[:]
dwarfArray := […]string{«Церера», «Плутон», «Хаумеа», «Макемаке», «Эрида»}dwarfSlice := dwarfArray[:] |
Разрез массива является одним из способов создания среза, однако вы также можете объявить срез напрямую. У среза строк тип []string, внутри квадратных скобок нет никакого значения. Это отличается от объявления массива, где всегда уточняется фиксированная длина или многоточие внутри квадратных скобок.
В следующем примере срез dwarfs инициализируется через уже знакомый синтаксис композитного литерала:
dwarfs := []string{«Церера», «Плутон», «Хаумеа», «Макемаке», «Эрида»}
dwarfs := []string{«Церера», «Плутон», «Хаумеа», «Макемаке», «Эрида»} |
Базовый массив по-прежнему существует. Незаметно, Go сам объявляет массив из пяти элементов, а затем делает срез, что показывает все элементы.
Задание для проверки:
Используйте специальный символ %T для сравнения типов dwarfArray и среза dwarfs.
fmt.Printf(«array %T
«, dwarfArray) // Выводит: array [5]string
fmt.Printf(«slice %T
«, dwarfs) // Выводит: slice []string
fmt.Printf(«array %T «, dwarfArray) // Выводит: array [5]stringfmt.Printf(«slice %T «, dwarfs) // Выводит: slice []string |
Преимущества среза массива в Golang
Представьте, что существует способ преодолеть преграды пространства и времени, что позволяет объединять миры и путешествовать в мгновение ока? Использование стандартной библиотеки Go в совокупности с некоторой изобретательностью позволяет функции hyperspace из Листинга 4 модифицировать срез worlds, убрав отступы между мирами разных планет.
package main
import (
«fmt»
«strings»
)
// hyperspace убирает отступы между мирами планет
func hyperspace(worlds []string) { // Данный аргумент является срезом, а не массивом
for i := range worlds {
worlds[i] = strings.TrimSpace(worlds[i])
}
}
func main() {
planets := []string{» Венера «, «Земля «, » Марс»} // Планеты, разделенные друг от друга пробелами
hyperspace(planets)
fmt.Println(strings.Join(planets, «»)) Выводит: ВенераЗемляМарс
}
// hyperspace убирает отступы между мирами планет func hyperspace(worlds []string) { // Данный аргумент является срезом, а не массивом worlds[i] = strings.TrimSpace(worlds[i]) planets := []string{» Венера «, «Земля «, » Марс»} // Планеты, разделенные друг от друга пробелами fmt.Println(strings.Join(planets, «»)) Выводит: ВенераЗемляМарс |
Оба worlds и planets являются срезами, и хотя worlds представляет собой копию, они оба указывают на один и тот же базовый массив.
Если бы hyperspace изменил точки начала или конца среза worlds, данные изменения не оказали бы никакого влияния на срез planets. Однако hyperspace может достигнуть базового массива, на который указывает worlds, и изменить его элементы. Данные изменения доступны другим срезам (видам) массива.
Срезы более изменчивые, нежели массивы и в других аспектах. У срезов есть длина, однако, в отличие от длины массивов, она не является частью типа. Вы можете передать срез любого размера функции hyperspace:
dwarfs := []string{» Церера «, » Плутон»}
hyperspace(dwarfs)
dwarfs := []string{» Церера «, » Плутон»} |
Массивы редко используются напрямую. Разработчики Go предпочитают срезы ввиду их гибкости, особенно при передачи аргументов функции.
Вопрос для проверки:
Просмотрите документацию Go, отыщите TrimSpace и Join. Какую функциональность они предоставляют?
- TrimSpace возвращает срез с удаленными пробелами в начале и в конце;
- Join конкатенирует срез элементов через разделитель, помещенный между ними.
Срезы с методами в Golang
В Go можно определить тип через базовый слайс или массив. При наличии типа вы можете закрепить за ним метод. Способность Go объявлять методы типам доказывает их многогранность по сравнению с классами в других языках.
Пакет sort стандартной библиотеки объявляет тип StringSlice:
type StringSlice []string
type StringSlice []string |
К StringSlice прикрепляется метод Sort:
func (p StringSlice) Sort()
func (p StringSlice) Sort() |
Для упорядочивания планет в алфавитном порядке в следующем примере planets конвертируется в тип sort.StringSlice, а затем вызывается метод Sort:
package main
import (
«fmt»
«sort»
)
func main() {
planets := []string{
«Меркурий», «Венера», «Земля», «Марс»,
«Юпитер», «Сатурн», «Уран», «Нептун»,
}
sort.StringSlice(planets).Sort() // Сортирует planets в алфавитном порядке
fmt.Println(planets) // Выводит: [Венера Земля Марс Меркурий Нептун Сатурн Уран Юпитер]
}
«Меркурий», «Венера», «Земля», «Марс», «Юпитер», «Сатурн», «Уран», «Нептун», sort.StringSlice(planets).Sort() // Сортирует planets в алфавитном порядке fmt.Println(planets) // Выводит: [Венера Земля Марс Меркурий Нептун Сатурн Уран Юпитер] |
В пакете sort есть вспомогательная функция Strings для конвертации типа и вызова метода Sort. Это значительно облегчает процесс:
Вопрос для проверки:
Что делает sort.StringSlice(planets)?
Переменная planets конвертирована из []string в тип StringSlice, что объявлена в пакете sort.
Заключение
- Срез можно назвать окном, или видом в массив;
- Ключевое слово range может итерировать через срезы;
- Срезы разделяют одинаковую базовую информацию во время присваивания или передачи другим функциям;
- Композитные литералы предоставляют удобные способы для инициализации срезов;
- Вы можете прикрепить методы к срезам.
Итоговое задание для проверки:
Напишите программу для преобразования слайса строки через добавление слова «Новый » перед названием планеты. Используйте программу для изменения названий планет Марс, Уран и Нептун.
В первой итерации может использоваться функция terraform, но в конечной реализации должен быть введен тип Planets с методом terraform, похожим на sort.StringSlice.
package main
import «fmt»
// Planets прикрепляет методы к []string.
type Planets []string
func (planets Planets) terraform() {
for i := range planets {
planets[i] = «Новый » + planets[i]
}
}
func main() {
planets := []string{
«Меркурий», «Венера», «Земля», «Марс»,
«Юпитер», «Сатурн», «Уран», «Нептун»,
}
Planets(planets[3:4]).terraform()
Planets(planets[6:]).terraform()
fmt.Println(planets) // Выводит: [Меркурий Венера Земля Новый Марс Юпитер Сатурн Новый Уран Новый Нептун]
}
// Planets прикрепляет методы к []string.func (planets Planets) terraform() { planets[i] = «Новый » + planets[i] «Меркурий», «Венера», «Земля», «Марс», «Юпитер», «Сатурн», «Уран», «Нептун», Planets(planets[3:4]).terraform() Planets(planets[6:]).terraform() fmt.Println(planets) // Выводит: [Меркурий Венера Земля Новый Марс Юпитер Сатурн Новый Уран Новый Нептун] |
Администрирую данный сайт с целью распространения как можно большего объема обучающего материала для языка программирования Go. В IT с 2008 года, с тех пор изучаю и применяю интересующие меня технологии. Проявляю огромный интерес к машинному обучению и анализу данных.
E-mail: vasile.buldumac@ati.utm.md
Образование
Технический Университет Молдовы (utm.md), Факультет Вычислительной Техники, Информатики и Микроэлектроники
- 2014 — 2018 Universitatea Tehnică a Moldovei, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Universitatea Tehnică a Moldovei, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»
Готовимся к интервью по 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
«, a)
foo(a)
fmt.Printf(«%#v
«, a) // что выведет?
bar(&a)
fmt.Printf(«%#v
«, a) // что выведет?
}
Ссылка на playground.
Интересно, перейти к каналу
Срезы (slices)
Срез можно
рассматривать как динамический массив.
Это значит, что вы можете изменять его размер.
Срез представляет собой структуру, в которой
содержится указатель на начало области
памяти (массива), длина слайса (length) и объем области
памяти (capacity)
В коде Golang slice определен как структура с указателем
на массив, длиной и емкостью:
https://github.com/golang/go/blob/master/src/runtime/slice.go#L15
Важной особенностью такого представления является то, что на одну и
ту же область памяти с данными (массив)
может ссылаться несколько слайсов.
Например, как показано на рисунке:
Два среза (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
«, menu)
menu = append(menu, «Coffee»)
fmt.Printf(«Menu: %v
«, menu)
menu = append(menu, addMenu…)
fmt.Printf(«Menu: %v
«, 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
", a[1])
foo(a)
fmt.Printf("a[1]=%d
", a[1]) // что выведет?
bar(a)
fmt.Printf("a=%v
", 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
«, a[1])
b := a[1:3]
b[0] = 10
fmt.Printf(«1. a[1]=%d
«, a[1]) // что выведет?
b = append(b, a…)
b[0] = 100
fmt.Printf(«2. a[1]=%d
«, a[1]) // что выведет?
foo(a)
fmt.Printf(«3. a[1]=%d
«, a[1]) // что выведет?
bar(&a)
fmt.Printf(«4. a=%v
«, 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-разработчика
Внимание! Данный пост был опубликован более года назад и, возможно, уже утратил свою былую актуальность. Но это не точно.
Структурирование информации — очень полезный навык.
И дабы привнести некоторый порядок в этап подготовки к интервью на должность Golang разработчика (и немножко техлида) решил записывать в этой заметке в формате FAQ те вопросы, которые я задавал, задавали мне или просто были мной найдены на просторах сети вместе с ответами на них. Стоит относиться к ним как к шпаргалке (если затупишь на реальном интервью — будет где подсмотреть) и просто набору тем, которым тебе стоит уделить внимание.
Чаще всего этот вопрос идёт первым и даёт возможность интервьюверу задать вопросы связанные с твоим резюме, познакомиться с тобой, попытаться понять твой характер для построения последующих вопросов. Следует иметь в виду, что интервьюверу не всегда удается подготовиться к интервью, или он банально не имеет перед глазами твоего резюме.
Тут есть смысл ещё раз представиться (часто в мессенджерах используются никнеймы, а твоё реальное имя он мог забыть), назвать свой возраст, образование, рассказать о предыдущих местах работы и должностях, сколько лет в индустрии, какие ЯП и технологии использовал — только “по верхам”, для того чтоб твой собеседник просто понял с кем он “имеет дело”.
Расскажи о своем самом интересном проекте?
К этому вопросу есть смысл подготовиться заранее и не спустя рукава.
Дело в том, что это тот момент, когда тебе надо подобно павлину распустить хвост и создать правильное первое впечатление о себе, так как этот вопрос тоже очень часто идёт впереди всех остальных.
Возьми и выпиши для себя где-нибудь на листочке основные тезисы о том, что это был за проект/сервис/задача, уделяя основное внимание тому какой профит это принесло для компании/команды в целом. Например:
- Я со своей командой гоферов из N человек в течении трех месяцев создали аналог сервиса у которого компания покупала данные за $4000 в месяц, а после перехода на наш сервис — расходы сократились до $1500 в месяц и значительно повысилось их качество и uptime;
- Внедренные мной практики в CI/CD пайплайны позволили сократить время на ревью изменений в проектах на 25..40%, а зная сколько стоит время работы разработчиков — вы сами всё понимаете;
- Разработанный мной сервис состоял из такого-то набора микросервисов, такие-то службы и протоколы использовал, были такие-то ключевые проблемы которые мы так-то зарешали; основной ценностью было то-то.
Кем был создан язык, какие его особенности?
Собеседование на позицию Golang разработчика | Паршин Павел
В статье я рассмотрю наиболее часто встречаемые вопросы на собеседованиях Golang разработчика. Эта выборка основана только на личном опыте по результатам моих интервью в компании Авито, Тинькофф, SberCloud, МТС Cloud, NVIDIA, Acronis, Mail.ru Cloud Solutions, VK Pay. Всё действие происходило летом 2021 года.
В зависимости от зрелости процессов отбора кандидатов все собеседования можно разделить на две группы:
- Собеседование состоит из трёх технических секций (знание языка, алгоритмы и архитектура) и одной или нескольких встреч с командами, в которые идет найм, с целью знакомства. Обычно такой подход практикуют компании, у которых большой поток кандидатов и они хотят стандартизировать этот процесс.
- Собеседование на «вольную тему». Здесь уже есть сильная зависимость от знаний и подготовки собеседующих людей, поскольку вопросы могут придумываться на ходу, а не браться из заранее заготовленного пула. Обычно такие собеседования длятся несколько часов и затрагивают все темы сразу (язык, базы данных, сети и т.д.).
На многих интервью требовалось писать код, решать алгоритмические задачи или обсуждать представленный фрагмент кода, объясняя что в нем не так и как можно исправить. Поэтому стоит заранее уточнить у HR процесс собеседования, чтобы быть готовым и иметь под рукой ноутбук.
В одном случае меня попросили решить тестовое задание, поскольку я не слишком хорошо прошел первую секцию на алгоритмы. Но в целом это исключение, поскольку немногие кандидаты готовы тратить на это время.
Общие вопросы
Вопросы по предыдущим местам работы. Причины ухода с текущего места, ожидания от будущего работодателя.
Рекомендую вспомнить не только свои основные достижения, но и проблемы, с которыми приходилось сталкиваться (например, нарушение сроков, некорректная реализация поставленной задачи, конфликты в коллективе и т.п.), как их удавалось решить и какие выводы были сделаны, чтобы не допустить повторения этих ситуаций в будущем.
Вопросы по soft skills. Например, опишите вашего идеального руководителя или кем вы видите себя через 5 лет.
Эта группа вопросов помогает понять насколько вы разделяете ценности компании, будет ли команде комфортно работать с вами и вашу мотивацию. Я встречал мало людей, которые целенаправленно задавали похожие вопросы и могли чётко объяснить, что они ожидают услышать и какие делают выводы из ответов кандидата.
Расскажите чем вам нравится язык Golang, какие есть недостатки?
Достаточно рассказать о своём опыте работы с языком. Можно сравнить с другими языками — везде есть свои плюсы и минусы.
Массивы и срезы в Go
В Go массивы и *срезы *представляют собой структуры данных, состоящие из упорядоченных последовательностей элементов. Эти наборы данных очень удобно использовать, когда вам требуется работать с большим количеством связанных значений. Они позволяют хранить вместе связанные данные, концентрировать код и одновременно применять одни и те же методы и операции к нескольким значениям.
Хотя и массивы, и срезы в Go представляют собой упорядоченные последовательности элементов, между ними имеются существенные отличия.
Массив в Go представляет собой структуру данных, состоящую из упорядоченной последовательности элементов, емкость которой определяется в момент создания. После определения размера массива его нельзя изменить.
Срез — это версия массива с переменной длиной, дающая разработчикам дополнительную гибкость использования этих структур данных. Срезы — это то, что обычно называют массивами в других языках.
С учетом этих отличий массивы и срезы предпочтительнее использовать в определенных ситуациях.
Если вы только начинаете работать с Go, вам может быть сложно определить, что использовать в каком-либо конкретном случае.
Благодаря универсальному характеру срезов, они будут полезнее в большинстве случаев, однако в некоторых ситуациях именно массивы могут помочь оптимизировать производительность программы.
В этой статье мы подробно расскажем о массивах и срезах и предоставим вам информацию, необходимую для правильного выбора между этими типами данных. Также вы узнаете о наиболее распространенных способах декларирования массивов и срезов и работы с ними. Вначале мы опишем массивы и манипуляции с ними, а затем расскажем о срезах и их отличиях.
Массивы
Массивы представляют собой структурированные наборы данных с заданным количеством элементов. Поскольку массивы имеют фиксированный размер, память для структуры данных нужно выделить только один раз, в то время как для структур данных переменной длины требуется динамическое выделение памяти в большем или меньшем объеме.
Хотя из-за фиксированной длины массивов они не отличаются гибкостью в использовании, одноразовое выделение памяти позволяет повысить скорость и производительность вашей программы.
В связи с этим, разработчики обычно используют массивы при оптимизации программ, в том числе, когда для структур данных не требуется переменное количество элементов.
Определение массива
Массивы определяются посредством декларирования размера массива в квадратных скобках [ ], после которых указывается тип данных элементов. Все элементы массива в Go должны относиться к одному и тому же типу данных. После типа данных вы можете декларировать отдельные значения элементов массива в фигурных скобках { }.
Ниже показана общая схема декларирования массива:
[capacity]data_type{element_values}
Примечание: важно помнить, что в каждом случае декларирования нового массива создается отдельный тип. Поэтому, хотя [2]int и [3]int содержат целочисленные элементы, из-за разницы длины типы данных этих массивов несовместимы друг с другом.
Если вы не декларируете значения элементов массива, по умолчанию используются нулевые значения, т. е. по умолчанию элементы массива будут пустыми. Это означает, что целочисленные элементы будут иметь значение 0, а строки будут пустыми.
Например, следующий массив numbers имеет три целочисленных элемента, у которых еще нет значения:
var numbers [3]int
Если вы выведете массив numbers, результат будет выглядеть следующим образом:
Output
[0 0 0]
Если вы хотите назначить значения элементов при создании массива, эти значения следует поместить в фигурные скобки. Массив строк с заданными значениями будет выглядеть следующим образом:
[4]string{«blue coral», «staghorn coral», «pillar coral», «elkhorn coral»}
Вы можете сохранить массив в переменной и вывести его:
coral := [4]string{«blue coral», «staghorn coral», «pillar coral», «elkhorn coral»}
fmt.Println(coral)
Запуск программы с вышеуказанными строчками даст следующий результат:
Output
[blue coral staghorn coral pillar coral elkhorn coral]
Обратите внимание, что при печати элементы массива не разделяются, и поэтому сложно сказать, где заканчивается один элемент и начинается другой. Поэтому иногда бывает полезно использовать функцию fmt.Printf вместо простой печати, поскольку данная функция форматирует строки перед их выводом на экран. Используйте с этой командой оператор %q, чтобы функция ставила кавычки вокруг значений:
fmt.Printf(«%q
«, coral)
Результат будет выглядеть следующим образом:
Output
[«blue coral» «staghorn coral» «pillar coral» «elkhorn coral»]
Теперь все элементы заключены в кавычки. Оператор
предписывает добавить в конце символ возврата строки.
Теперь вы понимаете основные принципы декларирования массивов и их содержание, и мы можем перейти к изложению того, как задавать элементы в массиве по номеру индекса.
Индексация массивов (и срезов)
Каждый элемент массива (и среза) можно вызвать отдельно, используя индексацию. Каждый элемент соответствует номеру индекса, который представляет собой значение int, начиная с номера индекса 0 с увеличением в восходящем порядке.
В следующих примерах мы будем использовать массив, однако данные правила верны и для срезов, поскольку индексация массивов и срезов выполняется одинаково.
Для массива coral индекс будет выглядеть следующим образом:
0 | 1 | 2 | 3 |
- Первый элемент, строка «blue coral», начинается с индекса 0, а заканчивается срез индексом 3 с элементом «elkhorn coral».
- Поскольку каждый элемент среза или массива имеет соответствующий ему номер индекса, мы можем получать доступ к этим элементам и выполнять с ними манипуляции точно так же, как и с другими последовательными типами данных.
- Теперь мы можем вызвать дискретный элемент среза по его номеру индекса:
fmt.Println(coral[1])
Output
staghorn coral
Номера индекса для этого среза входят в диапазон 0-3, как показано в предыдущей таблице. Поэтому для вызова любого отдельного элемента мы будем ссылаться на номера индекса, как показано здесь:
coral[0] = «blue coral»
coral[1] = «staghorn coral»
coral[2] = «pillar coral»
coral[3] = «elkhorn coral»
Если мы вызовем массив coral с любым номером индекса больше 3, результат будет за пределами диапазона и запрос будет недействителен:
fmt.Println(coral[18])
Output
Собеседование Golang разработчика (теоретические вопросы), Часть I
Привет! Эта статья — начало цикла статей на то, как сейчас проходят интервью на golang разработчика (без привязки к грейду).
Цикл будет в себя включать как теоретические вопросы (с примерами и кодом исходников языка), так и реальные практические задачи, которые спрашивают при устройстве на позиции go разработчика.
Оглавление
Предыстория
Немного предыстории. У меня не очень много опыта на go, пишу на нем я чуть менее 3-х лет. В определенный момент, научившись писать неплохой продуктовый код и решать проблемы бизнеса, я пришел к выводу, что не понимаю многих аспектов.
Почему так, а не иначе работает тот или иной интрумент языка программирования, на котором я пишу? Это побудило меня начать копать вширь и вглубь. Кроме того, для закрытия пробелов в знаниях, я начал проходить собеседования. Прямой цели сменить работодателя у меня не было, при этом, я понимал, что интервью — это реальный способ повысить свой технический уровень.
Потому что, многие из тех вопросов, которые мы решаем в повседневной жизни, не требуют глубинного понимания инструментов, с помощью которых они решаются.
В этой статье я хотел бы начать делиться с вами реальными вопросами, которые у меня спрашивали интервьюеры, а также ответами на них, которые в конечном итоге удовлетворяли этих самых интервьюеров. Как итог, ответы на эти вопросы помогли мне получить хорошие оферы.
Также, хочу сказать, что, конечно, уровень вопросов и задач зачастую зависит от той позиции, на которую вы проходите собеседование. К моему удивлению, многие вопросы и задачи в равной степени спрашивают на любую из позиций — джуна, мидла, синьора.
Зачастую интервьюеры пытаются вытянуть максимум глубинных знаний, которые может показать кандидат, чтобы адекватно оценить его технический уровень. По этой причине будьте готовы, что любой из перечисленных в статье вопросов попросят раскрыть более широко.
ООП в Golang
Один из вопросов, которые любят задавать кандидатам с самого начала собеседования — это «Как реализовано ООП в go?».
Ответ
Сейчас мы не будем вдаваться в то, что такое ООП. Об этом можно почитать на Хабре на других ресурсах. Информации про это великое множество.
На сам вопрос можно ответить, что в go нет классической реализация ООП, так как он не объектно-ориентированный язык. При этом в go есть свои приближения к этой реализации. Сейчас поговорим об этом конкретнее.
«Как реализовано наследование?»
Ответ
Как такового наследования в go нет, но при этом у нас есть структуры — это специальные типы, в которые мы можем включать другие типы, в том числе такие же структуры. При этом методы дочерних структур родительская структура также будет наследовать.
Отсюда интервьюер может задать вопрос: «Что будет, если и в родительской и дочерней структуре есть реализация методов с одинаковым названием?» Ответ на этот вопрос такой: «Реализация дочернего метода будет переписана реализацией родительского метода».
type Child struct{}
func (p *Parent) Print() {
fmt.Println(«parent»)
}
type Parent struct {
Child
}
func (c *Child) Print() {
fmt.Println(«child»)
}
func main() {
var x Parent
x.Print()
}
Вывод
«Как реализована Инкапсуляция в go?»
Ответ
Инкапсуляция в go — это возможность задавать переменным, функциям и методам первую букву названия в верхнем или нижнем регистре. Соответственно нижний регистр будет значить, что переменная, функция или метод доступна только в рамках пакета. Тогда как верхний регистр даст доступ к переменной, функции или методу за рамками пакета.
«Как реализован полиморфизм в go?»
Ответ
Полиморфизм в go реализован с помощью интерфейсов. Что такое интерфейс мы будем разбираться ниже.
Основная идея заключается в том, что мы можем объявить интерфейсы (контракты на определённое поведение) для наших типов. При этом, для типов мы должны реализовать методы, удовлетворяющие этим интерфейсам.
Таким образом, мы сможем работать со всем набором типов, у которых реализовали интерфейсы, как с единым интерфейсным типом.
Области видимости в Golang
«Что такое пакеты в go?»
Ответ
Пакет — это механизм переиспользования кода, при котором go файлы помещаются в общую директорию. В начале каждого такого файла объявляется зарезервированное слово package, а после него прописывается имя пакета. В рамках пакета все функции и глобальные переменные, объявленные как в верхнем, так и в нижнем регистре, видят друг друга.
«Что такое глобальная переменная?»
Ответ
Глобальная переменная — это переменная уровня пакета, то есть объявленная вне функции. Глобальная переменная также может быть доступна за рамками пакета, конечно только в том случае, если ее наименование начинается в верхнем регистре.
Один из вопросов на общие знания, которые вы вряд ли когда-то будете использовать в go — это вопрос «Что такое фигурные скобки с не объявленным оператором в go функции?».
Ответ
В go функции действительно можно объявить{} без оператора, ограничив область видимости куска кода в рамках этой функции.
func main() {
solder := «Bill»
{
solder := «Den»
fmt.Println(solder)
}
fmt.Println(solder)
}
Вывод
Операторы в Golang
Про операторы в go в основном спрашивают на код секциях, к примеру захват переменной в цикле и так далее. Но все же, однажды попался следующий вопрос.
«В go есть оператор switch case, можно ли выполнить несколько условий в одном объявленном операторе?»
Ответ
Такое возможно благодаря ключевому слову fallthrough. Оно заставляет выполнять код в следующей объявленной булевой секции, вне зависимости подходит ли булевое условие case этой секции.
func main() {
animals := []string{«bear», «bear», «rabbit», «wolf»}
for _, animal := range animals {
switch animal {
case «rabbit»:
fmt.Println(animal, «is so weak!»)
fallthrough
case «bear», «wolf»:
fmt.Println(animal, «is so strong!»)
}
}
}
Вывод
bear is so strong!
bear is so strong!
rabbit is so weak!
rabbit is so strong!
wolf is so strong!
Strings в Golang
«Что представляют из себя строки в go?»
Ответ
Строки в go — это обычный массив байт. Это надо понимать для того, чтобы ответить на следующие вопросы о строках.
type _string struct {
elements *byte // underlying bytes
len int // number of bytes
}
«Как можно оперировать строками?»
Ответ
Строки в go можно складывать(конкатенировать). Для многих операций есть стандартные пакеты, к примеру strings, fmt. Кроме того, надо понимать, что все варианты конкатенации имеют свою производительность. Подробнее о производительности мы поговорим в рамках последующих статей, в которых будут тестовые задания.
«Что будет если сложить строки?»
Ответ
Ранее мы говорили о том что, строки — это массивы байт. Из этого следует, что при работе со строками (конкатенация и тд) мы будем получать новые строки.
«Как определить количество символов для строки?» или «Какие есть нюансы при итерации по строке?»
Ответ
Исходя из того же знания, что строка это массив байт, взяв базовую функцию len() от строки мы получим количество байт. Похожее поведение будет при итерации по строке — итерация по байтам. Тогда как в зависимости от кодировки, символ в строке может занимать не один байт.
Для того, чтобы работать именно с символами, необходимо преобразовать строку в тип []rune.
Еще одним способом определения длинны строки является функция RuneCountInString пакета utf8.
func main() {
str := «世界, 你好!»
fmt.Printf(«len bytes: %dn», len(str))
fmt.Printf(«len runes: %dn», len([]rune(str)))
fmt.Printf(«len runes: %dn», utf8.RuneCountInString(str))
}
Вывод
len bytes: 15
len runes: 7
len runes: 7
Int в Golang
«Какие численные типы есть в go?»
Ответ
Тут достаточно перечислить:
- int/int8/int16/int32/int64;
- int/uint8/uint16/uint32/uint64;
- float32/float64;
- complex64/complex128;
- rune(int32).
«Чем отличается int от uint?»
Ответ
int содержит диапазон от отрицательных значений до положительных, тогда как uint — это диапазон от 0 в строну увеличения положительных значений.
Пример: int64 это диапазон от –9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 , uint64 от 0 до 18 446 744 073 709 551 615.
«Что такое обычный int и какие есть нюансы его реализации?»
Ответ
В зависимости от того какая архитектура платформы, на которой мы стартуем, компилятор преобразует int в int32 для 32 разрядной архитектуры и в int64 для 64 разрядной архитектуры.
«Как преобразовать строку в int и наоборот? Можно ли сделать int(string) и string(int) соответственно?»
Ответ
Преобразование типов между int и string указанным синтаксисом невозможно. Для преобразования необходимо использовать функции из пакета strconv стандартной библиотеки go.
При этом для преобразования строк в/из int и int64 используются разные функции, strconv.Atoi и strconv.Itoa для int, strconv.ParseInt и strconv.FormatInt соответственно.
«Сколько в памяти занимают реализации int32 и int64?»
Ответ
Из самого названия типа следует, что int32 занимает 4 байта (32/8), int64 занимает 8 байтов (64/8).
«Какие предельные значения int32 и int64?»
Ответ
В предыдущем вопросе мы отвечали сколько места в памяти занимают эти типы. Таким образом с помощью 4 или 8 байт можно закодировать разные по диапазону значения.
Для int64 это диапазон от –9 223 372 036 854 775 808 до 9 223 372 036 854 775 807, для int32 от –2 147 483 648 до 2 147 483 647.
«Какой результат получим если разделить int на 0 и float на 0?»
Ответ
Это вопрос с подвохом. Деление int на 0 в go невозможно и вызовет ошибку компилятора. Тогда как деление float на 0 дает в своем результате бесконечность.
func main() {
f := 500.0
fmt.Printf(«float: %vn», f/0)
}
Вывод
func main() {
i := 500
fmt.Printf(«int: %vn», i/0)
}
Вывод
Const в Golang
«Что такое константы и можно ли их изменять?»
Ответ
Константы — это неизменяемые переменные, изменить константу нельзя.
«Что такое iota?»
Ответ
iota — это синтаксический сахар go для объявления числовой последовательности констант. Первая константа задается значением 0.
Array и slice в Golang
Самый популярный вопрос на собеседовании на любую позицию go инженера — «Что такое слайс и чем он отличается от массива?».
Ответ
- Cлайс — это структура go, которая включает в себя ссылку на базовый массив, а также две переменные len(length) и cap(capacity).
- len это длина слайса — то количество элементов, которое в нём сейчас находится.
- cap — это ёмкость слайса — то количество элементов, которые мы можем записать в слайс сверх len без его дальнейшего расширения.
Array — это последовательно выделенная область памяти. Частью типа array является его размер, который в том числе является не изменяемым.
type slice struct {
array unsafe.Pointer
len int
cap int
}
«Как работает базовая функция append для go?»
Ответ
Функция принимает на вход слайс и переменное количество элементов для добавления в слайс. Append расширяет слайс за пределы его len, возвращая при этом новый слайс.
Если количество элементов, которые мы добавляем слайс, не будет превышать len, вернется новый слайс, который ссылается на тот же базовый массив, что и предыдущий слайс. Если количество добавляемых элементов превысит cap, то вернется новый слайс, базовым для которого будет новый массив.
func append(slice []Type, elems …Type) []Type
«Какой размер массива выделяется под слайс при его расширении за рамки его емкости?»
Ответ
- Если отвечать на вопрос поверхностно, то можно сказать, что базовый массив расширяется в два раза от нашей capacity.
- Отвечая более емко, следует учесть, что при больших значениях расширение будет не в два раза и будет вычисляться по специальной формуле.
- Если развернуть ответ полностью, то это будет звучать примерно так:
- если требуемая cap больше чем вдвое исходной cap, то новая cap будет равна требуемой;
- если это условие не выполнено, а также len текущего слайса меньше 1024, то новая cap будет в два раза больше базовой cap;
- если первое и второе условия не выполнены, то емкость будет увеличиваться в цикле на четверть от базовой емкости пока не будет обработано переполнение. Посмотреть эти условия более подробно можно в исходниках go.