Урок 3: Функции, методы и интерфейсы

Урок 3. Функции, методы и интерфейсы

🔄 Node.js → Go (ключевые отличия):

📋 Что изучаем

📦 Инициализация проекта

mkdir go-funcs && cd go-funcs
go mod init go-funcs

💻 Код программы

package main

import (
    "errors"
    "fmt"
    "math"
    "strings"
)

// ╔══════════════════════════════════════════════════════════╗
// ║  1. ФУНКЦИИ — БАЗОВЫЕ ВОЗМОЖНОСТИ                      ║
// ╚══════════════════════════════════════════════════════════╝

// Простая функция: параметры (a, b int) — сокращение для (a int, b int)
func add(a, b int) int {
    return a + b
}

// Множественный возврат — идиоматичный паттерн Go.
// В JS: const [result, error] = await divide(...)
// В Go: result, err := divide(...)
func divide(a, b float64) (float64, error) {
    if b == 0 {
        // errors.New создаёт простую ошибку. fmt.Errorf — с форматированием.
        return 0, fmt.Errorf("деление на ноль: %.2f / %.2f", a, b)
    }
    return a / b, nil // nil — отсутствие ошибки
}

// Именованные возвращаемые значения.
// Имена x,y в сигнатуре — это переменные, доступные внутри функции.
// "naked return" — возвращает текущие значения x и y.
func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return // "голый" возврат (использовать осторожно, ухудшает читаемость)
}

// Вариативная функция: parts ...string — аналог ...rest в JS
func concat(separator string, parts ...string) string {
    return strings.Join(parts, separator)
}

// Функция как first-class citizen: принимает другую функцию
func applyOperation(a, b int, op func(int, int) int) int {
    return op(a, b)
}

// Замыкание (closure): функция захватывает переменную из внешней области
func counter() func() int {
    count := 0
    return func() int {
        count++ // мутирует захваченную переменную
        return count
    }
}

// ╔══════════════════════════════════════════════════════════╗
// ║  2. СТРУКТУРЫ И МЕТОДЫ                                  ║
// ╚══════════════════════════════════════════════════════════╝

// Rectangle — структура (аналог класса без методов)
type Rectangle struct {
    Width  float64
    Height float64
}

// Value receiver: (r Rectangle) — метод получает КОПИЮ структуры.
// Не может изменить оригинал. Используй для иммутабельных операций.
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

func (r Rectangle) Description() string {
    return fmt.Sprintf("Прямоугольник %.1f × %.1f", r.Width, r.Height)
}

// Circle — ещё одна структура
type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// Pointer receiver: (c *Circle) — метод получает УКАЗАТЕЛЬ.
// Может ИЗМЕНИТЬ оригинал. Используй для мутаций и больших структур.
func (c *Circle) Scale(factor float64) {
    c.Radius *= factor // меняет оригинал через указатель
}

// Pointer receiver для проверки nil (метод можно вызвать на nil-указателе!)
func (c *Circle) IsValid() bool {
    return c != nil && c.Radius > 0
}

// ╔══════════════════════════════════════════════════════════╗
// ║  3. ИНТЕРФЕЙСЫ (неявная реализация)                     ║
// ╚══════════════════════════════════════════════════════════╝

// Shape — интерфейс. Любой тип с методами Area() и Perimeter()
// АВТОМАТИЧЕСКИ удовлетворяет этому интерфейсу.
// Не нужно писать "implements Shape" — duck typing на этапе компиляции.
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Describable — ещё один интерфейс
type Describable interface {
    Description() string
}

// PrintShapeInfo — функция, работающая с ЛЮБОЙ фигурой (полиморфизм)
func PrintShapeInfo(s Shape) {
    fmt.Printf("  Площадь: %.2f | Периметр: %.2f\n", s.Area(), s.Perimeter())
    // Если фигура ещё и Describable — выводим описание
    if d, ok := s.(Describable); ok { // type assertion
        fmt.Printf("  Описание: %s\n", d.Description())
    }
}

