Coder Social home page Coder Social logo

gosummary's Introduction

Оглавление 📖

  1. Общие вопросы
  2. Slices
  3. Maps
  4. Указатели
  5. Goroutines и Channels
  6. Работа со строками

Общие вопросы

Вопрос 1: Самый любимый вопрос и один из самых часто встречающихся?

Можете ли вы объяснить разницу между горутинами и потоками операционной системы?

Ответ Горутины (goroutines) в Go - это легковесные потоки выполнения, которые управляются рантаймом Go. Они разделяют общее адресное пространство и планируются внутри Go-процесса. В отличие от потоков операционной системы, горутины не имеют непосредственного доступа к системным ресурсам, что делает их более эффективными и управляемыми. Представьте, что горутины — это как Падаваны в мире Star Wars. Они молоды, гибки и могут выполнять множество заданий, управляемые Силой (рантаймом Go). Падаваны могут быстро и легко перемещаться, выполняя свои обязанности, используя Силу для совместной работы и взаимопомощи.

Потоки операционной системы — это как Джедаи. Они мудры, опытны и обладают большей мощью, но они не такие легковесные и подвижные, как Падаваны, и требуют больше ресурсов. Джедаи имеют доступ ко всем ресурсам Галактики (системе), но также им нужно управлять и защищать эти ресурсы.

Падаваны (горутины) делят между собой знания и Силу (общее адресное пространство), обучаясь и сражаясь вместе. Они планируют свои задания и тренировки вместе, работая как единое целое внутри Храма Джедаев (процесса Go).

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

Вопрос 2: Что такое пустой интерфейс в Go (interface{}) и в каких случаях он полезен?

Можете привести пример кода, где пустой интерфейс пригодился бы вам для обработки разных типов данных?

Ответ Пустой интерфейс (interface{}) в Go может представлять любой тип данных, так как он не имеет определенных методов. Он полезен в случаях, когда нужно работать с разными типами данных, например, при создании обобщенных функций. Пример использования пустого интерфейса: ```go func printValue(value interface{}) { fmt.Println(value) } func main() { printValue(42) // Можно передать целое число printValue("Hello") // Можно передать строку printValue(3.14) // Можно передать число с плавающей точкой } ```

Вопрос 3: Как бы вы реализовали в Go свой собственный примитив для управления памятью?

Можете предоставить код, который демонстрирует создание, выделение и освобождение памяти с использованием указателей.

