Go Курс

Структуры, методы и интерфейсы в Go

Самоучитель Python 3, собранный из материалов данного сайта.Предназначен в основном для тех, кто хочет изучить язык программирования Python с нуля.

Введение: Зачем нужны структуры и интерфейсы?

Представь, что ты строишь робота из конструктора LEGO. У тебя есть разные детали: колёса, моторы, датчики. Каждая деталь имеет свои характеристики (цвет, размер, форма). В программировании на Go такие "детали" называются структурами. А интерфейсы — это как инструкции, которые говорят: "Любая деталь, которая умеет крутиться, может быть колёсами".

В этом уроке мы научимся создавать свои типы данных с помощью структур и объединять их через интерфейсы. Это сделает код понятнее, как если бы ты собирал робота по чёткой схеме.

1. Структуры: создаём свои типы данных

1.1. Что такое структура?

Структура (struct) — это способ объединить несколько переменных разных типов в одну логическую группу. Например, чтобы описать книгу, тебе нужно: название (строка), автор (строка), год выпуска (число) и количество страниц (число).

Аналогия из жизни: Представь анкету ученика. В ней есть поля: имя, фамилия, возраст, класс. Анкета — это структура, а каждое поле — её часть.

1.2. Объявление структуры

В Go структура объявляется так:

// Объявляем структуру "Книга"
type Book struct {
    Title  string // Название книги
    Author string // Автор
    Year   int    // Год издания
    Pages  int    // Количество страниц
}

Ключевое слово type говорит: "Я создаю новый тип". Book — имя структуры. Внутри фигурных скобок перечислены поля с типами.

1.3. Создание экземпляров структуры

Теперь создадим конкретную книгу:

// Способ 1: с указанием имён полей
myBook := Book{
    Title: "Война миров",
    Author: "Герберт Уэллс",
    Year: 1898,
    Pages: 304,
}

// Способ 2: без имён полей (по порядку)
anotherBook := Book{"Маленький принц", "Антуан де Сент-Экзюпери", 1943, 96}

// Способ 3: пустая структура с последующим заполнением
var emptyBook Book
emptyBook.Title = "Неизвестная книга"

Важно! Второй способ опасен — если перепутать порядок, программа может работать неправильно. Лучше всегда указывать имена полей.

1.4. Доступ к полям структуры

Обращаться к полям можно через точку:

fmt.Println("Название:", myBook.Title)
fmt.Println("Автор:", myBook.Author)

// Изменяем поле
myBook.Year = 1899
fmt.Println("Новый год:", myBook.Year)

1.5. Структуры с методами

Метод — это функция, которая привязана к структуре. Например, добавим метод, который выводит информацию о книге:

// Структура "Книга"
type Book struct {
    Title  string
    Author string
    Year   int
}

// Метод для структуры Book
func (b Book) PrintInfo() {
    fmt.Println("Книга:", b.Title)
    fmt.Println("Автор:", b.Author)
    fmt.Println("Год:", b.Year)
}

// Использование
func main() {
    myBook := Book{Title: "1984", Author: "Джордж Оруэлл", Year: 1949}
    myBook.PrintInfo() // Вызов метода
}

Обрати внимание: (b Book) перед именем метода — это получатель. Он связывает метод с конкретным экземпляром структуры.

1.6. Указатели на структуры

Если метод должен изменять поля структуры, нужно использовать указатель:

func (b *Book) ChangeYear(newYear int) {
    b.Year = newYear // Меняем оригинал, а не копию
}

// Использование
func main() {
    myBook := Book{Title: "Метро 2033", Year: 2005}
    myBook.ChangeYear(2010) // Теперь год изменился
    fmt.Println(myBook.Year) // Выведет: 2010
}

2. Интерфейсы: общий язык для разных структур

2.1. Зачем нужны интерфейсы?

Представь, что у тебя есть разные животные: собака, кошка, птица. Каждое умеет издавать звук, но по-разному. Интерфейс Animal может описать общее поведение: "любое животное должно уметь издавать звук".

Аналогия: USB-порт. К нему можно подключить флешку, клавиатуру, мышь — любые устройства, которые поддерживают протокол USB. Интерфейс в Go работает так же: он определяет, какие методы должно иметь устройство, чтобы его можно было использовать.

2.2. Объявление интерфейса

Интерфейс содержит только сигнатуры методов (без реализации):

// Интерфейс "Животное"
type Animal interface {
    Speak() string // Любое животное должно уметь говорить
}

2.3. Реализация интерфейса

В Go не нужно явно указывать, что структура реализует интерфейс. Достаточно, чтобы у неё были все методы, перечисленные в интерфейсе. Это называется утиная типизация: "Если это ходит как утка и крякает как утка, то это утка".

