День 4: Structs, Generics, Error handling и финальная полировка
🎯 Темы дня
- Structs: embedding, alignment/padding
- Generics (Go 1.18+): type parameters, constraints
- Error handling: errors.Is/As, wrapping, best practices
- Финальный чек-лист перед собеседованием
📖 1. Structs — тонкости
Embedding (встраивание)
gotype Base struct { ID int } func (b Base) GetID() int { return b.ID } type User struct { Base // встроено Name string } u := User{Base: Base{ID: 1}, Name: "Alice"} fmt.Println(u.ID) // ✅ promoted field fmt.Println(u.GetID()) // ✅ promoted method
⚠️ Ловушка: Conflict при embedding
gotype A struct { X int } type B struct { X int } type C struct { A B } c := C{} // fmt.Println(c.X) // ❌ ambiguous selector fmt.Println(c.A.X) // ✅ явно
Struct alignment и padding
Компилятор выравнивает поля для оптимального доступа к памяти:
gotype Bad struct { a bool // 1 byte b int64 // 8 bytes c bool // 1 byte } // Размер: 24 bytes (с padding) type Good struct { b int64 // 8 bytes a bool // 1 byte c bool // 1 byte // padding: 6 bytes } // Размер: 16 bytes
Правило: Располагай поля от больших к маленьким.
Инструмент: go install github.com/orijtech/structslop@latest → structslop analyze.
📖 2. Generics (Go 1.18+)
Type parameters
gofunc Min[T constraints.Ordered](a, b T) T { if a < b { return a } return b } fmt.Println(Min(1, 2)) // int fmt.Println(Min(1.5, 2.3)) // float64 fmt.Println(Min("a", "b")) // string
Constraints
gotype Number interface { ~int | ~float64 // ~ означает "любой тип на основе" } func Sum[T Number](nums []T) T { var total T for _, n := range nums { total += n } return total }
⚠️ Ловушка: Type inference
gofunc Map[T any, U any](s []T, f func(T) U) []U { result := make([]U, len(s)) for i, v := range s { result[i] = f(v) } return result } // ✅ Type inference работает result := Map([]int{1, 2, 3}, func(x int) string { return strconv.Itoa(x) }) // ❌ Не работает, если компилятор не может вывести // result := Map([]int{1, 2, 3}, strconv.Itoa) // ошибка
Когда НЕ использовать generics
- Если типизация не нужна (используй
any) - Если логика сильно отличается для разных типов
- Если производительность критична (generics могут замедлить компиляцию)
📖 3. Error handling — best practices
errors.Is и errors.As (Go 1.13+)
govar ErrNotFound = errors.New("not found") func getUser(id int) (*User, error) { return nil, fmt.Errorf("getUser: %w", ErrNotFound) } func main() { _, err := getUser(1) // ❌ Старый способ if err == ErrNotFound { // не сработает, потому что err обернут } // ✅ Новый способ if errors.Is(err, ErrNotFound) { fmt.Println("user not found") } }
errors.As для извлечения типа
gotype MyError struct { Code int Msg string } func (e *MyError) Error() string { return fmt.Sprintf("%d: %s", e.Code, e.Msg) } func do() error { return &MyError{Code: 404, Msg: "not found"} } func main() { err := do() var myErr *MyError if errors.As(err, &myErr) { fmt.Println(myErr.Code) // 404 } }
Sentinel errors vs Custom types
go// Sentinel errors — для простых случаев var ( ErrNotFound = errors.New("not found") ErrUnauthorized = errors.New("unauthorized") ) // Custom types — когда нужна дополнительная информация type NotFoundError struct { Resource string ID string } func (e *NotFoundError) Error() string { return fmt.Sprintf("%s with id %s not found", e.Resource, e.ID) }
⚠️ Ловушка: Обертывание nil
gofunc do() error { var err error = nil return fmt.Errorf("context: %w", err) // ❌ вернет не-nil ошибку! } func main() { err := do() fmt.Println(err == nil) // false! }
Правило: Никогда не оборачивай nil. Проверяй перед оборачиванием:
goif err != nil { return fmt.Errorf("context: %w", err) } return nil
💻 Практика (1 час)
Задание 1: Struct alignment
Оптимизируй размер структуры:
gotype Data struct { a bool b int64 c bool d int32 e bool }
Проверь размер через unsafe.Sizeof.
Задание 2: Generics
Напиши функцию Filter, которая принимает слайс и предикат, возвращает отфильтрованный слайс:
gofunc Filter[T any](s []T, predicate func(T) bool) []T
Задание 3: Error wrapping
Реализуй цепочку ошибок:
gofunc layer1() error { return ErrNotFound } func layer2() error { return fmt.Errorf("layer2: %w", layer1()) } func layer3() error { return fmt.Errorf("layer3: %w", layer2()) }
Проверь, что errors.Is(err, ErrNotFound) работает.
🗣️ Каверзные вопросы
-
Что выведет?
gotype A struct{ X int } type B struct{ X int } type C struct { A; B } c := C{A: A{X: 1}, B: B{X: 2}} fmt.Println(c.X)Ответ: Compile error — ambiguous selector.
-
Какой размер у структуры?
gotype S struct { a bool b int64 c bool }Ответ: 24 bytes (с padding).
-
Что выведет?
gofunc do() error { var err error = nil return fmt.Errorf("ctx: %w", err) } func main() { err := do() fmt.Println(err == nil) }Ответ: false. Обертывание nil создает не-nil ошибку.
-
Когда НЕ использовать generics? Ответ: Когда типизация не нужна, логика сильно отличается, или критична скорость компиляции.
🏁 Финальный чек-лист перед собеседованием
Теория языка
- Slices: growth strategy, nil vs empty, 3-index slice
- Maps: nil map trap, итерация без порядка, map of slices
- Strings: rune vs byte, иммутабельность, zero-copy конверсии
- Interfaces: nil pointer trap, type assertion, type switch
- Structs: embedding, alignment/padding
- Goroutines: loop variable capture, утечки
- Channels: select fairness, nil channel tricks, close trap
- Defer/Panic/Recover: порядок LIFO, recover только в defer
- Generics: type parameters, constraints, когда не использовать
- Errors: errors.Is/As, wrapping, sentinel vs custom types
Практика
- Могу запустить escape analysis (
-gcflags="-m") - Могу найти утечку горутин через pprof
- Могу оптимизировать struct alignment
- Понимаю разницу между
type A = Bиtype A B
Топ-10 вопросов, которые точно спросят
- Чем буферизированный канал отличается от небуферизированного?
- Что выведет код с append и cap?
- Почему
var err error = (*MyError)(nil); err != nil? - Как работает escape analysis?
- Что будет при concurrent map access?
- Как избежать loop variable capture?
- Что выведет defer в цикле?
- Чем errors.Is отличается от ==?
- Как работает select с несколькими готовыми case?
- Когда использовать generics, а когда нет?
🧩 Проверь себя
🎬 Экспресс-тест (30 минут)
Попробуй ответить без IDE:
-
Что выведет?
gos := make([]int, 0, 5) s = append(s, 1, 2, 3) s2 := s[1:] s2[0] = 100 fmt.Println(s) -
Почему нельзя записать в nil map, но можно читать?
-
Что выведет?
gofunc foo() (result int) { defer func() { result++ }() return 0 } -
Где будет аллоцирован map?
-
Что выведет?
govar nilSlice []int emptySlice := []int{} b1, _ := json.Marshal(nilSlice) b2, _ := json.Marshal(emptySlice) fmt.Println(string(b1), string(b2))
Ответы:
[1 100 3]— общий underlying array- Чтение из nil map возвращает zero value, запись — panic
- 1 — defer выполняется после return
- Всегда в heap
null []