Ответ В 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...) } ```

Вопрос 4: Объясни в общих чертах как работает Garbage Collector в Golang?

Ответ Давайте представим Garbage Collector (Сборщик Мусора) в Go, как Р2-D2 в мире Star Wars. Р2-D2 — умный и изобретательный дроид, способный решать множество задач и обладающий множеством функций. Дроид работает в двух режимах:

Фаза Mark (Фаза Разметки)

В этой фазе Р2-D2 (Сборщик Мусора) бегает по всей Галактике (памяти программы), исследуя все планеты (объекты в памяти). Каждую планету, на которой он обнаруживает жизнь (доступные объекты), он помечает специальным цветом.

Белый Цвет: Планеты (объекты), которые еще не исследованы, помечаются белым цветом.
Серый Цвет: Планеты, обнаруженные, но еще не полностью исследованные, помечаются серым цветом.
Черный Цвет: Полностью исследованные планеты помечаются черным цветом.

Фаза Sweep (Фаза Очистки)

После того как все планеты исследованы, Р2-D2 начинает фазу очистки. Он возвращается на все планеты, помеченные белым цветом — те, на которых жизнь не была обнаружена (неиспользуемые объекты), и освобождает их ресурсы для новой жизни (возвращая память операционной системе). Три-Цветная Маркировка

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

Заключение Таким образом, Сборщик Мусора в Go, подобно Р2-D2, тщательно и умно ухаживает за Галактикой, убеждаясь, что все ресурсы используются эффективно, и что ненужные и заброшенные планеты (объекты) могут быть очищены и восстановлены для будущих нужд Галактики (программы).

Вопрос 4: В какой момент рантайм решает запустить сборщик мусора?

Ответ Сборщик мусора (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% по сравнению с последним освобождением.

Вопрос 5: Какие недостатки есть у подхода Mark-and-Sweep?

Ответ 1. Паузы Подход "Mark-and-Sweep" может приводить к паузам в выполнении программы, поскольку он требует прохода по всем живым объектам. В некоторых системах, особенно тех, где время отклика критично, такие паузы могут быть неприемлемыми. 2. Фрагментация памяти После нескольких циклов сбора мусора память может стать фрагментированной, поскольку объекты, освобождаемые сборщиком мусора, могут быть разбросаны по всей куче. Это может сделать сложным выделение больших континуальных блоков памяти и привести к неэффективному использованию памяти. 3. Накладные расходы Процесс разметки и очистки требует дополнительных вычислительных ресурсов, что может снизить общую производительность системы, особенно в высоконагруженных приложениях. 4. Отслеживание корней Определение, какие объекты являются корневыми, может быть сложным и требовать дополнительной информации от компилятора или рантайма, что увеличивает сложность системы.

Вопрос 6: Вопросы которые касаются отладки приложения и могут принимать разную форму:

Как вы интегрируете 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)
}

Инструкции:

  1. Запустите вашу программу как обычно: go run main.go
  2. После завершения программы, у вас будет два файла профиля: cpu.pprof и mem.pprof.
  3. Используйте утилиту командной строки pprof для анализа этих файлов: go tool pprof cpu.pprof или go tool pprof mem.pprof.
  4. В интерактивной сессии pprof вы можете использовать команды вроде top, list или web для анализа профиля.

Пример анализа профиля CPU:

go tool pprof cpu.pprof

Далее, в интерфейсе pprof, введите:

top

Это покажет вам, где ваша программа тратит больше всего времени на выполнение CPU.

Пример анализа профиля памяти:

go tool pprof mem.pprof

Также, в интерфейсе pprof, введите:

top

Это покажет вам, где ваша программа использует больше всего памяти.

Визуализация Профилей:

Используйте команду web в интерфейсе pprof для создания графического представления вашего профиля. Это может помочь вам лучше понять, где происходят узкие места в вашем коде.

Вопрос 7: Что за параметры GOGC и GODEBUG, зачем они нужны и для чего?

Ответ `GOGC` и `GODEBUG` являются переменными окружения, которые можно задать для изменения поведения сборщика мусора и режима отладки в Go. Их можно задать перед запуском вашей программы Go, например, в командной строке, или внутри вашего кода, используя пакет `os` для установки переменных окружения.

1. Задание переменных окружения перед запуском программы

a) В командной строке:

export GOGC=200
export GODEBUG=gctrace=1
go run myprogram.go

На Windows:

set GOGC=200
set GODEBUG=gctrace=1
go run myprogram.go

b) В Dockerfile, если ваше приложение запускается в контейнере:

ENV GOGC 200
ENV GODEBUG gctrace=1

2. Задание переменных окружения внутри кода

Вы можете также установить эти переменные окружения программно, используя пакет os в Go:

package main

import (
	"os"
)

func main() {
	_ = os.Setenv("GOGC", "200")
	_ = os.Setenv("GODEBUG", "gctrace=1")

	// Ваш код
}

Примеры Значений:

  • GOGC=200: Запускает сборщик мусора, когда объем выделенной памяти увеличивается в 2 раза (200%) по сравнению с объемом памяти, который был освобожден после последней сборки мусора.
  • GODEBUG=gctrace=1: Включает трассировку сборщика мусора, выводя информацию о каждом цикле сборки мусора.

Обратите внимание:

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

Вопрос 8: Что такое middleware и router в Golang?

Ответ

Middleware

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.

Analogy:

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).

Router

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.

Analogy:

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.

Slices

Вопрос 1: Каковы значения len(s) и cap(s) после выполнения кода?

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)` = 4

