1. Code
  2. Coding Fundamentals

Let's Go: объектно-ориентированное программирование на Голанге

Scroll to top

Russian (Pусский) translation by Anna k.Ivanova (you can also view the original English article)

Go - это странная смесь старых и новых идей. У него очень освежающий подход, при котором он не боится отбросить устоявшиеся представления о том, «как делать вещи». Многие люди даже не уверены, является ли Go объектно-ориентированным языком. Позвольте мне сказать это прямо сейчас. Является!

В этом руководстве вы узнаете обо всех тонкостях объектно-ориентированного проектирования в Go, о том, как такие основы объектно-ориентированного программирования, как инкапсуляция, наследование и полиморфизм, выражены в Go, и сравнение Go с другими языками.

Философия дизайна Go

Корни Go основаны на C и, в более широком смысле, на семействе Algol. Кен Томпсон в шутку сказал, что Роб Пайк, Роберт Грейнджер и он сам собрались вместе и решили, что они ненавидят C ++. Будь то шутка или нет, Go сильно отличается от C ++. Подробнее об этом позже. Go - это предельная простота Это подробно объясняется Робом Пайком в Меньше в геометрической прогрессии.

Go против Другие языки

В Go нет классов, объектов, исключений и шаблонов. Он имеет сборщик мусора и встроенный параллелизм. Самое поразительное упущение в отношении объектно-ориентированного подхода состоит в том, что в Go нет иерархии типов. Это отличается от большинства объектно-ориентированных языков, таких как C ++, Java, C #, Scala, и даже динамических языков, таких как Python и Ruby.

Go Особенности ОО

У Go нет классов, но есть типы. В частности, он имеет структуры. Структуры являются определяемыми пользователем типами. Структурные типы (с методами) служат тем же целям, что и классы в других языках.

Структуры

Структура определяет состояние. Вот структура Существа. Она имеет поле «Имя» и логический флаг «Реал», который говорит нам, является ли это реальным существом или воображаемым. Структуры содержат только состояние и никакого поведения.

1
type Creature struct {
2
3
  Name string
4
5
  Real bool
6
7
}
8

Методы

Методы - это функции, которые работают с определенными типами. У них есть пункт получателя, который указывает, на какой тип они работают. Вот метод Dump(), который работает со структурами Существ и печатает их состояние:

1
func (c Creature) Dump() {
2
  fmt.Printf("Name: '%s', Real: %t\n", c.Name, c.Real)
3
}

Это необычный синтаксис, но он очень явный и понятный (в отличие от неявного «this» или запутанного «self» Python).

Встраивание

Вы можете встраивать анонимные типы друг в друга. Если вы встраиваете безымянную структуру, то встроенная структура предоставляет свое состояние (и методы) непосредственно встраиваемой структуре. Например, в FlyingCreature встроена безымянная структура Creature, что означает, что FlyingCreature является Creature.

1
type FlyingCreature struct {
2
  Creature
3
  WingSpan int
4
}

Теперь, если у вас есть экземпляр FlyingCreature, вы можете напрямую обращаться к его атрибутам Name и Real.

1
dragon := &FlyingCreature{
2
    Creature{"Dragon", false, },
3
    15,
4
}
5
6
fmt.Println(dragon.Name)
7
fmt.Println(dragon.Real)
8
fmt.Println(dragon.WingSpan)

Интерфейсы

Интерфейсы являются отличительной чертой объектно-ориентированной поддержки Go. Интерфейсы - это типы, которые объявляют наборы методов. Подобно интерфейсам в других языках, они не имеют реализации.

Объекты, которые реализуют все методы интерфейса, автоматически реализуют интерфейс. Не существует наследования или подклассов или ключевого слова "Implements". В следующем фрагменте кода тип Foo реализует интерфейс Fooer (по соглашению имена интерфейсов Go заканчиваются на «er»).

1
type Fooer interface {
2
  Foo1()
3
  Foo2()
4
  Foo3()
5
}
6
7
type Foo struct {
8
}
9
10
func (f Foo) Foo1() {
11
    fmt.Println("Foo1() here")
12
}
13
14
func (f Foo) Foo2() {
15
    fmt.Println("Foo2() here")
16
}
17
18
func (f Foo) Foo3() {
19
    fmt.Println("Foo3() here")
20
}

Объектно-ориентированный дизайн: путь Go

Давайте посмотрим, как Go соотносится со столпами объектно-ориентированного программирования: инкапсуляция, наследование и полиморфизм. Это особенности языков программирования на основе классов, которые являются наиболее популярными объектно-ориентированными языками программирования.

По сути, объекты - это языковые конструкции, которые имеют состояние и поведение, которые воздействуют на состояние и выборочно предоставляют его другим частям программы.

Инкапсуляция

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

Например, здесь, чтобы скрыть приведенный выше тип Foo и предоставить только интерфейс, который вы могли бы переименовать в строчный foo, и предоставить функцию NewFoo(), которая возвращает открытый интерфейс Fooer:

1
type foo struct {
2
}
3
4
func (f foo) Foo1() {
5
    fmt.Println("Foo1() here")
6
}
7
8
func (f foo) Foo2() {
9
    fmt.Println("Foo2() here")
10
}
11
12
func (f foo) Foo3() {
13
    fmt.Println("Foo3() here")
14
}
15
16
func NewFoo() Fooer {
17
    return &Foo{}
18
}

Тогда код из другого пакета может использовать NewFoo() и получить доступ к интерфейсу Fooer, реализованному внутренним типом foo:

1
f := NewFoo()
2
3
f.Foo1()
4
5
f.Foo2()
6
7
f.Foo3()

Наследование

