Библиотека Go для собеса | вопросы с собеседований
6.88K subscribers
223 photos
7 videos
1 file
433 links
Вопросы с собеседований по Go и ответы на них.

По рекламе: @proglib_adv

Учиться у нас: https://proglib.io/w/0b524a15

Для обратной связи: @proglibrary_feeedback_bot

Наши каналы: https://t.me/proglibrary/9197
Download Telegram
💬Что делает ключевое слово defer в Go?

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

🔸Закрытие ресурсов. Один из самых распространенных примеров использования defer — убедиться, что ресурсы, такие как файлы, сетевые подключения или соединения с базой данных, будут закрыты после их использования.

file, err := os.Open("file.txt")
if err != nil {
//обработка ошибки
}
defer file.Close()


🔸Множественные отложенные вызовы: мы можем использовать несколько операторов defer в одной функции. Они будут выполнены в порядке LIFO.

func example() {
defer fmt.Println("1")
defer fmt.Println("2")
fmt.Println("Function body")
}


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

func example(a int) {
defer fmt.Println(a)
a *= 2
return
}
example(5) //5

🔸Использование с паникой: defer часто используется совместно с recover(), чтобы обрабатывать или логировать панику, которая может произойти в функции.

func mightPanic() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
//код, который может вызвать панику
}


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

🔸Затраты производительности: хотя ключевое слово defer удобно и безопасно, использование его внутри интенсивных по производительности циклов может вызвать незначительные, но всё же заметные накладные расходы.
👍62
💬Массивы и срезы в Go: продолжение

❗️Важно иметь представление о том, как работают массивы и срезы в Go, так как это позволяет эффективно управлять памятью и предотвращать ошибки.

🔸Внутренняя структура и работа

• Массивы в Go имеют фиксированный размер. Элементы массива располагаются последовательно в памяти. Размер массива является его частью, так что массивы [3]int и [4]int — это разные типы.
• Срезы — более гибкая структура данных, которая представляют последовательность элементов одного типа переменной длины. Срез состоит из трех компонентов: указателя на начало среза в массиве, длины среза и capacity, которая показывает, сколько элементов может содержать срез от начальной позиции до конца массива.

🔸Операции над массивами и их сложность

• Вставка или удаление элемента в массиве Go требует создания нового массива, так как размер массива неизменен. Однако для срезов можно использовать встроенные функции append и copy для добавления и удаления элементов.

🔸Срезы

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

🔸Проблемы и оптимизации

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

🔸Многомерные массивы

• В Go можно создать многомерные массивы, например, var matrix [3][3]int. Точно так же можно работать с многомерными срезами.

🔸Особенности в Go

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

• Функция make часто используется для создания срезов с заданной начальной длиной и capacity.
3👍1
💬Чем отличается make от new в Go?

Хотя Go предоставляет сборщик мусора, который автоматически очищает неиспользуемую память, нам все равно требуется выделять память для новых переменных. Go предоставляет две встроенные функции для этой цели: new и make.

🔸Функция new(T) выделяет память для новой переменной типа T, инициализирует её нулевым значением этого типа и возвращает адрес этой переменной (т. е. указатель на T). Например:

p := new(int) // p — это указатель на int, инициализированный нулем

• В этом примере p будет указателем на новый int, который был инициализирован значением 0.

🔸Функция make используется для инициализации срезов, типа map и каналов, которые являются встроенными типами данных в Go, требующими дополнительной инициализации перед использованием.

Срезы: когда мы создаем срез с помощью make, мы задаем начальную емкость и размер. Это позволяет Go предварительно выделить необходимую память.

s := make([]int, 5, 10)

map: используя make, мы можем инициализировать тип map перед добавлением элементов.

m := make(map[string]int)

Каналы: make также используется для создания каналов.

ch := make(chan int)

📌Основное различие:

new возвращает указатель на тип, инициализированный нулевым значением этого типа.
make возвращает инициализированный (не нулевой) экземпляр встроенного типа (срез, map или канал).
В большинстве случаев, когда разработчикам требуется выделить память для новой переменной, они используют литералы типа или make.
Функция new используется реже, и чаще всего она применяется в более специфичных случаях.
👍15
💬Что такое embedding в Go?

📌В Go нет наследования в традиционном смысле ООП, но есть механизм, который позволяет добиться похожего эффекта — это embedding. Embedding позволяет одному типу включать в себя поля и методы другого типа без явного наследования.