Вопрос 2: Каковы будут значения base после выполнения кода?

base := []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}

Вопрос 3: Каковы будут len(original) и cap(original) после выполнения кода?

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.

Вопрос 5: Каким будет значение slices после выполнения кода?

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.

Вопрос 10: Что выведет этот код?

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: [Погулять с собакой]

Maps

Вопрос 1: Семантика нулевых мап. Что произойдет при выполнении следующего кода?

var m map[string]int
m["key"] = 42

Почему это происходит и как это исправить?

Ответ Map в Golang это референсный тип данных, что означает что default значение в данном случаем будет nil и под мапу не будет выделено место на куче. Мапа будет не инициализирована. Данное поведение точно такое же как у slice в Golang. При попытке записать значение, происходит паника, так как мы пытаемся разыменовать nil указатель. Это поведение очень похоже на поведение в языке Си, которое вызывает segfault. Чтобы исправить это необходимо инициализировать мапу черезе функцию make: m := make(map[string]int)

Вопрос 2: Ссылочная природа мап. Какой будет результат на выходе и почему?

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)
    }
}
Ответ Код является безопасным, удаление произойдет корректно.

Вопрос 4: Определение отсутствия ключа.

В 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.") } ```

Вопрос 5: Порядок Итерации.

В каком порядке ключи возвращаются при итерации по мапе с помощью цикла range? Гарантирован ли этот порядок?

Ответ Порядок не гарантирован, так как мапа хранит в себе значения в неупорядоченном виде.

Указатели

Вопрос 1: Что выведет следующий код?

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, строки часто хранятся в неизменяемых массивах байтов, и когда вы "изменяете" строку, вы на самом деле создаете новую строку, указывающую на другой участок этого массива или на другой массив. Но для большинства случаев можно думать о строках как о данных в куче, на которые ссылаются переменные на стеке.

Вопрос 2: Что содержится в i?

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

Вопрос 3: Указатель на Массив vs Указатель на Слайс?

Можно ли получить указатель на массив arr? А что насчет указателя на слайс s? Каковы их различия?

arr := [3]int{1, 2, 3}
s := arr[:]
Ответ Массив в Golang это value type, Слайс - reference type Таким образом, чтобы получить указатель на слайc fmt.Printf("%p", s) Указатель на массив fmt.Printf("%p", &arr)

Вопрос 4: Функции с передачей по указателю. Что будет выведено на экран?

Почему было решено использовать передачу по указателю в этом примере?

func modifyValue(x *int) {
    *x = 5
}

func main() {
    var num int = 2
    modifyValue(&num)
    fmt.Println(num)
}
Ответ На экран будет выведено 5, так как в функцию modifyValue передается указатель на ячейку памяти где храниться num Таким образом мы изменяем значение по адресу в котором хранится значение 2.

Вопрос 5: Как Go управляется с указателями в контексте сборки мусора?

Что может случиться, если у вас есть указатель на большой кусок памяти,на который никто не ссылается?

Ответ В 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) необходимо переместиться по указателю на следующий элемент в списке и взять значение

Вопрос 2: Будет ли напечатан ok?

func main() {
    defer func() {
        recover()
    }()
    panic("test panic")
    fmt.Println("ok")
}
Ответ Нет, `"ok"` не будет напечатано. Почему: Функция `panic` прекращает выполнение текущей функции и начинает распространять панику по стеку вызовов. Однако благодаря `defer` и `recover()` программа не завершится аварийно, но после panic `"ok"` уже не будет выполнено.

Вопрос 3: Исправь код, функция должна выводить:

// 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`, что ему нужно остановиться из-за тайм-аута по контексту.