Наследование или подтипы всегда были спорным вопросом. Существует много проблем с наследованием реализации (в отличие от наследования интерфейса). Множественное наследование, реализованное в C ++, Python и других языках, страдает от проблемы смертельного алмаза смерти, но даже одиночное наследование не является пикником для хрупкой проблемы базового класса.

Современные языки и объектно-ориентированное мышление теперь предпочитают композицию, а не наследование. Go принимает это близко к сердцу и не имеет никакой иерархии типов. Это позволяет вам делиться деталями реализации через композицию. Но Go, в очень странном повороте (который, вероятно, возник из-за прагматических проблем), позволяет анонимную композицию посредством встраивания.

Для всех намерений и целей составление путем встраивания анонимного типа эквивалентно наследованию реализации. Встроенная структура так же хрупка, как и базовый класс. Вы также можете встроить интерфейс, который эквивалентен наследованию от интерфейса в таких языках, как Java или C ++. Это может даже привести к ошибке времени выполнения, которая не обнаруживается во время компиляции, если тип встраивания не реализует все методы интерфейса.

Здесь SuperFoo встраивает интерфейс Fooer, но не реализует его методы. Компилятор Go с радостью позволит вам создать новый SuperFoo и вызвать методы Fooer, но, очевидно, потерпит неудачу во время выполнения. Это компилируется:

1
type SuperFooer struct {
2
  Fooer
3
}
4
5
func main() {
6
  s := SuperFooer{}
7
  s.Foo2()

Запуск этой программы приводит к панике:

1
panic: runtime error: invalid memory address or nil pointer dereference
2
[signal 0xb code=0x1 addr=0x28 pc=0x2a78]
3
4
goroutine 1 [running]:
5
panic(0xde180, 0xc82000a0d0)
6
  /usr/local/Cellar/go/1.6/libexec/src/runtime/panic.go:464 +0x3e6
7
main.main()
8
  /Users/gigi/Documents/dev/go/src/github.com/oop_test/main.go:104 +0x48
9
exit status 2
10
11
Process finished with exit code 1

Полиморфизм

Полиморфизм - это основа объектно-ориентированного программирования: способность обрабатывать объекты разных типов одинаково, если они придерживаются одного и того же интерфейса. Интерфейсы Go предоставляют эту возможность очень прямым и интуитивно понятным способом.

Вот подробный пример, когда несколько существ (и дверь!), Которые реализуют интерфейс Dumper, создаются и сохраняются в срезе, а затем вызывается метод Dump() для каждого из них. Вы также заметите разные стили создания объектов.

1
package main
2
3
import "fmt"
4
5
type Creature struct {
6
  Name string
7
  Real bool
8
}
9
10
func Dump(c*Creature) {
11
  fmt.Printf("Name: '%s', Real: %t\n", c.Name, c.Real)
12
}
13
14
func (c Creature) Dump() {
15
  fmt.Printf("Name: '%s', Real: %t\n", c.Name, c.Real)
16
}
17
18
type FlyingCreature struct {
19
  Creature
20
  WingSpan int
21
}
22
23
func (fc FlyingCreature) Dump() {
24
  fmt.Printf("Name: '%s', Real: %t, WingSpan: %d\n",
25
    fc.Name,
26
    fc.Real,
27
    fc.WingSpan)
28
}
29
30
type Unicorn struct {
31
  Creature
32
}
33
34
type Dragon struct {
35
  FlyingCreature
36
}
37
38
type Pterodactyl struct {
39
  FlyingCreature
40
}
41
42
func NewPterodactyl(wingSpan int) *Pterodactyl {
43
  pet := &Pterodactyl{
44
    FlyingCreature{
45
      Creature{
46
        "Pterodactyl",
47
        true,
48
      },
49
      wingSpan,
50
    },
51
  }
52
  return pet
53
}
54
55
type Dumper interface {
56
  Dump()
57
}
58
59
type Door struct {
60
  Thickness int
61
  Color     string
62
}
63
64
func (d Door) Dump() {
65
  fmt.Printf("Door => Thickness: %d, Color: %s", d.Thickness, d.Color)
66
}
67
68
func main() {
69
  creature := &Creature{
70
    "some creature",
71
    false,
72
  }
73
74
  uni := Unicorn{
75
    Creature{
76
      "Unicorn",
77
      false,
78
    },
79
  }
80
81
  pet1 := &Pterodactyl{
82
    FlyingCreature{
83
      Creature{
84
        "Pterodactyl",
85
        true,
86
      },
87
      5,
88
    },
89
  }
90
91
  pet2 := NewPterodactyl(8)
92
93
  door := &Door{3, "red"}
94
95
  Dump(creature)
96
  creature.Dump()
97
  uni.Dump()
98
  pet1.Dump()
99
  pet2.Dump()
100
101
  creatures := []Creature{
102
    *creature,
103
    uni.Creature,
104
    pet1.Creature,
105
    pet2.Creature}
106
  fmt.Println("Dump() through Creature embedded type")
107
  for _, creature := range creatures {
108
    creature.Dump()
109
  }
110
111
  dumpers := []Dumper{creature, uni, pet1, pet2, door}
112
  fmt.Println("Dump() through Dumper interface")
113
  for _, dumper := range dumpers {
114
    dumper.Dump()
115
  }
116
}

Заключение

Go - добросовестный объектно-ориентированный язык программирования. Он позволяет объектно-ориентированное моделирование и продвигает лучшую практику использования интерфейсов вместо конкретных иерархий типов. Go сделал несколько необычных синтаксических решений, но в целом работа с типами, методами и интерфейсами кажется простой, легкой и естественной.

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