// Структура "Собака"
type Dog struct {
    Name string
}

// Метод Speak для собаки
func (d Dog) Speak() string {
    return "Гав-гав!"
}

// Структура "Кошка"
type Cat struct {
    Name string
}

// Метод Speak для кошки
func (c Cat) Speak() string {
    return "Мяу!"
}

// Теперь и Dog, и Cat реализуют интерфейс Animal

2.4. Использование интерфейсов

Главная сила интерфейсов — в функциях, которые могут работать с любым типом, реализующим интерфейс:

// Функция, которая принимает любого Animal
func MakeSound(a Animal) {
    fmt.Println(a.Speak())
}

func main() {
    dog := Dog{Name: "Шарик"}
    cat := Cat{Name: "Мурка"}

    MakeSound(dog) // Выведет: Гав-гав!
    MakeSound(cat) // Выведет: Мяу!
}

2.5. Интерфейсы с несколькими методами

Интерфейс может содержать несколько методов. Например, опишем транспортное средство:

// Интерфейс "Транспорт"
type Vehicle interface {
    Start() string
    Stop() string
    Speed() int
}

// Структура "Автомобиль"
type Car struct {
    Brand string
}

func (c Car) Start() string {
    return "Машина завелась"
}

func (c Car) Stop() string {
    return "Машина остановилась"
}

func (c Car) Speed() int {
    return 120
}

// Структура "Велосипед"
type Bike struct {
    Type string
}

func (b Bike) Start() string {
    return "Начал крутить педали"
}

func (b Bike) Stop() string {
    return "Остановился"
}

func (b Bike) Speed() int {
    return 25
}

2.6. Пустой интерфейс

В Go есть специальный пустой интерфейс interface{}, который может хранить значение любого типа. Это как "коробка для всего":

var anything interface{}

anything = 42
fmt.Println(anything) // 42

anything = "Привет"
fmt.Println(anything) // Привет

anything = Dog{Name: "Бобик"}
fmt.Println(anything) // {Бобик}

Но будь осторожен: чтобы использовать значение из пустого интерфейса, нужно утверждение типа:

value := anything.(Dog) // Преобразуем к типу Dog
fmt.Println(value.Name)

3. Практические примеры

3.1. Пример: библиотека книг

Создадим программу для управления библиотекой:

package main

import "fmt"

// Структура "Книга"
type Book struct {
    Title  string
    Author string
    Year   int
}

// Структура "Библиотека" содержит срез книг
type Library struct {
    Books []Book
}

// Метод для добавления книги
func (l *Library) AddBook(b Book) {
    l.Books = append(l.Books, b)
}

// Метод для поиска книг по автору
func (l Library) FindByAuthor(author string) []Book {
    var result []Book
    for _, book := range l.Books {
        if book.Author == author {
            result = append(result, book)
        }
    }
    return result
}

func main() {
    // Создаём библиотеку
    myLibrary := Library{}

    // Добавляем книги
    myLibrary.AddBook(Book{Title: "1984", Author: "Оруэлл", Year: 1949})
    myLibrary.AddBook(Book{Title: "Скотный двор", Author: "Оруэлл", Year: 1945})
    myLibrary.AddBook(Book{Title: "Мастер и Маргарита", Author: "Булгаков", Year: 1966})

    // Ищем книги Оруэлла
    orwellBooks := myLibrary.FindByAuthor("Оруэлл")
    fmt.Println("Книги Оруэлла:")
    for _, book := range orwellBooks {
        fmt.Printf("- %s (%d)\n", book.Title, book.Year)
    }
}

3.2. Пример: фигуры с интерфейсом

Создадим интерфейс для геометрических фигур:

package main

import (
    "fmt"
    "math"
)

// Интерфейс "Фигура"
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Структура "Круг"
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
}

// Структура "Прямоугольник"
type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

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

// Функция, печатающая свойства фигуры
func PrintShapeInfo(s Shape) {
    fmt.Printf("Площадь: %.2f\n", s.Area())
    fmt.Printf("Периметр: %.2f\n", s.Perimeter())
}

func main() {
    circle := Circle{Radius: 5}
    rectangle := Rectangle{Width: 4, Height: 6}

    fmt.Println("Круг:")
    PrintShapeInfo(circle)

    fmt.Println("\nПрямоугольник:")
    PrintShapeInfo(rectangle)
}

4. Продвинутые концепции

4.1. Вложенные структуры

Структуры могут содержать другие структуры. Это как матрёшка:

// Структура "Адрес"
type Address struct {
    City    string
    Street  string
    House   int
}