// Пустой интерфейс interface{} (или any в Go 1.18+)
// Принимает ЛЮБОЙ тип. Аналог unknown в TS.
func printAny(v interface{}) {
    fmt.Printf("Значение: %v (тип: %T)\n", v, v)
}

// ╔══════════════════════════════════════════════════════════╗
// ║  4. ВСТРАИВАНИЕ (embedding) — композиция вместо наследования ║
// ╚══════════════════════════════════════════════════════════╝

// Animal — базовая структура
type Animal struct {
    Name string
    Age  int
}

func (a Animal) Speak() string {
    return fmt.Sprintf("%s издаёт звук", a.Name)
}

func (a Animal) Info() string {
    return fmt.Sprintf("%s, %d лет", a.Name, a.Age)
}

// Dog встраивает Animal (композиция, НЕ наследование)
type Dog struct {
    Animal        // embedding: поля и методы Animal "продвигаются" в Dog
    Breed  string
}

// "Переопределение" метода Speak (shadowing)
func (d Dog) Speak() string {
    return fmt.Sprintf("%s (породы %s) лает: Гав-гав!", d.Name, d.Breed)
}

// Cat тоже встраивает Animal
type Cat struct {
    Animal
    Color string
}

func (c Cat) Speak() string {
    return fmt.Sprintf("%s (окрас %s) мяукает: Мяу!", c.Name, c.Color)
}

// Speaker — интерфейс для всех, кто умеет говорить
type Speaker interface {
    Speak() string
}

func makeThemSpeak(speakers ...Speaker) {
    for _, s := range speakers {
        fmt.Println("  ", s.Speak())
    }
}

// ╔══════════════════════════════════════════════════════════╗
// ║  MAIN                                                    ║
// ╚══════════════════════════════════════════════════════════╝

