To-do

  • atomic, что такое и зачем нужны?
  • Как в памяти представляются числа c плавающей точкой в Go?
  • Можно ли использовать один и тот же буфер []byte в нескольких горутинах?
  • Что такое string builder?
  • Что возвращает []?
  • Как проверить тип интерфейса?
  • Будет ли работать type assertion с generics?
  • new vs make

Общие

Зачем могут потребоваться {} дополнительные?

  • Объявить локальные переменные там
  • Помочь компилятору быстрее скомпилировать (там дерево)

Замыкание (closure), зачем нужны?

GMP паттерн

Сборщик муссора

Когда срабатывает?

Когда в программе не осталось ссылки на объект.

default в select

Если ничего не пришло в канал, то запуск дефолт кейса.

Типы данных

Базовые типы данных

  • Integers ( Signed and Unsigned):
  • Floating
  • Complex
  • Byte
  • Rune
  • String
  • Boolean

Чем отличается int от uint?

int содержит диапазон от отрицательных значений до положительных, тогда как uint - это диапазон от 0 в строну увеличения положительных значений.

Какой результат получим если разделить int на 0 и float на 0?

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

Что такое руна?

Руна является псевдонимом для типа int32 и обычно используется для хранения символов в Unicode.

Что такое iota?

Это идентификатор, который позволяет создавать последовательные не типизированные целочисленные константы.

const (
	C1 = iota + 1
	C2
	C3
)
fmt.Println(C1, C2, C3) // "1 2 3"

Composite Data Types

  • Non-Reference Types: Arrays, Structs
  • Reference Types: Slices, Maps
  • Interface

Выравнивание памяти (struct padding)

Порядок типов в структуре влияет на объем выделенной памяти. Чтобы оптимизировать порядок в структуре нужно указывать объем в обратном порядке. Также можно использовать специальные утилиты:

Slice

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

Slice – это структура go, которая включает в себя ссылку на базовый массив, а также две переменные len(length) и cap(capacity).

  • len это длина слайса - то количество элементов, которое в нём сейчас находится.
  • cap - это ёмкость слайса - то количество элементов, которые мы можем записать в слайс сверх len без его дальнейшего расширения.
  • Array - это последовательно выделенная область памяти. Частью типа array является его размер, который в том числе является не изменяемым.
Лучшие практики
  • слайс логично проверять на пустоту с помощью len() == 0
var list []int // true
list = []int{} // false
  • По возможности, аллоцировать память для слайса
  • Если хотим изменить слайс, создаём его копию
  • Результат append присваивать той же переменной

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

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

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

Если количество добавляемых элементов превысит cap, то вернется новый слайс, базовым для которого будет новый массив.

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

func append(slice []Type, elems ...Type) []Type

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

Если отвечать на вопрос поверхностно, то можно сказать, что базовый массив расширяется в два раза от нашей capacity.

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

Если развернуть ответ полностью, то это будет звучать примерно так:

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

Как слайс передается в функцию: по ссылке или значению?

Длина и вместимость передаются по значению, но массив значений передается по ссылке.

Это означает, что если вы измените элементы в слайсе внутри функции, эти изменения будут видны и в вызывающей функции.

Однако если вы измените сам слайс (например, используя append так, что ему потребуется новое место в памяти), это не повлияет на исходный слайс в вызывающей функции.

Reference: GoLang Slice в деталях, простым языком - YouTube

Map в Golang

Сама  map  в go - это структура, реализующая операции хеширования. При этом, так же как и любую структуру, содержащую ссылки на области памяти, map  необходимо инициализировать.

map ссылается на такие элементы как bucket. Каждый bucket содержит в себе:

  • 8 экстра бит, с помощью которых осуществляется доступ до значений в этом bucket;
  • ссылку на следующий коллизионный bucket;
  • 8 пар ключ-значение, уложенных в массив.

Бакет увеличивается, когда заполняется на 6.5 элементами.

Как на самом деле устроен тип Map в Golang? | Golang под капотом - YouTube

Коллизия в map

Так как хэш-функция не идеальна, передав в нее два разных значения мы можем получить один и тот же результат. В случае с бакетами нам нужно два разных значения положить в один и тот же бакет. Это называется коллизией. Для реализации hashmap необходимо иметь алгоритм их разрешения.

В golang используется separate chain.

При коллизии (когда сложили в один бакет), то обходим бакет в поисках ключа.

Зачем аллоцировать память через make?

Почему нельзя взять указатель на элемент map?

  1. Расширение мапы поломает значение.
  2. Дизайн сделан через пару ключ-значение
  3. Непотокобезопасная структура.

