Введение: Зачем нужны структуры и интерфейсы?
Представь, что ты строишь робота из конструктора 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). Затем:
- Создай 3 фильма (придумай любые)
- Напиши метод
PrintInfo(), который выводит всю информацию о фильме - Напиши функцию
FindBestFilm(films []Film) Film, которая возвращает фильм с самым высоким рейтингом
Задание 2: Зоопарк с интерфейсами
Уровень сложности: ★★☆
Создай интерфейс Animal с методами Eat() string и Move() string. Реализуй его для трёх разных животных (например, Лев, Пингвин, Змея). Напиши функцию DescribeAnimal(a Animal), которая выводит, как животное ест и двигается.
Подсказки:
- Лев: ест мясо, ходит на четырёх лапах
- Пингвин: ест рыбу, плавает и ходит
- Змея: ест мышей, ползает
Задание 3: Система заказов (сложное)
Уровень сложности: ★★★
Создай структуру Order (заказ) с полями: ID (int), Items ([]string — список товаров), Total (float64 — общая стоимость). Реализуй:
- Метод
AddItem(name string, price float64)— добавляет товар и увеличивает сумму - Интерфейс
Discountс методомCalculate(price float64) float64(возвращает цену со скидкой) - Две структуры, реализующие скидку:
PercentDiscount(процентная скидка, например 10%) иFixedDiscount(фиксированная скидка, например 50 рублей) - Метод для заказа
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. Теперь ты можешь создавать сложные программы, которые легко расширять и поддерживать. Попробуй выполнить задания — это поможет закрепить материал!
Совет: Если что-то непонятно, перечитай раздел с аналогиями из жизни. Представь, что структура — это форма для печенья, а интерфейс — это рецепт, которому должны следовать разные формы.