:octocat: Вопросы и ответы для собеседования Back-end/Golang разработчика и не только
Здесь собирается большая коллекция вопросов и ответов на них, необходимых не только для прохождения собеседований, но и для комплексного развития кругозора
Тут ответ
Быстрый экскурс по вопросам Go. Будет дополняться
Цель проекта — создать современную альтернативу C и C++ и сделать разработку ПО в Google более быстрой.
Язык должен был решить такие проблемы, как:
В основе языка Golang — база лучших функций из языков C и C++, Python, Pascal, Oberon и Modula. Сначала Go использовали внутри Google, но затем он стал применяться в компаниях по всему миру: HP, Adobe, Microsoft, Facebook, BBC, Uber, Dropbox, Netflix, Яндекс, ВКонтакте, Avito, Ozon и других.
Если охарактеризовать Go одним предложением, то можно сказать так: «Похож на Python, но быстрее и с лучшим параллелизмом». Как и Python, Go рассчитан на интенсивное использование функций, что позволяет разработчикам сравнительно быстро создавать мощную функциональность.
Однако, в отличие от Python, Go — компилируемый язык (технически Python также компилируется, но не в традиционном смысле). Как правило, это сокращает время выполнения программного кода. Кроме того, важной целью при проектировании Go было обеспечить удобный параллелизм (несколько задач могут выполняться одновременно). В большинстве других языков также реализован параллелизм, но благодаря таким функциям, как собственные встроенные подпрограммы, goroutine, проще реализовать высокоэффективный параллелизм в приложении Go. Это значимое преимущество в наш век микрослужб и многоядерных процессоров, когда нужно полностью использовать преимущества параллельных вычислений.
Что касается Java, то тут больше схожести, чем с Python. Java является таким же компилируемым, строго типизированным языком программирования с возможностью работать в многопточном режиме.
Пожалуй одним из главных отличий от Go (кроме синтаксиса), является объектно-ориентированная природа языка Java и то, что, для достижения кроссплатформенности, она работает на виртуальной машине JVM (Java Virtual Machine). В то же время Go программа исполняется в своем внутреннем Runtime и в Go нет классов с конструкторами. Вместо экземпляра методов, иерархии наследия классов и динамического метода, Go предоставляет структуры и интерфейсы.
Также в Java для реализации параллелизма исаользуются потоки (threads) и задачи (tasks), и более абстрагированные concurrency API - исполнители (executors), callable и фьючерсы (future).
В то время как в Go для достижения параллелизма есть горутины (goroutine) и каналы (channels), а вся "тяжелая" работа лежит на планировщике (sheduler) исполняемой программы.
К преимуществам можно отнести:
В Go нет наследования, классов, объектов и сложных функций. Всё лаконично и аккуратно — это позволяет просто писать на Go и читать чужой код. Для понимания не понадобятся стандарты и комментарии — всё и так максимально прозрачно.
Основное руководство Go занимает всего 50 страниц. Благодаря строгости и простому синтаксису изучение языка Go — тривиальная задача даже для тех, у кого совсем нет опыта в разработке. Он построен так, что буквально ведёт разработчика за руку и защищает от ошибок и опечаток.
Внутрь языка встроены инструменты тестирования, утилита для создания документации, дополнения для поиска ошибок в коде и другие полезные функции. Поэтому разработка на языке Go — довольно простой и приятный процесс, нет чувства, что нужно постоянно искать какие-то сторонние инструменты для облегчения работы.
Практически для каждой задачи есть готовые стандартные библиотеки внутри языка. Сторонние тоже есть, их список постоянно растёт. К коду на Go можно подключать библиотеки С и С++, которых очень много из-за популярности этих языков.
Если переписать код с другого языка на Go, можно даже без специальной оптимизации повысить производительность в 5–10 раз.
Программы на Go грамотно используют память и вычислительные ресурсы, поэтому работают стабильно.
Недостатки:
Область применения языка Go — сетевые и серверные приложения. А вот с созданием графических интерфейсов он справляется плохо. Поэтому полностью написать на Go пользовательское приложение будет сложно из-за ограниченных возможностей, да и в целом он неприменим для многих задач. Его нужно использовать с умом и там, где он действительно нужен.
Это одновременно и плюс, и минус. Некоторые вещи, доступные на других языках, на Go сделать просто не выйдет. Например, разрабатывать большие проекты из-за отсутствия объектов, полезных для совместной работы с распределённым кодом.
Слайсы и массивы очень важная часть языка Go и их частенько любят спрашивать на собеседованиях.
Хотя и массивы, и срезы в Go представляют собой упорядоченные последовательности элементов, между ними имеются существенные отличия. Массив в Go представляет собой структуру данных, состоящую из упорядоченной последовательности элементов, емкость которой определяется в момент создания. После определения размера массива его нельзя изменить. Срез — это версия массива с переменной длиной, дающая разработчикам дополнительную гибкость использования этих структур данных. Срезы — это то, что обычно называют массивами в других языках.
Массивы:
Массивы представляют собой структурированные наборы данных с заданным количеством элементов. Поскольку массивы имеют фиксированный размер, память для структуры данных нужно выделить только один раз, в то время как для структур данных переменной длины требуется динамическое выделение памяти в большем или меньшем объеме. Хотя из-за фиксированной длины массивов они не отличаются гибкостью в использовании, одноразовое выделение памяти позволяет повысить скорость и производительность вашей программы. В связи с этим, разработчики обычно используют массивы при оптимизации программ, в том числе, когда для структур данных не требуется переменное количество элементов.
var numbers [3]int
var strings [3]string
// Если вы не декларируете значения элементов массива, по умолчанию используются нулевые значения,
// т. е. по умолчанию элементы массива будут пустыми.
// Это означает, что целочисленные элементы будут иметь значение 0, а строки будут пустыми.
fmt.Println(numbers) // [ 0 0 0 ]
fmt.Println(strings) // [ "" "" "" ]
Примечание: важно помнить, что в каждом случае декларирования нового массива создается отдельный тип. Поэтому, хотя [2]int
и [3]int
содержат целочисленные элементы, из-за разницы длины типы данных этих массивов несовместимы друг с другом.
Срезы:
Срез — это тип данных Go, представляющий собой мутируемую или изменяемую упорядоченную последовательность элементов. Поскольку размер срезов не постоянный, а переменный, его использование сопряжено с дополнительной гибкостью. При работе с наборами данных, которые в будущем могут увеличиваться или уменьшаться, использование среза обеспечит отсутствие ошибок при попытке изменения размера набора. В большинстве случаев возможность изменения стоит издержек перераспределения памяти, которое иногда требуется для срезов, в отличие от массивов. Если вам требуется сохранить большое количество элементов или провести итерацию большого количества элементов, и при этом вам нужна возможность быстрого изменения этих элементов, вам подойдет тип данных среза.
// Создадим срез, содержащий элементы строкового типа данных:
seaCreatures := []string{"shark", "cuttlefish", "squid", "mantis shrimp", "anemone"} // len: 5, cap: 5
// Если вы хотите создать срез определенной длины без заполнения элементов коллекции,
// вы можете использовать встроенную функцию make()
oceans := make([]string, 3) // output: [ "" "" "" ], len: 3, cap: 3
// Если вы хотите заранее выделить определенный объем памяти, вы можете использовать в команде make() третий аргумент:
oceans := make([]string, 3, 5) // output: [ "" "" "" ], len: 3, cap: 5
Cлайс - это структура go, которая включает в себя ссылку на базовый массив, а также две переменные len(length) и cap(capacity).
len - это длина слайса - то количество элементов, которое в нём сейчас находится. cap - это ёмкость слайса - то количество элементов, которые мы можем записать в слайс сверх len без его дальнейшего расширения.
// структура слайса
type slice struct {
array unsafe.Pointer
len int
cap int
}
Массив - это последовательно выделенная область памяти. Частью типа array является его размер, который в том числе является не изменяемым.
Способы создания слайса:
var nums []int // nil slice, длина 0 емкость 0
var nums = []int{1, 2, 3} // слайс с 3 значениями, длина 3, емкость 3
nums := []int{1, 2, 3} // тоже самое, что и выше, слайс с 3 значениями и емкостью 3
nums := make([]int{}, 0, 3) // слайс без значенией но с емкостью 3
nums := make([]int, 3) // слайс с длиной 3 и с емкостью 3
var (
nums = make([]int{}, 0, 3) // аналог выше
nums = make([]int, 3) // аналог выше
)
Zero-value - переменным, объявленным без явного начального значения, присваивается нулевое значение.
var i int
var f float64
var b bool
var s string
var p *int
var t time.Time
fmt.Printf("%v %v %v %q %v %v\n", i, f, b, s, p, t) // output: 0 0 false "" <nil> 0001-01-01 00:00:00 +0000 UTC
Zero-value для слайса - это nil, а len и cap равны нулю, так как "под ним" нет инициализированного массива:
var a []int
fmt.Prinln((a == nil, len(a), cap(a)) // output: true 0 0
a = append(a, 1)
fmt.Println(a == nil, len(a), cap(a)) // output: false 1 1
В Go с nil слайслм можно соверщать те же операции, которые могли бы делать с инициализированным слайсом
var slice []int
fmt.Println(len(slice), cap(slice)) // output 0 0
slice = append(slice, 10)
fmt.Println(slice, len(slice), cap(slice)) // output [ 10 ] 1 1
Самый надежный спосб проверить слайс на пустоту в большинчтве случаем - это вроверить его длину на ноль. Не стоит проверять слайс на nil
var a []int
// не правильно
fmt.Println(a == nil) // true
// а если так
a := []int{}
fmt.Println(a == nil) // false
// поэтому правильно так
fmt.Println(len(a) == 0) // true
Функция принимает на вход слайс и переменное количество элементов для добавления в слайс. Append расширяет слайс за пределы его len, возвращая при этом новый слайс.
// функция append
func append(slice []Type, elems ...Type) []Type
Если количество элементов, которые мы добавляем в слайс, не будет превышать cap, вернется новый слайс, который ссылается на тот же базовый массив, что и предыдущий слайс. Если количество добавляемых элементов превысит cap, то вернется новый слайс, базовым для которого будет новый массив.
// в данном блоке кода продемонстрирована работа функции append
// создаем слайс с capacity равным 3 и длиной 0
slice := make([]int, 0, 3) // len: 0, cap: 3
// далее заполняем слайс тремя элементами
slice = append(slice, 1) // len: 1, cap: 3
slice = append(slice, 2, 3) // len: 3, cap: 3
// получаем ожидаемый результат
fmt.Println(slice) // output [ 1, 2, 3 ]
// окей, теперь попробуем присводить слайс другому слайсу
// помним то, что слайс является структурой из трех элементов len, cap и указателем н первый элемент массива
// поэтому в sliceCopy мы получаем скопированные значение len и cap, а так же указатель на тот же массив, что и у переменной slice
sliceCopy := slice
// пробуем менять первый элемент в новом слайсе
sliceCopy[1] = 10
// убеждаемся, что в обоих слайсах изменились значения, все из-за базового массива
fmt.Println(slice, sliceCopy) // output: slice: [ 1, 10, 3 ] sliceCopy: [ 1, 10, 3 ]
// хорошо, теперь пробуем добавить новый элемент в первый слайс
slice = append(slice, 4)
// тут у нас функция append "видит", что мест больше нет и увеличивает cap в двое, увеличивает len на один
// и создает новый базовый массив с местимостью в 6 элементов, что и видим на печати
fmt.Println(slice) // output: [ 1, 10, 3, 4] len: 4, cap: 6
// но что случилось тут? ничего, просто ничего, теперь первая переменная смотрит на другой базовый массив и они больше никак не связаны
fmt.Println(sliceCopy) // output: [ 1, 2, 3 ] len: 3, cap: 3
// точно не связаны? ну давай убедимся! пробуем менять значения первых элементов в обоих слайсах
sliceCopy[0] = 50
slice[0] = 80
// убедились? :)
fmt.Println(slice, sliceCopy) // output: slice: [ 80, 10, 3, 4 ] sliceCopy: [ 50, 10, 3 ]
```
А вот с мссивами функцию append использовать нелья иначе получим ошибку: `first argument to append must be slice; have T`
```go
array := [3]int{}
array = append(array, 3) // first argument to append must be a slice; have array (variable of type [3]int)
Теперь напишем свою функцию:
// она будет проще, только с добавлением одного элемента
func main() {
fmt.Println(Append([]int{1, 2, 3}, 4))
}
func Append[T any](dst []T, el T) []T {
var res []T
resLen := len(dst) + 1
if resLen <= cap(dst) {
res = dst[:resLen]
} else {
resCap := resLen
if resCap < 2*len(dst) {
resCap = 2 * len(dst)
}
res = make([]T, resLen, resCap)
copy(res, dst)
}
res[len(dst)] = el
return res
}
Один из способов добавления элементов с слайс мы уже обсудили выше, с использованием функцию append:
slice := make([]int, 0, 10) // len: 0, cap: 10
for i := 0; i < 10; i++ {
slice = append(slice, i*2)
}
но есть еще 1 способ, через индексы, выглядит это так
slice := make([]int, 10) // len: 10, cap: 10
for i := 0; i < 10; i++ {
slice[i] = i*2
}
но у последнего способа есть недостаток, если количество элементов, которые мы хотим добавить в слайс премвысит емкость исходного слайса, тогда мы получим панику: panic: runtime error: index out of range [10] with length 10
// достаточно поменять условие на <=
slice := make([]int, 10) // len: 10, cap: 10
for i := 0; i <= 10; i++ {
slice[i] = i * 2
}
в то время append расширил бы базовый массив слайса и продолжил дальше раюотать не паникуя.
Встроенная функция copy копирует элементы в целевой срез dst из исходного среза src.
func copy(dst, src []Type) int
Возвращает количество скопированных элементов, которое будет минимумом len(dst) и len(src). Результат не зависит от того, перекрываются ли аргументы.
// Копировать из одного среза в другой
var slice = make([]int, 3)
num := copy(slice, []int{0, 1, 2, 3})
fmt.Println(num, slice) // output: num == 3, slice == []int{0, 1, 2}
Второй способ копирования слайсов - использовать функцию append
slice := make([]byte, 0, len(a))
slice = append(c, []int{0, 1, 2, 3}...)
fmt.Println(slice) // output: slice == []int{0, 1, 2}
Объединение фрагментов в Go легко достигается с помощью той же встроенной функции append. Как мы помним он принимает срез (s1) в качестве первого аргумента и все элементы из второго среза (s2) в качестве второго. Возвращается обновленный срез со всеми элементами из s1 и, который может быть присвоен другой переменной.
s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}
s3 := append(s1, s2...)
fmt.Println(s3) // output: [ 1, 2, 3, 4, 5, 6 ]
В Go можно сделать подслайз из слайса или массива:
slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
subSlice := slice[3:8] // [ 4, 5, 6, 7, 8 ]
Но что будет, если мы изменим значение под слайса или еще хуже, добавим туда элементы через функцию append?
subSlice[0] = 101
fmt.Println(slice) // [1 2 3 101 5 6 7 8 9 10]
fmt.Println(subSlice) // [101 5 6 7 8]
Видим, что в базовом слайсе тоже поменялись значения, а все потому, что у под слайса все тот же базовый массив, а для подслайса нулевой элемент это элемент под индексом 3 в базовом. Примерно такое же поведение наблюдается у функции append, если его применить к под слайсу базового слайса:
slice := make([]int, 10, 25)
subSlice := slice[3:5] // [ 0, 0, 0, 0, 0 ]
fmt.Println(len(slice), cap(slice)) // 10 25
fmt.Println(len(subSlice), cap(subSlice)) // 2 22
subSlice = append(subSlice, 11)
fmt.Println(slice) // [0 0 0 0 0 11 0 0 0 0]
fmt.Println(subSlice) // [0 0 11]
причина данного поведения в том, что у обоих слайсов один базовый массив, а так же у под слайса своя "копия" слайса с полями len и cap и когда мы пытаемся добавить в дочерний слайс элемент, при условии, что в родитльском хватает емкости, мы просто перезаписываем значение в базовом массива.
полезные материалы:
полезные материалы:
полезные материалы:
полезные ресурсы:
func main() {
nums := 1 << 5 // 32
defer fmt.Println(nums) // туть как?
nums = nums >> 1 //16
fmt.Println("done")
}
полезные материалы:
zikwall |
dreddsa5dies |
gonnafaraway (kj22k0m) |