Простота: embedding очень прост в использовании — просто определите один тип внутри другого.
Композиция вместо наследования: вместо того чтобы наследовать методы и поля, Go предпочитает композицию, где один тип может включать в себя другой, дополняя его функциональностью.
Поведение и интерфейсы: если встроенный тип реализует определенный интерфейс, то и тип, в который он встроен, автоматически реализует этот интерфейс.

📌Рассмотрим на примере:

type Engine struct {
Power int
Type string
}

type Car struct {
Engine
Brand string
Model string
}

func main() {
c := Car{
Engine: Engine{Power: 150, Type: "Petrol"},
Brand: "Ford",
Model: "Fiesta",
}
fmt.Println(c.Power)
}


📌Пример со встроенными методами:

type Writer interface {
Write([]byte) (int, error)
}

type Logger struct {
Writer
}


👉 Теперь Logger автоматически реализует интерфейс Writer, но только в том случае, если его встроенное поле Writer также реализует методы этого интерфейса.

📌Важно:

🔸Имена полей и конфликты: если встроенный и внешний типы имеют поля или методы с одинаковыми именами, приоритет будет у внешнего типа.
🔸Неявное поведение: одним из возможных «подводных камней» является то, что методы встроенного типа становятся частью внешнего типа, что может быть не всегда очевидным при чтении кода.
🔸Интерфейсы и встраивание: в Go можно также встраивать интерфейсы, что позволяет создавать сложные интерфейсы на основе уже существующих.
👍81
💬Какие изменения в Go 1.21 коснулись срезов?

🔥В Go 1.21 в стандартную библиотеку добавили пакет slices, который значительно облегчил работу со срезами. Вот только некоторые новые функции:

🔸Поиск элемента в срезе: slices.Contains(s, v)

🔸Сортировка (для любых ordered типов): slices.Sort(s)

🔸Поиск максимума: maxVal := slices.Max(s)

🔸Удаление элемента:

letters := []string{"a", "b", "c", "d", "e"}
letters = slices.Delete(letters, 1, 4)


🔸Сравнение двух срезов: areEqual := slices.Compare(a, b)

🔸Compact — заменяет последовательные серии одинаковых элементов одной копией — как команда uniq в *nix.

seq := []int{0, 1, 1, 2, 3, 5, 8}
seq = slices.Compact(seq)


🔸Equal — проверяет, равны ли два среза: одинаковая длина и все элементы равны.

numbers := []int{0, 42, 8}
fmt.Println(slices.Equal(numbers, []int{0, 42, 8}))
// true

🔸Clip — удаляет неиспользуемую емкость из среза, возвращая s[:len(s):len(s)].

list = slices.Clip(list)

🔸Clone — возвращает копию среза.

🔸BinarySearch — ищет элемент в отсортированном срезе и возвращает позицию, где элемент найден, или позицию, где он появится в порядке сортировки.

names := []string{"Alice", "Bob", "Vera"}
n, found := slices.BinarySearch(names, "Vera")


Подробнее:
🔗 Release notes & pkg.go.dev & Ardan Labs
Please open Telegram to view this post
VIEW IN TELEGRAM
👍32
💬Какие основные отличия горутины от thread (потока) в Go

1⃣Уровень абстракции:
Горутины — это абстракции уровня языка, предоставляемые Go. Они позволяют выполнять функции или методы конкурентно.
Потоки — это более традиционные сущности операционной системы для параллельного выполнения задач.

2⃣Размер стека:
Горутины начинаются с очень маленького стека, который может динамически расти и сокращаться в зависимости от потребности (обычно начинается с 2KB).
Потоки, в зависимости от ОС, обычно имеют стек фиксированного размера, который может быть значительно больше (обычно от 1MB и выше).

3⃣Создание и переключение:
Горутины легко создать (просто используя ключевое слово go перед вызовом функции), и они дешевы в плане создания и переключения контекста.
Потоки дороже по стоимости создания и контекстного переключения, так как это требует прямого взаимодействия с операционной системой.

4⃣Планировщик:
Горутины управляются планировщиком Go, который работает в пользовательском пространстве (user space) и распределяет горутины по доступным ОС потокам (обычно один поток на ядро CPU).
Потоки управляются планировщиком ОС.

5⃣Изоляция:
• Ошибка в одной горутине (например, паника) может повлиять на все другие горутины в той же программе.

👀 Выше представлен пример создания горутины и потока в Go. Мы создаем горутину с помощью ключевого слова. Затем, меняя GOMAXPROCS, мы фактически заставляем Go использовать дополнительный поток ОС, что делает выполнение кода более похожим на многопоточное.
Please open Telegram to view this post
VIEW IN TELEGRAM
14👍6🔥1
💬Что из себя представляет паттерн «Одиночка» (Singleton) и как его реализовать на Go

