4295 просмотров
от 4 июня 2024
Go

Вопросы и ответы с собеседований по Go

Вопросы и ответы с собеседований Go-разработчиков. Типы данных, рефлексия, парадигма ООП, атомарные операции, горутины, особенность работы с каналами, операции со строками, эвакуация, работа с интерфейсами, generics и многое другое.

1

Какие типы данных используются в Go?

1

Go работает со следующими типами: - Method (метод); - Boolean (логический тип); - Numeric (численный); - String (строковый); - Array (массив); - Slice (срез); - Struct (структура); - Pointer (указатель); - Function (функция); - Interface (интерфейс); - Map (карта); - Channel (канал).

Комментарии
0/3000
2

Как проверить тип переменной в среде выполнения?

1

Лучшим способом проверки типа переменной во время выполнения является Type Switch. Он анализирует переменные по типу, а не значению. Например: package main import "fmt" func do(i interface{}) { switch v := i.(type) { case int: fmt.Printf("Double %v is %v\n", v, v*2) case string: fmt.Printf("%q is %v bytes long\n", v, len(v)) default: fmt.Printf("I don't know type %T!\n", v) } } func main() { do(21) do("hello") do(true) } Также можно это сделать с помощью рефлексии: package main import ( "fmt" "reflect" ) func main() { x := 42 fmt.Println("Тип переменной x:", reflect.TypeOf(x)) }

Комментарии
0/3000
3

Что такое рефлексия в Go?

