
Синтаксис в Go очень простой. Вопросов по ней правда минимум, оригинальностью они совсем не отличаются. Хоть пул вопросов зависит от компании, но список часто очень сильно похож.
Поскольку гошка славится своей ультра-удобной конкурентностью, то будь готов, что дрочить тебя будут именно по ней. И в ширь и в глубину. На всех осях. Хоть это в большей степени зависит от фантазии и настроения интервьюера, тебя обязательно спросят, что такое горутина, и чем она отличается от потока.
Самое сложное в домене многопоточки это та самая скуфовская база. Если ты понимаешь как работают потоки, ядра и память хотя бы на базовом уровне, то многопоточку ты поймешь очень легко. Однако поэтому она и настолько многоуровневая. Считай - это азы computer science, которые в нас очень активно запихивали на первых курсах универа.
Я часто буду ссылаться на внутренню базу вопросов и знаний, так что лови ссылки на них сразу: Roadmap, Вопросы.
Тренажер в базе научит тебя правильно отвечать на вопросы из базы и проходить собесы.
Многопоточка
Горутина это коробка, а поток - грузовик. В плюсах ты сам напихиваешь потоки задачами, и сам за ними следишь. А гошка позволяет не думать о грузовиках, создавая только задачи. Отсюда рождается эта знаменитая GMP модель.
- G - горутина (коробка)
- M - поток операционной системы (грузовик)
- P - Логический процессор: контекст выполнения с локальной очередью горутин (погрузочная площадка)
Горутины распихиваюстся по логическим процессорам, а поток операционной системы разгребает их, выполняя весь тот контекст, что был в очереди.
Горутина это по сути управляемая задачка. У нее есть состояние, контекст выполнения и свой собственный небольшой стэк с кадрами вызовов. Ее можно, передавать с процессора на процессор, отправлять в очередь ожидающих, если надо получить HTTP ответ.
В этой же под-области валяются механизмы синхронизации. Нужны они для того, чтобы твои данные между горутинами передавались, и синхронизировались.
- Channel - труба, в которую одна горутина данные запихивает, а другая достает. И это как бы потоко-безопастно, за одним исключением. Если канал не инициализирован, и ты попытался что то оттуда прочитать, то ты заблокирован навсегда. Если канал закрыт, и ты что-то туда пишешь, то программа упадет. И таких ЕСЛИ можно насчитать порядка 5.
- Waitgroup - счетчик.
.Add()счетчик увеличивает,Wait()ждет пока счетчик не упадет до нуля. Поэтому если main() горутина запустила себе 10 воркерков горутин, и должна их ждать, то ПЕРЕД запуском каждого работяги надо счетчик увеличивать, а когда работяга отработал - внутри него счетчик уменьшать. И дрочить тебя будут как раз на понимание этого процесса. Например, почему нельзя увеличить счетчик внутри горутины? Об этом можешь прочитать в блоке по многопоточке. - Мьютексы. Когда два потока меняют одну и ту же переменную, можно попасть в ситуация аля Data Race. Два потока столкнулись лбами, хотят изменить переменную, но друг о друге не знают. Мьютекс блокирует горутину в том случае, если его коллега УЖЕ с этой секцией работает.
- Атомики. Современная тэкнолоджия привела нас к тому, что невероятно быстрые потоки могут АТОМАРНО работать с некоторыми типами данных. Увеличить счетчик, например, можно в одну операцию процессора, и тогда по сути никаких критических секций, где много потоков будут рыться в одной тарелке - не будет. Но возможно это только в том случае, если твой процессор это вообще поддерживает. Не часто копают в устройство процессора, просто ты должен знать что это и с чем его едят.
Загвоздка лишь в том, что в их основе так же лежит база Computer science. А весь этот синтаксический сахар на безлактозном лишь надстройка. Разберешься чем мьютексы отличаются от атомиков, и уже закроешь половину всех основных вопросов.
Начни с темы в роадмапе, и двигайся по ней. Если возникнет желание, можешь углубится в тему на других ресурсах 🙂
Для закрепления результата обязательно напиши свой worker pool: программа создает N горутин, которые ассинхронно ходят в очередь задач, разгребают ее, и результат печатают в консоль.
Конкурентность в Go
Горутины, планировщик GMP, локальные и глобальные очереди выполнения, work-stealing, обработка syscall и вытеснение в рантайме Go.
Лично мне база по C++ выстроила фундамент, на который абстракции по Golang легли как литые.
Контейнеры
Но правды ради, это не все. Следующий по списку относительно сложный аспект это контейнеры. В них ебанесся сколько нюансов. Но тебе надо в первую очередь понять эти три:
String
ЭТО НЕИЗМЕНЯЕМЫЙ МАССИВ БАЙТ. Не слайс, не список (которого даже нет в go). Хранит не символы, не секреты или еще какую нибудь дичь. Это именно голые байты, которые ты должен привести к другому виду для вывода в консоль.
Байт - это просто маленькое число от 0 до 255. Английской букве обычно хватает одного байта, а русской букве или эмодзи — нескольких.
Поэтому len("hello") вернёт 5, а len("привет") — 12: len считает байты, а не буквы.
Когда Go нужно прочитать строку как текст, он объединяет байты в руны.
Руна - это число, обозначающее одну букву, знак или другую единицу текста. Перебираешь строку через range — получаешь руны. Обращаешься через s[i] — получаешь отдельный байт. На собесе это спрашивают всегда.
Короче в Go для хранения символов используются байты, но для декодирования есть руны. Древнегреческие.
func main() {
s := "Привет"
fmt.Println(len(s)) // 12 байт
fmt.Println(len([]rune(s))) // 6 Unicode-кодовых точек
fmt.Printf("%x\n", s[0]) // первый байт: d0
fmt.Printf("%c\n", []rune(s)[0]) // первая rune: П
}
Строки массив байт, а массивы неизменямы. Поэтому просто добавить символ в уже созданную строку не выйдет. И интервьюер тебя обязательно об этом спросит.
Слайсы, мапы и строки
Встроенные коллекции Go от основ до устройства — slice и его трёхсловный заголовок, выражения среза, рост и алиасинг, map с comma-ok и устройством hmap, nil-карта, строки как неизменяемые байты и безопасность памяти.
Массивы и Слайсы
Классический тупой вопрос - в чем разница этих двух контейнеров. Но по сути, массив статический, добавить туда элменты нельзя, а изменить можно.
Элементы слайса лежат в динамической памяти. Они могут быть перемещены, удалены, и выделены заново. Но нюансов тут хватает, и именно туда будут бить на интервью.
Слайс - это структура, которая хранит:
- Текущее кол-во элементов
- Максимальное количество элементов
- Указатель на массив данных
Так вот, самый популярный вопрос:
Цитата
А че будет, если слайс в 3 элемента, у которого максимум может быть 5, передать в функцию по значению и добавить туда один новый элемент? Поменяется локальный, или переданный?
Разумеется если б ничего не менялось, тебе бы этот вопрос не задавали.
Поскольку указатель на массив данных внутри слайса передается так же по значению, то любые махинации с копией меняют и исходный массив. Но не всегда. Если максимального размера в слайсе хватает, то добавление нового элемента в копию изменит и исходник. Элемент появится. Но если место закончилось, то будет ре-аллокация: выделится новый массив, в который скопируются исходные данные. Короче тот еще мозговынос, и подробнее можешь прочитать на платформе.
func add(s []int) {
s = append(s, 4)
fmt.Println("внутри:", s)
}
func main() {
a := make([]int, 3, 5)
copy(a, []int{1, 2, 3})
add(a)
fmt.Println("снаружи:", a) // [1 2 3], len всё ещё 3
fmt.Println("после reslice:", a[:4]) // [1 2 3 4]
b := []int{1, 2, 3} // len=3, cap=3
add(b) // append создаст новый backing array
fmt.Println("снаружи:", b) // [1 2 3]
}
Слайсы, мапы и строки
Встроенные коллекции Go от основ до устройства — slice и его трёхсловный заголовок, выражения среза, рост и алиасинг, map с comma-ok и устройством hmap, nil-карта, строки как неизменяемые байты и безопасность памяти.
Hash map
Его величество - map. Мапа это ассоциативная структура данных, где хранятся контейнеры, ака бакеты, и в бакетах лежат элементы. Каждый элемент это парочка твикс - ключ и значение. Чтобы найти значение, тебе нужен ключ.
Маппка перед добавлением нового элемента считает особое значение для ключа - хэш. Хэш же это число в несколько десятков циферок, которое по хорошему должно быть уникальным, но получается это не всегда. Поэтому существует такая штука, как хэш коллизии - в одном бакете у тебя валяется несколько элементов. Но решается это через разделение на две части.
Первая половина хэша выбирает бакет, в котором значение должно лежать. а вторая половина находит сам элемент внутри бакета.
Ну и вопросы на собесах очень часто про ток, как устроены эти бакеты, что такое овуляция эвакуация и т.д.
Слайсы, мапы и строки
Встроенные коллекции Go от основ до устройства — slice и его трёхсловный заголовок, выражения среза, рост и алиасинг, map с comma-ok и устройством hmap, nil-карта, строки как неизменяемые байты и безопасность памяти.
Память
Ну и последнее в нашем небольшом дроч-топе - это память. Но не потому что альцгеймер, а потому что ребятки на интервью не могут внятно объяснить чем стэк отличается от кучи, и почему эскейп анализ вообще существует. Поясняю
У твоей программы есть два основных месте, где лежат твои переменные и структруы - стэк и куча. Все переменные, которые нельзя положить в стэк - кладутся в кучу.
Какие же переменные нельзя положить в стэк: Все те, что возвращаются из функции, передаются внутрь замыкания, т.е анонимной функции. Но это тоже не гарант. Нюансы можешь прочитать тут.
Память в Go
Стек против кучи, escape-анализ, принудительное размещение в куче, new против make и локальность кэша.
Вердикт же выводится на этапе компиляции. Собирается твоя программа, и компилятор Golang анализирует каждую переменную. Если все ок - остается в стэке, если нет - то попадает в кучу.
Две памяти нам надо по двум причинам. Стэк быстрее, но он статичен. Во время работы программы, ты память там не выделишь. Выделяется и чистится память там автоматически, каждый раз когда функция начинает и заканчивает свое выполнение. Оттуда и автоматика - функция вызвалась → выделила память → завершилась → освободила память. Поэтому и нагрузки на GC у тебя нет, и сами аллокации у тебя дешевые.
Куча - динамическая, но медленная. Че-то там выделить всегда стоит времени. Поэтому эскейп анализ настолько важная штука, что избавляет тебя от нужды задумываться о размещении данных. Ручками следить за памятью, как это происходит в C++ не приходится. Нужна переменная на стэке или в хипе - решает Golang.
Но каждый интервьюер хочет, чтобы ты знал его механизм.
Память в Go
Стек против кучи, escape-анализ, принудительное размещение в куче, new против make и локальность кэша.
Заключаем
С базой по гошке разобрались, в следующем посте обсудим базы данных и необходимую основу, чтобы фиксик на интервью тебе галочку все-же поставил.
Обнял, не теряйся.