Одиночка — это порождающий паттерн, который гарантирует существование только одного объекта определённого класса/типа и предоставляет к нему глобальную точку доступа.

В первом примере для гарантии создания одного экземпляра структуры используются мьютексы (sync.Mutex). Мьютекс блокирует выполнение кода так, чтобы только одна горутина могла его выполнять в данный момент времени.

📌Важно:

• В начале нужна nil-проверка, с ее помощью мы убеждаемся, что первый экземпляр — пустой. Благодаря этому мы можем избежать ресурсоемких операций блокировки при каждом вызове obtainUnique. Если эта проверка не пройдена, тогда поле uniqueItem уже заполнено.
• Структура uniqueItem создается внутри блокировки.
• После блокировки используется еще одна nil-проверка. В случаях, когда первую проверку проходит более одного потока, вторая обеспечивает создание экземпляра одиночки единым потоком. В противном случае, все потоки создавали бы свои экземпляры структуры одиночки.

📌Существуют и другие методы создания экземпляра одиночки в Go.

1⃣Функция init: Go предоставляет функцию init, которая вызывается автоматически при инициализации пакета. Мы можем использовать эту функцию для создания экземпляра одиночки.

2⃣sync.Once: Этот метод гарантирует, что функция будет выполнена только один раз, даже если она будет вызвана из нескольких горутин.
👍12🔥5
💬Что такое контекст (context) в Go и для чего он применяется?

Context в Go — это специальный пакет, предназначенный для передачи параметров между API и управления жизненным циклом горутин.

Основное его назначение — передача метаданных, установка временных рамок выполнения и отслеживание отмены долгосрочных операций.

📌Основные моменты:

🔹Context введен в Go 1.7 и с тех пор является предпочтительным механизмом для управления временем выполнения и отменами.
🔹Context Interface: интерфейс context.Context является основным типом, который вы передаете между функциями.
🔹Основные методы: WithCancel, WithDeadline, WithTimeout и WithValue.

WithCancel — возвращает копию переданного контекста и cancelFunc. Вызов cancelFunc отменяет этот контекст.
WithDeadline & WithTimeout — позволяют задать временные рамки контексту.
WithValue — позволяет передать произвольные пары ключ/значение в context.

🔹Отмена родительского context автоматически отменяет все дочерние.
🔹Context используется для уведомления о том, что пора завершать работу, — это особенно удобно через канал ctx.Done().
🔹Возвращаемая функция cancel позволяет рано завершить context.
🔹Не храните в context чувствительные данные: контекст может быть выведен и сохранен в логах, что может раскрыть чувствительные данные.
🔹При отмене context можно узнать причину через ctx.Err(), где возможные значения — context.Canceled или context.DeadlineExceeded.

📌Пример:

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // освобождаем ресурсы

go someOperation(ctx)

if ctx.Err() == context.Canceled {
fmt.Println("Operation was canceled")
}


📌Важно:

context.Background() и context.TODO() — одно и то же. Разница лишь в том, что context.TODO() выставляется в местах, где пока нет понимания, что необходимо использовать context.Background() и возможно его надо заменить на дочерний context.
• Когда context отменяется (через cancel, timeout или deadline), ctx.Done() возвращает закрытый канал. Это удобный механизм для оповещения горутин о том, что пора завершать работу.
• Context следует передавать первым аргументом в функцию:

func DoSomething(ctx context.Context, arg Arg) error
👍16🔥5
💬Что из себя представляет паттерн «Функциональные опции» в Go и как его реализовать?

▪️Паттерн «Функциональные опции» в Go предлагает гибкий способ настройки структур без раскрытия внутренних полей.

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

📌Базовый пример без функциональных опций:

type Server struct {
host string
port int
protocol string
}

func NewServer(host string, port int) *Server {
return &Server{
host: host,
port: port,
protocol: "http",
}
}


▪️Со временем наши требования меняются, и нам может понадобиться поддерживать больше вариантов конфигурации. Вместо изменения сигнатуры функции NewServer, что может быть проблематично и несовместимо с предыдущими версиями, мы можем использовать функциональные опции.

👉 Сначала мы определяем функциональную опцию:

type ServerOption func(*Server)

👉 и функцию, удовлетворяющую типу:

func WithPort (port int) ServerOption {
return func(s *Server) {
s.port = port
}


👉 изменяем нашу функцию NewServer:

func NewServer(host string, opts ...ServerOption) *Server {
server : = &Server{
host: host,
port: 443,
protocol: "https"
}

for _, opt : = range opts {
opt (server)
}

return server

}


👉 и теперь мы можем делать вот так:

server1 := NewServer("localhost")
server2 := NewServer("localhost", WithPort(8080))


📌Этот паттерн позволяет нам гибко настраивать параметры, сохраняя при этом читабельность и не раскрывая внутренние поля.
👍37
💬Для чего предназначен и как работает планировщик Go (Go Scheduler) на базовом уровне?

📌Планировщик Go отвечает за управление горутинами и переключение контекста между ними, обеспечивая эффективное использование ресурсов процессора.

🔸M:N планирование
: Go использует модель M:N для планирования, где M горутин могут быть запланированы на N потоках ОС. Это позволяет Go создавать тысячи горутин, минимизируя накладные расходы, связанные с созданием и управлением реальными потоками ОС.

🔸G, M и P:
• G (goroutine) — горутина
• M (machine thread) — поток ОС, на котором выполняются горутины
• P (processor) — контекст выполнения, который содержит очередь горутин и другую необходимую информацию. Число P обычно равно числу ядер процессора

🔸Работа планировщика: когда горутина начинает выполнение, она захватывает P и привязывается к M. Если горутина блокируется (например, ожидает ввода/вывода), то P может быть отсоединен и присоединен к другой горутине, которая готова к выполнению.

🔸Рост и уменьшение: планировщик динамически увеличивает или уменьшает количество потоков ОС (M), в зависимости от нагрузки и блокировок.

🔸Взаимодействие с планировщиком: горутины взаимодействуют с планировщиком Go. Они могут быть прерваны, чтобы освободить поток ОС для других горутин. Этот механизм прерывания позволяет гарантировать, что одна горутина не занимает поток ОС слишком долго, предоставляя возможность другим горутинам получить время на выполнение.
👍19
💬Что из себя представляют дженерики в Go и для чего они нужны?

▪️Поддержка дженериков была добавлена в Go 1.18. Это своего рода механизм, который предоставляет инструмент для создания функций и структур данных, способных взаимодействовать с множеством типов, не требуя их явного указания при объявлении.

▪️Преимуществом использования дженериков является то, что они позволяют уменьшить дублирование кода, предоставляя универсальные решения для общих задач, и улучшают производительность, избегая необходимости в преобразованиях типов.

▪️До введения дженериков разработчики использовали интерфейсы и преобразование типов для достижения подобной гибкости, что могло приводить к потере производительности и безопасности типов.

▪️Дженерики позволяют писать более чистый, эффективный и безопасный код, минимизируя повторение кода и предоставляя строгую проверку типов. Они делают Go более универсальным и конкурентоспособным по сравнению с другими языками программирования, в которых дженерики давно были стандартом.

📌А теперь немного кода:

👉 Без дженериков

Раньше, если вы хотели создать функцию для поиска минимума в срезе, вам пришлось бы создавать отдельные функции для каждого типа или использовать пустой интерфейс interface{}.

func MinInts(arr []int) int { /*...*/ }
func MinFloats(arr []float64) float64 { /*...*/ }

👉 С дженериками

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

func Min[T comparable](arr []T) T { /*...*/ }

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

🔸Обобщенные структуры данных

Допустим, вы хотите создать структуру данных типа очередь (Queue).

👉 Без дженериков

type Queue struct {
data []interface{}
}


Использование interface{} снижает производительность из-за преобразования типов.

👉 С дженериками

type Queue[T any] struct {
data []T
}


Здесь T может быть любым типом, и нет необходимости в преобразовании типов.

🤔В общем и целом, большинство разработчиков приветствует введение дженериков в Go, но при этом подчеркивают важность их правильного и обдуманного применения.
👍13🔥1
💬Что такое и для чего предназначен Escape analysis в Go?

▫️Escape analysis — это техника компилятора Go, которая позволяет определить, где лучше всего размещать переменные: на стеке или в куче. Этот анализ важен для оптимизации производительности и эффективного использования памяти.

▫️В Go нет ни одного ключевого слова или функции, которую вы могли бы использовать, чтобы указать компилятору какое решение ему принять. Только то, как вы пишете свой код, условно позволяет повлиять на это решение.

📌Как это работает?

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

▫️Если же она выходит — например, возвращается из функции или сохраняется в глобальной переменной — то она будет размещена в куче, и за её освобождение будет отвечать сборщик мусора.

🚀Теперь немного кода

Переменная остается в рамках функции:

func sum(a, b int) int {
result := a + b
return result
}


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

func newInt() *int {
result := 42
return &result
}


Переменная сохраняется в глобальной переменной:

var globalVar *int

func setGlobalVar() {
x := 100
globalVar = &x
}


Эти примеры упрощены, но они иллюстрируют базовую концепцию. Escape analysis помогает языку Go работать эффективно, оптимизируя использование памяти и улучшая производительность.
👍22
💬Что из себя представляет паттерн «Прототип» в Go и как его реализовать?

🔻Паттерн Прототип позволяет создавать новые объекты, путем копирования (клонирования) созданного ранее объекта-оригинала-продукта (прототипа).

🔻Паттерн описывает процесс создания объектов-клонов на основе имеющегося объекта-прототипа, другими словами, он позволяет описать способ организации процесса клонирования.

📌А теперь практика (код прикреплен к посту)

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

Каждый файл или директория может быть представлен интерфейсом Element. Этот интерфейс имеет метод clone.
Файл и директория (назовем их Document и Directory) реализуют методы display и clone, соответствующие интерфейсу Element. При клонировании мы добавляем суффикс “_copy” к имени элемента.

В примере выше мы видим, как изначальная иерархия файловой системы успешно клонировалась, причем каждый элемент в клонированной версии имеет суффикс "_copy".

Результат выполнения:

Displaying structure for Dir2
Dir2
Dir1
Doc1
Doc2
Doc3

Displaying structure for copied Directory
Dir2_copy
Dir1_copy
Doc1_copy
Doc2_copy
Doc3_copy
👍6
💬Для чего в Go используется ключевое слово fallthrough

🔹В Go ключевое слово fallthrough используется внутри оператора switch. Оно позволяет продолжить выполнение кода в следующем блоке case, даже если его условие не выполняется.

🔹Это поведение аналогично тому, как switch работает в некоторых других языках, таких как C или C++, но в Go вы должны явно использовать fallthrough.

👀 Пример

package main

import "fmt"

func main() {
x := 3

switch x {
case 3:
fmt.Println("x is 3")
fallthrough
case 4:
fmt.Println("x is 4")
default:
fmt.Println("default case")
}
}


В этом примере, когда x равно 3, программа выведет:

x is 3
x is 4


Хотя x не равно 4, из-за использования fallthrough в case 3, выполнение продолжается с case 4.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
💬Что такое inlining в Go и для чего он предназначен?

💡Inlining (встраивание) — это процесс оптимизации, при котором компилятор заменяет вызов функции её телом. Это может ускорить выполнение программы, так как уменьшает накладные расходы, связанные с вызовом функции.

📌Например:

func add(a, b int) int {
return a + b
}

func main() {
result := add(3, 4)
fmt.Println(result)
}

После встраивания код может выглядеть примерно так:

func main() {
result := 3 + 4
fmt.Println(result)
}

📌Важно:

Автоматическое принятие решения: компилятор Go автоматически решает, стоит ли делать встраивание для конкретной функции. Это решение основано на различных критериях, включая размер функции и сложность вызова.
Преимущества: основное преимущество встраивания заключается в уменьшении накладных расходов на вызов функции. Это может значительно ускорить выполнение кода, особенно если функция вызывается многократно.
Ограничения: не все функции подходят для встраивания. Очень большие функции или функции, вызывающие множество других функций, могут не быть встроены. Также слишком активное использование встраивания может увеличить размер исполняемого файла, что может привести к другим проблемам производительности.
Просмотр решений компилятора: мы можем использовать флаг -gcflags="-m" при компиляции, чтобы увидеть, какие решения принимает компилятор относительно встраивания и других оптимизаций.
👍17
💬Какие изменения в Go 1.21 коснулись работы с типом map?

🔥В Go 1.21 в стандартную библиотеку добавили пакет maps, который предоставляет несколько общих операций с типом map.

🔸func Clone(m M) M
🔸func Copy(dst M1, src M2)
🔸func DeleteFunc(m M, del func(K, V) bool)
🔸func Equal(m1 M1, m2 M2) bool
🔸func EqualFunc(m1 M1, m2 M2, eq func(V1, V2) bool) bool

📌Примеры:

👉 DeleteFunc

package main

import (
"fmt"
"maps"
)

func main() {
m := map[string]int{
"one": 1,
"two": 2,
"three": 3,
"four": 4,
}
maps.DeleteFunc(m, func(k string, v int) bool {
return v%2 != 0 // delete odd values
})
fmt.Println(m)
}


Результат: map[four:4 two:2]

👉 EqualFunc

package main

import (
"fmt"
"maps"
"strings"
)

func main() {
m1 := map[int]string{
1: "one",
10: "Ten",
1000: "THOUSAND",
}
m2 := map[int][]byte{
1: []byte("One"),
10: []byte("Ten"),
1000: []byte("Thousand"),
}
eq := maps.EqualFunc(m1, m2, func(v1 string, v2 []byte) bool {
return strings.ToLower(v1) == strings.ToLower(string(v2))
})
fmt.Println(eq)
}

Результат: true
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥15👍2
💬Как в Go происходит передача параметров в функцию?

🔹Параметры в Go всегда передаются по значению. Это значит, что всякий раз, когда мы передаем аргумент в функцию, функция получает копию первоначального значения.

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

📌Передача базовых типов данных (int, string, bool и т.д.)

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

func main() {
num := 10
modifyValue(num)
fmt.Println(num) // 10
}


Значение переменной num не изменилось после вызова функции modifyValue, так как она получила копию значения num.

📌Передача срезов и типа map

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

func modifySlice(s []int) {
s[0] = 100
}

func main() {
numbers := []int{1, 2, 3}
modifySlice(numbers)
fmt.Println(numbers) // [100 2 3]
}


📌Передача указателей

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

func modifyWithPointer(x *int) {
*x = *x * 2
}


func main() {
num := 10
modifyWithPointer(&num)
fmt.Println(num) // 20
}


📝Таким образом, передача параметров в функции в Go зависит от типа данных параметра. Базовые типы передаются по значению (копией), а срезы, тип map и указатели передаются по ссылке, что позволяет изменять значения внутри функции и видеть эти изменения за пределами функции.
👍15
💬Что из себя представляют указатели (pointers) в Go?

🔻Указатели — это одна из фундаментальных концепций во многих языках программирования, включая Go. Указатель представляет собой переменную, которая хранит адрес другой переменной в памяти. В Go указатели используются для различных целей, включая оптимизацию производительности и управление данными на более низком уровне.

📌Определение и инициализация
• Указатель определяется с использованием символа * перед типом. Например, var x *int определяет указатель на переменную типа int.
• Для получения адреса переменной используется оператор &. Например, если у нас есть var y int, то &y вернет указатель на y.

📌Dereferencing (разыменование)
• Чтобы получить значение, на которое ссылается указатель, мы также используем символ *. Если x — это указатель на переменную y, то *x вернет значение y.

📌Nil указатели
• Указатель может быть nil — то есть он не указывает ни на какую область памяти. Разыменование nil указателя приведет к ошибке времени выполнения.

📌Указатели и функции
• По умолчанию все параметры передаются в функцию по значению. Если нам все-таки надо менять значение передаваемой переменной, мы можем использовать указатели.

📌Указатели на структуры
• В Go часто используются указатели на структуры. Это позволяет эффективно изменять данные в структуре и избегать копирования. Когда у нас есть указатель на структуру, мы можем использовать '.' для доступа к ее полям, не разыменовывая указатель явно, например, pointerToStruct.FieldName.

📌Осторожность с указателями
• Неправильное использование указателей может привести к ошибкам, таким как разыменование nil указателя или «висячие» указатели.

📌Сравнение с другими языками
• В отличие от некоторых других языков вроде C или C++, Go упрощает работу с указателями, предоставляя автоматическое управление памятью и сборку мусора. Однако необходимо быть внимательным и понимать, как и когда их использовать.
👍15
💬Что такое GOMAXPROCS и зачем она нужна в Go?

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

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

🔹Значение по умолчанию: по умолчанию GOMAXPROCS устанавливается равным количеству ядер CPU вашей машины, что обычно является разумным выбором для большинства приложений.

🔹Изменение значения: в случае необходимости мы можем изменить значение GOMAXPROCS с помощью функции runtime.GOMAXPROCS().

💡Изменение GOMAXPROCS может повлиять на производительность вашего приложения. Например, установка GOMAXPROCS в 1 приведет к тому, что все горутины будут выполняться на одном системном потоке, что может быть полезно для отладки, но неэффективно для производительности.

🤩У команды Uber есть замечательный инструмент automaxprocs, который позволяет автоматически устанавливать GOMAXPROCS в соответствии с квотой процессора Linux-контейнера.
👍6