Глиф Учебники

Урок 8. Структуры, методы и интерфейсы

Курс «Go» · урок 8 из 10 · 4 минуты чтения

Классов в Go нет. Свои типы данных описывают структурами, поведение добавляют методами, а общий интерфейс для разных типов задают интерфейсами. Этого хватает для всего, что в других языках делают классами.

Структура

Структура (struct) — набор именованных полей. Новый тип объявляют через type:

Go
package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    p := Person{Name: "Анна", Age: 20}
    fmt.Println(p.Name, p.Age)
    p.Age++
    fmt.Println(p)
    fmt.Printf("%+v\n", p)

    var empty Person
    fmt.Printf("%+v\n", empty)
}
Вывод
Анна 20
{Анна 21}
{Name:Анна Age:21}
{Name: Age:0}

Структуры, как и числа, копируются при присваивании: после q := p изменение q.Age не меняет p.

Указатели

Указатель — значение, которое хранит адрес другой переменной. &p — адрес p, тип *Person — «указатель на Person». Через указатель функция может изменить чужую переменную:

Go
package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func birthday(p *Person) {
    p.Age++
}

func main() {
    anna := Person{Name: "Анна", Age: 20}
    birthday(&anna)
    fmt.Println(anna.Age)

    ptr := &anna
    ptr.Name = "Анна К."
    fmt.Println(anna.Name)
}
Вывод
21
Анна К.

К полям через указатель обращаются той же точкой: p.Age вместо (*p).Age — Go подставляет * сам.

main birthday(&anna) anna Person Name "Анна" Age 20 21 birthday(p *Person) p в p — адрес anna, а не копия p.Age++ меняется сама anna: после вызова Age = 21
birthday получает не копию anna, а её адрес &anna: через указатель p строка p.Age++ меняет саму переменную anna, и Age становится 21.

Указатель, которому не дали адрес, равен nil. Обращение к полю через него — паника invalid memory address or nil pointer dereference. Если функция возвращает указатель, проверяйте его на nil или вместе с ним проверяйте ошибку.

Методы

Метод — функция, привязанная к типу. Тип пишут в скобках перед именем — это приёмник:

Go
package main

import "fmt"

type Counter struct {
    n int
}

func (c *Counter) Inc() {
    c.n++
}

func (c Counter) IncCopy() {
    c.n++ // меняется копия, а не исходный счётчик
}

func (c Counter) Value() int {
    return c.n
}

func main() {
    var c Counter
    c.Inc()
    c.Inc()
    c.IncCopy()
    fmt.Println(c.Value())
}
Вывод
2
приёмник-указатель (c *Counter) Inc() c.n 0 c.Inc() c.n 1 c.Inc() c.n 2 метод меняет саму переменную c приёмник-значение (c Counter) IncCopy() c.n 2 c.IncCopy() c.n 3 копия выброшена c осталась 2 c.Value() 2
Inc с приёмником-указателем дважды увеличивает саму c до 2, а IncCopy получает копию: копия становится 3 и выбрасывается, поэтому c.Value() возвращает 2.

Правило на практике: если хоть один метод меняет структуру, делайте приёмники всех методов этого типа указателями — так поведение не путается. В примере выше правило нарушено нарочно, чтобы показать разницу.

Конструктор

В Go нет конструкторов. Вместо них пишут обычную функцию с именем NewИмяТипа, которая создаёт и возвращает значение, готовое к работе:

Go
func NewCounter(start int) *Counter {
    return &Counter{n: start}
}

Интерфейсы

Интерфейс — список методов. Любой тип, у которого есть все эти методы, подходит под интерфейс автоматически — писать implements не нужно:

Go
package main

import (
    "fmt"
    "math"
)

type Shape interface {
    Area() float64
}

type Rect struct {
    W, H float64
}

type Circle struct {
    R float64
}

func (r Rect) Area() float64 {
    return r.W * r.H
}

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

func main() {
    shapes := []Shape{Rect{W: 3, H: 4}, Circle{R: 1}}
    total := 0.0
    for _, s := range shapes {
        fmt.Printf("%T: %.2f\n", s, s.Area())
        total += s.Area()
    }
    fmt.Printf("всего: %.2f\n", total)
}
Вывод
main.Rect: 12.00
main.Circle: 3.14
всего: 15.14

В срезе []Shape лежат значения разных типов, и для каждого вызывается его собственный Area. Функция, которая принимает Shape, будет работать с любой фигурой — даже с той, которую напишут позже. Интерфейсы в Go принято делать маленькими, из одного-двух методов.

Shape interface Area() float64 Rect W, H float64 Area() float64 r.W * r.H Circle R float64 Area() float64 math.Pi * c.R * c.R подходят сами []Shape for … s.Area() Rect{W: 3, H: 4} 12.00 Circle{R: 1} 3.14 всего: 15.14 у каждой фигуры — свой Area
Rect и Circle подходят под интерфейс Shape сами, потому что у обоих есть метод Area() float64; в цикле по срезу фигур для каждой вызывается свой Area — 12.00 и 3.14, всего 15.14.

Интерфейс Stringer

Пакет fmt проверяет, есть ли у значения метод String() string, и если есть — печатает его результат:

Go
package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%s (%d)", p.Name, p.Age)
}

func main() {
    p := Person{Name: "Борис", Age: 31}
    fmt.Println(p)
}
Вывод
Борис (31)

Так устроены многие интерфейсы стандартной библиотеки: error — это тоже интерфейс с одним методом Error() string.

any и проверка типа

Интерфейс без методов interface{} (короткое имя — any) подходит под любой тип. Узнать, что в нём лежит, помогает switch по типу:

Go
package main

import "fmt"

func describe(v any) string {
    switch x := v.(type) {
    case int:
        return fmt.Sprintf("целое %d", x)
    case string:
        return fmt.Sprintf("строка %q", x)
    default:
        return fmt.Sprintf("другое: %T", x)
    }
}

func main() {
    fmt.Println(describe(42))
    fmt.Println(describe("go"))
    fmt.Println(describe(3.5))
}
Вывод
целое 42
строка "go"
другое: float64

Один тип проверяют короче: s, ok := v.(string) — ok будет true, если внутри строка. Без ok (s := v.(string)) при другом типе случится паника. Злоупотреблять any не стоит: компилятор перестаёт проверять типы, и ошибки всплывают только при запуске.

Встраивание вместо наследования

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

Go
type Animal struct {
    Name string
}

func (a Animal) Hello() string {
    return "Это " + a.Name
}

type Dog struct {
    Animal
    Breed string
}

У Dog{Animal: Animal{Name: "Шарик"}, Breed: "дворняга"} можно вызвать Hello() и прочитать Name. Это не наследование: Dog не становится Animal, и передать Dog туда, где ждут Animal, нельзя. Зато Dog подходит под все интерфейсы, которым соответствует Animal.

Коротко

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

1. Как тип в Go начинает соответствовать интерфейсу?
2. Что выведет программа?
Go
package main

import "fmt"

type Box struct {
    Size int
}

func grow(b Box) {
    b.Size = 10
}

func main() {
    b := Box{Size: 1}
    grow(b)
    fmt.Println(b.Size)
}
3. Какой приёмник нужен методу, который меняет поля структуры?
4. Что выведет fmt.Printf("%+v", p) для p := Person{Name: "Ира", Age: 9}?