Типы, методы и интерфейсы

46c3adb1304fb1172660886debe46d2b.png

0877bb868078debafa24bb4a568bbf79.jpgАвтор статьи: Рустем Галиев

IBM Senior DevOps Engineer & Integration Architect. Официальный DevOps ментор и коуч в IBM

Всем привет. Сегодня на примере разберем методы и интерфейсы в go.

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

В статье вы узнаете про:

  • Объявление собственных типов;

  • Добавление методов к типам;

  • Объявление и использование интерфейсов.

Начнем с объявления собственных типов.

Давайте напишем простую систему управления персоналом, чтобы показать пользовательские типы. Чтобы увидеть основы объявления и использования типов, начнем с определения Employee.

Скопируйте следующий код в новый файл с именем people.go:

package main

import (
	"fmt"
	"time"
)

type Employee struct {
	ID    	int
	FirstName string
	LastName  string
	DateHired time.Time
}

func main() {
	e1 := Employee{
    	ID:    	1,
    	FirstName: "Bob",
    	LastName:  "Bobson",
    	DateHired: time.Date(2020, time.January, 10, 0, 0, 0, 0, time.UTC),
	}
	e2 := Employee{
    	ID:    	2,
    	FirstName: "Mary",
    	LastName:  "Maryson",
    	DateHired: time.Date(2007, time.March, 30, 0, 0, 0, 0, time.UTC),
	}
	fmt.Println(e1.FirstName)
	fmt.Println(e2.DateHired)
	// Bob got married and changed his last name
	e1.LastName = "Bobson-Smith"
	fmt.Println(e1.LastName)
}

В терминале введите:   go run people.go

74fd6d599f89d4a613f6ae3bbac84e02.png

Объявим пользовательский тип с ключевым словом type, за которым следует имя типа (в данном случае Employee), а затем тип, которому мы даем имя. В большинстве случаев это будет структура, в которой перечислены поля. Как и во всех объявлениях Go, имя поля идет первым, а тип поля — вторым.

Создаем экземпляр нашего типа Employee, указав его значение. Далее читаем и записываем поля в структуре, используя точечную запись.

Тип Time в пакете time — это стандартный способ Go для представления момента времени. В этой программе мы используем два других типа из пакета time. Когда вызываем функцию time.Date для создания экземпляра time. Time, мы передаем экземпляр time.Month для представления месяца и экземпляр *time.Location для представления часового пояса. Если вы посмотрите исходный код в стандартной библиотеке, то увидите кое-что интересное: тип time.Month объявлен как:

type Month int

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

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

Измените код в people.go на:

package main

import (
	"fmt"
	"time"
)

type Employee struct {
	ID    	int
	FirstName string
	LastName  string
	DateHired time.Time
}

var Employees = map[int]Employee{}
var nextID = 0

func AddEmployee(firstName, lastName string, dateHired time.Time) int {
	nextID++
	Employees[nextID] = Employee{
    	ID:    	nextID,
    	FirstName: firstName,
    	LastName:  lastName,
    	DateHired: dateHired,
	}
	return nextID
}

func GetEmployee(id int) (Employee, bool) {
	p, ok := Employees[id]
	return p, ok
}

func DMYToTime(day int, month time.Month, year int) time.Time {
	return time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
}

func main() {
	e1ID := AddEmployee("Bob", "Bobson", DMYToTime(10, time.January, 2020))
	e2ID := AddEmployee("Mary", "Maryson", DMYToTime(30, time.March, 2007))
	e1, exists1 := GetEmployee(e1ID)
	e2, exists2 := GetEmployee(e2ID)
	fmt.Println(e1, exists1)
	fmt.Println(e2, exists2)
	e3, exists3 := GetEmployee(2000)
	fmt.Println(e3, exists3)
}

Строка var Employees = map[int]Employee{} объявляет и создает хранилище данных для наших сотрудников.

В следующей строке var nextID = 0 мы создаем переменную для хранения следующего уникального идентификатора вновь созданного сотрудника.

Вместо прямого доступа к карте Employees мы используем функции AddEmployee и GetEmployee для изменения и чтения ее состояния. AddEmployee получает информацию о новом сотруднике, создает новый идентификатор сотрудника, сохраняет экземпляр структуры Employee, представляющий сотрудника, в Employees и возвращает новый идентификатор. GetEmployee ищет сотрудника, используя идентификатор, возвращая логическое значение, указывающее, был ли соответствующий сотрудник для предоставленного идентификатора. Это распространенный шаблон в Go.

