Skip to main content

Переменные и операции

Как проверить тип переменной в Go во время выполнения

Почему данные сбегают

Неправильный Ctrl-C Ctrl-V

Когда i нормально, а d — преступление

От логирования к передаче вверх

Как проверить тип переменной в Go во время выполнения

В Go есть несколько способов узнать тип переменной в рантайме. Разберём каждый подробно.1️⃣

Type Assertion

Самый простой способ для проверки конкретного типа:

var i interface{} = "hello"

// Проверка с обработкой ошибки
s, ok := i.(string)
if ok {
    fmt.Printf("Это строка: %s\n", s)
} else {
    fmt.Println("Это не строка")
}

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

Type Switch

Элегантный способ для проверки нескольких типов:

func checkType(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Целое число: %d\n", v)
    case string:
        fmt.Printf("Строка: %s\n", v)
    case bool:
        fmt.Printf("Булево: %t\n", v)
    default:
        fmt.Printf("Неизвестный тип: %T\n", v)
    }
}

Когда использовать: когда нужно обработать разные типы по-разному.

Пакет reflect

Для продвинутой работы с типами:

import "reflect"

var x float64 = 3.14

// Получить тип
t := reflect.TypeOf(x)
fmt.Println("Тип:", t) // float64

// Получить значение
v := reflect.ValueOf(x)
fmt.Println("Тип через Value:", v.Type())
fmt.Println("Kind:", v.Kind()) // float64

Форматирование %T

Быстрый способ для вывода типа:

var x = 42
fmt.Printf("Тип переменной: %T\n", x) // int

Когда использовать: для быстрой отладки.

Best practises:
• Избегайте reflect там, где можно обойтись type assertion или type switch
• Используйте type switch вместо цепочки type assertion
• Проверяйте ok при type assertion, чтобы избежать паники
• Предпочитайте интерфейсы вместо проверки конкретных типов

Почему данные сбегают

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

Стек: быстрый и дисциплинированный

Представьте стопку тарелок. Положили одну, сверху ещё одну, потом ещё. Снимаете тарелки строго сверху — это и есть принцип работы стека. Последним пришёл — первым ушёл, то есть LIFO.

В стеке хранятся локальные переменные функций и информация о вызовах. Когда вызываете функцию, для неё создаётся стековый фрейм — туда помещаются все её параметры и локальные переменные.

Хип: гибкий и непредсказуемый

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

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

Практический пример:

func createUser(name string) *User {
    count := 42  // в стеке
    user := &User{Name: name}  // escape to heap
    return user
}

func processData() {
    data := make([]byte, 100)  // скорее всего в стеке
    // ... используем data только внутри функции
}

Переменная count останется в стеке и исчезнет при выходе из функции. Структура User уйдёт в хип, потому что мы возвращаем указатель на неё — компилятор видит, что данные сбегают из функции.

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

Как Go решает, что куда положить

Go использует escape analysis во время компиляции. Компилятор анализирует код и решает, может ли переменная безопасно жить в стеке:

// Останется в стеке
func stackAlloc() {
    x := 42
    fmt.Println(x)
}

// Уйдёт в хип
func heapAlloc() *int {
    x := 42
    return &x  // escape: возвращаем указатель
}

Можете проверить сами:

go build -gcflags="-m" your_file.go

Компилятор покажет, какие переменные сбегают в хип и почему.

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

Неправильный Ctrl-C Ctrl-V

В Go есть правило: типы из пакета sync нельзя копировать.

Почему нельзя копировать sync-типы

sync.Mutex содержит внутренние поля и при копировании значения эти поля дублируются, но копии не синхронизированы.

Две копии используют разные адреса в памяти.
Результат: мьютекс перестаёт выполнять свою функцию синхронизации.

Практический пример:

type Counter struct {
    mu       sync.Mutex
    counters map[string]int
}

func (c Counter) Increment(name string) { 
    c.mu.Lock()
    defer c.mu.Unlock()
    c.counters[name]++
}

Здесь каждый вызов Increment создаёт копию всей структуры Counter, включая sync.Mutex. Каждая горутина получает свой мьютекс, что полностью ломает синхронизацию.

Решением этой проблемы может стать указатель на структуру или на мьютекс:

type Counter struct {
    mu       *sync.Mutex
    counters map[string]int
}

func NewCounter() Counter {
    return Counter{
        mu: &sync.Mutex{},
        counters: map[string]int{},
    }
}

Даже если Counter скопируется, обе копии будут использовать один мьютекс через указатель.

Когда i нормально, а d — преступление

Go поощряет краткость но не в ущерб смыслу. Правило простое: коротко для локального и широко для интерфейса и домена

Список общепринятых сокращений переменных:

i, j, k — циклы
err — ошибки
ctx — контекст
r, w — Reader, Writer
u, d, s — Receiver'ы методов. Первая буква типа.
db — Database
cfg — Config
buf — буфер
ok — флаг успеха
n — number, количество
v — value. Только с range
_ — Throwaway. Игнорируем результат.
conn — Connection

От логирования к передаче вверх

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

user, err := repo.GetUser(id)
if err != nil {
    log.Println("failed to get user:", err)
    return nil
}

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

Вот как это делают опытные разработчики:

user, err := repo.GetUser(ctx, id)
if err != nil {
    return nil, fmt.Errorf("get user %d: %w", id, err)
}

%w — это ключ к вложенным ошибкам в Go 1.13 и новее. Такой подход сохраняет смысл ошибки и ее цепочку, что помогает в отладке и трассировке.