🐹 Go

Structs e Interfaces em Go

Структуры и интерфейсы в Go: ООП без классов

В Go нет классов — и это сделано намеренно! Вместо них используются структуры (struct) и интерфейсы (interface). Это другой подход к объектно-ориентированному программированию — более простой и гибкий. Давай разберёмся!

Структуры: данные без классов

package main

import "fmt"

// Объявление структуры
type Person struct {
    Name string
    Age  int
    City string
}

type Dog struct {
    Name  string
    Breed string
    Age   int
}

func main() {
    // Создание экземпляра структуры
    alice := Person{
        Name: "Алиса",
        Age:  25,
        City: "Москва",
    }
    
    // Сокращённая форма (порядок важен)
    bob := Person{"Борис", 30, "СПб"}
    
    // Через var (zero values)
    var vera Person
    vera.Name = "Вера"
    vera.Age = 22
    
    // Доступ к полям
    fmt.Println(alice.Name) // Алиса
    fmt.Println(bob.Age)    // 30
    
    // Изменение полей
    alice.City = "Новосибирск"
    fmt.Println(alice)
    
    // Указатель на структуру
    p := &alice
    p.Age = 26 // Go автоматически разыменовывает (*p).Age
    fmt.Println(alice.Age) // 26 — изменилось через указатель!
}

Методы: функции привязанные к структуре

package main

import (
    "fmt"
    "math"
)

type Circle struct {
    X, Y   float64 // Центр
    Radius float64
}

type Rectangle struct {
    Width, Height float64
}

// Метод — функция с "получателем" (receiver)
// (c Circle) — значение-получатель (копия)
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

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

func (c Circle) String() string {
    return fmt.Sprintf("Circle(r=%.2f)", c.Radius)
}

// Методы Rectangle
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

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

// Указатель-получатель — изменяет оригинал
func (c *Circle) Scale(factor float64) {
    c.Radius *= factor // Изменяем оригинал через указатель
}

func main() {
    c := Circle{X: 0, Y: 0, Radius: 5}
    r := Rectangle{Width: 4, Height: 6}
    
    fmt.Printf("Круг: %s\n", c)              // Circle(r=5.00)
    fmt.Printf("Площадь: %.2f\n", c.Area())  // 78.54
    fmt.Printf("Периметр: %.2f\n", c.Perimeter()) // 31.42
    
    fmt.Printf("Прямоугольник: %.0fx%.0f\n", r.Width, r.Height) // 4x6
    fmt.Printf("Площадь: %.2f\n", r.Area()) // 24.00
    
    // Метод с указателем изменяет оригинал
    c.Scale(2) // Радиус становится 10
    fmt.Printf("После Scale(2): %s\n", c) // Circle(r=10.00)
}

Интерфейсы: утиная типизация

«Утиная типизация» — если что-то ходит как утка и крякает как утка, то это утка». В Go: если тип реализует все методы интерфейса — он автоматически реализует интерфейс. Не нужно явно писать «implements»!

package main

import (
    "fmt"
    "math"
)

// Интерфейс Shape
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Структуры
type Circle2 struct{ Radius float64 }
type Rectangle2 struct{ Width, Height float64 }
type Triangle struct{ A, B, C float64 }

// Реализация Shape для Circle2
func (c Circle2) Area() float64      { return math.Pi * c.Radius * c.Radius }
func (c Circle2) Perimeter() float64 { return 2 * math.Pi * c.Radius }

// Реализация Shape для Rectangle2
func (r Rectangle2) Area() float64      { return r.Width * r.Height }
func (r Rectangle2) Perimeter() float64 { return 2 * (r.Width + r.Height) }

// Реализация Shape для Triangle (формула Герона)
func (t Triangle) Area() float64 {
    s := (t.A + t.B + t.C) / 2
    return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C))
}
func (t Triangle) Perimeter() float64 { return t.A + t.B + t.C }