Учитывая все вышеперечисленные моменты, следует

  1. Использовать канал с ограниченной емкостью.
  2. Закрыть канал errrCh
  3. Передать 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 может быть полезным в определенных ситуациях. Вот примеры, которые могут помочь понять, почему и когда это может быть полезно:

  1. Избежание блокировки горутины: Представьте ситуацию, где горутина пытается отправить сообщение в канал, но нет другой горутины, готовой принять это сообщение. Если канал не буферизован, отправляющая горутина заблокируется. С буферизованным каналом, горутина не будет заблокирована, пока есть свободное место в буфере.

    Пример: У вас есть система, которая отправляет логи через канал. Если обработчик логов временно занят и не может принять новые сообщения, буферизованный канал позволит продолжать добавлять логи в буфер, предотвращая блокировку отправляющих горутин.

  2. Производительность: Буферизованные каналы часто могут улучшить производительность, так как отправка и получение сообщений не требует немедленной синхронизации между горутинами.

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

  3. Ограничение ресурсов: Буферизованные каналы позволяют ограничивать количество обрабатываемых данных, что может быть полезно, чтобы избежать излишнего потребления памяти или других ресурсов.

    Пример: У вас есть сервис, который загружает изображения. Пользователи могут запросить загрузку сотен изображений одновременно. Буферизованный канал позволяет ограничивать количество одновременно обрабатываемых изображений, предотвращая перегрузку системы.

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

Если все еще непонятно, почему мы используем буферизированный канал, читайте дальше:

  1. Длительная блокировка может привести к таймаутам: Если у вас есть внешний сервис или клиент, который ожидает ответа от вашего приложения, длительная блокировка может привести к превышению времени ожидания.

Пример: Вы разрабатываете веб-сервер, который принимает запросы на обработку данных. Каждый запрос инициирует горутину, которая пытается отправить данные на обработку через канал. Если обработчик временно занят, горутина будет заблокирована, и клиент может столкнуться с таймаутом.

  1. Неэффективное использование ресурсов: Горутины, заблокированные из-за попытки отправки в канал, продолжают занимать системные ресурсы, даже если они не выполняют полезной работы.

Пример: Ваше приложение начинает тысячи горутин для обработки задач. Если все они блокируются из-за канала, это может привести к значительному потреблению памяти и ресурсов CPU.

  1. Опасность взаимной блокировки: Если и отправляющая, и принимающая горутины ожидают друг друга, это может привести к взаимной блокировке, и ваше приложение может "зависнуть".

Пример: Горутина A ожидает, пока горутина B прочитает из канала, чтобы продолжить работу. Одновременно горутина B ожидает, пока горутина A что-то сделает, прежде чем читать из канала. Обе горутины блокируются навсегда.

  1. Опеределение поведения приложения: В некоторых случаях блокировка может быть желаемым поведением, чтобы контролировать скорость обработки или для синхронизации задач.

РЕАЛЬНЫЙ ПРИМЕР: Давайте представим завод, на котором есть конвейерная лента для производства игрушек. Этот конвейер разделен на два этапа:

Этап А: Машина, которая делает детали для игрушек.
Этап В: Работник, который собирает игрушку из этих деталей.

Между этими этапами у нас есть корзина, в которую машина (Этап А) кладет детали, и из которой работник (Этап В) берет детали для сборки игрушки.

Взаимная блокировка на примере завода:

Представьте, что машина на Этапе А может работать только тогда, когда в корзине нет деталей. Она ждет, пока работник на Этапе В не заберет все детали из корзины. С другой стороны, работник на Этапе В может начать сборку только после того, как в корзине накопится определенное количество деталей.

Взаимная блокировка произойдет в ситуации, когда в корзине будет недостаточно деталей для работника на Этапе В, чтобы начать сборку, но и достаточно деталей, чтобы машина на Этапе А не могла продолжить свою работу. Оба этапа будут ждать друг друга, и производство остановится.

Таким образом, если система (или код) не предусматривает механизма разрешения такой блокировки или предотвращения ее возникновения, это может привести к тому, что весь процесс "зависнет".

Вопрос 6: Что выведет программа?

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 обе ссылки на этот новый массив.

Вопр���� 7: Что выведет этот код?

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 секун����.

