Урок 8. Структуры, методы и интерфейсы
Классов в Go нет. Свои типы данных описывают структурами, поведение добавляют методами, а общий интерфейс для разных типов задают интерфейсами. Этого хватает для всего, что в других языках делают классами.
Структура
Структура (struct) — набор именованных полей. Новый тип объявляют через type:
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}- К полю обращаются через точку:
p.Name. - В литерале
Person{Name: "Анна", Age: 20}поля названы по именам — так понятнее, и порядок не важен. Поля, которые не указаны, получают нулевые значения. %vпечатает структуру в фигурных скобках,%+v— с именами полей.- Поля с большой буквы видны из других пакетов, с маленькой — только внутри своего.
Структуры, как и числа, копируются при присваивании: после q := p изменение q.Age не меняет p.
Указатели
Указатель — значение, которое хранит адрес другой переменной. &p — адрес p, тип *Person — «указатель на Person». Через указатель функция может изменить чужую переменную:
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 подставляет * сам.
Указатель, которому не дали адрес, равен nil. Обращение к полю через него — паника invalid memory address or nil pointer dereference. Если функция возвращает указатель, проверяйте его на nil или вместе с ним проверяйте ошибку.
Методы
Метод — функция, привязанная к типу. Тип пишут в скобках перед именем — это приёмник:
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)— метод работает с самой переменной и может её менять. - Приёмник-значение
(c Counter)— метод получает копию. ПоэтомуIncCopyничего не изменил. - Вызов
c.Inc()для переменнойcработает без&: Go берёт адрес сам.
Правило на практике: если хоть один метод меняет структуру, делайте приёмники всех методов этого типа указателями — так поведение не путается. В примере выше правило нарушено нарочно, чтобы показать разницу.
Конструктор
В Go нет конструкторов. Вместо них пишут обычную функцию с именем NewИмяТипа, которая создаёт и возвращает значение, готовое к работе:
func NewCounter(start int) *Counter {
return &Counter{n: start}
}Интерфейсы
Интерфейс — список методов. Любой тип, у которого есть все эти методы, подходит под интерфейс автоматически — писать implements не нужно:
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 принято делать маленькими, из одного-двух методов.
Интерфейс Stringer
Пакет fmt проверяет, есть ли у значения метод String() string, и если есть — печатает его результат:
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 по типу:
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 не стоит: компилятор перестаёт проверять типы, и ошибки всплывают только при запуске.
Встраивание вместо наследования
Если в структуру вписать другой тип без имени поля, его поля и методы становятся доступны напрямую:
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.
Коротко
type Имя struct { … }— свой тип; литералИмя{Поле: значение}.&x— адрес,*T— указатель наT; через указатель функция меняет исходную переменную.- Метод — функция с приёмником:
func (c *Counter) Inc(). Приёмник-указатель меняет значение, приёмник-значение работает с копией. - Интерфейс — набор методов; тип подходит под интерфейс сам, если у него есть эти методы.
String() stringменяет вид вfmt.Println;any— любой тип, проверка —switch v.(type).