Рефлексия в Go реализована в пакете reflect и представляет собой механизм, позволяющий коду исследовать значения, типы и структуры во время выполнения, без заранее известной информации о них. Рефлексия полезна в ситуациях, когда нам нужно работать с данными неизвестного типа, например, при сериализации/десериализации данных, реализации ORM систем и так далее. С помощью рефлексии мы можем, например, определить тип переменной, прочитать и изменить её значения, вызвать методы динамически. Это делает код более гибким, но следует использовать рефлексию осторожно, так как она может привести к сложному и трудночитаемому коду, а также снизить производительность. Простые примеры: Определение типа переменной: package main import ( "fmt" "reflect" ) func main() { x := 42 fmt.Println("Тип переменной x:", reflect.TypeOf(x)) } В примере мы используем функцию reflect.TypeOf(), чтобы определить тип переменной x. Программа выведет int, так как x — целое число. Чтение и изменение значений: package main import ( "fmt" "reflect" ) func main() { x := 42 v := reflect.ValueOf(&x).Elem() // Получаем reflect.Value fmt.Println("Исходное значение x:", x) v.SetInt(43) // Изменяем значение x fmt.Println("Новое значение x:", x) } Здесь мы используем reflect.ValueOf() для получения reflect.Value переменной x, а затем изменяем её значение с помощью SetInt(). Динамический вызов методов: package main import ( "fmt" "reflect" ) type MyStruct struct { Field int } func (m *MyStruct) UpdateField(val int) { m.Field = val } func main() { x := MyStruct{Field: 10} // Получаем reflect.Value структуры v := reflect.ValueOf(&x) // Получаем метод по имени method := v.MethodByName("UpdateField") // Вызываем метод с аргументами method.Call([]reflect.Value{reflect.ValueOf(20)}) fmt.Println("Обновленное значение поля:", x.Field) } В этом примере мы создаем экземпляр структуры MyStruct, получаем метод UpdateField с помощью MethodByName и вызываем его динамически с помощью Call. Метод обновляет значение поля структуры.

Комментарии
0/3000
4

Является ли Go объектно-ориентированным языком?

Да и нет. Хотя в Go есть типы и методы, и он допускает объектно-ориентированный стиль программирования, в нем нет иерархии типов. Концепция «интерфейс» в Go предоставляет другой подход, считающийся простым в использовании и в некотором роде более общим. Есть также способы встраивать типы в другие типы, чтобы обеспечить нечто подобное, но не идентичное подклассам. Более того, методы в Go более общие, чем в C++ или Java: они могут быть определены для любого типа данных, даже для встроенных, таких как простые "unboxed" (представляющие значения) целые числа. Они не ограничиваются структурами (классами). Кроме того, отсутствие иерархии типов делает "объекты" в Go намного легче, чем в таких языках, как C++ или Java.

Комментарии
0/3000
5

Как в Go реализовано наследование?

Как такового наследования в Go нет, но при этом у нас есть структуры - это специальные типы, в которые мы можем включать другие типы, в том числе такие же структуры. Можно сказать, что Go предпочитает наследованию композицию.

Комментарии
0/3000
6

Как в Go реализована инкапсуляция?

Инкапсуляция в Go - это возможность задавать переменным, функциям и методам первую букву названия в верхнем или нижнем регистре. Соответственно нижний регистр будет значить, что переменная, функция или метод доступна только в рамках пакета. Тогда как верхний регистр даст доступ к переменной, функции или методу за рамками пакета.

Комментарии
0/3000
7

Как в Go реализован полиморфизм?

Полиморфизм в Go реализован с помощью интерфейсов. Основная идея заключается в том, что мы можем объявить интерфейсы (контракты на определённое поведение) для наших типов. При этом, для типов мы должны реализовать методы, удовлетворяющие этим интерфейсам. Таким образом, мы сможем работать со всем набором типов, у которых реализовали интерфейсы, как с единым интерфейсным типом.

Комментарии
0/3000
8

Какие механизмы синхронизации доступны в Go?

В Go примитивы синхронизации — это инструменты из пакета sync (и не только), которые помогают нам гарантировать, что множество горутин может безопасно взаимодействовать с общими данными или координировать свою работу. sync.Mutex: основной примитив блокировки для исключения одновременного доступа к данным. Мьютексы позволяют только одной горутине получить доступ к общему ресурсу в определенный момент времени. sync.RWMutex: разрешает множественное чтение или одну операцию записи в текущий момент времени. sync.WaitGroup: используется для ожидания завершения группы горутин перед продолжением выполнения основной программы. sync.Once: гарантирует, что функция будет вызвана только один раз, несмотря на количество вызовов. sync.Cond: предоставляет механизм для блокирования горутины, пока не будет выполнено некоторое условие. Не так давно Расс Кокс отменил предложение удалить данные тип в будущей версии Go. Подобную роль играют: Каналы. Каналы в Go хоть и не являются примитивами синхронизации в традиционном понимании, они играют ключевую роль в управлении горутинами, позволяют обеспечить безопасный обмен данными между ними. Каналы обеспечивают синхронизацию и блокируют выполнение до тех пор, пока данные не будут переданы или приняты. Атомарные операции: Golang предоставляет атомарные операции для безопасного выполнения операций чтения и записи разделяемых данных.

Комментарии
0/3000
9

Что такое атомарная операция?

Атомарная операция выполняется за один шаг относительно других потоков или, в контексте Go, других горутин. Это означает, что атомарную операцию нельзя прервать в середине ее работы. Стандартная библиотека Go содержит пакет atomic, который в некоторых простых случаях может помочь избежать использования мьютекса. С помощью него мы получаем доступ к атомарным счетчикам из нескольких горутин, не имея проблем с синхронизацией и не беспокоясь о race condition.

Комментарии
0/3000
10

Что такое горутина? Как ее остановить?

Горутина  —  это функция или метод, которые выполняются конкурентно с любыми другими горутинами, используя специальный поток. Потоки горутин более легковесны, чем стандартные потоки, и большинство программ Go одновременно используют тысячи горутин. Для создания горутины перед объявлением функции нужно добавить ключевое слово go. go f(x, y, z) Остановить горутину можно отправкой сигнала в специальный канал. При этом горутины могут отвечать на такие сигналы, только если им сказано выполнять проверку. Поэтому нужно будет включить проверки в подходящие места, например в начало цикла for. package main func main() { quit := make(chan bool) go func() { for { select { case <-quit: return default: // … } } }() // … quit <- true }

Комментарии
0/3000
11

Что такое канал и какие виды каналов есть в Go?

Каналы — это инструменты коммуникации между горутинами. Технически это конвейер/труба, откуда можно считывать или помещать данные. То есть одна горутина может отправить данные в канал, а другая — считать помещенные в этот канал данные. Для создания канала в Go есть ключевое слово chan. Канал может передавать данные только одного типа. package main import "fmt" func main() { var c chan int fmt.Println(c) } При простом определении переменной канала она имеет значение nil, то есть по сути канал неинициализирован. Для инициализации применяется функция make(). В зависимости от определения емкости канала он может быть буферизированным или небуферизированным. Для создания небуферизированного канала вызывается функция make() без указания емкости канала: var intCh chan int = make(chan int) Буферизированные каналы также создаются с помощью функции make(), только в качестве второго аргумента в функцию передается емкость канала. Если канал пуст, то получатель ждет, пока в канале появится хотя бы один элемент. chanBuf := make(chan bool, 3) С каналом можно произвести 4 действия: - создать канал - записать данные в канал - вычесть что-то из канала - закрыть канал Однонаправленные каналы: в Go можно определить канал, как доступный только для отправки данных или только для получения данных. Канал может быть возвращаемым значением функции. Однако, следует внимательно подходить к операциям записи и чтения в возвращаемом канале.

Комментарии
0/3000
12

Как работают буферизованные и небуферизованные каналы?

Буферизованные каналы позволяют вам быстро помещать задания в очередь, чтобы вы могли работать с большим количеством запросов и обрабатывать их позже. Кроме того, буферизованные каналы можно использовать в качестве семафоров, ограничивая пропускную способность вашего приложения. Суть: все входящие запросы перенаправляются на канал, который обрабатывает их по очереди. Завершая обработку запроса, канал отправляет исходному, вызвавшему сообщение о готовности обработать новый запрос. Таким образом, ёмкость буфера канала ограничивает количество одновременных запросов, которые он может хранить. Вот так выглядит код, который реализует данный метод: package main import ( "fmt" ) func main() { numbers := make(chan int, 5) // канал numbers не может хранить более пяти целых чисел — это буферный канал с емкостью 5 counter := 10 for i := 0; i < counter; i++ { select { // здесь происходит обработка case numbers <- i * i: fmt.Println("About to process", i) default: fmt.Print("No space for ", i, " ") } // мы начинаем помещать данные в numbers, однако когда канал заполнен, он перестанет сохранять данные и будет выполняться ветка default } fmt.Println() for { select { case num := <-numbers: fmt.Print("*", num, " ") default: fmt.Println("Nothing left to read!") return } } } Аналогично, мы пытаемся считывать данные из numbers, используя цикл for. Когда все данные из канала считаны, выполнится ветка default и программа завершится с помощью оператора return. При выполнении кода выше мы получаем такой вывод: $ go run bufChannel.go About to process 0 . . . About to process 4 No space for 5 No space for 6 No space for 7 No space for 8 No space for 9 *0 *1 *4 *9 *16 Nothing left to read! В общем: Буферизированный канал заблокирует горутину только в том случае, если весь буфер забит. И происходит попытка еще одной записи. Как только будет выполнено чтение из канала - горутина разблокируется. В случае, если горутина всего одна (только функция main) и канал её заблокирует — программа выпадет с ошибкой, так как все горутины блокированы и выполнять нечего. Небуферизированный канал заблокирует горутину до момента, пока с него ничего не прочитают.

Комментарии
0/3000
13

Можно ли в Go закрыть канал со стороны читателя?

Закрытие канала обычно выполняется отправителем, а не получателем. Это связано с тем, что закрытие канала со стороны получателя может привести к панике при попытке отправителя записать в уже закрытый канал. Однако, в некоторых случаях, получатель может определить, что данные больше не нужны, и хочет уведомить отправителя о прекращении отправки. В таком случае, обычно используется дополнительный канал, называемый каналом управления или сигнальным каналом, который получатель может использовать для отправки сигнала об остановке. После получения сигнала, отправитель может корректно закрыть основной канал данных. Простой пример: func main() { dataCh := make(chan int) stopCh := make(chan struct{}) go func() { for { select { case data, ok := <-dataCh: if !ok { // Канал закрыт, прекращаем обработку return } // Обработка данных fmt.Println(data) case <-stopCh: // Получен сигнал остановки, закрываем канал dataCh close(dataCh) return } } }() // Отправка данных в канал dataCh <- 1 dataCh <- 2 // Отправка сигнала остановки stopCh <- struct{}{} } stopCh используется для уведомления горутины о необходимости закрыть канал dataCh. Это безопасный способ обеспечить корректное управление жизненным циклом канала.

Комментарии
0/3000
14

Что такое пакеты в Go?

Пакет - это механизм переиспользования кода, при котором Go файлы помещаются в общую директорию. В начале каждого такого файла объявляется зарезервированное слово package, а после него прописывается имя пакета. В рамках пакета все функции и глобальные переменные, объявленные как в верхнем, так и в нижнем регистре, видят друг друга.

Комментарии
0/3000
15

Как работает управление памятью в Go?

Go использует сборщик мусора для автоматического управления памятью. Разработчику не нужно явно выделять и освобождать память, как в языках типа C или C++. Однако нужно быть внимательным при работе с большими структурами данных, чтобы избежать утечек памяти. Некоторые ключевые аспекты управления памятью в Go: - Go применяет алгоритм сборки мусора с маркировкой и освобождением. Сборщик мусора отмечает активные объекты, после чего освобождает память от неактивных. - В Go можно работать с указателями, но нет прямого управления выделением и освобождением памяти через них. Память выделяется при создании объектов и автоматически освобождается сборщиком мусора. - Хотя Go управляет памятью автоматически, неправильное использование, например, из-за циклических ссылок, может вызвать утечки памяти. Поэтому важно контролировать использование ресурсов. - Срезы в Go — это динамические массивы, обеспечивающие автоматическое управление памятью при изменении их размера. - Go разделяет память на стек и кучу. Стек — для локальных переменных и контекста функций; каждый поток имеет свой стек. Куча — для долгоживущих объектов и данных, которые могут быть доступны из разных частей программы. Управление памятью в куче осуществляется сборщиком мусора. - Escape analysis в Go определяет, следует ли объекту быть на стеке или в куче, опираясь на его использование в программе. Этот анализ помогает оптимизировать управление памятью, делая его более эффективным.

Комментарии
0/3000
16

Что такое глобальная переменная?

Глобальная переменная - это переменная уровня пакета, то есть объявленная вне функции. Глобальная переменная также может быть доступна за рамками пакета, конечно только в том случае, если ее наименование начинается в верхнем регистре.

Комментарии
0/3000
17

Что такое heap и stack?

Стек (stack) — это область оперативной памяти, которая создаётся для каждого потока. Он работает в порядке LIFO (Last In, First Out), то есть последний добавленный в стек кусок памяти будет первым в очереди на вывод из стека. Каждый раз, когда функция объявляет новую переменную, она добавляется в стек, а когда эта переменная пропадает из области видимости (например, когда функция заканчивается), она автоматически удаляется из стека. Когда стековая переменная освобождается, эта область памяти становится доступной для других стековых переменных. Стек быстрый, так как часто привязан к кэшу процессора. Размер стека ограничен, и задаётся при создании потока. Куча (heap) — это хранилище памяти, также расположенное в ОЗУ, которое допускает динамическое выделение памяти и не работает по принципу стека: это просто склад для ваших переменных. Когда вы выделяете в куче участок памяти для хранения переменной, к ней можно обратиться не только в потоке, но и во всем приложении. Именно так определяются глобальные переменные. По завершении приложения все выделенные участки памяти освобождаются. Размер кучи задаётся при запуске приложения, но, в отличие от стека, он ограничен лишь физически, и это позволяет создавать динамические переменные. В сравнении со стеком, куча работает медленнее, поскольку переменные разбросаны по памяти, а не сидят на верхушке стека. То что попадает в кучу, живёт там пока не придёт GC. Но почему стек так быстр? Основных причин две: - Стеку не нужно иметь сборщик мусора (garbage collector). Как мы уже упоминали, переменные просто создаются и затем вытесняются, когда функция завершается. Не нужно запускать сложный процесс освобождения памяти от неиспользуемых переменных и т.п. - Стек принадлежит одной горутине, переменные не нужно синхронизировать в сравнении с теми, что находятся в куче. Что также повышает производительность

Комментарии
0/3000
18

Что делает runtime.newobject()?

runtime.newobject() выделяет память в куче.

Комментарии
0/3000
19

Что такое {} с необъявленным оператором в Go функции?

В Go функции действительно можно объявить {} без оператора, ограничив область видимости куска кода в рамках этой функции.

Комментарии
0/3000
20

Как выполнить ряд условий в одном операторе switch case?

Такое возможно благодаря ключевому слову fallthrough. Оно заставляет выполнять код в следующей объявленной булевой секции, вне зависимости от того, подходит ли булевое условие case этой секции.

Комментарии
0/3000
21

Что такое строки в Go?

Строки в Go - это обычный массив байт.

Комментарии
0/3000
22

Как можно оперировать строками?

Строки в Go можно складывать (конкатенировать). Для многих операций есть стандартные пакеты, к примеру strings, fmt. Все варианты конкатенации имеют свою производительность. strings.Builder — рекомендуемое решение для конкатенации списка строк. Обычно это решение следует использовать в циклах. Если просто нужно объединить несколько строк, использование strings.Builder не рекомендуется, так как это сделает код менее читаемым, чем использование оператора += или fmt.Sprintf.

Комментарии
0/3000
23

Как узнать длину строки?

Исходя из знания, что строка - это массив байт, взяв базовую функцию len(), от строки мы получим количество байт. Похожее поведение будет при итерации по строке - итерация по байтам. Тогда как в зависимости от кодировки, символ в строке может занимать не один байт. Для того, чтобы работать именно с символами, необходимо преобразовать строку в тип []rune. Еще одним способом определения длины строки является функция RuneCountInString пакета utf8.

Комментарии
0/3000
24

Какие численные типы есть в Go?

- int int8 int16 int32 int64; - uint uint8 uint16 uint32 uint64; - float32 float64; - complex64 complex128; - rune(int32).

Комментарии
0/3000
25

Расскажите про числовые константы в Go

Числовые константы в Go — это фиксированные значения, которые не изменяются во время выполнения программы. Они представлены точными значениями, не имеющими ограничений по размеру или точности, в отличие от переменных. Это означает, что числовые константы могут быть представлены с гораздо большей точностью, чем обычные числовые переменные. Они принимают свой тип (например, int, float64) только когда это необходимо, например, при присваивании значения переменной или при использовании в операции, где требуется определённый тип. Это дает гибкость и предотвращает потерю информации из-за ограничений размера типа, особенно при выполнении математических операций с константами. Простой пример: package main import "fmt" const ( Big = 1 << 100 Small = Big >> 99 ) func needInt(x int) int { return x*10 + 1 } func needFloat(x float64) float64 { return x * 0.1 } func main() { fmt.Println(needInt(Small)) fmt.Println(needFloat(Small)) fmt.Println(needFloat(Big)) }

Комментарии
0/3000
26

Чем отличается 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.

Комментарии
0/3000
27

Что такое обычный int и какие есть нюансы его реализации?

В зависимости от того, какая архитектура платформы, на которой мы исполняем код, компилятор преобразует int в int32 для 32-разрядной архитектуры и в int64 для 64-разрядной архитектуры.

Комментарии
0/3000
28

Как преобразовать строку в int и наоборот?

Для преобразования необходимо использовать функции из пакета strconv стандартной библиотеки Go. При этом, для преобразования строк в/из int и int64 используются разные функции, strconv.Atoi и strconv.Itoa для int, strconv.ParseInt и strconv.FormatInt для int64 соответственно.

Комментарии
0/3000
29

Сколько в памяти занимают int32 и int64?

Из самого названия типа следует, что int32 занимает 4 байта (32/8), int64 занимает 8 байтов (64/8).

Комментарии
0/3000
30

Какие предельные значения int32 и int64?

С помощью 4 (int32) или 8 (int64) байт можно закодировать разные по диапазону значения. Для int64 это диапазон от –9 223 372 036 854 775 808 до 9 223 372 036 854 775 807, для int32 от –2 147 483 648 до 2 147 483 647.

Комментарии
0/3000
31

Какой результат у деления int на 0 и float на 0?

Деление int на 0 в Go невозможно и вызовет ошибку компилятора. Тогда как деление float на 0 дает в своем результате бесконечность.

Комментарии
0/3000
32

Что такое iota?

iota - идентификатор, который позволяет создавать последовательные нетипизированные целочисленные константы. Значением iota является индекс ConstSpec. Не смотря на то, что первым индексом является 0, значение первой константы можно задать отличным от 0, что в свою очередь повлияет на значения последующих констант.

Комментарии
0/3000
33

Что такое слайс и чем он отличается от массива?

Cлайс - это структура Go, которая включает в себя ссылку на базовый массив, а также две переменные len(length) и cap(capacity). len - это длина слайса - то количество элементов, которое в нём сейчас находится. cap - это ёмкость слайса - то количество элементов, которые мы можем записать в слайс сверх len без его дальнейшего расширения. Array - это последовательно выделенная область памяти. Частью типа array является его размер, который в том числе является неизменяемым.

Комментарии
0/3000
34

Как работает базовая функция append для Go?

Функция принимает на вход слайс и переменное количество элементов для добавления в слайс. append расширяет слайс за пределы его len, возвращая при этом новый слайс. Если количество элементов, которые мы добавляем в слайс, не будет превышать cap, вернется новый слайс, который ссылается на тот же базовый массив, что и предыдущий слайс. Если количество добавляемых элементов превысит cap, то вернется новый слайс, базовым для которого будет новый массив.

Комментарии
0/3000
35

Какой размер массива выделяется под слайс при его расширении за рамки емкости?

Если отвечать на вопрос поверхностно, то можно сказать, что базовый массив расширяется в два раза от нашей capacity. Отвечая более ёмко, следует учесть, что при больших значениях расширение будет не в два раза и будет вычисляться по специальной формуле. Если развернуть ответ полностью, то это будет звучать примерно так: - если требуемая cap больше, чем вдвое исходной cap, то новая cap будет равна требуемой; - если это условие не выполнено, а также len текущего слайса меньше 1024, то новая cap будет в два раза больше базовой cap; - если первое и второе условия не выполнены, то емкость будет увеличиваться в цикле на четверть от базовой емкости, пока не будет обработано переполнение. Посмотреть эти условия более подробно можно в исходниках Go.

Комментарии
0/3000
36

Как реализована map (карта) в Go?

Сама map в Go - это структура, реализующая операции хеширования. При этом, так же, как и любую структуру, содержащую ссылки на области памяти, map необходимо инициализировать. map ссылается на такие элементы как bucket. Каждый bucket содержит в себе: - 8 экстра бит, с помощью которых осуществляется доступ до значений в этом bucket; - ссылку на следующий коллизионный bucket; - 8 пар ключ-значение, уложенных в массив.

Комментарии
0/3000
37

Почему нельзя брать ссылку на значение, хранящееся по ключу в map?

map поддерживает процедуру эвакуации. Значения, хранящиеся в определённой ячейке памяти в текущий момент времени, в следующий момент времени уже могут там не храниться.

Комментарии
0/3000
38

Что такое эвакуация, и когда она происходит?

Эвакуация - это процесс, когда map переносит свои значения из одной области памяти в другую. Это происходит из-за того, что кол-во значений в каждом отдельном bucket максимально равно 8. В тот момент времени, когда среднее количество значений в bucket составляет 6.5, Go понимает, что размер map не удовлетворяет необходимому. Начинается процесс расширения map. Следует отметить, что сам процесс эвакуации может происходить некоторое время, на протяжение которого новые и старые данные будут связаны.

Комментарии
0/3000
39

Какие есть особенности синтаксиса получения и записи значений в map?

1

Получить значение из map, которую мы предварительно не аллоцировали, нельзя - приложение упадет в панику. Если ключ не найден в map, в ответ мы получим дефолтное значение для типа значений map. То есть, для строки - это будет пустая строка, для int - 0 и так далее. Для того, чтобы точно понять, что в map действительно есть значение, хранящееся по переданному ключу, необходимо использовать специальный синтаксис. А именно, возвращать не только само значение, но и булевую переменную, которая показывает, удалось ли получить значение по ключу.

Комментарии
0/3000
40

Как происходит поиск по ключу в map?

1. вычисляется хэш от ключа; 2. с помощью значения хэша и размера bucket вычисляется используемый для хранения bucket; 3. вычисляется дополнительный хэш - это первые 8 бит уже полученного хэша; 4. в полученном bucket последовательно сравнивается каждый из 8 его дополнительных хэшей с дополнительным хэшем ключа; 5. если дополнительные хэши совпали, то получаем ссылку на значение и возвращаем его; 6. если дополнительные хэши не совпали, и в bucket больше нет дополнительных хэшей, алгоритм переходит в следующий bucket, ссылка на который хранится в текущем; 7. если в текущем bucket нет ссылки на следующий bucket, а значение так и не найдено, возвращается дефолтное значение.

Комментарии
0/3000
41

Что такое интерфейсы в Go?

Интерфейс можно рассматривать, как некое соглашение (контракт), что тот или иной объект будет реализовывать указанное в интерфейсе поведение. Переводя на более человеческий язык, интерфейс - это структура, в которой описаны методы, которые должны быть реализованы для других структур, которые будут удовлетворять этому интерфейсу. Удовлетворение интерфейсу поддерживается на неявном уровне. То есть для объекта достаточно описать реализацию методов интерфейса. И объект без дополнительных объявлений в кодовой базе начинает удовлетворять этому интерфейсу.

Комментарии
0/3000
42

Приведите пример реализации интерфейсов

Перед нами поставили задачу посчитать площадь неравного участка земли. Самый простой способ - это поделить участок на соответствующие фигуры, высчитать площадь каждой фигуры и сложить все площади. Допустим, участок делится на 2 геометрические фигуры: квадрат и прямоугольный треугольник. Для каждой фигуры мы можем создать тип. Описать интерфейс Squarer, условием реализации которого будет метод расчета площади. Написать для каждого типа метод расчета площади, который будет реализовывать объявленный интерфейс Squarer. После этого мы можем написать функцию которая, будет принимать на вход любой из типов, реализующих интерфейс площади, считать площадь каждого и складывать ее в общую сумму.

Комментарии
0/3000
43

Что такое пустой интерфейс?

Исходя из определения интерфейса, пустой интерфейс - это интерфейс, для реализации которого не нужно описывать ни одного метода. Таким образом, пустому интерфейсу соответствует абсолютно любой тип.

Комментарии
0/3000
44

Что такое nil интерфейс?

Интерфейс реализован в Go, как структура, которая содержит в себе ссылку на само значение и ссылку на структуру itab. itab предоставляет служебную информацию об интерфейсе и базовом типе. nil интерфейс не ссылается на какое либо значение, но при этом содержит в себе служебную информацию поля itab. По этой причине булево сравнение nil с интерфейсом всегда ложное.

Комментарии
0/3000
45

Как преобразовать интерфейс к другому типу?

Интерфейс можно преобразовать в базовый тип значения (скастить). Для этого используется синтаксис, возвращающий две переменные, одна из которых булевая. В случае, если не удалось скастить интерфейс, булевая переменная будет ложной, а переменная базового типа, к которому приводим интерфейс, будет равна дефолтному значению этого типа.

Комментарии
0/3000
46

Как определить тип интерфейса?

С помощью инструкции switch case можно определить тип интерфейса, указав возможные варианты базового типа его значения.

Комментарии
0/3000
47

Зачем используется ключевое слово defer в Go?

Ключевое слово defer используется для отложенного вызова функции. При этом, место объявления одной инструкции defer в коде никак не влияет на то, когда та выполнится. Функция с defer всегда выполняется перед выходом из внешней функции, в которой defer объявлялась.

Комментарии
0/3000
48

Порядок возврата при использовании несколько функций с defer?

func main() { fmt.Println("counting") for i := 1; i < 4; i++ { defer fmt.Println(i) } fmt.Println("done") } defer добавляет переданную после него функцию в стек. При возврате внешней функции вызываются все добавленные в стек вызовы. Поскольку стек работает по принципу LIFO (last in first out), значения стека возвращаются в порядке от последнего к первому. Таким образом, функции c defer будут вызываться в обратной последовательности от их объявления во внешней функции.

Комментарии
0/3000
49

Как передаются значения в функции, перед которыми указано defer?

func main() { nums := 1 << 5 // 32 defer fmt.Println(nums) nums = nums >> 1 //16 fmt.Println("done") } Аргументы функций, перед которыми указано ключевое слово defer оцениваются немедленно. То есть на тот момент, когда переданы в функцию.

Комментарии
0/3000
50

Что такое замыкания функций?

Замыкание функции  —  это значение функции, ссылающееся на переменные вне ее тела. Такая функция может обращаться к этим переменным и присваивать им значения. Например, adder() возвращает замыкание, привязанное к собственной переменной sum, на которую оно ссылается. package main import "fmt" func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } func main() { pos, neg := adder(), adder() for i := 0; i < 10; i++ { fmt.Println( pos(i), neg(-2*i), ) } }

Комментарии
0/3000
51

Что такое lock-free структуры данных?

Lock-free структуры данных — это тип структур данных, разработанных для многопоточных операций без использования традиционных блокировок, таких как мьютексы. Основная идея заключается в том, чтобы обеспечить безопасность потоков и избежать проблем, связанных с блокировками, включая взаимную блокировку (deadlock) и узкие места производительности (bottlenecks). Lock-free структуры данных обычно используют атомарные операции, такие как CAS (compare-and-swap), для обеспечения согласованности данных между потоками. Эти операции позволяют потокам соревноваться за изменение данных, но гарантируют, что только один поток сможет успешно изменить данные в любой момент времени. В Go, языке с поддержкой конкурентности, есть несколько примеров lock-free или почти lock-free структур данных, особенно в стандартной библиотеке. Например: Каналы: хотя каналы в Go не являются полностью lock-free, они предоставляют высокоуровневый способ обмена данными между горутинами без явного использования блокировок. Атомарные операции: пакет sync/atomic в Go предоставляет примитивы для атомарных операций, которые являются ключевыми компонентами для создания lock-free структур данных. sync.Map: предназначен для использования в кейсах, где ключи в основном не меняются, и он использует оптимизации для уменьшения необходимости блокировок.

Комментарии
0/3000
52

Как устроен сетевой ввод-вывод в Go?

Сетевой ввод-вывод в Go организован через пакет net стандартной библиотеки, который предоставляет обширный API для работы с сетью. Он использует модель неблокирующего ввода-вывода с горутинами для обеспечения масштабируемости и эффективности. Когда мы создаем сетевое соединение или слушаем порт, каждая операция ввода-вывода (например, чтение или запись данных) может выполняться в отдельной горутине, позволяя обрабатывать множество соединений параллельно без блокировки главного потока выполнения. Go автоматически управляет множеством горутин, что упрощает написание масштабируемого асинхронного сетевого кода по сравнению с традиционными подходами, основанными на потоках. Вот простой пример, из него должно быть всё понятно: package main import ( "fmt" "io" "net" "os" ) func main() { // Слушаем на порту 8080 listener, err := net.Listen("tcp", ":8080") if err != nil { fmt.Println("Ошибка при создании слушателя:", err) os.Exit(1) } defer listener.Close() fmt.Println("Сервер запущен и слушает на порту 8080") for { // Принимаем входящее подключение conn, err := listener.Accept() if err != nil { fmt.Println("Ошибка при принятии подключения:", err) continue } // Обработка подключения в отдельной горутине go handleConnection(conn) } } // handleConnection обрабатывает отдельное подключение func handleConnection(conn net.Conn) { defer conn.Close() fmt.Println("Подключился клиент:", conn.RemoteAddr().String()) // Отправляем сообщение клиенту _, err := io.WriteString(conn, "Привет от сервера!\n") if err != nil { fmt.Println("Ошибка при отправке сообщения:", err) return } fmt.Println("Сообщение отправлено клиенту:", conn.RemoteAddr().String()) }

Комментарии
0/3000
53

Что такое дженерики (обобщения)?

Дженерики или обобщения — это средства языка, позволяющего работать с различными типами данных без изменения их описания. В версии 1.18 появились дженерики (вообще-то они были и ранее, но мы не могли их использовать в своём коде — вспомним функцию make(T type)). Дженерики позволяют объявлять (описывать) универсальные методы, т.е. в качестве параметров и возвращаемых значений указывать не один тип, а их наборы. Появились новые ключевые слова: - any — аналог interface{}, можно использовать в любом месте (func do(v any) any, var v any, type foo interface { Do() any }) - comparable — интерфейс, который определяет типы, которые могут быть сравнены с помощью == и != (переменные такого типа создать нельзя — var j comparable будет вызывать ошибку)

Комментарии
0/3000
54

Для чего нужна функция recover()?

Панику можно обработать внутри отложенной функции и восстановить нормальное выполнение программы. Для этого предназначена глобальная функция recover(). Формат функции: recover() interface{} Если возникла паника, то функция вернет объект ошибки, указанный в функции panic(). Если паника не возникла, то возвращается значение nil. Вызывать функцию recover() нужно внутри отложенной функции (функции, зарегистрированной с помощью инструкции defer). После вызова функции recover() считается, что паника обработана и можно продолжить выполнение программы. Вот пример обработки деления на 0: package main import "fmt" func main() { fmt.Println(division(10, 2)) fmt.Println(division(10, 0)) fmt.Println("Выполнение программы продолжается!") } func division(x, y int) (n int) { defer func() { if r := recover(); r != nil { fmt.Println(r) n = 0 // Возвращаем из функции division() ноль } }() fmt.Println("Инструкция до деления") n = x / y fmt.Println("Инструкция после деления") return } // Инструкция до деления // Инструкция после деления // 5 // Инструкция до деления // runtime error: integer divide by zero // 0 // Выполнение программы продолжается!

Комментарии
0/3000
Смежные категории
Базы данных
10 вопросов
Вопросы с собеседований о репликации баз данных
1457 просмотров
Computer Science
28 вопросов
Объяснение паттернов проектирования с примерами
1424 просмотра
Python
32 вопроса
Вопросы и ответы с собеседований по Python
2449 просмотров
Базы данных
11 вопросов
Вопросы с собеседований про шардинг баз данных
1282 просмотра
Computer Science
13 вопросов
Вопросы и ответы с собеседований про ООП
1230 просмотров
Computer Science
11 вопросов
Вопросы и ответы про интернет-протоколы
1464 просмотра
Рекомендуем
Базы данных
60 вопросов
Вопросы и ответы с собеседований по SQL
2377 просмотров
Computer Science
15 вопросов
Вопросы и ответы с собеседований по DDD
1526 просмотров
Computer Science
28 вопросов
Объяснение паттернов проектирования с примерами
1424 просмотра
Computer Science
11 вопросов
Вопросы и ответы про интернет-протоколы
1464 просмотра
Git
20 вопросов
Вопросы и ответы с собеседований по Git
1783 просмотра
Computer Science
13 вопросов
Вопросы и ответы с собеседований про ООП
1230 просмотров
Другие разделы

Лента

Активность пользователей Девстанции

Перейти к ленте

Лидеры

Рейтинг самых результативных пользователей сообщества

Перейти к лидерам

Треды

Общение по интересам и связь с разработчиками

Перейти к тредам

Задачи

Решение алгоритмических задач с собеседований

Перейти к задачам

Вопросы

Ответы на вопросы с технических собеседований

Вы находитесь здесь

Викторины

Интерактивные викторины по вопросам с собеседований

Перейти к викторинам
Мы в Telegram
Новости проекта, общение с разработчиками, общение по интересам - присоединяйтесь!