День 4: Structs, Generics, Error handling и финальная полировка

🎯 Темы дня

  1. Structs: embedding, alignment/padding
  2. Generics (Go 1.18+): type parameters, constraints
  3. Error handling: errors.Is/As, wrapping, best practices
  4. Финальный чек-лист перед собеседованием

📖 1. Structs — тонкости

Embedding (встраивание)

go
type 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

go
type 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

Компилятор выравнивает поля для оптимального доступа к памяти:

go
type 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

go
func 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

go
type Number interface {
    ~int | ~float64 // ~ означает "любой тип на основе"
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

⚠️ Ловушка: Type inference

go
func 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+)

go
var 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 для извлечения типа

go
type 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

go
func do() error {
    var err error = nil
    return fmt.Errorf("context: %w", err) // ❌ вернет не-nil ошибку!
}

func main() {
    err := do()
    fmt.Println(err == nil) // false!
}

Правило: Никогда не оборачивай nil. Проверяй перед оборачиванием:

go
if err != nil {
    return fmt.Errorf("context: %w", err)
}
return nil

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

Задание 1: Struct alignment

Оптимизируй размер структуры:

go
type Data struct {
    a bool
    b int64
    c bool
    d int32
    e bool
}

Проверь размер через unsafe.Sizeof.

Задание 2: Generics

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

go
func Filter[T any](s []T, predicate func(T) bool) []T

Задание 3: Error wrapping

Реализуй цепочку ошибок:

go
func 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) работает.


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

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

    go
    type 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.

  2. Какой размер у структуры?

    go
    type S struct {
        a bool
        b int64
        c bool
    }

    Ответ: 24 bytes (с padding).

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

    go
    func do() error {
        var err error = nil
        return fmt.Errorf("ctx: %w", err)
    }
    
    func main() {
        err := do()
        fmt.Println(err == nil)
    }

    Ответ: false. Обертывание nil создает не-nil ошибку.

  4. Когда НЕ использовать 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 вопросов, которые точно спросят

  1. Чем буферизированный канал отличается от небуферизированного?
  2. Что выведет код с append и cap?
  3. Почему var err error = (*MyError)(nil); err != nil?
  4. Как работает escape analysis?
  5. Что будет при concurrent map access?
  6. Как избежать loop variable capture?
  7. Что выведет defer в цикле?
  8. Чем errors.Is отличается от ==?
  9. Как работает select с несколькими готовыми case?
  10. Когда использовать generics, а когда нет?

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

1. Что выведет этот код? func do() error { var err error = nil; return fmt.Errorf("ctx: %w", err) }
2. Когда НЕ стоит использовать generics? — выберите все подходящие
3. Что выведет fmt.Println(c.X) для struct C { A; B }, если у A и B есть поле X?
4. Что означает тильда в constraint: type Number interface { ~int | ~float64 }?
5. Что делает fmt.Errorf("...: %w", err)?
6. Почему if err == ErrNotFound не срабатывает после fmt.Errorf("getUser: %w", ErrNotFound)?
7. Как располагать поля структуры, чтобы минимизировать padding?

🎬 Экспресс-тест (30 минут)

Попробуй ответить без IDE:

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

    go
    s := make([]int, 0, 5)
    s = append(s, 1, 2, 3)
    s2 := s[1:]
    s2[0] = 100
    fmt.Println(s)
  2. Почему нельзя записать в nil map, но можно читать?

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

    go
    func foo() (result int) {
        defer func() { result++ }()
        return 0
    }
  4. Где будет аллоцирован map?

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

    go
    var nilSlice []int
    emptySlice := []int{}
    b1, _ := json.Marshal(nilSlice)
    b2, _ := json.Marshal(emptySlice)
    fmt.Println(string(b1), string(b2))

Ответы:

  1. [1 100 3] — общий underlying array
  2. Чтение из nil map возвращает zero value, запись — panic
  3. 1 — defer выполняется после return
  4. Всегда в heap
  5. null []