В языке Go отсутствует традиционное наследование. Вместо этого он использует композицию и интерфейсы для достижения полиморфизма и повторного использования кода. Рассмотрим, как это работает.
Позволяет включать одну структуру в другую, что дает возможность использовать методы встроенной структуры. Это часто называют "встраиванием" или "композицией" вместо наследования.
package main
import "fmt"
// Определение структуры
type Engine struct {
Power int
}
func (e Engine) Start() {
fmt.Println("Engine started with power:", e.Power)
}
// Определение другой структуры, которая включает первую
type Car struct {
Brand string
Engine
}
func main() {
myCar := Car{
Brand: "Toyota",
Engine: Engine{Power: 150},
}
fmt.Println("Car brand:", myCar.Brand)
myCar.Start() // Вызов метода встроенной структуры
}
Определяют набор методов, которые должны быть реализованы типом. Любой тип, реализующий все методы интерфейса, автоматически рассматривается как реализующий этот интерфейс. Это дает возможность полиморфизма.
package main
import "fmt"
// Определение интерфейса
type Drivable interface {
Drive()
}
// Определение структуры, реализующей интерфейс
type Car struct {
Brand string
}
func (c Car) Drive() {
fmt.Println(c.Brand, "is driving")
}
// Еще одна структура, реализующая интерфейс
type Bike struct {
Brand string
}
func (b Bike) Drive() {
fmt.Println(b.Brand, "is driving")
}
// Функция, принимающая интерфейс
func StartDriving(d Drivable) {
d.Drive()
}
func main() {
car := Car{Brand: "Toyota"}
bike := Bike{Brand: "Yamaha"}
StartDriving(car)
StartDriving(bike)
}
Интерфейсы также могут быть встроены друг в друга, что позволяет создавать сложные структуры интерфейсов.
package main
import "fmt"
// Определение базового интерфейса
type Printer interface {
Print()
}
// Определение другого интерфейса, включающего первый
type AdvancedPrinter interface {
Printer
Scan()
}
// Реализация структуры, реализующей расширенный интерфейс
type MultiFunctionPrinter struct{}
func (m MultiFunctionPrinter) Print() {
fmt.Println("Printing...")
}
func (m MultiFunctionPrinter) Scan() {
fmt.Println("Scanning...")
}
func main() {
mfp := MultiFunctionPrinter{}
mfp.Print()
mfp.Scan()
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Это модули кода, объединенные по функциональному назначению. Они помогают структурировать проект, упрощая его поддержку и повторное использование.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Go runtime — это встроенная среда выполнения, которая управляет памятью, потоками, сборкой мусора и планировщиком горутин. Он делает Go удобным для многопоточного и высоконагруженного программирования без необходимости вручную управлять потоками.
Горутины (goroutines) — лёгкие потоки, управляемые Go.
Планировщик (scheduler) — распределяет горутины по потокам ОС.
Модель памяти — управление стеком и кучей.
Сборщик мусора (GC) — автоматически очищает неиспользуемую память.
Syscalls — взаимодействие с ОС.
Горутины легче, чем потоки ОС: Go может запускать миллионы горутин, а планировщик (M:N) распределяет их по потокам ОС.
Go использует M:N-модель, где:
-
M
— системные потоки (OS threads). -
N
— горутины (goroutines). -
P
— процессоры (logical processors) — управляют выполнением горутин. P
привязан к M
, а G
(горутины) распределяются между `P` динамически. Это позволяет Go эффективно использовать CPU без ручного управления потоками.
Goroutine (G1, G2, G3, ...)
↓
Scheduler → P (Processor) → M (OS Thread) → CPU
Стек (stack) в Go динамический — он начинается с 2 KB и увеличивается при необходимости.
Если стек заполнен, Go автоматически расширяет его.
В отличие от C, Go не требует
malloc/free
, так как есть GC (garbage collector). Go использует автоматический сборщик мусора, который:
Работает параллельно с программой
Минимизирует паузы (low-latency GC)
Очищает неиспользуемые объекты из кучи
import "runtime"
runtime.GC() // Принудительный запуск сборщика мусора (обычно не нужно)
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Интерфейс состоит из двух частей: указателя на динамический тип и указателя на данные, связанные с этим типом. Такая структура позволяет интерфейсу хранить и управлять значениями разных типов динамически.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Составной индекс (Composite Index) — это индекс, который создаётся сразу на нескольких колонках таблицы базы данных. Он помогает ускорить поиск и сортировку данных, особенно если запросы используют условия по нескольким полям.
Если часто выполняются запросы с фильтрацией по нескольким колонкам (
WHERE col1 = X AND col2 = Y
), составной индекс ускоряет их выполнение.Если запросы используют
ORDER BY col1, col2
, индекс помогает избежать дополнительной сортировки.Без индекса СУБД вынуждена выполнять полный перебор (Full Table Scan), что занимает больше времени.
Он строится по нескольким колонкам и фактически представляет собой отсортированную структуру данных. Например, в PostgreSQL или MySQL создаётся так:
CREATE INDEX idx_name ON users (last_name, first_name);
Теперь база данных будет использовать индекс для запросов:
SELECT * FROM users WHERE last_name = 'Иванов' AND first_name = 'Петр';
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
В Go мапа (map[K]V) — это указатель на внутреннюю хеш-структуру. Её "вес" при передаче — 8 байт (64-битная архитектура), так как это просто ссылка.
Но фактический размер в памяти зависит от количества элементов и структуры хеш-таблицы, которая может занимать значительно больше.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Строки являются основным типом данных и представляют собой неизменяемые последовательности байт. Понимание того, как строки устроены внутри, поможет лучше управлять производительностью и памятью при работе со строками. Вот основные аспекты внутреннего устройства строк:
Строки неизменяемы, что означает, что их содержимое не может быть изменено после создания. Это важное свойство обеспечивает несколько преимуществ, включая безопасность при передаче строк между функциями и горутинами без необходимости блокировок или других форм синхронизации.
Внутренне строка представлена структурой, которая содержит два поля:
Указатель на массив байтов: Это указатель на первый элемент массива байт, который фактически хранит символы строки в кодировке UTF-8.
Длина: Количество байт в строке, а не количество рун или символов. Это важное различие, поскольку в UTF-8 один символ может занимать от 1 до 4 байт.
Go использует UTF-8 как стандартную кодировку для строк. Это позволяет эффективно работать с международным текстом, поддерживая широкий спектр символов без сложностей, связанных с другими кодировками. Однако это также означает, что операции, такие как получение длины строки в рунах (символах) или доступ к отдельному символу, могут потребовать дополнительных вычислений для обработки многобайтовых символов.
Поскольку строки неизменяемы, любая операция, которая кажется "изменяющей" строку, на самом деле создает новую строку. Операции среза строк в Go особенно эффективны, потому что новые строки создаются путем указания на тот же массив байтов, что и исходная строка, с изменением только начальной позиции и длины. Это делает срезы строк очень быстрыми и экономичными с точки зрения использования памяти.
Благодаря неизменяемости и способу хранения строк в виде срезов байтов, Go обеспечивает эффективное управление памятью и производительность при работе со строками. Однако необходимо быть осторожным с операциями, которые могут казаться невинными, но приводят к частому созданию новых строк, так как это может повлечь за собой издержки на выделение памяти и сборку мусора.
s := "Hello, world" // Создание строки
t := s[7:] // Срез строки, создает новую строку "world"
fmt.Println(s) // Выводит: Hello, world
fmt.Println(t) // Выводит: world
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Это потокобезопасные операции с примитивными данными без мьютексов. Они включают операции чтения, записи, инкремента, сравнения и замены. Используются для повышения производительности при работе с простыми объектами, такими как счетчики или флаги.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
В Go
sync.WaitGroup
используется для синхронизации выполнения горутин. Она позволяет основной горутине (или любой другой горутине) ждать завершения группы горутин перед продолжением работы. Это особенно полезно, когда нужно убедиться, что все фоновые задачи завершены до выполнения дальнейших действий.Увеличивает (или уменьшает) счетчик горутин на заданное значение
delta
.Обычно вызывается до запуска горутин, чтобы установить количество горутин, которые нужно дождаться.
Уменьшает счетчик горутин на 1.
Вызывается горутиной, когда она завершает свою работу.
Блокирует выполнение до тех пор, пока счетчик горутин не станет равен нулю.
Обычно вызывается основной горутиной для ожидания завершения всех горутин.
Мы используем
WaitGroup
для ожидания завершения нескольких горутин.package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Уменьшает счетчик на 1 при завершении работы горутины
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // Увеличивает счетчик горутин на 1
go worker(i, &wg)
}
wg.Wait() // Ожидает завершения всех горутин
fmt.Println("All workers done")
}
worker
.WaitGroup
увеличивается на 1 перед запуском каждой горутины с помощью wg.Add(1)
.wg.Done()
при завершении, уменьшая счетчик на 1.wg.Wait()
, блокируясь до тех пор, пока все горутины не завершат свою работу.Позволяет основной горутине дождаться завершения всех запущенных горутин, что особенно важно для корректного завершения программы или выполнения зависимых задач.
Гарантирует, что основная горутина не завершит выполнение программы до того, как завершатся все горутины, предотвращая возможные дедлоки или незавершенные операции.
Позволяет легко управлять множеством горутин, не требуя сложной логики для отслеживания их завершения.
Без использования
WaitGroup
основной поток может завершиться до завершения всех горутин, что приведет к неполной обработке данных. В этом примере использование time.Sleep
для ожидания является ненадежным и не гарантирует завершение всех горутин. Вместо этого правильное использование WaitGroup
обеспечивает корректное завершение всех задач.package main
import (
"fmt"
"time"
)
func worker(id int) {
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
for i := 1; i <= 5; i++ {
go worker(i)
}
time.Sleep(2 * time.Second) // Это не гарантирует завершение всех горутин
fmt.Println("All workers done")
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Пустой интерфейс
interface{}
является универсальным контейнером, который может содержать значение любого типа. Это связано с тем, что в Go любой тип автоматически реализует пустой интерфейс, поскольку в нем нет методов, которые нужно реализовать. числа, строки, булевы значения и т.д.
массивы, срезы, карты, структуры.
функции различных типов.
значения, которые реализуют другие интерфейсы.
Примитивные типы
package main
import "fmt"
func main() {
var i interface{}
i = 42
fmt.Println(i) // Output: 42
i = "hello"
fmt.Println(i) // Output: hello
i = true
fmt.Println(i) // Output: true
}
Композитные типы
package main
import "fmt"
func main() {
var i interface{}
i = []int{1, 2, 3}
fmt.Println(i) // Output: [1 2 3]
i = map[string]int{"one": 1, "two": 2}
fmt.Println(i) // Output: map[one:1 two:2]
type Person struct {
Name string
Age int
}
i = Person{Name: "Alice", Age: 30}
fmt.Println(i) // Output: {Alice 30}
}
Функции
package main
import "fmt"
func main() {
var i interface{}
i = func() {
fmt.Println("Hello from function")
}
if f, ok := i.(func()); ok {
f() // Output: Hello from function
}
}
Другие интерфейсы
package main
import "fmt"
type Stringer interface {
String() string
}
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}
func main() {
var i interface{}
i = Person{Name: "Alice", Age: 30}
if str, ok := i.(Stringer); ok {
fmt.Println(str.String()) // Output: Alice (30 years old)
}
}
Утверждение типа
package main
import "fmt"
func main() {
var i interface{} = 42
if v, ok := i.(int); ok {
fmt.Println("Integer:", v) // Output: Integer: 42
} else {
fmt.Println("Not an integer")
}
}
Использование
switch
для проверки типаpackage main
import "fmt"
func printType(i interface{}) {
switch v := i.(type) {
case string:
fmt.Println("String:", v)
case int:
fmt.Println("Integer:", v)
case bool:
fmt.Println("Boolean:", v)
default:
fmt.Printf("Unknown type: %T\n", v)
}
}
func main() {
printType("hello") // Output: String: hello
printType(42) // Output: Integer: 42
printType(true) // Output: Boolean: true
printType(3.14) // Output: Unknown type: float64
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Для защиты можно использовать мьютексы, sync.Map или обрабатывать все операции с картой в отдельной горутине через каналы. Это исключает возможность одновременного доступа.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Объектно-ориентированная модель отличается от традиционных ООП-языков, таких как C# или Java. Нет классов и наследования в привычном понимании. Вместо этого используются структуры (structs) и интерфейсы для реализации основных принципов ООП: инкапсуляции, композиции и полиморфизма.
Служат аналогом классов. Они позволяют объединять данные в логически связанные группы.
type Person struct {
Name string
Age int
}
Могут быть определены для структур, что позволяет связывать функции с типами.
func (p Person) Greet() {
fmt.Printf("Hello, my name is %s\n", p.Name)
}
Достигается через модификаторы доступа на уровне пакета. Поля и методы, начинающиеся с заглавной буквы, экспортируемые (public), остальные — нет (private).
type Person struct {
name string // неэкспортируемое поле
Age int // экспортируемое поле
}
Go не поддерживает классическое наследование. Вместо этого используется композиция для включения функциональности одного типа в другой.
type Employee struct {
Person
Position string
}
Используются для определения поведения. Типы могут реализовывать интерфейсы неявно, просто предоставляя методы, указанные в интерфейсе.
type Greeter interface {
Greet()
}
func SayHello(g Greeter) {
g.Greet()
}
type Person struct {
Name string
}
func (p Person) Greet() {
fmt.Printf("Hello, my name is %s\n", p.Name)
}
func main() {
p := Person{Name: "John"}
SayHello(p)
}
Достигается через интерфейсы. Любой тип, который реализует интерфейс, может быть использован вместо него.
type Greeter interface {
Greet()
}
func SayHello(g Greeter) {
g.Greet()
}
type Robot struct {
ID string
}
func (r Robot) Greet() {
fmt.Printf("Greetings, I am robot %s\n", r.ID)
}
func main() {
p := Person{Name: "John"}
r := Robot{ID: "XJ-9"}
SayHello(p)
SayHello(r)
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Объектно-ориентированное программирование (ООП) в Go и C# реализовано с использованием различных подходов и парадигм, отражающих философию и дизайн каждого языка.
Используются структуры (
struct
).type Person struct {
Name string
Age int
}
func (p *Person) Greet() {
fmt.Printf("Hello, my name is %s\n", p.Name)
}
Используется композиция вместо наследования.
type Employee struct {
Person
Position string
}
Модификаторы доступа на уровне пакета (экспортируемые и неэкспортируемые поля).
type Person struct {
name string // неэкспортируемое поле
Age int // экспортируемое поле
}
Реализуется через интерфейсы.
type Greeter interface {
Greet()
}
type Person struct {
Name string
}
func (p *Person) Greet() {
fmt.Printf("Hello, my name is %s\n", p.Name)
}
Используются классы.
public class Person {
public string Name { get; set; }
public int Age { get; set; }
public void Greet() {
Console.WriteLine($"Hello, my name is {Name}");
}
}
Поддерживается классическое наследование.
public class Employee : Person {
public string Position { get; set; }
}
Модификаторы доступа (
public
, private
, protected
).public class Person {
private string name;
public int Age { get; set; }
}
Через виртуальные методы и интерфейсы.
public class Person {
public virtual void Greet() {
Console.WriteLine("Hello!");
}
}
public class Employee : Person {
public override void Greet() {
Console.WriteLine("Hello, I am an employee!");
}
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Стандартные уровни:
1. Read Uncommitted — минимальная изоляция, возможны "грязные" чтения.
2. Read Committed — читаются только зафиксированные данные.
3. Repeatable Read — одна и та же строка не изменяется другими транзакциями.
4. Serializable — максимальная изоляция, предотвращает все типы конфликтов, но снижает производительность.
Чем выше уровень — тем безопаснее, но медленнее.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Да, в SQL возможен
JOIN
с вложенными запросами. Это позволяет объединять результаты подзапросов с другими таблицами, что полезно, когда нужно предварительно отфильтровать или агрегировать данные перед объединением.JOIN можно использовать не только с реальными таблицами, но и с результатами подзапросов, если они представляют собой временные таблицы.
Допустим, у нас есть две таблицы:
-
orders (id, user_id, total)
-
users (id, name)
Мы хотим получить всех пользователей и их заказы, но только те заказы, где сумма больше 100
SELECT u.name, sub.total
FROM users u
JOIN (
SELECT user_id, total
FROM orders
WHERE total > 100
) AS sub ON u.id = sub.user_id;
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Это фундаментальные концепции, используемые в операционных системах для управления выполнением программ. Хотя они тесно связаны, между ними есть ключевые различия:
Это экземпляр выполняющейся программы. Процесс имеет собственное изолированное адресное пространство памяти, что означает, что память, которую использует один процесс, не может быть напрямую доступна другому процессу. Каждый процесс предоставляет ресурсы, необходимые для выполнения программы, включая память, дескрипторы файлов, и переменные среды. Процессы изолированы друг от друга, что повышает безопасность и устойчивость системы в целом, поскольку сбой в одном процессе обычно не влияет на другие процессы. Однако эта изоляция требует больших затрат ресурсов при переключении между процессами и при их создании.
Это более легковесная единица выполнения, которая существует внутри процесса. Все потоки внутри одного процесса делят одно и то же адресное пространство памяти и системные ресурсы, такие как файловые дескрипторы. Это позволяет потокам более эффективно общаться друг с другом, поскольку они могут обмениваться данными без использования специальных механизмов межпроцессного взаимодействия. Потоки идеально подходят для выполнения задач, которые могут быть эффективно распараллелены, поскольку они позволяют программе выполнять множество операций одновременно. Однако, поскольку потоки делят память, разработчику необходимо тщательно управлять доступом к общим данным, чтобы избежать условий гонки и других проблем синхронизации.
Процессы изолированы друг от друга, в то время как потоки делят состояние и ресурсы внутри одного процесса.
Каждый процесс имеет собственное адресное пространство, в то время как все потоки внутри процесса делят его адресное пространство.
Создание нового процесса более ресурсоемко, чем создание потока внутри существующего процесса.
Взаимодействие между процессами требует использования межпроцессного взаимодействия (IPC), такого как сокеты, разделяемая память, очереди сообщений и т. д. Потоки внутри процесса могут общаться друг с другом напрямую через общую память.
Ошибка в одном процессе обычно не влияет на другие процессы, но ошибка в одном потоке может привести к сбою всего процесса.
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Нельзя напрямую взять ссылку на значение, хранящееся по ключу в карте (map), из-за особенностей реализации карт и управления памятью. Рассмотрим подробнее, почему это так.
Карты реализованы на основе хеш-таблиц. Внутреннее устройство карты предполагает, что значения могут перемещаться в памяти при выполнении операций, таких как добавление или удаление элементов. Хеш-таблица может перераспределять (реорганизовывать) свои внутренние структуры для оптимизации доступа к элементам. Это может происходить, например, когда карта увеличивается в размере.
Если бы была возможность брать ссылки на значения, хранящиеся в карте, то при любой операции изменения карты (добавление, удаление элементов) ссылки могли бы становиться недействительными. Это привело бы к потенциально небезопасному поведению программы, так как указатель (ссылка) мог бы указывать на уже несуществующую или перемещенную область памяти.
Здесь попытка взять ссылку на значение из карты могла бы привести к проблемам
package main
import "fmt"
func main() {
m := map[string]int{"a": 1, "b": 2}
// Нельзя делать так:
// p := &m["a"]
// Вместо этого можно работать с копией значения
value := m["a"]
p := &value
fmt.Println("Value:", *p) // 1
// Изменение карты
m["c"] = 3
// Ссылка на значение в карте могла бы стать недействительной
// fmt.Println("Value after map change:", *p)
}
Для работы с ними лучше использовать копии значений. Вот несколько способов, как это можно сделать:
Получить значение из карты и сохранить его в переменную.
value := m["a"]
Если необходимо изменить значение в карте, его нужно сначала извлечь, изменить, а затем снова записать в карту.
value := m["a"]
value = value + 10
m["a"] = value
В некоторых случаях можно использовать указатели в качестве значений карты, чтобы можно было изменять значения через указатели.
package main
import "fmt"
func main() {
m := map[string]*int{"a": new(int), "b": new(int)}
*m["a"] = 1
*m["b"] = 2
// Теперь можно брать указатели на значения
p := m["a"]
fmt.Println("Value:", *p) // 1
// Изменение значения через указатель
*p = 42
fmt.Println("Updated Value:", *m["a"]) // 42
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM