День 2: Maps, Interfaces и система типов
🎯 Темы дня
- Maps: внутреннее устройство, nil map, map of slices
- Interfaces: type assertion, type switch, nil trap
- Type system: aliases, named types, type conversions
📖 1. Maps — глубокое погружение
Внутреннее устройство
Map — это hmap с бакетами (buckets). Каждый бакет хранит до 8 key-value пар. При переполнении — рост и rehashing.
gotype hmap struct { count int // количество элементов flags uint8 B uint8 // log2 числа бакетов noverflow uint16 hash0 uint32 // seed для рандомизации buckets unsafe.Pointer // ... }
Nil map vs Empty map
govar 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
gom := make(map[string][]int) m["key"] = append(m["key"], 1) // ✅ работает!
Почему: m["key"] вернет nil slice, append к nil slice работает корректно.
⚠️ Ловушка: Итерация и порядок
gom := 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 специально рандомизирует порядок, чтобы код не полагался на него.
⚠️ Ловушка: Указатели как ключи
gotype 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 — магия и ловушки
Устройство интерфейса
gotype iface struct { tab *itab // таблица методов (тип) data unsafe.Pointer // значение }
Интерфейс = (тип, значение). Оба должны быть nil, чтобы interface == nil.
⚠️ Главная ловушка: Nil pointer в интерфейсе
gotype 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.
Правильно:
gofunc do() error { var err *MyError = nil if err == nil { return nil // возвращаем именно nil-интерфейс } return err }
Type assertion
govar i interface{} = "hello" // Безопасный вариант s, ok := i.(string) if ok { fmt.Println(s) } // Небезопасный (panic, если тип не совпадает) s2 := i.(string)
Type switch
gofunc 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+). Означает "любой тип".
gofunc 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
Разница:
govar 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*)
gotype 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") // ✅
Но для интерфейсов:
gotype 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, и исправь его:
govar m map[string]int m["key"] = 1 // panic
Задание 2: Interface nil trap
Реализуй функцию, которая возвращает ошибку, но при проверке err == nil возвращает true:
gofunc do() error { // твой код } func main() { err := do() fmt.Println(err == nil) // должно быть true }
Задание 3: Type assertion
Напиши функцию, которая принимает any и возвращает строковое представление:
- int → "число: X"
- string → "строка: X"
- bool → "логическое: X"
- остальное → "неизвестный тип"
🗣️ Каверзные вопросы
-
Что выведет?
gom := make(map[string][]int) m["a"] = append(m["a"], 1, 2, 3) fmt.Println(m["a"])Ответ:
[1 2 3]— append к nil slice работает. -
Почему
var err error = (*MyError)(nil); err != nil? Ответ: Интерфейс хранит{type: *MyError, value: nil}, а!= nilпроверяет оба поля. -
Чем
type MyInt = intотличается отtype MyInt int? Ответ: Первое — alias (тот же тип), второе — named type (новый тип). -
Можно ли вызвать метод с pointer receiver на value? Ответ: Да, Go автоматически берет адрес. Но для интерфейсов — только если тип указателя.
🧩 Проверь себя
✅ Чек-лист дня
- Понимаю разницу nil map vs empty map
- Знаю, что итерация map не гарантирует порядок
- Могу объяснить nil pointer trap в интерфейсах
- Понимаю разницу type alias vs named type
- Знаю правило method sets для value vs pointer receivers