Последней новой функцией является DMYToTime, вспомогательная функция, которая создает экземпляр time. Time из предоставленных дня, месяца и года. Обратите внимание, что день и год имеют тип int, а месяц — тип month.Month.

В основном мы больше не создаем экземпляры Employee напрямую. Вместо этого мы используем вызовы AddEmployee для создания экземпляров Employee, которые хранятся в мапе Employees, и вызываем GetEmployee для доступа к ним.

В терминале снова введите:   go run people.go

b5606496fadc25014905685342969df2.png

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

Как и многие другие языки, Go позволяет объявлять методы для любого определяемого пользователем типа. Давайте перестанем использовать состояние уровня пакета для информации о нашем сотруднике и поместим его в структуру. Измените people.go на следующее:

package main

import (
	"fmt"
	"time"
)

type Employee struct {
	ID    	int
	FirstName string
	LastName  string
	DateHired time.Time
}

//more1

type SimpleEmployeeData struct {
	employees map[int]Employee
	//more2
	nextID	int
}

func NewSimpleEmployeeData() *SimpleEmployeeData {
	return &SimpleEmployeeData{
    	employees: map[int]Employee{},
    	//more3
    	nextID:	0,
	}
}

func (ed *SimpleEmployeeData) AddEmployee(firstName, lastName string, dateHired time.Time) int {
	ed.nextID++
	ed.employees[ed.nextID] = Employee{
    	ID:    	ed.nextID,
    	FirstName: firstName,
    	LastName:  lastName,
    	DateHired: dateHired,
	}
	return ed.nextID
}

func (ed SimpleEmployeeData) GetEmployee(id int) (Employee, bool) {
	e, ok := ed.employees[id]
	return e, ok
}

//more4

func DMYToTime(day int, month time.Month, year int) time.Time {
	return time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
}

func main() {
	ed := NewSimpleEmployeeData()
	manageEmployees(ed)
}

func manageEmployees(ed *SimpleEmployeeData) {
	e1ID := ed.AddEmployee("Bob", "Bobson", DMYToTime(10, time.January, 2020))
	e2ID := ed.AddEmployee("Mary", "Maryson", DMYToTime(30, time.March, 2007))
	e1, exists1 := ed.GetEmployee(e1ID)
	e2, exists2 := ed.GetEmployee(e2ID)
	fmt.Println(e1, exists1)
	fmt.Println(e2, exists2)
	e3, exists3 := ed.GetEmployee(2000)
	fmt.Println(e3, exists3)
	//more5
}

//more6

Наш новый тип SimpleEmployeeData объединяет состояние и бизнес-логику. Мапа Employees и nextID int, которые у нас были как состояние уровня пакета в предыдущем примере, теперь являются полями в SimpleEmployeeData, а функции AddEmployee и GetEmployee теперь являются методами в SimpleEmployeeData.

Объявления методов и объявлений функций в Go очень похожи. Например, объявление функции GetEmployee выглядело так:

func GetEmployee(id int) (Employee, bool)

и объявление метода GetEmployee выглядит следующим образом:

func (ed SimpleEmployeeData) GetEmployee(id int) (Employee, bool)

Единственное различие заключается в объявлении приемника между ключевым словом func и именем метода. В теле метода GetEmployee мы получаем доступ к полям экземпляра SimpleEmployeeData с помощью приемника ed:

e, ok := ed.employees[id]

У нас также есть фабричная функция NewSimpleEmployeeData, чтобы убедиться, что мы используем правильно сконструированный экземпляр SimpleEmployeeData не нужно писать фабричную функцию, но это хорошая практика, если вам нужно убедиться, что определенные поля в структуре правильно заполнены, прежде чем они будут использоваться. В этом случае необходимо убедиться, что карта сотрудников не равна нулю.

Наша основная функция теперь намного меньше. Она вызывает NewSimpleEmployeeData для создания экземпляра *SimpleEmployeeData, а затем вызывает manageEmployees для выполнения этой работы. В общем, структурируйте свои программы таким образом, чтобы работа основной функции заключалась в загрузке исходной информации о конфигурации, создании экземпляров структур данных и последующем вызове функции или метода для запуска бизнес-логики. Это делает ваши программы более модульными и тестируемыми.

Между тем, функциональность, которая раньше была в main, теперь находится в функции manageEmployees. Эта функция принимает экземпляр *SimpleEmployeeData, который является указателем. Поскольку это указатель, мы можем изменить состояние экземпляра.