Он отдал вам запрос и вы вызвали explain.

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, которые имеют два этапа сделки, и затем присоединяем этот результат к основному запросу. Это позволит базе данных оптимизировать выполнение запроса, избегая многократного выполнения вложенного подзапроса.

Goroutines и Channels

Вопрос 1: Блокируется ли этот код, или нет?

ch := make(chan int)
go func() {
    <-ch
}()
ch <- 1
Ответ ch - является небуфери��ированным каналом, соответственно горутина заблокируется на шаге <-ch Разблокируется только после того как в канал будет отправлено значение 1.

Вопрос 2: Что произойдет после выполнения этого кода?

ch := make(chan int)
close(ch)
ch <- 1
Ответ ch - является небуферизированным каналом, который закрывается после инициализации В закрытый канал писать ничего нельзя, можно только читать, соответственно при попытке что то туда записать будет паника.

Вопрос 3: Что содержится в переменной val после выполнения этого кода?

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 показывает количество элементо�� в к��нале

Вопрос 6: Каким образом следующий код будет синхронизирован, чтобы избежать состояние гонки?

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

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 которые позволяют эффективно осуществлять синхронизацию

Вопрос 7: Что произойдет при выпо��нении следующего кода и почему?

Как бы вы решили проблему в этом коде?

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
Ответ Можно реализовать это через функцию len, но есть нюанс что в этот же момент времени другой участок кода (горутина) считывает элемент из канала и на самом деле значений в канале уже меньше чем перед тем как мы запустили функцию

Работа со строками

Вопрос 1: Как бы вы конкатенировали большое количество строк эффективно, минимизируя накладные расходы связанные с выделением памяти?

Ответ Вариант 1 - я бы использовал strings.Builder Вариант 2 - посчитал количество байт которое необходимо конкатенировать, командой make создал слайс рун, и с помощью команды append добавил необъодимые строки, например так: concat = append(concat, []rune(str1), []rune(str2), []rune(str3))

Вопрос 2: Каковы различия между len и utf8.RuneCountInString при работе со строками, и в каких ситуациях вы бы использовали один метод вместо другого?

Ответ Различия состоят в том, что функция len возвращает количество байт, бывает так что некоторые символы занимают более одного байта, так например кириллические символы занимают два байта японские символы могут занимать и до 3 байт. RuneCountInString - возвращает количество рун в строке. Руна является алиасом для int32 и соответственно одна руна вмещает в себя до 4 байт информации. Это очень важно понимать когда мы хотим узнать количество символов в строке например, а не количество байт, которые они занимают.

Вопрос 3: Как в Go осуществляется сравнение строк, и что нужно учитывать при сравнении строк в разных языках и кодировках?

Ответ Можно воспользоваться функцией strings.Compare которая возвращает true или false в зависимости от схожести строк. Как я писал выше в зависимости от кодироки мы получаем соответствующее количество байт, которые занимают данные символы. Может случится так что str1 := "ab", a str2:= "ф" при этом len(str1) = 2 и len(str2) = 2, но при этом строки не равны между собой и сравнивать по длине их некорректно.

Вопрос 4: Расскажите, как вы бы извлекли подстроку из строки в Go. Как бы вы обработали строки, содержащие многобайтовые символы, такие как руны Unicode?

Ответ Можно воспользоваться функцией strings.Substring, которая вернет нужный нам результат. Можно воспользоваться вариантом преобразования строки в слайс рун и с помощью срезов получить нужную нам подстроку.

Вопрос 5: Объясните, как в Go преобразовать строку в число и число в строку, и как обрабатывать возможные ошибки при этих преобразованиях?

Ответ На ум приходит функция Atoi и функция Itoa, которые преобразовывают строку в число и число в строку соответственно.

Бонусный Вопрос: Как бы вы обрабатывали и изменяли строки для создания эффективного и безопасного веб-сервера, который может обрабатывать входные данные от пользователя и предотвращать атаки, такие как XSS и SQL инъекции?

gosummary's People

Contributors

hexarchy avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.