func main() {
    fmt.Println("╔══════════════════════════════════════════╗")
    fmt.Println("║   ФУНКЦИИ, МЕТОДЫ И ИНТЕРФЕЙСЫ         ║")
    fmt.Println("╚══════════════════════════════════════════╝")

    // ==========================================
    // 1. ВЫЗОВЫ ФУНКЦИЙ
    // ==========================================
    fmt.Println("\n── 1. ФУНКЦИИ ──")

    fmt.Printf("add(3, 5) = %d\n", add(3, 5))

    // Обработка ошибки: идиоматический паттерн
    if result, err := divide(10, 3); err != nil {
        fmt.Printf("Ошибка: %v\n", err)
    } else {
        fmt.Printf("10/3 = %.2f\n", result)
    }

    if _, err := divide(10, 0); err != nil {
        fmt.Printf("Ошибка (ожидаемо): %v\n", err)
    }

    x, y := split(17)
    fmt.Printf("split(17) = %d, %d\n", x, y)

    fmt.Printf("concat: %s\n", concat("", "Go", "Rust", "TypeScript"))

    // Функция как аргумент (анонимная/лямбда)
    multiply := func(a, b int) int { return a * b }
    fmt.Printf("applyOperation(4,7, multiply) = %d\n",
        applyOperation(4, 7, multiply))

    // Замыкание
    next := counter()
    fmt.Println("counter:", next(), next(), next()) // 1 2 3

    // ==========================================
    // 2. СТРУКТУРЫ И МЕТОДЫ
    // ==========================================
    fmt.Println("\n── 2. СТРУКТУРЫ И МЕТОДЫ ──")

    rect := Rectangle{Width: 5, Height: 3}
    fmt.Printf("Rectangle: %.1f×%.1f | Area=%.1f | Perimeter=%.1f\n",
        rect.Width, rect.Height, rect.Area(), rect.Perimeter())

    circle := Circle{Radius: 2.5}
    fmt.Printf("Circle: r=%.1f | Area=%.2f | Perimeter=%.2f\n",
        circle.Radius, circle.Area(), circle.Perimeter())

    // Pointer receiver: Scale меняет оригинал
    fmt.Printf("  До Scale(2): r=%.1f\n", circle.Radius)
    circle.Scale(2) // Go автоматически берёт (&circle).Scale(2)
    fmt.Printf("  После Scale(2): r=%.1f\n", circle.Radius)

    // Проверка nil-указателя
    var nilCircle *Circle
    fmt.Printf("nil circle valid? %v\n", nilCircle.IsValid())

    // ==========================================
    // 3. ИНТЕРФЕЙСЫ
    // ==========================================
    fmt.Println("\n── 3. ИНТЕРФЕЙСЫ ──")

    // Слайс интерфейсов — полиморфизм
    shapes := []Shape{
        Rectangle{Width: 5, Height: 3},
        Circle{Radius: 2.5},
        Rectangle{Width: 10, Height: 2},
    }

    for i, s := range shapes {
        fmt.Printf("Фигура %d:\n", i+1)
        PrintShapeInfo(s)
    }

    // Пустой интерфейс
    fmt.Println("\nПустой интерфейс (any):")
    printAny(42)
    printAny("hello")
    printAny(Rectangle{Width: 3, Height: 4})

    // Type assertion — извлечение конкретного типа из интерфейса
    var s Shape = Circle{Radius: 5}
    if c, ok := s.(Circle); ok {
        fmt.Printf("Извлекли Circle: r=%.1f\n", c.Radius)
    }

    // Type switch
    fmt.Println("\nType switch:")
    describeShape := func(s Shape) {
        switch v := s.(type) {
        case Rectangle:
            fmt.Printf("  Прямоугольник %.1f×%.1f\n", v.Width, v.Height)
        case Circle:
            fmt.Printf("  Круг r=%.1f\n", v.Radius)
        default:
            fmt.Printf("  Неизвестная фигура\n")
        }
    }
    describeShape(Rectangle{Width: 5, Height: 3})
    describeShape(Circle{Radius: 2})

    // ==========================================
    // 4. ВСТРАИВАНИЕ (композиция)
    // ==========================================
    fmt.Println("\n── 4. ВСТРАИВАНИЕ (embedding) ──")

    dog := Dog{
        Animal: Animal{Name: "Бобик", Age: 3},
        Breed:  "Лабрадор",
    }
    cat := Cat{
        Animal: Animal{Name: "Мурка", Age: 2},
        Color:  "Чёрный",
    }

    // Прямой доступ к полям встроенной структуры
    fmt.Printf("Собака: %s | %s\n", dog.Name, dog.Info())
    fmt.Printf("Кошка: %s | %s\n", cat.Name, cat.Info())

    // Полиморфизм через интерфейс Speaker
    fmt.Println("\nВсе говорят:")
    makeThemSpeak(dog, cat)

    // ==========================================
    // 5. СРАВНЕНИЕ VALUE vs POINTER RECEIVER
    // ==========================================
    fmt.Println("\n── 5. VALUE vs POINTER RECEIVER ──")

    fmt.Println("Когда использовать pointer receiver (*T):")
    fmt.Println("  ✓ Метод изменяет состояние структуры")
    fmt.Println("  ✓ Структура большая (копирование дорого)")
    fmt.Println("  ✓ Для согласованности, если хоть один метод pointer")
    fmt.Println("Когда использовать value receiver (T):")
    fmt.Println("  ✓ Структура маленькая и неизменяемая")
    fmt.Println("  ✓ Тип — map, slice, chan (уже ссылочные)")
    fmt.Println("  ✓ Нужна потокобезопасность через копирование")
}

🚀 Запуск программы

# Запуск
go run main.go

# Сборка
go build -o funcs-demo main.go
./funcs-demo

📊 Value receiver vs Pointer receiver

КритерийValue receiver (T)Pointer receiver (*T)
Изменение оригинала❌ Нет (копия)✅ Да
КопированиеКопирует всю структуруКопирует только адрес (8 байт)
Вызов на nilПаникаМожно проверить if r == nil
ПотокобезопасностьБезопаснее (копия)Нужна синхронизация
Реализация интерфейсаT и *TТолько *T

🔑 Ключевые концепции

Что нужно запомнить из этого урока:

💡 Для Node.js разработчика:

← Предыдущий урок Следующий урок →