У нас есть два метода для SimpleEmployeeData: AddEmployee и GetEmployee. Обратите внимание, что получателем для AddEmployee является указатель; он объявлен как (ed *SimpleEmployeeData). Точно так же, как аргумент указателя, переданный функции, означает, что вы можете изменить значение этого аргумента внутри функции и увидеть его отражение вне функции, получатель указателя означает, что вы можете изменить состояние структуры внутри метода и состояние останется измененным, когда метод выходит. Метод GetEmployee имеет приемник значения, объявленный как (ed SimpleEmployeeData). Поскольку этот метод не изменяет состояние структуры, нет необходимости использовать приемник указателя.

go run people.go

85d3189cfd051529bc9a2486130e9b2a.png

У нас немного больше кода, чем раньше, но программа лучше структурирована и ее легче понять.

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

func (ed *SimpleEmployeeData) AddEmployee(firstName, lastName string, dateHired time.Time) int {

На:

func (ed SimpleEmployeeData) AddEmployee (firstName, lastName string, dateHired time.Time) int {

(Разница незначительна; все, что мы сделали, это удалили * перед SimpleEmployeeData.)

0493b3a829c45b2d82dc868f7e7269b1.png

Что случилось? Здесь мы рассмотрим разницу между полем указателя и полем значения в структуре. Мы распечатали запись сотрудника для Мэри Мэрисон дважды, потому что поле nextID сбрасывается обратно на 0 каждый раз, когда вызывается ошибочный метод AddEmployee. Мы записали запись для Боба Бобсона при первом вызове AddEmployee, а затем перезаписали ее, когда снова вызвали AddEmployee для Мэри Мэрисон.

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

(Кстати, если сделать GetEmployee приемником указателя, код все равно будет работать корректно.)

Отмените редактирование AddEmployee и снова сделайте его получателем указателя.

Давайте добавим в нашу систему новый тип человека, менеджера. Мы также собираемся хранить их в нашей структуре SimpleEmployeeData.

Замените //more1 на:

type Manager struct {
    Employee
    Reports []int
}

Вместо того, чтобы создавать менеджера с нуля, мы повторно используем определение сотрудника. Мы сделали это, добавив его как поле без имени. Это называется встроенным полем. Скоро увидим преимущества встроенных полей.

Далее добавим поддержку нашего менеджера в программу. Сначала изменим SimpleEmployeeData, чтобы в нем было поле для хранения информации о менеджере. Замените //more2 объявлением поля:

managers  map[int]Manager

Мы добавили новое поле, называемое менеджерами, в нашу структуру SimpleEmployeeData, чтобы удерживать связь между идентификатором сотрудника и экземплярами менеджера.

Также нужно убедиться, что поле менеджеров правильно инициализировано. Для этого мы также модифицируем NewSimpleEmployeeData. Замените //more3 следующей инициализацией поля:

managers:  map[int]Manager{}


Теперь мы инициализируем поле менеджеров в SimpleEmployeeData точно так же, как мы инициализируем поле сотрудников.

Далее нам нужны методы для взаимодействия с нашим новым полем. Добавьте эти два объявления метода, где //more4 находится в коде:

func (ed *SimpleEmployeeData) AddManager(firstName, lastName string, dateHired time.Time, reports []int) int {
    ed.nextID++
    ed.managers[ed.nextID] = Manager{
    Employee: Employee{
            ID:        ed.nextID,
            FirstName: firstName,
            LastName:  lastName,
            DateHired: dateHired,
        },
        Reports: reports,
    }
    return ed.nextID
}

func (ed SimpleEmployeeData) GetManager(id int) (Manager, bool) {
    m, ok := ed.managers[id]
    return m, ok
}


Методы AddManager и GetManager являются аналогами методов AddEmployee и GetEmployee и позволяют добавлять и получать экземпляры Manager. Обратите внимание, что мы используем одно и то же поле nextID для создания идентификаторов как для экземпляров Manager, так и для экземпляров Employee. В AddManager обратите внимание, как мы инициализировали встроенное поле. При инициализации менеджера вы указываете поле «Employee» и передаете экземпляр «Employee».

Наконец, чтобы продемонстрировать, что наш новый код работает правильно, добавьте следующие строки в конец manageEmployees:

 m1ID := ed.AddManager("Boss", "BossPerson", DMYToTime(17, time.June, 1982), []int{e1ID, e2ID})
    m1, _ := ed.GetManager(m1ID)
    fmt.Println(m1.FirstName, m1.Reports)
    //more5

Здесь мы начинаем видеть преимущества встроенных полей. Когда вы получаете доступ к полям из Employee в Manager, вы делаете это так, как если бы поля были объявлены непосредственно в Manager. Вот почему мы могли бы написать fmt.Println(m1.FirstName, m1.Reports) даже несмотря на то, что FirstName является полем в Employee.

go run people.go

118604d787a4c80364cbfd4e816d962f.png

Имейте в виду, что встраивание не является наследованием. Вы не можете присвоить значение типа Manager переменной или полю типа Employee. Добавьте следующие строки в конец файла manageEmployees:

var e4 Employee = m1
    fmt.Println(e4.LastName)
    //more5

go run people.go

Должна быть ошибка

6ea6fc7ce75039c2216c8de09fd63e98.png

Если вы хотите получить доступ к экземпляру Employee напрямую в Manager, вы делаете это, используя Employee в качестве имени поля.

Измените строку var e4 Employee = m1 на:

e4 := m1.Employee

87df661f6670d387d6be9712b71dd403.png

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

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

Мы объявляем интерфейс, используя ключевое слово type. Замените //more6 следующим кодом в people.go:

type Dater interface {
	TimeAtCompany() time.Duration
}

func FormatTenure(d Dater) string {
	// convert from hours to years and days (ignoring leap years)
	hours := int(d.TimeAtCompany().Hours())
	years := hours / (24 * 365)
	hours = hours % (24 * 365)
	days := hours / 24
	return fmt.Sprintf("%d years, %d days", years, days)
}

//more6

В интерфейсе мы перечисляем методы, которые необходимо реализовать, чтобы соответствовать интерфейсу. Для интерфейса Dater у нас есть только один метод, TimeAtCompany, который возвращает time.Duration (тип в стандартной библиотеке, представляющий период времени). В функции FormatTenure мы принимаем параметр типа Dater и вызываем метод TimeAtCompany для параметра (и метод Hours для time.Duration). Вычисляем годы и дни, а затем используем функцию fmt.Sprintf для построения возвращаемой строки.

Теперь давайте добавим метод в Employee. Замените комментарий //more6 на:

func (e Employee) TimeAtCompany() time.Duration {
    return time.Since(e.DateHired)
}

//more6

Мы также добавим код в конец manageEmployees:

 fmt.Println(FormatTenure(e1))
    fmt.Println(FormatTenure(e2))
    //more5

ae17d151d29fe51466bc3df7919def7a.png

Добавив метод в Employee, он автоматически встретился с интерфейсом Dater.

Давайте заставим Employee реализовать другой интерфейс. В стандартной библиотеке есть интерфейс fmt.Stringer. Реализуя этот интерфейс, вы указываете, как будет выглядеть вывод, когда ваш тип передается в fmt.Println (и во многие другие функции). Замените комментарий //more6 в people.go следующим кодом:

func (e Employee) String() string {
    return e.FirstName + " " + e.LastName + ": Tenure " + FormatTenure(e)
}

//more6

b94e9d9b46dd7cdf92e19287ff3f338a.png

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

Добавьте следующее в конец manageEmployees:

fmt.Println(m1.TimeAtCompany())
    //more5

5ec39d14a49b091f3b7a4590c515b96c.png

Мало того, что мы можем вызвать метод TimeAtCompany непосредственно для экземпляра типа Manager, менеджер автоматически реализует любые интерфейсы, которые делает Employee. Добавьте следующие строки в конец manageEmployees:

fmt.Println(FormatTenure(m1))
    fmt.Println(m1)
    //more5

8b59562e0ee171f3c26da97113ce8e35.png

Тип Manager реализует Dater и Stringer. Хотя результаты от реализации Dater такие, как мы и ожидали, можно было бы захотеть получить другой вывод, когда распечатываем Manager. Давайте заменим //more6 другим определением метода:

func (m Manager) String() string {
    return fmt.Sprintf("%s, reports: %v", m.Employee, m.Reports)
}

//more6

d8caf95bcc7a052610285ba8c5cb59bc.png

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

Замените //more6 следующим определением типа:

type EmployeeData interface {
    AddEmployee(firstName, lastName string, dateHired time.Time) int
    GetEmployee(id int) (Employee, bool)
    AddManager(firstName, lastName string, dateHired time.Time, reports []int) int
    GetManager(id int) (Manager, bool)
}

Измените определение manageEmployees на:

func manageEmployees(ed EmployeeData) {

45eb6753b5a7b4d1b814e2aaf9761514.png

Отлично!

Полный код получится таким:

package main

import (
	"fmt"
	"time"
)

type Employee struct {
	ID    	int
	FirstName string
	LastName  string
	DateHired time.Time
}

type Manager struct {
	Employee
	Reports []int
}


type SimpleEmployeeData struct {
	employees map[int]Employee
	managers  map[int]Manager

	nextID	int
}

func NewSimpleEmployeeData() *SimpleEmployeeData {
	return &SimpleEmployeeData{
    	employees: map[int]Employee{},
    	managers:  map[int]Manager{},

    	nextID:	0,
	}
}

func (ed *SimpleEmployeeData) AddEmployee(firstName, lastName string, dateHired time.Time) int {
	ed.nextID++
	ed.employees[ed.nextID] = Employee{
    	ID:    	ed.nextID,
    	FirstName: firstName,
    	LastName:  lastName,
    	DateHired: dateHired,
	}
	return ed.nextID
}

func (ed SimpleEmployeeData) GetEmployee(id int) (Employee, bool) {
	e, ok := ed.employees[id]
	return e, ok
}

func (ed *SimpleEmployeeData) AddManager(firstName, lastName string, dateHired time.Time, reports []int) int {
	ed.nextID++
	ed.managers[ed.nextID] = Manager{
	Employee: Employee{
        	ID:    	ed.nextID,
        	FirstName: firstName,
        	LastName:  lastName,
        	DateHired: dateHired,
    	},
    	Reports: reports,
	}
	return ed.nextID
}

func (ed SimpleEmployeeData) GetManager(id int) (Manager, bool) {
	m, ok := ed.managers[id]
	return m, ok
}


func DMYToTime(day int, month time.Month, year int) time.Time {
	return time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
}

func main() {
	ed := NewSimpleEmployeeData()
	manageEmployees(ed)
}

func manageEmployees(ed EmployeeData) {

	e1ID := ed.AddEmployee("Bob", "Bobson", DMYToTime(10, time.January, 2020))
	e2ID := ed.AddEmployee("Mary", "Maryson", DMYToTime(30, time.March, 2007))
	e1, exists1 := ed.GetEmployee(e1ID)
	e2, exists2 := ed.GetEmployee(e2ID)
	fmt.Println(e1, exists1)
	fmt.Println(e2, exists2)
	e3, exists3 := ed.GetEmployee(2000)
	fmt.Println(e3, exists3)
    	m1ID := ed.AddManager("Boss", "BossPerson", DMYToTime(17, time.June, 1982), []int{e1ID, e2ID})
	m1, _ := ed.GetManager(m1ID)
	fmt.Println(m1.FirstName, m1.Reports)
    	e4 := m1.Employee

	fmt.Println(e4.LastName)
    	fmt.Println(FormatTenure(e1))
	fmt.Println(FormatTenure(e2))
    	fmt.Println(FormatTenure(m1))
	fmt.Println(m1)
	//more5




}

type Dater interface {
	TimeAtCompany() time.Duration
}

func FormatTenure(d Dater) string {
	// convert from hours to years and days (ignoring leap years)
	hours := int(d.TimeAtCompany().Hours())
	years := hours / (24 * 365)
	hours = hours % (24 * 365)
	days := hours / 24
	return fmt.Sprintf("%d years, %d days", years, days)
}

func (e Employee) TimeAtCompany() time.Duration {
	return time.Since(e.DateHired)
}

func (e Employee) String() string {
	return e.FirstName + " " + e.LastName + ": Tenure " + FormatTenure(e)
}

func (m Manager) String() string {
	return fmt.Sprintf("%s, reports: %v", m.Employee, m.Reports)
}

type EmployeeData interface {
	AddEmployee(firstName, lastName string, dateHired time.Time) int
	GetEmployee(id int) (Employee, bool)
	AddManager(firstName, lastName string, dateHired time.Time, reports []int) int
	GetManager(id int) (Manager, bool)
}

На этом все. Напоследок хочу порекомендовать вам бесплатный вебинар от моих коллег из OTUS, по теме: «пишем веб-сервер на Go». Узнать подробнее о вебинаре можно по этой ссылке.

© Habrahabr.ru