// Функция принимает ЛЮБОЙ Shape
func printShapeInfo(s Shape) {
    fmt.Printf("Площадь: %.2f, Периметр: %.2f\n", s.Area(), s.Perimeter())
}

func totalArea(shapes []Shape) float64 {
    total := 0.0
    for _, s := range shapes {
        total += s.Area()
    }
    return total
}

func main() {
    shapes := []Shape{
        Circle2{Radius: 5},
        Rectangle2{Width: 4, Height: 6},
        Triangle{A: 3, B: 4, C: 5},
    }
    
    for _, s := range shapes {
        printShapeInfo(s)
    }
    
    fmt.Printf("Суммарная площадь: %.2f\n", totalArea(shapes))
}

Пустой интерфейс: any

package main

import "fmt"

// interface{} (или any в Go 1.18+) принимает ЛЮБОЙ тип
func printAnything(value interface{}) {
    fmt.Printf("Тип: %T, Значение: %v\n", value, value)
}

func describe(i interface{}) {
    // Type assertion — проверка и извлечение типа
    switch v := i.(type) {
    case int:
        fmt.Printf("Целое: %d\n", v)
    case string:
        fmt.Printf("Строка: %s\n", v)
    case bool:
        fmt.Printf("Булево: %t\n", v)
    case []int:
        fmt.Printf("Срез int длиной %d\n", len(v))
    default:
        fmt.Printf("Неизвестный тип: %T\n", v)
    }
}

func main() {
    printAnything(42)
    printAnything("привет")
    printAnything(3.14)
    printAnything(true)
    
    describe(42)
    describe("Go")
    describe([]int{1, 2, 3})
    
    // any — алиас для interface{} (Go 1.18+)
    var x any = "строка"
    x = 42
    x = []string{"a", "b"}
    fmt.Println(x)
}

Embedding: встраивание структур

package main

import "fmt"

type Animal struct {
    Name string
    Age  int
}

func (a Animal) Describe() {
    fmt.Printf("%s, %d лет\n", a.Name, a.Age)
}

func (a Animal) Sleep() {
    fmt.Printf("%s спит\n", a.Name)
}

// Dog встраивает Animal (embedding)
type DogE struct {
    Animal           // Встроенная структура
    Breed  string
}

func (d DogE) Bark() {
    fmt.Printf("%s: Гав!\n", d.Name) // Доступ к полям Animal!
}

// Cat тоже встраивает Animal
type CatE struct {
    Animal
    Indoor bool
}

func (c CatE) Meow() {
    fmt.Printf("%s: Мяу!\n", c.Name)
}

// Переопределение метода
func (c CatE) Describe() {
    fmt.Printf("Кот %s, %d лет", c.Name, c.Age)
    if c.Indoor {
        fmt.Print(" (домашний)")
    }
    fmt.Println()
}

func main() {
    dog := DogE{
        Animal: Animal{Name: "Рекс", Age: 3},
        Breed:  "Овчарка",
    }
    
    // Прямой доступ к полям Animal
    fmt.Println(dog.Name)  // Рекс (не dog.Animal.Name!)
    dog.Describe()         // Рекс, 3 лет
    dog.Sleep()            // Рекс спит
    dog.Bark()             // Рекс: Гав!
    
    cat := CatE{
        Animal: Animal{Name: "Мурка", Age: 5},
        Indoor: true,
    }
    cat.Meow()    // Мурка: Мяу!
    cat.Describe() // Кот Мурка, 5 лет (домашний)
    cat.Sleep()    // Мурка спит (унаследован от Animal)
}

Итого

  • ✅ struct — структура данных, аналог класса без наследования
  • ✅ Методы — функции с получателем (t Type) или (t *Type)
  • ✅ Указатель-получатель *Type — изменяет оригинал, значение Type — работает с копией
  • ✅ interface — набор сигнатур методов; тип реализует интерфейс автоматически
  • ✅ Утиная типизация — «если ходит как утка» — явного «implements» нет
  • ✅ interface{} / any — принимает любой тип
  • ✅ Embedding — встраивание структур для переиспользования кода

