Структуры и интерфейсы

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

package main

import ("fmt"; "math")

func distance(x1, y1, x2, y2 float64) float64 {
    a := x2 – x1
    b := y2 – y1
    return math.Sqrt(a*a + b*b)
}
func rectangleArea(x1, y1, x2, y2 float64) float64 {
    l := distance(x1, y1, x1, y2)
    w := distance(x1, y1, x2, y1)
    return l * w
}
func circleArea(x, y, r float64) float64 {
    return math.Pi * r*r
}
func main() {
    var rx1, ry1 float64 = 0, 0
    var rx2, ry2 float64 = 10, 10
    var cx, cy, cr float64 = 0, 0, 5

    fmt.Println(rectangleArea(rx1, ry1, rx2, ry2))
    fmt.Println(circleArea(cx, cy, cr))
}

Отслеживание всех переменных мешает нам понять, что делает программа, и наверняка приведет к ошибкам.

Структуры

С помощью структур эту программу можно сделать гораздо лучше. Структура — это тип, содержащий именованные поля. Например, мы можем представить круг таким образом:

type Circle struct {
    x float64
    y float64
    r float64
}

Ключевое словоtypeвводит новый тип. За ним следует имя нового типа (Circle) и ключевое словоstruct, которое говорит, что мы определяем структуру и список полей внутри фигурных скобок. Каждое поле имеет имя и тип. Как и с функциями, мы можем объединять поля одного типа:

type Circle struct {
    x, y, r float64
}

Инициализация

Мы можем создать экземпляр нового типаCircleнесколькими способами:

var c Circle

Подобно другим типами данных, будет создана локальная переменная типаCircle, чьи поля по умолчанию будут равны нулю (0дляint,0.0дляfloat,""дляstring,nilдля указателей, …). Также, для создания экземпляра можно использовать функциюnew.

c := new(Circle)

Это выделит память для всех полей, присвоит каждому из них нулевое значение и вернет указатель (*Circle). Часто, при создании структуры мы хотим присвоить полям структуры какие-нибудь значения. Существует два способа сделать это. Первый способ:

c := Circle{x: 0, y: 0, r: 5}

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

c := Circle{0, 0, 5}

Поля

Получить доступ к полям можно с помощью оператора.(точка):

fmt.Println(c.x, c.y, c.r)
c.x = 10
c.y = 5

Давайте изменим функциюcircleAreaтак, чтобы она использовала структуруCircle:

func circleArea(c Circle) float64 {
    return math.Pi * c.r*c.r
}

В функцииmainу нас будет:

c := Circle{0, 0, 5}
fmt.Println(circleArea(c))

Очень важно помнить о том, что аргументы в Go всегда копируются. Если мы попытаемся изменить любое поле в функцииcircleArea, оригинальная переменная не изменится. Именно поэтому мы будем писать функции так:

func circleArea(c *Circle) float64 {
    return math.Pi * c.r*c.r
}

И изменимmain:

c := Circle{0, 0, 5}
fmt.Println(circleArea(&c))

Методы

Не смотря на то, что программа стала лучше, мы все еще можем значительно её улучшить, используя метод — функцию особого типа:

func (c *Circle) area() float64 {
    return math.Pi * c.r*c.r
}

Между ключевым словомfuncи именем функции мы добавили «получателя». Получатель похож на параметр — у него есть имя и тип, но объявление функции таким способом позволяет нам вызывать функцию с помощью оператора.:

fmt.Println(c.area())

Это гораздо проще прочесть, нам не нужно использовать оператор&(Go автоматически предоставляет доступ к указателю наCircleдля этого метода), и поскольку эта функция может быть использована только дляCircleмы можем назвать её простоarea.

Давайте сделаем то же самое с прямоугольником:

type Rectangle struct {
    x1, y1, x2, y2 float64
}
func (r *Rectangle) area() float64 {
    l := distance(r.x1, r.y1, r.x2, r.y2)
    w := distance(r.x1, r.y1, r.x2, r.y1)
    return l * w
}

Вmainбудет написано:

r := Rectangle{0, 0, 10, 10}
fmt.Println(r.area())

Встраиваемые типы

Обычно, поля структур представляют отношения принадлежности (включения). Например, уCircle(круга) естьradius(радиус). Предположим, у нас есть структураPerson(личность):

type Person struct {
    Name string
}
func (p *Person) Talk() {
    fmt.Println("Hi, my name is", p.Name)
}

И если мы хотим создать новую структуруAndroid, то можем сделать так:

type Android struct {
    Person Person
    Model string
}

Это будет работать, но мы можем захотеть создать другое отношение. Сейчас у андроида «есть» личность, можем ли мы описать отношение андроид «является» личностью. Go поддерживает подобные отношения с помощью встраиваемых типов, также называемых анонимными полями. Выглядят они так:

type Android struct {
    Person
    Model string
}

Мы использовали тип (Person) и не написали его имя. Объявленная таким способом структура доступна через имя типа:

a := new(Android)
a.Person.Talk()

Но мы также можем вызвать любой методPersonпрямо изAndroid:

a := new(Android)
a.Talk()

Это отношение работает достаточно интуитивно: личности могут говорить, андроид это личность, значит андроид может говорить.

Интерфейсы

Вы могли заметить, что названия методов для вычисления площади круга и прямоугольника совпадают. Это было сделано не случайно. И в реальной жизни и в программировании отношения могут быть очень похожими. В Go есть способ сделать эти случайные сходства явными с помощью типа называемого интерфейсом. Пример интерфейса для фигуры (Shape):

type Shape interface {
    area() float64
}

Как и структуры, интерфейсы создаются с помощью ключевого словаtype, за которым следует имя интерфейса и ключевое словоinterface. Однако, вместо того, чтобы определять поля, мы определяем «множество методов». Множество методов - это список методов, которые будут использоваться для «реализации» интерфейса.

В нашем случае уRectangleиCircleесть методarea, который возвращаетfloat64, получается они оба реализуют интерфейсShape. Само по себе это не очень полезно, но мы можем использовать интерфейсы как аргументы в функциях:

func totalArea(shapes ...Shape) float64 {
    var area float64
    for _, s := range shapes {
        area += s.area()
    }
    return area
}

Мы будет вызывать эту функцию так:

fmt.Println(totalArea(&c, &r))

Интерфейсы также могут быть использованы в качестве полей:

type MultiShape struct {
    shapes []Shape
}

Мы можем даже хранить вMultiShapeданныеShape, определив в ней методarea:

func (m *MultiShape) area() float64 {
    var area float64
    for _, s := range m.shapes {
        area += s.area()
    }
    return area
}

ТеперьMultiShapeможет содержатьCircle,Rectangleи даже другиеMultiShape.

Задачи

  • Какая разница между методом и функцией?

  • В каких случаях могут пригодиться встроенные (скрытые) поля?

  • Добавьте новый методperimeterв интерфейсShape, который будет вычислять периметр фигуры. Имплементируйте этот метод дляCircleиRectangle.

results matching ""

    No results matching ""