Можете ли вы объяснить разницу между горутинами и потоками операционной системы?
Ответ
Горутины (goroutines) в Go - это легковесные потоки выполнения, которые управляются рантаймом Go. Они разделяют общее адресное пространство и планируются внутри Go-процесса. В отличие от потоков операционной системы, горутины не имеют непосредственного доступа к системным ресурсам, что делает их более эффективными и управляемыми. Представьте, что горутины — это как Падаваны в мире Star Wars. Они молоды, гибки и могут выполнять множество заданий, управляемые Силой (рантаймом Go). Падаваны могут быстро и легко перемещаться, выполняя свои обязанности, используя Силу для совместной работы и взаимопомощи.Потоки операционной системы — это как Джедаи. Они мудры, опытны и обладают большей мощью, но они не такие легковесные и подвижные, как Падаваны, и требуют больше ресурсов. Джедаи имеют доступ ко всем ресурсам Галактики (системе), но также им нужно управлять и защищать эти ресурсы.
Падаваны (горутины) делят между собой знания и Силу (общее адресное пространство), обучаясь и сражаясь вместе. Они планируют свои задания и тренировки вместе, работая как единое целое внутри Храма Джедаев (процесса Go).
Таким образом, хотя Падаваны и Джедаи используют Силу для защиты и обучения, Падаваны делают это более эффективно и гибко, позволяя им быстро адаптироваться и реагировать на изменения в Галактике. В то время как Джедаи, обладая большим опытом и мудростью, способны справляться с более крупными и сложными задачами, но им требуется больше времени и ресурсов для выполнения своих обязанностей.
Можете привести пример кода, где пустой интерфейс пригодился бы вам для обработки разных типов данных?
Ответ
Пустой интерфейс (interface{}) в Go может представлять любой тип данных, так как он не имеет определенных методов. Он полезен в случаях, когда нужно работать с разными типами данных, например, при создании обобщенных функций. Пример использования пустого интерфейса: ```go func printValue(value interface{}) { fmt.Println(value) } func main() { printValue(42) // Можно передать целое число printValue("Hello") // Можно передать строку printValue(3.14) // Можно передать число с плавающей точкой } ```Можете предоставить код, который демонстрирует создание, выделение и освобождение памяти с использованием указателей.
Ответ
В Go управление памятью автоматизировано, и непосредственная работа с выделением и освобождением памяти обычно не требуется. Однако, если бы мы хотели создать свой примитив для управления памятью, это могло бы выглядеть следующим образом: ```go type MemoryManager struct { data []byte } func NewMemoryManager(size int) \*MemoryManager { return &MemoryManager{data: make([]byte, size)} } func (mm *MemoryManager) Allocate(size int) []byte { if len(mm.data) < size { return nil // Недостаточно памяти } allocated := mm.data[:size] mm.data = mm.data[size:] return allocated } func (mm \*MemoryManager) Free(allocated []byte) { mm.data = append(mm.data, allocated...) } ```Ответ
Давайте представим Garbage Collector (Сборщик Мусора) в Go, как Р2-D2 в мире Star Wars. Р2-D2 — умный и изобретательный дроид, способный решать множество задач и обладающий множеством функций. Дроид работает в двух режимах:Фаза Mark (Фаза Разметки)
В этой фазе Р2-D2 (Сборщик Мусора) бегает по всей Галактике (памяти программы), исследуя все планеты (объекты в памяти). Каждую планету, на которой он обнаруживает жизнь (доступные объекты), он помечает специальным цветом.
Белый Цвет: Планеты (объекты), которые еще не исследованы, помечаются белым цветом.
Серый Цвет: Планеты, обнаруженные, но еще не полностью исследованные, помечаются серым цветом.
Черный Цвет: Полностью исследованные планеты помечаются черным цветом.
Фаза Sweep (Фаза Очистки)
После того как все планеты исследованы, Р2-D2 начинает фазу очистки. Он возвращается на все планеты, помеченные белым цветом — те, на которых жизнь не была обнаружена (неиспользуемые объекты), и освобождает их ресурсы для новой жизни (возвращая память операционной системе). Три-Цветная Маркировка
С использованием три-цветной маркировки, сборщик мусора может выполняться конкурентно с вашей программой, минимизируя задержки и паузы, ассоциированные с процессом сбора мусора.
Заключение Таким образом, Сборщик Мусора в Go, подобно Р2-D2, тщательно и умно ухаживает за Галактикой, убеждаясь, что все ресурсы используются эффективно, и что ненужные и заброшенные планеты (объекты) могут быть очищены и восстановлены для будущих нужд Галактики (программы).
Ответ
Сборщик мусора (GC) в Go стремится сбалансировать использование процессорного времени и потребление памяти. Он запускается автоматически во время выполнения программы, и его поведение настраивается через механизмы, такие как GODEBUG и runtime/debug пакет.Триггеры Запуска GC
Выделение Памяти:
Основной триггер для GC — это выделение памяти. Когда программа продолжает выделять память, и общий объем выделенной памяти достигает определенного порога, GC будет запущен.
Ручной Запуск:
Программист также может явно запустить GC, используя функцию runtime.GC() из стандартной библиотеки, но в большинстве случаев лучше полагаться на автоматический запуск GC рантаймом Go.
Алгоритм Запуска GC Рантайм Go использует концепцию "GC pacer", который стремится определить оптимальное время для запуска GC, основываясь на текущем потреблении памяти и на том, как быстро программа выделяет новую память. Цель состоит в том, чтобы максимизировать производительность при минимальных паузах на сборку мусора.
Настройка GC Используя переменную окружения GODEBUG, можно установить gctrace=1 для получения информации о каждом цикле сборки мусора, такой как его длительность и объем освобожденной памяти. Это может быть полезно для мониторинга и оптимизации поведения GC в вашей программе. Также, параметр GOGC позволяет контролировать частоту сборки мусора. Значение GOGC=100 (по умолчанию) запускает GC каждый раз, когда объем выделенной памяти на куче будет увеличиваться в 2 раза (вдвое) по сравнению с объемом памяти, освобожденным после последнего цикла сборки мусора. Увеличение GOGC уменьшит частоту сборки мусора, а уменьшение — увеличит. Пример Допустим, у вас есть программа, и после выполнения сборщика мусора остается 4 МБ "живых" объектов на куче. Если GOGC=100, сборщик мусора будет запущен в следующий раз, когда объем выделенной памяти на куче достигнет 8 МБ (4 МБ живых объектов + еще 4 МБ новых объектов).
Контекст GOGC Увеличение и уменьшение GOGC позволяет изменить этот коэффициент, чтобы контролировать, насколько часто сборщик мусора будет запускаться. Например:
GOGC=200 означает, что сборщик мусора будет запускаться, когда объем памяти увеличится в 3 раза по сравнению с последним освобождением.
GOGC=50 означает, что сборщик мусора будет запускаться, когда объем памяти увеличится на 50% по сравнению с последним освобождением.
Ответ
1. Паузы Подход "Mark-and-Sweep" может приводить к паузам в выполнении программы, поскольку он требует прохода по всем живым объектам. В некоторых системах, особенно тех, где время отклика критично, такие паузы могут быть неприемлемыми. 2. Фрагментация памяти После нескольких циклов сбора мусора память может стать фрагментированной, поскольку объекты, освобождаемые сборщиком мусора, могут быть разбросаны по всей куче. Это может сделать сложным выделение больших континуальных блоков памяти и привести к неэффективному использованию памяти. 3. Накладные расходы Процесс разметки и очистки требует дополнительных вычислительных ресурсов, что может снизить общую производительность системы, особенно в высоконагруженных приложениях. 4. Отслеживание корней Определение, какие объекты являются корневыми, может быть сложным и требовать дополнительной информации от компилятора или рантайма, что увеличивает сложность системы.Как вы интегрируете pprof в ваше приложение для сбора данных профилирования CPU и памяти? Интеграция pprof: Ожидается, что кандидат знает, как интегрировать pprof в приложение и как собирать профили CPU и памяти. Какие команды и подходы вы используете для анализа данных профилирования и определения узких мест в производительности вашего приложения? Анализ данных профилирования: Интересует, насколько хорошо кандидат умеет анализировать данные профилирования и определять, где приложение тратит больше всего ресурсов. Поделитесь примером, когда вы успешно использовали pprof для нахождения и устранения проблемы с производительностью в реальном проекте. Какова была проблема, и как вы её решили? Реальный опыт: Понимание того, как кандидат использовал pprof в реальной ситуации, может показать его опыт в оптимизации производительности приложений на Go. Как вы оптимизируете использование памяти в вашем приложении на основе данных, полученных с помощью pprof? Оптимизация памяти: Ожидается, что кандидат знает, как использовать данные pprof для оптимизации использования памяти в приложении.
Ответ
Лучше один раз сделать чем 3 раза прочитать, поэтому ниже инструкция по использованию pprof `pprof` - это пакет в Go, который помогает с профилированием приложений Go. Давайте рассмотрим пример того, как использовать `pprof` для анализа использования CPU и памяти.Сначала, создадим простую программу Go, которую мы хотим проанализировать:
package main
import (
"fmt"
"os"
"runtime/pprof"
"time"
)
func main() {
cpuFile, err := os.Create("cpu.pprof")
if err != nil {
fmt.Println(err)
return
}
pprof.StartCPUProfile(cpuFile)
defer cpuFile.Close()
defer pprof.StopCPUProfile()
memFile, err := os.Create("mem.pprof")
if err != nil {
fmt.Println(err)
return
}
defer memFile.Close()
// Простая функция для имитации нагрузки на CPU и выделения памяти.
for i := 0; i < 1000000; i++ {
s := make([]byte, 1000)
if i%1000 == 0 {
time.Sleep(500 * time.Millisecond)
}
for i := 0; i < 1000; i++ {
s[i] = byte(i % 256)
}
}
pprof.WriteHeapProfile(memFile)
}
- Запустите вашу программу как обычно:
go run main.go
- После завершения программы, у вас будет два файла профиля:
cpu.pprof
иmem.pprof
. - Используйте утилиту командной строки
pprof
для анализа этих файлов:go tool pprof cpu.pprof
илиgo tool pprof mem.pprof
. - В интерактивной сессии
pprof
вы можете использовать команды вродеtop
,list
илиweb
для анализа профиля.
go tool pprof cpu.pprof
Далее, в интерфейсе pprof
, введите:
top
Это покажет вам, где ваша программа тратит больше всего времени на выполнение CPU.
go tool pprof mem.pprof
Также, в интерфейсе pprof
, введите:
top
Это покажет вам, где ваша программа использует больше всего памяти.
Используйте команду web
в интерфейсе pprof
для создания графического представления вашего профиля. Это может помочь вам лучше понять, где происходят узкие места в вашем коде.
Ответ
`GOGC` и `GODEBUG` являются переменными окружения, которые можно задать для изменения поведения сборщика мусора и режима отладки в Go. Их можно задать перед запуском вашей программы Go, например, в командной строке, или внутри вашего кода, используя пакет `os` для установки переменных окружения.export GOGC=200
export GODEBUG=gctrace=1
go run myprogram.go
На Windows:
set GOGC=200
set GODEBUG=gctrace=1
go run myprogram.go
ENV GOGC 200
ENV GODEBUG gctrace=1
Вы можете также установить эти переменные окружения программно, используя пакет os
в Go:
package main
import (
"os"
)
func main() {
_ = os.Setenv("GOGC", "200")
_ = os.Setenv("GODEBUG", "gctrace=1")
// Ваш код
}
GOGC=200
: Запускает сборщик мусора, когда объем выделенной памяти увеличивается в 2 раза (200%) по сравнению с объемом памяти, который был освобожден после последней сборки мусора.GODEBUG=gctrace=1
: Включает трассировку сборщика мусора, выводя информацию о каждом цикле сборки мусора.
Задание переменных окружения программно влияет только на текущий процесс и его дочерние процессы, и не будет иметь эффекта на другие процессы или на последующие запуски вашей программы.
Ответ
Middleware is essentially a function that is called before or after your main request handler, depending on where you put it in the chain of function calls. It allows you to process the request and/or response to perform various tasks: logging, authentication, CORS headers setting, and so on.
Imagine a series of traffic checkpoints on a road leading to a destination (your main request handler). Each checkpoint (middleware) has a specific task. One might check for proper documentation (authentication), another might count the number of passengers in the car (logging), and yet another might ensure the car meets environmental standards (CORS headers, content type setting, etc.). If a car doesn't pass any of these checkpoints, it might be turned around and never reach the destination (an HTTP error response). But if it passes all of them, it's allowed to proceed to its destination (the main handler).
A Router in Go is responsible for directing incoming HTTP requests to their corresponding handler functions based on criteria like the request's URL and HTTP method (GET, POST, etc.). For example, if an SUV with a label "Fetch-Data" (a GET request to /data) approaches, the traffic cop directs it to a road specifically designed for fetching data. Similarly, if a truck labeled "Store-Items" (a POST request to /items) comes, it gets directed to a different road where items are stored.
A traffic cop (router) stands at an intersection and directs vehicles (HTTP requests) based on their type or destination. For instance, trucks (GET requests) might be directed to one road (endpoint handler), cars (POST requests) to another, and bicycles (PUT requests) to a bike path. If a vehicle (request) tries to go down a path that's not meant for it, the traffic cop stops it and tells it where to go or turns it around (sends an HTTP 404 Not Found response).
In Golang, popular libraries like Gorilla Mux or Chi are often used to handle routing, and they also support middleware, allowing developers to chain together multiple functions for streamlined request processing.
a := [5]int{1, 2, 3, 4, 5}
s := a[1:4]
Ответ
a - это массив (не слайс), в котором находится 5 элементов. `Len(a)` = 5, `Cap(a)` = 5 s - это слайс, который ссылается на массив a и включает в себя элементы - {2, 3, 4}. `Len(s)` = 3, `Cap(s)` = 4base := []int{10, 20, 30, 40}
newSlice := base[1:3]
newSlice[1] = 50
Ответ
base - это слайс, в котором находится 4 элемента. `Len(base)` = 4, `Cap(base)` = 4 newSlice - это слайс, который ссылается на массив `base` и включает в себя элементы - {20, 30}. `Len(s)` = 2, `Cap(s)` = 3 Так как слайс newSlice ссылается на тот же массив с данными, что и `base`, то изменение элемента по индексу 1 приведет к изменениям в основном массиве, в итоге получится: base - []int{10, 20, 50, 40} newSlice - []int{20, 50}original := make([]int, 3, 5)
original = append(original, 1, 2, 3)
Ответ
original - слайс, который создан функцией make с len(original) = 3 и cap(original) = 5. После создания слайса таким способом он будет заполнен 3 значениями int по-умолчанию, original - []int{0,0,0} После функции append слайс original будет выглядеть так - original - []int{0,0,0,1,2,3} с len(original) = 6 и cap(original) = 10.Вопрос 4: Чем отличаются nilSlice и emptySlice, и что вернёт следующая проверка: nilSlice == nil и emptySlice == nil?
var nilSlice []int
emptySlice := make([]int, 0)
Ответ
В Go слайсы это ссылочный тип данных, у которого default значения будет nil, таким образом на куче не будет выделена память для nilSlice. При этом emptySlice будет инициализирован и для которого будет выделена память на куче с len(emptySlice) = 0 и cap(emptySlice) = 0. Проверка nilSlice == nil вернет true, emptySlice == nil вернет false.slices := [][]int{
{1, 2},
{3, 4},
}
slices[0] = append(slices[0], 3)
Ответ
slices - двумерный слайс, при добавлении элемента 3 к первому слайсу в slices (то есть {1, 2}), новый элемент добавляется в конец этого слайса. Поэтому slices станет: ```go slices := [][]int{ {1, 2, 3}, {3, 4}, } ```Вопрос 6: Почему, когда вы добавляете элемент в слайс с помощью append
, иногда вам может понадобиться новый участок памяти, и иногда — нет?
Как это связано с емкостью (capacity) слайса?
Ответ
Слайс представляет собой структуру, в которой есть `len`, `cap` и указатель на массив данных. `Len` - это количество элементов в данном массиве, а `Cap` - максимальная емкость, при превышении которой в случае append (например) происходит переаллокация памяти (обычно в два раза больше текущего) для нового массива в котором будет достаточно места для добавляемых элементов. Данная операция является затратной, так как происходит копирование всех элементов из одного массива в новый.Вопрос 7: Nil vs Empty slice: Какова разница между nil слайсом и пустым слайсом? В каких случаях один из них предпочтительнее другого?
Ответ
`Nil` слайс и пустой слайс – это разные вещи. `Nil` слайс не имеет выделенной памяти и его длина и емкость равны нулю. Но это не значит, что вы не можете добавить в него элементы с помощью append. Пустой слайс, с другой стороны, уже может иметь выделенную память (например, после создания с помощью make([]T, 0)), но его текущая длина равна нулю.Вопрос 8: Как бы вы удалили элемент из слайса без использования стандартной библиотеки, не нарушив порядок следования элементов?
Ответ
В случае если элемент, который необходимо удалить находится в начале или конце это можно сделать с помощью среза, например - > [!NOTE] > new_slice[index_of_element_to delete+1 :][!NOTE] new_slice[ :index_of_element_to delete]
или если в середине -
[!Если порядок важен] append(slice[:s], slice[s+1:]...)
[!Если порядок НЕ важен]
func remove(s []int, i int) []int {
s[i] = s[len(s)-1]
return s[:len(s)-1]
Вопрос 9: Можно ли утверждать, что после обрезания большого слайса до меньшего (например, largeSlice = largeSlice[:5]) память, занимаемая оставшимися элементами, будет освобождена?
Если нет, почему и как это может привести к утечке памяти?
Ответ
Память освобождена не будет так как в Golang `cap` для нового слайса расчитывается из формулы:[!NOTE] cap(new_slice) = cap(original_slice)−start_index_of_new_slice
Таким образом новый слайс будет ссылаться на тот же массив что и старый, до тех пор пока не произойдет реаалокация памяти в случае превышения cap текущего слайса. Только после реалокации памяти и переноса значений в новый слайс, garbage collector очистит память старого массива если на него не будут указывать другие слайсы.
Если у вас есть большой слайс, и вы создаете из него маленький срез, это может привести к неожиданному удержанию памяти. Если вы знаете, что оригинальный большой слайс больше не нужен, и вы хотите избежать утечек памяти, можете явно скопировать данные в новый слайс с помощью copy
.
taskList := []string{
"Проснуться",
"Покушать",
"Поработать",
}
wakeup := taskList[0:2] // Какой len/cap
work := taskList[2:3] // Какой len/cap
wakeup = append(wakeup, "Погулять с собакой")
fmt.Println("Wakeup staff: ", wakeup)
fmt.Println("Workstaff:", work)
Ответ
Исходный слайс taskList содержит: `[Проснуться, Покушать, Поработать]`Когда вы делаете срез wakeup := taskList[0:2]
, вы получаете слайс, который содержит:
[Проснуться, Покушать]
len(wakeup) = 2
cap(wakeup) = 3
(вместимость включает в себя все элементы оригинального слайса с начального индекса среза до конца, в данном случае это 3 элемента: Проснуться, Покушать и Поработать)
Теперь, когда вы делаете срез work := taskList[2:3]
, вы получаете слайс, который содержит:
[Поработать]
len(work) = 1
cap(work) = 1
(срез начинается с последнего элемента исходного слайса, так что вместимость равна длине)
Формула расчета новой вместимости cap(new_slice)=cap(original_slice)−start_index_of_new_slice
Когда вы добавляете "Погулять с собакой" в wakeup
с помощью append, wakeup
станет:
[Проснуться, Покушать, Погулять с собакой]
Так как у wakeup
оставалась вместимость 1 до достижения максимальной вместимости (которая равна 3), элемент "Погулять с собакой" будет добавлен в тот же участок памяти.
Таким образом, исходный слайс taskList был изменен и теперь выглядит так:
[Проснуться, Покушать, Погулять с собакой]
В итоге:
Wakeup staff: [Проснуться Покушать Погулять с собакой]
Workstaff: [Погулять с собакой]
var m map[string]int
m["key"] = 42
Почему это происходит и как это исправить?
Ответ
Map в Golang это референсный тип данных, что означает что default значение в данном случаем будет nil и под мапу не будет выделено место на куче. Мапа будет не инициализирована. Данное поведение точно такое же как у slice в Golang. При попытке записать значение, происходит паника, так как мы пытаемся разыменовать nil указатель. Это поведение очень похоже на поведение в языке Си, которое вызывает segfault. Чтобы исправить это необходимо инициализировать мапу черезе функцию make: m := make(map[string]int)func modifyMap(m map[int]string) {
m[2] = "changed"
}
func main() {
myMap := map[int]string{1: "one", 2: "two", 3: "three"}
modifyMap(myMap)
fmt.Println(myMap)
}
Ответ
Map в Golang это референсный(ссылочный) тип данных. Таким образом, произойдет замена значения по ключу `2` на значение `changed`. Данное поведение будет точно таким же если передать slice и поменять значение, оно измениться и в оригинальном слайсе, который был передан. В итоге будет напечатано: map[int]string{1: "one", 2: "changed", 3: "three"}Вопрос 3: Удаление из мапы во время итерации. Является ли следующий код безопасным, и если нет, почему?
m := map[int]bool{1: true, 2: true, 3: true}
for k := range m {
if k == 2 {
delete(m, k)
}
}
Ответ
Код является безопасным, удаление произойдет корректно.В Go мапа возвращает нулевое значение для типа значения, если ключ отсутствует. Как вы можете различить случай, когда ключ действительно отсутствует в мапе, и когда ассоциированное значение является нулевым значением (например, 0 для int или "" для string)?
Ответ
Мапа в Golang при проверке через if возвращает два значения, первое это значение по ключу (default значение если значения нет) и второе это значение типа bool - false в случае отсутствия значения по ключу и true, в случае его наличия. Пример: ```go if val, ok := checkMap["key"]; ok { fmt.Println("Ok is true, so the value is exist.") } else { fmt.Println("Ok is not true, so the key you provided does not exits.") } ```В каком порядке ключи возвращаются при итерации по мапе с помощью цикла range? Гарантирован ли этот порядок?
Ответ
Порядок не гарантирован, так как мапа хранит в себе значения в неупорядоченном виде.func setLinkHome(link *string) {
*link = "http://home"
}
link := "http://other"
setLinkHome(&link)
fmt.Println(link)
Ответ
Код выведет: `"http://home"` Почему: Функция `setLinkHome` принимает указатель на строку и устанавливает значение этой строки как `"http://home"`. Поэтому значение переменной `link` будет изменено на `"http://home"`. На первый взгляд достаточно просто, но давайте разберемся почему так происходит, ведь строки являются не изменяемыми типами данных в Golang `link := "http://other"` - Вы создаете переменную `link` и присваиваете ей значение "http://other".Представим как это будет выглдяеть на стеке:
link -> адрес в куче #1
В куче:
адрес #1: "http://other"
Вы вызываете setLinkHome(&link), передавая адрес переменной link в функцию.
Внутри setLinkHome
, вы присваиваете новое значение по этому адресу: *link = "http://home"
.
Теперь память выглядит следующим образом: На стеке: link -> адрес в куче #2
В куче: адрес #1: "http://other" (больше не используется) адрес #2: "http://home"
Значение по адресу #1 ("http://other") теперь не имеет ссылок на него, поэтому оно может быть освобождено сборщиком мусора в будущем.
Когда вы вызываете fmt.Println(link), выведется "http://home", так как переменная link теперь указывает на новую строку в куче.
Итак, ваш код действительно выведет "http://home".
Отмечу, что с точки зрения реализации Go, строки часто хранятся в неизменяемых массивах байтов, и когда вы "изменяете" строку, вы на самом деле создаете новую строку, указывающую на другой участок этого массива или на другой массив. Но для большинства случаев можно думать о строках как о данных в куче, на которые ссылаются переменные на стеке.
var ptr *int
i := 10
ptr = &i
*ptr++
Ответ
```go var ptr *int // Этот указатель инициализирован как nil. i := 10 // i присвоено значение 10. ptr = &i // ptr теперь содержит адрес переменной i. *ptr++ // Значение, на которое указывает ptr (т.е. i), увеличивается на 1. ``` Это значит, что мы идем по по адресу, котороый хранится в ptr, берем значение и увеличиваем на 1 В итоге i = 11Можно ли получить указатель на массив arr? А что насчет указателя на слайс s? Каковы их различия?
arr := [3]int{1, 2, 3}
s := arr[:]
Ответ
Массив в Golang это value type, Слайс - reference type Таким образом, чтобы получить указатель на слайc fmt.Printf("%p", s) Указатель на массив fmt.Printf("%p", &arr)Почему было решено использовать передачу по указателю в этом примере?
func modifyValue(x *int) {
*x = 5
}
func main() {
var num int = 2
modifyValue(&num)
fmt.Println(num)
}
Ответ
На экран будет выведено 5, так как в функцию modifyValue передается указатель на ячейку памяти где храниться num Таким образом мы изменяем значение по адресу в котором хранится значение 2.Что может случиться, если у вас есть указатель на большой кусок памяти,на который никто не ссылается?
Ответ
В Go присутствует сборщик мусора, который автоматически освобождает память от объектов, на которые больше нет ссылок. В отличие от некоторых других языков, Go не использует подсчет ссылок. Вместо этого он использует технику трассировки для определения живых и мертвых объектов.Вопрос 6: В приведенном коде есть двойное разыменование. Можете ли вы объяснить, что это такое и почему это работает?
type Node struct {
value int
next *Node
}
first := &Node{value: 1}
second := &Node{value: 2}
first.next = second
fmt.Println(first.next.value)
Ответ
Структура представляет собой связный список, в переменную next записывается следующее значени в списке (second) Таким образом, чтобы получить данные в следующем элементе списка (Node) необходимо переместиться по указателю на следующий элемент в списке и взять значениеfunc main() {
defer func() {
recover()
}()
panic("test panic")
fmt.Println("ok")
}
Ответ
Нет, `"ok"` не будет напечатано. Почему: Функция `panic` прекращает выполнение текущей функции и начинает распространять панику по стеку вызовов. Однако благодаря `defer` и `recover()` программа не завершится аварийно, но после panic `"ok"` уже не будет выполнено.// one
// two
// three
// (в любом порядке и в конце обязательно)
// Done!
// Исправь код
func printText(data []string) {
wg := sync.WaitGroup{}
for _, v := range data {
go func(v string ) {
wg.Add(1)
fmt.Println(v)
wg.Done()
}()
}
fmt.Println("done!")
}
data := []string{"one", "two", "three"}
printText(data)
Ответ
Проблема заключается в том, что функция `wg.Add(1)` вызывается внутри горутины, что может привести к непредсказуемым результатам. Исправленный код: ```go func printText(data []string) { wg := sync.WaitGroup{} for _, v := range data { wg.Add(1) go func(v string ) { defer wg.Done() fmt.Println(v) }(v) } wg.Wait() fmt.Println("Done!") } ```Вопрос 4: Мы пытаемся подсчитать количество выполненных параллельно операций, что может пойти не так?
var callCounter uint
func main() {
for i := 0; i < 10000; i++ {
go func() {
// Ходим в базу, делаем долгую работу
time.Sleep(time.Second)
// Увеличиваем счетчик
callCounter++
}()
}
fmt.Println("Call counter value = ", callCounter)
}
Ответ
Проблема в том, что доступ к `callCounter` не синхронизирован, что может привести к состоянию гонки и неправильным результатам. Почему: Несколько горутин могут попытаться увеличить значение `callCounter` одновременно.Вопрос 5: Есть функция processDataInternal, которая может выполняться неопределенно долго. Чтобы контролировать процесс, мы добавили таймаут выполнения ф-ии через context. Какие недостатки кода ниже?
func (s *Service) ProcessData(timeoutCtx context.Context, r io.Reader) error {
errCh := make(chan error)
go func() {
errCh <- s.processDataInternal(r)
}()
select {
case err := <-errCh:
return err
case <-timeoutCtx.Done():
return timeoutCtx.Err()
}
}
Ответ
1. Если `processDataInternal` превышает установленный тайм-аут, горутина будет продолжать работать в фоне даже после того, как `ProcessData` вернет ошибку тайм-аута. 2. Канал `errCh` не закрывается, что может привести к утечкам памяти. 3. Нет способа передать информацию в `processDataInternal`, что ему нужно остановиться из-за тайм-аута по контексту.Учитывая все вышеперечисленные моменты, следует
- Использовать канал с ограниченной емкостью.
- Закрыть канал
errrCh
- Передать
timeoutCtx
вprocessDataInternal
, чтобы можно было отслеживать состояние тайм-аута и прерывать выполнение при необходимости.
Исправленный код:
func (s *Service) ProcessData(timeoutCtx context.Context, r io.Reader) error {
// Создаем канал с емкостью 1, чтобы избежать блокировки при отправке данных
errCh := make(chan error, 1)
go func() {
defer close(errCh) // Гарантируем закрытие канала при завершении горутины
errCh <- s.processDataInternal(timeoutCtx, r)
}()
select {
case err := <-errCh:
return err
case <-timeoutCtx.Done():
return timeoutCtx.Err()
}
}
// Обновляем processDataInternal, чтобы принимать context.Context
// и проверять его состояние при выполнении долгой операции
func (s *Service) processDataInternal(ctx context.Context, r io.Reader) error {
// TODO: Ваша реализация. Включите проверки ctx.Done() в долгих операциях,
// чтобы прервать выполнение при тайм-ауте.
return nil
}
Эти изменения гарантируют, что:
Канал будет закрыт, исключая утечку памяти.
Если `processDataInternal` поддерживает отслеживание состояния context, можно завершить операцию при достижении тайм-аута.
Однако следует учесть, что завершение processDataInternal
при тайм-ауте зависит от того, как реализована функция и проверяется ли состояние контекста внутри нее. Если долгая операция не поддерживает прерывание, то она будет продолжать работать в фоне до своего завершения.
Использование каналов с ограниченной емкостью (буферизованных каналов) в Go может быть полезным в определенных ситуациях. Вот примеры, которые могут помочь понять, почему и когда это может быть полезно:
-
Избежание блокировки горутины: Представьте ситуацию, где горутина пытается отправить сообщение в канал, но нет другой горутины, готовой принять это сообщение. Если канал не буферизован, отправляющая горутина заблокируется. С буферизованным каналом, горутина не будет заблокирована, пока есть свободное место в буфере.
Пример: У вас есть система, которая отправляет логи через канал. Если обработчик логов временно занят и не может принять новые сообщения, буферизованный канал позволит продолжать добавлять логи в буфер, предотвращая блокировку отправляющих горутин.
-
Производительность: Буферизованные каналы часто могут улучшить производительность, так как отправка и получение сообщений не требует немедленной синхронизации между горутинами.
Пример: Представьте себе конвейер в заводе. Если каждый рабочий (горутина) должен будет ждать следующего рабочего перед передачей детали, это будет медленным. Но если у них есть промежуточные корзины (буферы), они могут продолжать работать, пока корзина не заполнится.
-
Ограничение ресурсов: Буферизованные каналы позволяют ограничивать количество обрабатываемых данных, что может быть полезно, чтобы избежать излишнего потребления памяти или других ресурсов.
Пример: У вас есть сервис, который загружает изображения. Пользователи могут запросить загрузку сотен изображений одновременно. Буферизованный канал позволяет ограничивать количество одновременно обрабатываемых изображений, предотвращая перегрузку системы.
Тем не менее, важно отметить, что неправильное использование буферизованных каналов также может привести к проблемам, таким как утечки памяти (если вы постоянно добавляете в канал, но никогда не читаете из него). Всегда стоит тщательно анализировать и тестировать код, чтобы обеспечить правильное и эффективное использование каналов.
Если все еще непонятно, почему мы используем буферизированный канал, читайте дальше:
- Длительная блокировка может привести к таймаутам: Если у вас есть внешний сервис или клиент, который ожидает ответа от вашего приложения, длительная блокировка может привести к превышению времени ожидания.
Пример: Вы разрабатываете веб-сервер, который принимает запросы на обработку данных. Каждый запрос инициирует горутину, которая пытается отправить данные на обработку через канал. Если обработчик временно занят, горутина будет заблокирована, и клиент может столкнуться с таймаутом.
- Неэффективное использование ресурсов: Горутины, заблокированные из-за попытки отправки в канал, продолжают занимать системные ресурсы, даже если они не выполняют полезной работы.
Пример: Ваше приложение начинает тысячи горутин для обработки задач. Если все они блокируются из-за канала, это может привести к значительному потреблению памяти и ресурсов CPU.
- Опасность взаимной блокировки: Если и отправляющая, и принимающая горутины ожидают друг друга, это может привести к взаимной блокировке, и ваше приложение может "зависнуть".
Пример: Горутина A ожидает, пока горутина B прочитает из канала, чтобы продолжить работу. Одновременно горутина B ожидает, пока горутина A что-то сделает, прежде чем читать из канала. Обе горутины блокируются навсегда.
- Опеределение поведения приложения: В некоторых случаях блокировка может быть желаемым поведением, чтобы контролировать скорость обработки или для синхронизации задач.
РЕАЛЬНЫЙ ПРИМЕР: Давайте представим завод, на котором есть конвейерная лента для производства игрушек. Этот конвейер разделен на два этапа:
Этап А: Машина, которая делает детали для игрушек.
Этап В: Работник, который собирает игрушку из этих деталей.
Между этими этапами у нас есть корзина, в которую машина (Этап А) кладет детали, и из которой работник (Этап В) берет детали для сборки игрушки.
Взаимная блокировка на примере завода:
Представьте, что машина на Этапе А может работать только тогда, когда в корзине нет деталей. Она ждет, пока работник на Этапе В не заберет все детали из корзины. С другой стороны, работник на Этапе В может начать сборку только после того, как в корзине накопится определенное количество деталей.
Взаимная блокировка произойдет в ситуации, когда в корзине будет недостаточно деталей для работника на Этапе В, чтобы начать сборку, но и достаточно деталей, чтобы машина на Этапе А не могла продолжить свою работу. Оба этапа будут ждать друг друга, и производство остановится.
Таким образом, если система (или код) не предусматривает механизма разрешения такой блокировки или предотвращения ее возникновения, это может привести к тому, что весь процесс "зависнет".
func a() {
x := []int{}
x = append(x, 0)
x = append(x, 1)
x = append(x, 2)
y := append(x, 3)
z := append(x, 4)
fmt.Println(y, z)
}
func main() {
a()
}
Ответ
func a() { x := []int{} x = append(x, 0) // длина = 1, емкость = 2 x = append(x, 1) // длина = 2, емкость = 2 x = append(x, 2) // длина = 3, емкость = 4 y := append(x, 3) // длина = 4, емкость = 4 z := append(x, 4) // длина = 4, емкость = 4 fmt.Println(y, z) // [0, 1, 2, 3] [0, 1, 2, 4] }func main() { a() }
Вывод: [0, 1, 2, 3] [0, 1, 2, 4] Ко��да у вас недостаточно емкости в слайсе, чтобы добавить новый элемент, Go создает новый мас��и��, в два раза ����льше предыдущего, и копирует все элементы. Слайсы y и z обе ссылки на этот новый массив.
s := "test"
println(s[0]) // 116 (код ASCII для 't')
// Невозможно изменить символ в строке напрямую. Строки в Go неизменяемы.
// s[0] = "R" // Это вызовет ошибку компи��яции
var newS string = "R"
counter := 0
for _, item := range s {
counter++
if counter == 1 {
continue
}
newS = strings.Join([]string{newS, string(item)}, "")
}
println(newS) // Rest
Вопрос 9: Mysql. DevOps говорит, что в slowlog есть запрос, который выполняется дольше 10 секун����.
id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | PRIMARY | mc | NULL | ref | idx_manager_id_client_id_uindex | idx_manager_id_client_id_uindex | 1023 | const | 1 | 100 | Using where; Using index |
1 | PRIMARY | m | NULL | eq_ref | idx_user_id | idx_user_id | 1022 | bind.mc.client_id | 1 | 100 | Using where |
2 | DEPENDENT SUBQUERY | cdp | NULL | index | idx_client_id | idx_client_id | 1022 | NULL | 189480 | 20.61 | Using where |
Можно ли ускорить этот запрос? Запрос выбирает к��иентов определенного менеджера, у которых указано два этапа сделки
SELECT m.*
FROM members m
LEFT JOIN manager_clients mc on m.user_id = mc.client_id
WHERE mc.manager_id = '152734'
AND m.user_id IN (SELECT client_id
FROM client_deal_phases cdp
WHERE cdp.phase_id IN (45, 47)
GROUP BY client_id
HAVING count(client_id) = 2
);
Ответ
Анализ вывода EXPLAIN:В таблице mc используется индекс idx_manager_id_client_id_uindex, и только одна запись соответствует критериям фильтрации.
В таблице m используется индекс idx_user_id, и только одна запись соответствует критериям фильтрации.
Но главная проблема здесь - это DEPENDENT SUBQUERY для таблицы cdp. Это подзапрос выполняется дл�� каждой строки основного запроса. Для этого запроса используется индекс idx_client_id, и он возвращает 189480 строк, из которых только 20.61% проходят фильтрацию.
Рекомендации по оптимизации:
Избавьтесь от вложенного подзапроса, используя соединение (JOIN).
Используйте дополнительный индекс для столбца phase_id в таблице client_deal_phases для ускорения фильтрации.
Оптимизированный запрос:
SELECT m.*
FROM members m
JOIN manager_clients mc on m.user_id = mc.client_id
JOIN (
SELECT client_id
FROM client_deal_phases
WHERE phase_id IN (45, 47)
GROUP BY client_id
HAVING count(client_id) = 2
) cdp ON m.user_id = cdp.client_id
WHERE mc.manager_id = '152734';
Здесь мы используем внутренний запрос с группировкой, чтобы выбрать все client_id, которые имеют два этапа сделки, и затем присоединяем этот результат к основному запросу. Это позволит базе данных оптимизировать выполнение запроса, избегая многократного выполнения вложенного подзапроса.
ch := make(chan int)
go func() {
<-ch
}()
ch <- 1
Ответ
ch - является небуфери��ированным каналом, соответственно горутина заблокируется на шаге <-ch Разблокируется только после того как в канал будет отправлено значение 1.ch := make(chan int)
close(ch)
ch <- 1
Ответ
ch - является небуферизированным каналом, который закрывается после инициализации В закрытый канал писать ничего нельзя, можно только читать, соответственно при попытке что то туда записать будет паника.ch1 := make(chan chan int)
ch2 := make(chan int)
go func() {
ch2 <- 1
}()
ch1 <- ch2
val := <-<-ch1
Ответ
ch1 - является небуферизированным каналом, в который может быть направлен канал int ch2 - является небуферизированным каналом, в который могут быть направлены значени�� с типом int Соответственно в горутине в канал ch2 направляется значение 1 и он сразу блокируется Далее в ch1 направляется канал ch2 со значением 1 и в val происходит считывания данного значения = 1 val = 1Вопрос 4: Как вы можете узнать, закрыт ли канал, если при попытке чтения из закрытого канала возвращается нулевое значение?
Ответ
Можно использовать range при итерации по каналу, в этом случает если канал будет закрыт, то итерация по значениям прекратится Также можно использовать второе значение типа bool, которое возвращается при итерации по нему - false - канал закрыт, true - канал открытval, ok := <- ch1 if !ok { fmt.Println("Канал закрыт") } else { fmt.Println("Канал открыт") }Вопрос 5: Есть ли стандартный способ узнать, сколько элементов в настоящий момент находится в канале? Если нет, как бы вы обошли это ограничение?
Ответ
Функция len показывает количество элементо�� в к��налеКакие другие методы синхронизации горутин вы знаете?
counter := 0
var mu sync.Mutex
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
Ответ
Синхронизация достигается с помощью такого примитива синхронизации как mutex. Перед тем как что-то записать в переменную counter mutex блокируется и происходит запись в переменную counter После того как запись произошла mutex разблокируется и другой участок кода который ожидал открытия mutex может произвести запись в переменную Кроме того в Golang существуют WaitGroup, Каналы и atomic которые позволяют эффективно осуществлять синхронизациюКак бы вы решили проблему в этом коде?
ch := make(chan int)
ch <- 1
Ответ
Программа заблокируется так как канал небуферизированный и необходимо, чтобы кто-то прочитал значение из канала, чтобы программа пошла дальшеУлучшить ко�� можно через создание буферизированного канала, например:
ch := make(chan int, 1)
ch <- 1
В этом случае канал не заблокируется и программа продолжит работать. Канал будет заблокирован только когда в нем cap(ch) + 1 значений Второй вариант это поместить отправку значения в канал и надеятся что дальше его кто-то прочитает, но если нет, то может возникнуть утечка памяти
ch := make(chan int)
go func() {
ch <- 1
}
// возможно далее кто-то прочитает значение
Вопрос 8: Представьте, что у вас есть два канала ввода и один канал вывода. Как бы вы организовали чтение из обоих каналов ввода и отправку результатов в канал вывода?
in1 := make(chan int)
in2 := make(chan int)
out := make(chan int)
Ответ
Можно применить паттерн for-select ```go in1 := make(chan int) in2 := make(chan int) out := make(chan int) for { select { case val := <-in1: out <- val case val := <-in2: out <- val } } ``` Можно создать еще один канал, который будет буферизированным ```go in1 := make(chan int) in2 := make(chan int) out := make(chan int) cache := make(chan chan int, 2) cache <- in1 cache <- in2 out <-<- cache ```Вопрос 9: Каким образом можно определить, что канал был закрыт, если канал может передавать значения типа int, и значение 0 является допустимым значением в канале?
ch := make(chan int, 1)
ch <- 0
close(ch)
Ответ
Можно использовать второе значение типа bool, которое возвращается из канала - false - канал закрыт, true - канал открыт val, ok := <- ch1 if !ok { fmt.Println("Канал закрыт") } else { fmt.Println("Канал открыт") }Вопрос 10: Предположим, что у вас есть буферизированный канал с вместимостью 3, и вы хотите знать, сколько элементов в нем в данный момент. Как бы вы это сделали?
ch := make(chan int, 3)
ch <- 1
ch <- 2