// Структура "Человек" содержит адрес
type Person struct {
    Name    string
    Age     int
    Address Address // Вложенная структура
}

func main() {
    person := Person{
        Name: "Иван",
        Age: 25,
        Address: Address{
            City:   "Москва",
            Street: "Тверская",
            House:  10,
        },
    }

    // Доступ к вложенным полям
    fmt.Println(person.Name, "живёт в городе", person.Address.City)
}

4.2. Интерфейсы как параметры функций

Интерфейсы можно передавать в функции, что позволяет писать универсальный код:

// Функция, которая находит фигуру с максимальной площадью
func MaxArea(shapes []Shape) Shape {
    var maxShape Shape
    maxArea := 0.0
    for _, s := range shapes {
        if s.Area() > maxArea {
            maxArea = s.Area()
            maxShape = s
        }
    }
    return maxShape
}

func main() {
    shapes := []Shape{
        Circle{Radius: 3},
        Rectangle{Width: 5, Height: 8},
        Circle{Radius: 10},
    }

    biggest := MaxArea(shapes)
    fmt.Printf("Самая большая фигура имеет площадь: %.2f\n", biggest.Area())
}

4.3. Ошибки как интерфейсы

В Go ошибки реализованы через интерфейс error. Любая структура с методом Error() string может быть ошибкой:

// Свой тип ошибки
type MyError struct {
    Code    int
    Message string
}

func (e MyError) Error() string {
    return fmt.Sprintf("Ошибка %d: %s", e.Code, e.Message)
}

// Функция, возвращающая ошибку
func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, MyError{Code: 100, Message: "Деление на ноль"}
    }
    return a / b, nil
}

func main() {
    result, err := Divide(10, 0)
    if err != nil {
        fmt.Println("Произошла ошибка:", err)
    } else {
        fmt.Println("Результат:", result)
    }
}

5. Практические задания

Задание 1: Каталог фильмов

Уровень сложности: ★☆☆

Создай структуру Film с полями: Title (строка), Director (строка), Year (int), Rating (float64). Затем:

  1. Создай 3 фильма (придумай любые)
  2. Напиши метод PrintInfo(), который выводит всю информацию о фильме
  3. Напиши функцию FindBestFilm(films []Film) Film, которая возвращает фильм с самым высоким рейтингом

Задание 2: Зоопарк с интерфейсами

Уровень сложности: ★★☆

Создай интерфейс Animal с методами Eat() string и Move() string. Реализуй его для трёх разных животных (например, Лев, Пингвин, Змея). Напиши функцию DescribeAnimal(a Animal), которая выводит, как животное ест и двигается.

Подсказки:

  • Лев: ест мясо, ходит на четырёх лапах
  • Пингвин: ест рыбу, плавает и ходит
  • Змея: ест мышей, ползает

Задание 3: Система заказов (сложное)

Уровень сложности: ★★★

Создай структуру Order (заказ) с полями: ID (int), Items ([]string — список товаров), Total (float64 — общая стоимость). Реализуй:

  1. Метод AddItem(name string, price float64) — добавляет товар и увеличивает сумму
  2. Интерфейс Discount с методом Calculate(price float64) float64 (возвращает цену со скидкой)
  3. Две структуры, реализующие скидку: PercentDiscount (процентная скидка, например 10%) и FixedDiscount (фиксированная скидка, например 50 рублей)
  4. Метод для заказа ApplyDiscount(d Discount), который применяет скидку к общей сумме

Пример использования:

order := Order{ID: 1}
order.AddItem("Ноутбук", 50000)
order.AddItem("Мышь", 1500)

percentDisc := PercentDiscount{Percent: 10}
order.ApplyDiscount(percentDisc)
fmt.Println("Итого со скидкой 10%:", order.Total) // Должно быть 46350

fixedDisc := FixedDiscount{Amount: 1000}
order.ApplyDiscount(fixedDisc)
fmt.Println("Итого с фиксированной скидкой:", order.Total) // Должно быть 45350

Заключение

Сегодня ты научился:

  • Создавать свои типы данных с помощью структур
  • Добавлять к ним методы для выполнения действий
  • Использовать интерфейсы для объединения разных типов
  • Писать гибкий код, который работает с любыми структурами, реализующими интерфейс

Эти инструменты — основа объектно-ориентированного программирования в Go. Теперь ты можешь создавать сложные программы, которые легко расширять и поддерживать. Попробуй выполнить задания — это поможет закрепить материал!

Совет: Если что-то непонятно, перечитай раздел с аналогиями из жизни. Представь, что структура — это форма для печенья, а интерфейс — это рецепт, которому должны следовать разные формы.