Методы на указателях vs значениях

package main

import "fmt"

type Temperature struct {
    Celsius float64
}

// Получатель-значение — работает с копией
func (t Temperature) Fahrenheit() float64 {
    return t.Celsius*9/5 + 32
}

// Получатель-указатель — изменяет оригинал
func (t *Temperature) SetCelsius(c float64) {
    t.Celsius = c
}

// Правило: если метод изменяет состояние — используй указатель
// Если только читает — можно значение (но для единообразия лучше тоже указатель)

func main() {
    temp := Temperature{Celsius: 100}
    fmt.Printf("%.1f°C = %.1f°F\n", temp.Celsius, temp.Fahrenheit())
    // 100.0°C = 212.0°F
    
    temp.SetCelsius(37)
    fmt.Printf("Температура тела: %.1f°C = %.1f°F\n", temp.Celsius, temp.Fahrenheit())
    // 37.0°C = 98.6°F
}

Сортировка с интерфейсом sort.Interface

package main

import (
    "fmt"
    "sort"
)

type Student struct {
    Name  string
    Grade int
}

type ByGrade []Student

func (b ByGrade) Len() int           { return len(b) }
func (b ByGrade) Less(i, j int) bool { return b[i].Grade > b[j].Grade } // По убыванию
func (b ByGrade) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }

func main() {
    students := []Student{
        {"Алиса", 95},
        {"Борис", 72},
        {"Вера", 88},
        {"Гриша", 91},
    }
    
    sort.Sort(ByGrade(students))
    
    fmt.Println("Рейтинг студентов:")
    for i, s := range students {
        fmt.Printf("%d. %s: %d\n", i+1, s.Name, s.Grade)
    }
    
    // Современный способ (Go 1.18+)
    sort.Slice(students, func(i, j int) bool {
        return students[i].Name < students[j].Name  // По алфавиту
    })
    fmt.Println("\nПо алфавиту:")
    for _, s := range students {
        fmt.Printf("  %s: %d\n", s.Name, s.Grade)
    }
}

Встроенные интерфейсы стандартной библиотеки

package main

import (
    "fmt"
    "strings"
)

// Stringer — интерфейс для вывода
type Point struct{ X, Y float64 }

func (p Point) String() string {
    return fmt.Sprintf("(%.1f, %.1f)", p.X, p.Y)
}

// io.Writer — запись в поток
type Logger struct {
    prefix string
    buf    strings.Builder
}

func (l *Logger) Write(p []byte) (n int, err error) {
    l.buf.WriteString(l.prefix)
    l.buf.Write(p)
    return len(p), nil
}

func (l *Logger) String() string { return l.buf.String() }

func main() {
    p := Point{3.5, 4.7}
    fmt.Println(p)  // (3.5, 4.7) — вызывает String() автоматически!
    
    log := &Logger{prefix: "[INFO] "}
    fmt.Fprintf(log, "Сервер запущен на порту %d\n", 8080)
    fmt.Print(log.String())  // [INFO] Сервер запущен на порту 8080
}

Итого

  • ✅ struct — структура данных, аналог класса без наследования
  • ✅ Методы: func (r ReceiverType) MethodName()
  • ✅ Получатель-указатель *T — изменяет оригинал
  • ✅ Получатель-значение T — работает с копией
  • ✅ interface — набор сигнатур методов, реализуется неявно
  • ✅ Утиная типизация — если метод есть — интерфейс реализован
  • ✅ interface{} / any — принимает любой тип
  • ✅ Embedding — встраивание для переиспользования кода
  • ✅ type assertion: v.(Type) или v, ok := x.(Type)

Работа с JSON

package main

import (
    "encoding/json"
    "fmt"
)