Что может быть ключём map?

  • int, float64, rune, string, comparable array and structure, pointer, etc.

Структура может быть ключём до тех пор пока не в полях не будет мапы/слайса.

Что будет в map, если не делать присвоение?

Будет паника.

Что такое Sync.map?

Предоставляет атомарный доступ.

Потокобезопасна ли map?

Нет. Для этого нужно:

  1. Заворачиваем в мьютекс.
  2. Sync.Map

Что такое эвакуация, и в каком случае она будет происходить?

Эвакуация – это процесс когда map переносит свои значения из одной области памяти в другую. Это происходит из-за того что число значений в каждом отдельном bucket максимально равно 8.

В тот момент времени, когда среднее количество значений в bucket составляет 6.5, go понимает, что размер map не удовлетворяет необходимому. Начинается процесс расширения map.

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

Что происходит с мапой при передаче через канал?

Когда вы передаете карту (map) через канал в Go, вы фактически передаете ссылку на эту карту, а не ее копию. Это означает, что получатель этой карты из канала будет иметь доступ к той же самой карте в памяти, что и отправитель.

Особенности

  1. Передача по ссылке: Когда вы отправляете мапу через канал, вы передаете указатель на эту мапу. Это означает, что как отправитель, так и получатель будут работать с одной и той же картой в памяти.

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

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

  4. Закрытие канала: Передача мапы через канал не влияет на жизненный цикл самой карты. Даже если канал будет закрыт, карта останется доступной для дальнейшего использования.

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

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

Почему не гарантирован порядок обхода?

  • Для того, чтобы разработчик не рассчитывал на нее. Для этого требуется лишняя структура, которая бы хранила этот порядок → замедление производительности
  • Для безопасности.

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

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

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

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

Может ли slice быть быстрее map в переборе?

Конструкция слайса такова, что нет указателей на предыдущей или следующий элемент, что позволяет использовать кэши PCA.

Как словить race condition?

