Структуры и интерфейсы
Несмотря на то, что вполне можно писать программы на 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
.