// Теги для JSON сериализации
type User struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email,omitempty"` // omitempty — пропустить если пусто
    Password string `json:"-"`               // - — всегда пропускать
    Age      int    `json:"age"`
}

func main() {
    // Структура → JSON (Marshal)
    user := User{ID: 1, Name: "Алиса", Email: "alice@example.com", Password: "secret123", Age: 25}
    
    data, err := json.MarshalIndent(user, "", "  ")
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }
    fmt.Println(string(data))
    // {
    //   "id": 1,
    //   "name": "Алиса",
    //   "email": "alice@example.com",
    //   "age": 25
    // }
    // Password НЕТ — помечен как json:"-"
    
    // JSON → Структура (Unmarshal)
    jsonStr := `{"id":2,"name":"Борис","age":30}`
    var user2 User
    if err := json.Unmarshal([]byte(jsonStr), &user2); err != nil {
        fmt.Println("Ошибка:", err)
        return
    }
    fmt.Printf("Пользователь: %s, %d лет\n", user2.Name, user2.Age)
    
    // Массив структур
    users := []User{
        {ID: 1, Name: "Алиса", Age: 25},
        {ID: 2, Name: "Борис", Age: 30},
    }
    data2, _ := json.Marshal(users)
    fmt.Println(string(data2))
}

Практика: стек и очередь

package main

import "fmt"

// Стек на основе среза
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T)  { s.items = append(s.items, item) }
func (s *Stack[T]) Pop() (T, bool) {
    var zero T
    if len(s.items) == 0 { return zero, false }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}
func (s *Stack[T]) Peek() (T, bool) {
    var zero T
    if len(s.items) == 0 { return zero, false }
    return s.items[len(s.items)-1], true
}
func (s *Stack[T]) Size() int { return len(s.items) }

func main() {
    stack := &Stack[int]{}
    stack.Push(1); stack.Push(2); stack.Push(3)
    
    fmt.Println("Стек (LIFO):")
    for stack.Size() > 0 {
        if val, ok := stack.Pop(); ok {
            fmt.Println(val)  // 3, 2, 1
        }
    }
}

Паттерн Option: цепочка методов

package main

import "fmt"

// Functional options — гибкая конфигурация
type Server struct {
    host    string
    port    int
    timeout int
    debug   bool
}

type Option func(*Server)

func WithHost(host string) Option {
    return func(s *Server) { s.host = host }
}

func WithPort(port int) Option {
    return func(s *Server) { s.port = port }
}

func WithDebug(debug bool) Option {
    return func(s *Server) { s.debug = debug }
}

func NewServer(opts ...Option) *Server {
    s := &Server{host: "localhost", port: 8080, timeout: 30}
    for _, opt := range opts {
        opt(s)
    }
    return s
}

func (s *Server) Info() string {
    return fmt.Sprintf("Server{%s:%d, debug:%v, timeout:%ds}",
        s.host, s.port, s.debug, s.timeout)
}

func main() {
    // Минимальная конфигурация
    s1 := NewServer()
    fmt.Println(s1.Info())
    
    // Полная конфигурация
    s2 := NewServer(
        WithHost("0.0.0.0"),
        WithPort(9090),
        WithDebug(true),
    )
    fmt.Println(s2.Info())
}

Полезные паттерны работы со структурами

package main

import "fmt"

// Конструктор (по соглашению New...)
type Point struct{ X, Y float64 }

func NewPoint(x, y float64) *Point { return &Point{x, y} }

func (p *Point) Distance(other *Point) float64 {
    dx, dy := p.X-other.X, p.Y-other.Y
    return math.Sqrt(dx*dx + dy*dy)
}

func (p *Point) String() string { return fmt.Sprintf("(%.1f,%.1f)", p.X, p.Y) }

func main() {
    p1 := NewPoint(0, 0)
    p2 := NewPoint(3, 4)
    fmt.Println(p1, p2) // (0.0,0.0) (3.0,4.0)
}