Есть флаг race condition – `$ go run -race mysrc.go

Как реализовано ООП в go?

В go нет классической реализация ООП, так как он не объектно-ориентированный язык. При этом в go есть свои приближения к этой реализации.

Наследование

Есть структуры – это специальные типы, в которые мы можем включать другие типы, в том числе такие же структуры. При этом методы дочерних структур родительская структура также будет наследовать.

Composition

Embedding

Что будет, если и в родительской и дочерней структуре есть реализация методов с одинаковым названием? – Реализация родительского метода будет переписана реализацией дочернего метода

type Parent struct{}
 
func (c *Parent) Print() {
	fmt.Println("parent")
}
 
type Child struct {
	Parent
}
 
func (p *Child) Print() {
	fmt.Println("child")
}
 
func main() {
	var x Child
 
	x.Print()
}

Child

Инкапсуляция

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

Полиморфизм

Полиморфизм в go реализован с помощью интерфейсов.

Операторы в Go

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

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

Интерфейсы в Golang

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

Интерфейс – контракт, что тот или иной объект будет реализовывать указанное в интерфейсе поведение.

Нужны для:

  1. Чтобы можно было использовать моки
  2. Уменьшение дублирование кода
  3. Для реализации полиморфмизма. То есть когда разные типы данных, но у метода схожее поведение.

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

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

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

Интерфейсное значение считается “нилом”, если и его тип, и его значение равны nil.

var x interface{}

Но интерфейсное значение, которое содержит указатель на некоторый тип, который в свою очередь равен nil, не считается nil интерфейсом!

var a *int = nil
var i interface{} = a

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

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

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

var i interface{} = "hello" 
s, ok := i.(string) 
if ok {
	fmt.Println(s)
} else {
	fmt.Println("Not a string")
}

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

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

Инструкция defer

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

В Go есть ключевое слово defer, которое позволяет отложить выполнение метода до выхода их функции, а также для того чтобы ловить паники через recover.

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

Аргументы функций, перед которыми указано ключевое слово defer оцениваются немедленно. То есть на тот момент, когда переданы в функцию.

Конкуретность

Горутины

Горутина (goroutine) — это функция, выполняющаяся конкурентно с другими горутинами в том же адресном пространстве. Легковесны, потому что они управляются рантаймом языка, а не операционной системой.

Преимущества горутин

  1. Они легковесны. Их называют легковесными потоками, потому что они управляются рантаймом языка, а не операционной системой. Стоимость переключения контекста и расход памяти намного ниже, чем у потоков ОС.
  2. Легко и без проблем масштабируют.
  3. Требуют меньше памяти (2KB).

Каждая Машина работает в отдельном потоке и способна выполнять только одну горутину в момент времени.

Что происходит при написании go?

  1. runtime.newproc(fn):
    1. Получаем текущий процессор и текущую горутину
    2. Создаем новую горутину
    3. Вешаем нашу целевую функцию на горутину
    4. Добавить горутину в массив горутин Ничего не запускается!
  2. schedule()
    1. Проверяем нужно ли выполнить сборку мусора.
    2. Берем горутину из нашей текущей очереди на нашем треде.
    3. Если в текущей очереди на нашем треде ничего нету, то мы либо “крадем” горутину из других тредов либо ждем.

Как завершить много горутин?

Несколько методов:

  1. Использование канала для завершения: Один из распространенных способов - это использовать канал для уведомления горутин о завершении. Вы можете создать канал и передать его каждой горутине. Когда горутина завершается, она может отправить сообщение в канал. Главная горутина может ожидать, когда все горутины завершатся, считывая из канала.

     func worker(quit chan struct{}, id int) {
         for {
             select {
             case <-quit:
                 fmt.Printf("Worker %d exiting\n", id)
                 return
             default:
                 fmt.Printf("Worker %d working\n", id)
                 time.Sleep(1 * time.Second)
             }
         }
     }
     
     func main() {
         quit := make(chan struct{})
         for i := 0; i < 5; i++ {
             go worker(quit, i)
         }
     
         time.Sleep(3 * time.Second)
         close(quit) // This will signal all the workers to exit
         time.Sleep(2 * time.Second) // Give workers time to exit
     }
  2. Использование контекста для отмены: Вы также можете использовать контекст для управления завершением горутин. Вы создаете контекст и передаете его каждой горутине. Когда вы хотите завершить все горутины, вы отменяете контекст, и горутины могут проверить отмену и завершить свою работу.

     func worker(ctx context.Context, id int) {
         for {
             select {
             case <-ctx.Done():
                 fmt.Printf("Worker %d exiting\n", id)
                 return
             default:
                 fmt.Printf("Worker %d working\n", id)
                 time.Sleep(1 * time.Second)
             }
         }
     }
     
     func main() {
         ctx, cancel := context.WithCancel(context.Background())
         for i := 0; i < 5; i++ {
             go worker(ctx, i)
         }
     
         time.Sleep(3 * time.Second)
         cancel() // This will signal all the workers to exit
         time.Sleep(2 * time.Second) // Give workers time to exit
     }

Горутины и потоки разница

ПотокиГорутины
Потоки ОС управляются ядром ОСГорутины управляются “рантаймом” Go
Потоки ОС в основном имееют фиксированый размер в 1-2MBГорутины обычно имеют размер стэка 2KB
Размер стэка определяется во время компиляции и не может увеличиватсяРазмер стэка определяется во время рантайма и может расти вплоть до 1GB что возможно благодаря аллокации и освобождения места из хипа
У потоков нет простого способа коммуникации между собой. Такая коммуникация имеет большую задержкуГорутины используют “каналы” для быстрого общения между собой с маленькой задержкой

Scheduler

  • М - Машина
  • G – Горутина
  • P - Процессор

Цель планировщика (scheduler) в том, чтобы распределять готовые к выполнению горутины (G) по свободным машинам (M).

Готовые к исполнению горутины выполняются в порядке очереди, то есть FIFO (First In, First Out). Исполнение горутины прерывается только тогда, когда она уже не может выполняться: то есть из-за системного вызова или использования синхронизирующих объектов (операции с каналами, мьютексами и т.п.).

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

Как работает планировщик?

Внутри функции schedule():

  1. Проверяем нужно ли выполнить сборку мусора.
  2. Берем горутину из нашей текущей очереди на нашем треде.
  3. Если в текущей очереди на нашем треде ничего нету, то мы либо “крадем” горутину из дргуих тредов либо ждем.

golang-scheduler

Что такое deadlock?

Как планировщик определяет deadlock?

  1. Проверка активных горутин: Если все горутины находятся в состоянии ожидания и нет активных горутин, которые могли бы выполнить работу, среда выполнения Go определяет это как deadlock. В таком случае Go паникует и выводит сообщение о дедлоке.

  2. Ожидание главной горутины: Если главная горутина завершается, но другие горутины продолжают работать или ожидают (например, на блокировке мьютекса или канале), Go также считает это дедлоком.

  3. Ожидание на глобальных блокировках: Если горутина ожидает глобальную блокировку (например, блокировку пакета sync), и нет других горутин, которые могли бы освободить эту блокировку, Go также определяет это как дедлок.

Многозадачность в Golang

Теперь как в ОС – не кооперативная (вытесняющая) многозадачность.

Подробнее о многозадачности – Многозадачность и её виды

Состояния Горутин

Точно так же, как потоки, у горутин есть те же три состояния высокого уровня. Они определяют роль, которую планировщик Go играет с любой горутиной. Горутина может находиться в одном из трех состояний:

  • Ожидание: это означает, что горутина остановлена и ждет чего-то, чтобы продолжить. Это может происходить по таким причинам, как ожидание операционной системы (системные вызовы) или синхронизация вызовов (атомарные и мьютексные операции). Эти типы задержек являются основной причиной плохой производительности.
  • Готовность: это означает, что горутина хочет получить время, чтобы выполнить назначенные инструкции. Если у вас много горутин, которым нужно время, то горутине придется ждать дольше, чтобы получить время. Кроме того, индивидуальное количество времени, которое получает любая горутина, сокращено, поскольку больше горутин конкурируют за время. Этот тип задержки планирования также может быть причиной плохой производительности.
  • Выполнение: это означает, что горутина была помещена в M и выполняет свои инструкции. Работа, связанная с приложением, завершена. Это то, что все хотят.

Могут ли горутины воровать у друг друга задачи?

Да, могут.

Ограничения планировщика

  1. LIFO
  2. Нету гарантии времени выполнения.
  3. Горутины перемещаются между тредами – снижение эффективности кэшей.

Каналы

Что такое канал?

Виды:

  • Буферизированный (кидаем пока есть пространство)
  • Небуферизированный (сразу лочится)

Как устроены каналы под капотом?

type hchan struct {
	qcount   uint           // total data in the queue
	dataqsiz uint           // size of the circular queue
	buf      unsafe.Pointer // points to an array of dataqsiz elements
	elemsize uint16
	closed   uint32
	elemtype *_type // element type
	sendx    uint   // send index
	recvx    uint   // receive index
	recvq    waitq  // list of recv waiters
	sendq    waitq  // list of send waiters
 
	// lock protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
	//
	// Do not change another G's status while holding this lock
	// (in particular, do not ready a G), as this can deadlock
	// with stack shrinking.
	lock mutex
}
 
type waitq struct {
	first *sudog
	last  *sudog
}

Буферизированный канал

Обычно каналы работают синхронно - каждая из сторон ждёт, когда другая сможет получить или передать сообщение. Но буферизированный канал работает асинхронно — получение или отправка сообщения не заставляют стороны останавливаться. Но канал теряет пропускную способность, когда он занят, в данном случае, если мы отправим в канал 1 сообщение, то мы не сможем отправить туда ещё одно до тех пор, пока первое не будет получено.

WaitGroups

Это механизм синхронизации в языке программирования Go (Golang), который позволяет дождаться завершения выполнения нескольких горутин (concurrent goroutines) перед продолжением выполнения основной программы.

WaitGroup имеет три основных метода:

  1. Add(delta int): Инкрементирует счетчик WaitGroup на значение delta. Обычно используется перед запуском каждой горутины.
  2. Done(): Декрементирует счетчик WaitGroup на 1. Обычно вызывается в конце выполнения каждой горутины, чтобы указать, что она завершила свою работу.
  3. Wait(): Блокирует выполнение программы до тех пор, пока счетчик WaitGroup не станет равным нулю. Этот метод используется в основной горутине, чтобы ожидать завершения всех других горутин.

Graceful shutdown

Корректное и безопасное завершение работы программы или сервиса, позволяя всем активным операциям завершиться перед выходом. Это особенно важно для серверных приложений, чтобы избежать потери данных или состояния.

Можно создать канал, который будет принимать значения SIGINT (Ctrl+C) или SIGTERM (посылаются при выполнении команды kill). И при получении его server.shutdown().

Что такое захват переменной?

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

Где используется контекст?

  1. Таймауты
  2. Отмена операций (напр., горутин)

Generics

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

То есть можно писать функции для разных типов данных. Это как шаблоны в С++.

Объявление переменных – разница

Map

Если объявить так: var m map[string]int и потом присвоить значение, то будет паника.

Slice

Если в пустой слайс загонять через append, то все нормально. Если в пустой слайс, где нет еще capacity, то будет паника.

Мьютексы

Механизм синхронизации, который используется для обеспечения доступа к общим данным из нескольких горутин.

Два основных:

  1. sync.Mutex: Это классический мьютекс для обеспечения взаимного исключения. Он блокирует доступ к общим данным, позволяя только одной горутине захватить его одновременно.
  2. sync.RWMutex: Это мьютекс с поддержкой чтения/записи (read-write lock). Он позволяет нескольким горутинам читать данные одновременно или одной горутине писать.

Escape Analysis, Стэк, Куча

Info

Understanding Allocations in Go: the stack, the heap, allocs/op, trace, and more | Eureka Engineering

Escape analysis (анализ утечек) — это компиляторный процесс в Go (или Golang), который определяет, где должны размещаться переменные: на стеке или в куче.

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

Escape analysis помогает компилятору Go принимать решения о том, где размещать переменные. Если компилятор определяет, что переменная “утекает” за пределы своей области видимости, он размещает ее в куче.

Чтобы увидеть результат escape analysis компилятора Go, вы можете скомпилировать программу с флагом -gcflags='-m':

go build -gcflags='-m' main.go

Зачем нужен он нужен?

  1. Оптимизация производительности: Понимание того, когда и почему переменные размещаются в куче, может помочь в оптимизации производительности. Переменные, размещенные в стеке, обычно создаются и уничтожаются быстрее, чем те, что в куче. Кроме того, чем меньше объектов вы размещаете в куче, тем меньше работы для сборщика мусора.
  2. Понимание ошибок: В некоторых случаях неправильное понимание того, где переменная размещается, может привести к ошибкам. Например, если вы возвращаете указатель на локальную переменную из функции, ожидая, что она будет размещена в куче, но на самом деле она размещена в стеке, это может привести к неопределенному поведению.
  3. Оптимизация использования памяти: Если вы работаете с ограниченными ресурсами или хотите минимизировать использование памяти, понимание escape analysis может помочь вам принимать решения о структуре вашего кода.

Стек и Куча

Стек характеристики

  • Быстрый: Доступ к данным на стеке обычно быстрее, чем к данным в куче.
  • Локальные переменные: Локальные переменные функций обычно размещаются на стеке.
  • Автоматическое управление: Когда функция завершается, все ее локальные переменные автоматически освобождаются. Это делает управление памятью на стеке очень эффективным.
  • Ограниченный размер: Стек имеет ограниченный размер. Если ваша программа использует слишком много памяти на стеке (например, из-за глубокой рекурсии), это может привести к ошибке переполнения стека.
  • LIFO (Last-In-First-Out): Данные добавляются и удаляются из стека в порядке LIFO.

Куча характеристики

  • Динамическое выделение: Когда вам нужно динамически выделить память (например, для массива, размер которого определяется во время выполнения), эта память выделяется в куче.
  • Глобальные данные: Глобальные переменные и данные, которые должны существовать вне вызовов функций, обычно размещаются в куче.
  • Управление сборщиком мусора: В Go куча управляется сборщиком мусора, который автоматически освобождает неиспользуемую память.
  • Медленнее стека: Выделение и освобождение памяти в куче обычно медленнее, чем на стеке.
  • Больший размер: Куча обычно имеет гораздо больший размер, чем стек, и может расти в зависимости от доступной системной памяти.

В Go, как и во многих современных языках программирования, большинство решений о том, где размещать данные (на стеке или в куче), принимаются компилятором. Компилятор Go использует “escape analysis” для определения, должны ли переменные размещаться на стеке или в куче. Если компилятор определяет, что переменная “убегает” из своей локальной области видимости, он размещает ее в куче.

Sharing up vs. Sharing down:

Передача указателей вверх по стеку может привести к выделению памяти в куче, в то время как передача указателей вниз по стеку, как правило, этого не делает

  • Sharing up: Когда вы передаете указатель “вверх” по стеку (например, возвращая указатель из функции), компилятор может решить, что переменная должна быть размещена в куче, чтобы избежать ошибок висячих указателей.

    Если функция B() возвращает указатель на свои локальные данные функции A(), это называется “делиться вверх по стеку”. Это проблематично, потому что когда B() завершится, ее данные будут удалены со стека, но A() все еще будет иметь указатель на эти данные. Чтобы избежать проблем, компилятор Go выделяет эти данные в куче, а не на стеке.

    +-----+ | A | имеет указатель на данные B после завершения B +-----+ | B | верх стека (но завершилась и ее данные убраны) +-----+ | .…. |

  • Sharing down: Когда вы передаете указатель “вниз” по стеку (например, передавая указатель в функцию как аргумент), переменная может оставаться в стеке, так как ее жизненный цикл явно ограничен областью видимости вызывающей функции.

    Если функция A() передает указатель на свои данные функции B(), это называется “делиться вниз по стеку”.

    +-----+ | B | верх стека +-----+ | A | +-----+ | ..… |

Зачем использовать в структуре указатель?

Тестирование

….

Посмотреть позже