День 2: Maps, Interfaces и система типов

🎯 Темы дня

  1. Maps: внутреннее устройство, nil map, map of slices
  2. Interfaces: type assertion, type switch, nil trap
  3. Type system: aliases, named types, type conversions

📖 1. Maps — глубокое погружение

Внутреннее устройство

Map — это hmap с бакетами (buckets). Каждый бакет хранит до 8 key-value пар. При переполнении — рост и rehashing.

go
type hmap struct {
    count     int     // количество элементов
    flags     uint8
    B         uint8   // log2 числа бакетов
    noverflow uint16
    hash0     uint32  // seed для рандомизации
    buckets   unsafe.Pointer
    // ...
}

Nil map vs Empty map

go
var nilMap map[string]int        // nil
emptyMap := make(map[string]int) // не nil, len=0
emptyMap2 := map[string]int{}    // не nil, len=0

// nilMap["key"] = 1  // ❌ panic: assignment to entry in nil map
val := nilMap["key"]  // ✅ вернет zero value (0)

Правило: Чтение из nil map безопасно, запись — panic.

⚠️ Ловушка: Map of slices

go
m := make(map[string][]int)
m["key"] = append(m["key"], 1) // ✅ работает!

Почему: m["key"] вернет nil slice, append к nil slice работает корректно.

⚠️ Ловушка: Итерация и порядок

go
m := map[int]string{1: "a", 2: "b", 3: "c"}
for i := 0; i < 5; i++ {
    for k := range m {
        fmt.Print(k, " ")
    }
    fmt.Println()
}

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

⚠️ Ловушка: Указатели как ключи

go
type Key struct{ ID int }
m := make(map[*Key]int)

k1 := &Key{1}
k2 := &Key{1}

m[k1] = 100
fmt.Println(m[k2]) // 0 — разные указатели!

Для структур-ключей Go сравнивает значения, для указателей — адреса.


📖 2. Interfaces — магия и ловушки

Устройство интерфейса

go
type iface struct {
    tab  *itab          // таблица методов (тип)
    data unsafe.Pointer // значение
}

Интерфейс = (тип, значение). Оба должны быть nil, чтобы interface == nil.

⚠️ Главная ловушка: Nil pointer в интерфейсе

go
type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }

func do() error {
    var err *MyError = nil
    return err // возвращаем *MyError(nil) как error
}

func main() {
    err := do()
    fmt.Println(err == nil) // ❌ false!
    if err != nil {
        panic("unexpected error") // выполнится!
    }
}

Почему: Интерфейс хранит {tab: *MyError, data: nil}. tab != nil → интерфейс не nil.

Правильно:

go
func do() error {
    var err *MyError = nil
    if err == nil {
        return nil // возвращаем именно nil-интерфейс
    }
    return err
}

Type assertion

go
var i interface{} = "hello"

// Безопасный вариант
s, ok := i.(string)
if ok {
    fmt.Println(s)
}

// Небезопасный (panic, если тип не совпадает)
s2 := i.(string)

Type switch

go
func process(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Println("int:", v)
    case string:
        fmt.Println("string:", v)
    case *MyError:
        fmt.Println("error:", v)
    default:
        fmt.Println("unknown")
    }
}

Empty interface interface{} vs any

any — это алиас для interface{} (Go 1.18+). Означает "любой тип".

go
func printAny(v any) {
    fmt.Println(v)
}

📖 3. Type system: Aliases и Named types

Type alias vs Named type

go
// Type alias (Go 1.9+)
type MyInt = int // это ТО ЖЕ САМОЕ, что int

// Named type
type MyInt2 int // это НОВЫЙ тип, не совместимый с int

Разница:

go
var a MyInt = 42
var b int = a // ✅ работает, это один тип

var c MyInt2 = 42
var d int = c // ❌ ошибка: cannot use c (type MyInt2) as type int

Method sets

Правило:

  • У типа T — только методы с receiver (T)
  • У типа *T — методы с receiver (T) И (T*)
go
type User struct{ Name string }

func (u User) GetName() string { return u.Name }
func (u *User) SetName(name string) { u.Name = name }

var u = User{}
u.GetName()  // ✅
u.SetName("Alice") // ✅ (Go автоматически берет &u)

var p = &User{}
p.GetName()  // ✅
p.SetName("Bob") // ✅

Но для интерфейсов:

go
type Namer interface {
    GetName() string
    SetName(string)
}

var u = User{}
var n Namer = u  // ❌ ошибка: User не реализует SetName
var n2 Namer = &u // ✅ *User реализует оба метода

💻 Практика (1–2 часа)

Задание 1: Nil map trap

Напиши код, который паникует при записи в nil map, и исправь его:

go
var m map[string]int
m["key"] = 1 // panic

Задание 2: Interface nil trap

Реализуй функцию, которая возвращает ошибку, но при проверке err == nil возвращает true:

go
func do() error {
    // твой код
}

func main() {
    err := do()
    fmt.Println(err == nil) // должно быть true
}

Задание 3: Type assertion

Напиши функцию, которая принимает any и возвращает строковое представление:

  • int → "число: X"
  • string → "строка: X"
  • bool → "логическое: X"
  • остальное → "неизвестный тип"

🗣️ Каверзные вопросы

  1. Что выведет?

    go
    m := make(map[string][]int)
    m["a"] = append(m["a"], 1, 2, 3)
    fmt.Println(m["a"])

    Ответ: [1 2 3] — append к nil slice работает.

  2. Почему var err error = (*MyError)(nil); err != nil? Ответ: Интерфейс хранит {type: *MyError, value: nil}, а != nil проверяет оба поля.

  3. Чем type MyInt = int отличается от type MyInt int? Ответ: Первое — alias (тот же тип), второе — named type (новый тип).

  4. Можно ли вызвать метод с pointer receiver на value? Ответ: Да, Go автоматически берет адрес. Но для интерфейсов — только если тип указателя.


🧩 Проверь себя

1. Что будет при итерации по map дважды подряд?
2. Что вернёт чтение из nil map: var m map[string]int; v := m["key"]?
3. Что произойдёт при записи в nil map?
4. Что делает безопасная форма type assertion: s, ok := i.(string)?
5. Чем type MyInt = int отличается от type MyInt int?
6. Почему err := do(); err == nil может быть false, хотя err хранит nil-указатель (*MyError)(nil)?
7. Как гарантировать, что функция вернёт nil-интерфейс error?

✅ Чек-лист дня

  • Понимаю разницу nil map vs empty map
  • Знаю, что итерация map не гарантирует порядок
  • Могу объяснить nil pointer trap в интерфейсах
  • Понимаю разницу type alias vs named type
  • Знаю правило method sets для value vs pointer receivers