Объектно-ориентированное программирование в Golang

Golang

Давайте поучимся работать с объектно-ориентированной архитектурой в Golang. Здесь нет классов, зато есть структуры, работа с которыми является единственным способом поддержки объектно-ориентированной модели.

Создание структур в Golang

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

Обратимся к конкретному примеру. Допустим, нам надо представить в качестве объекта сотрудника нашей организации. Для этого понадобится комбинация пар «ключ — значение» со всеми данными о сотруднике. Объект, представляющий сотрудника, может быть составлен из нескольких ключей/параметров, например, имени, возраста, должности и оклада. Все эти атрибуты или свойства в совокупности представляют сотрудника организации.

Теперь создадим простую структуру сотрудника Employee с основными его параметрами.

type Employee struct {
  Name string
  Age int
  Designation string
  Salary int
}

В этом коде можно выделить следующие стандартные блоки:

  1. Ключевое слово type, которое используется для определения нового типа в Golang.
  2. Employee передан в качестве имени создаваемой структуры.
  3. Ключевое слово struct указывает на тип данных — структура.
  4. И, наконец, в структуру добавляются параметры вместе с их типами.

В Golang предусмотрена строгая типобезопасность, поэтому типы параметров надо указывать во время объявления структуры. Присвоение значения любого другого типа в этих параметрах чревато ошибками, которые обнаружит компилятор.

Создание объектов из структуры

Теперь, когда у нас есть новая структура, можно создавать из неё объекты. Давайте подробно разберём, как это делать на примере уже определённой нами структуры (Employee). Существует несколько способов создания объектов, и мы рассмотрим разные возможные сценарии со всеми их преимуществами и недостатками.

Передача в структуру значений, разделённых запятыми

Это самый простой способ создания объектов: все необходимые значения передаются в структуру в виде значений, разделённых запятыми. Эти значения должны следовать в том же порядке, в котором они указаны при объявлении структуры.

type Employee struct {
  Name string
  Age int
  Designation string
  Salary int
}

var newEmployee = Employee{"Mayank", 30, "Developer", 40}

В этом коде мы создаём объект структуры Employee (как видите, значения разделены запятыми), причём объект этот присваивается переменной newEmployee.

Проблемы, которые здесь возникают:

  1. Во время создания объекта приходится держать в голове все параметры да ещё и в нужном порядке.
  2. В структуру надо передавать все значения.

Передача пар «ключ — значение»

Решаем эти проблемы, передавая пары «ключ — значение» во время объявления.

Что это нам даёт?

  1. Во-первых, больше не нужно следить за порядком следования значений.
  2. Во-вторых, не нужно указывать все пары «ключ — значение».
type Employee struct {
  Name string
  Age int
  Designation string
  Salary int
}

var newEmployee = Employee{Name: "Mayank", Age: 40, Designation: "Developer"}

var otherEmployee = Employee{Designation: "Developer", Name: "Mayank", Age: 40}

Видите? Мы передаём пары «ключ — значение» в произвольном порядке и даже пропускаем некоторые параметры. Тем не менее структура создаётся!

Стандартные значения для недостающих параметров

При создании объекта newEmployee мы пропустили одну из пар «ключ — значение» (Salary). Golang по умолчанию добавляет к параметру Salary некое стандартное значение. Каким же именно будет это стандартное значение? А это зависит от типа параметра. В соответствии с типом параметра задаются следующие стандартные значения данных:

  • Значение целочисленного типа int задаётся равным 0.
  • Значения строкового типа string оставляются пустыми “” (пустая строка).
  • Значениям логического типа bool по умолчанию задаётся false.

В нашем случае параметру Salary соответствует целочисленный тип int, поэтому стандартным значением этого параметра будет 0. Теперь давайте посмотрим, как получить доступ к этим значениям параметра, используя объект struct.

type Employee struct {
  Name string
  Age int
  Designation string
  Salary int
}

var newEmployee = Employee{Name: "Mayank", Age: 40, Designation: "Developer"}

// Обращение к параметрам «имени»...
fmt.Println(newEmployee.Name)

Ссылка на объект или значение объекта?

Ещё нам важно в создании структуры определить, представляет ли параметр newEmployee тип значения или ссылочный тип. Объект, возвращаемый нам при объявлении, приносит не ссылки, а значение объекта. newEmployee не указывает на адрес в памяти.

Если этот объект в качестве параметра передаётся какой-то другой функции, мы даём вызываемой функции значения объекта. Исходный объект копируется, и новый объект данных присваивается параметру вызывающей функции. Объект не передаётся как ссылка. А раз так, никакие изменения копированного объекта не дублируются в исходном. Рассмотрим пример:

func UpdateEmployee(empDetials Employee) {
  empDetails.Name = "Anshul";
}

var newEmployee = Employee{Name: "Mayank", Age: 40, Designation: "Developer"}

UpdateEmployee(newEmployee)

fmt.Println(newEmployee.Name)

Здесь даже при обновлении значений в функции UpdateEmployee никакого влияния на исходный объект newEmployee не оказывается, ведь объект мы передали не как ссылку, а как значение.

Передача объекта как ссылки

Итак, мы увидели, что обновления объекта никак не отразятся на функции, потому что в функцию не была передана ссылка. Для передачи объекта как ссылки можно вместо значений передать ссылку на объект: просто ставим в начале оператор взятия адреса &. Чтобы принялась ссылка на объект, а не значение, вызываемую функцию тоже надо обновить.

func UpdateEmployee(empDetials *Employee) {
  empDetails.Name = "Anshul";
}

var newEmployee = Employee{Name: "Mayank", Age: 40, Designation: "Developer"}

UpdateEmployee(&newEmployee)

// Исходный объект отправлен в обновлённую функцию...
fmt.Println(newEmployee.Name)

Здесь функцию (и вызов функции) обновили, чтобы можно было отправлять и получать не значение объекта, а адрес в памяти. Теперь, если обновятся данные в вызываемой функции, то же самое произойдёт и в исходном объекте данных.

В чём проблема этого подхода? В том, что приходится использовать оператор взятия адреса, чтобы вытащить адрес объекта и отправить его в функцию.

Использование ключевого слова new для создания объектов

Следующий способ создания объекта из структуры — использовать ключевое слово new. При этом Golang создаёт новый объект типа struct и возвращает переменной его адрес в памяти. То есть, ключевое слово new возвращает адрес этого объекта.

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

  1. Стандартным значением для булева типа будет false.
  2. Стандартным значением для целочисленного типа будет 0.
  3. Стандартным значением для строкового типа будет “” (пустая строка).

Для лучшего понимания снова обратимся к коду:

type Employee struct {
  Name string
  Age int
  Designation string
  Salary int
}

var newEmployee = new(Employee)

fmt.Println(newEmployee.Name)

Мы создаём новый объект с помощью ключевого слова new, которое не позволяет передавать параметрам объекта стандартные значения. При создании объекта это делает за нас Golang. В нашем случае обращение к параметру name вернёт пустую строку (“”).

В коде с помощью ключевого слова new возвращается адрес вновь созданного объекта. Переменная newEmployee выступает здесь в роли указателя на объект Employee.

Передача объекта в функцию

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

func UpdateEmployee(empDetials *Employee) {
  empDetails.Name = "Anshul";
}

var newEmployee = new(Employee)

newEmployee.Name = "Mayank"
newEmployee.Age = 30

UpdateEmployee(newEmployee)

fmt.Println(newEmployee.Name)

Здесь от ключевого слова new возвращается ссылка на объект. А значит, что при вызове функции нет необходимости использовать & для отправки ссылки на объект.

Добавление функции в структуру

Структура не только определяет свойства, относящиеся к объекту, она представляет поведение объекта. Попробуем понять, как это возможно, добавив в структуру функции. Код для этой операции в Golang довольно-таки своеобразный.

Пример:

type Employee struct {
  Name string
  Age int
  Designation string
  Salary int
}

func (emp Employee) ShowDetails() {
  fmt.Println("User Name: ", emp.Name)
}

Здесь мы добавляем новую функцию, которая бы имела привязку к структуре Employee. То есть чётко привязываем функцию к структуре. Функция определена и может принимать ссылку на созданный объект с помощью emp.

Посмотрите, как в Golang вызывается добавленная к структуре функция.

type Employee struct {
  Name string
  Age int
  Designation string
  Salary int
}

func (emp Employee) ShowDetails() {
  fmt.Println("User Name: ", emp.Name)
}

var newEmployee = new(Employee)
newEmployee.Name = "Mayank"

newEmployee.ShowDetails()

Заключение

Надеюсь, вам понравилась статья.

Если хотите узнать о Golang больше, читайте следующие статьи:

1)Конкурентность и параллелизм в Golang. Горутины.

2)Введение в каналы Golang

3) Обработка ошибок в Golang с помощью Panic, Defer и Recover

Читайте также:


Перевод статьи Mayank Gupta: Golang Object-Oriented Programming