Go — это компилируемый язык со статической типизацией, который завоевал популярность благодаря своей простоте и производительности. Его часто относят к процедурным языкам программирования, прежде всего потому, что он не включает классы и наследование, которые являются отличительными чертами традиционных объектно-ориентированных языков. Однако Go поддерживает принципы объектно-ориентированного программирования (ООП), хотя и с уникальным подходом. В этом сообщении в блоге будет подробно рассказано о подходе Go к объектно-ориентированному программированию и о том, как шаблоны проектирования могут быть реализованы с использованием функций Go.
I. Структуры: строительные блоки
В Go структуры используются для инкапсуляции связанных данных в единое целое, подобно классам в других языках ООП. Вот простой пример структуры:
type Employee struct { ID int FirstName string LastName string Position string }
Здесь Employee
— это структура, которая объединяет четыре поля: ID
, FirstName
, LastName
и Position
.
II. Методы: добавление поведения к структурам
В Go методы — это функции, связанные с определенным типом, известным как приемник. Метод определяется специальным аргументом получателя, который привязывает функцию к определенному типу структуры. Вот как определить методы и вызвать их:
type Employee struct { ID int FirstName string LastName string Position string } // This is a method with Employee as its receiver. // It's used to get the full name of the employee. func (e Employee) FullName() string { return e.FirstName + " " + e.LastName } // This method is used to check if the employee is an engineer. func (e Employee) IsEngineer() bool { return e.Position == "Engineer" } // This method is used to get employee's ID func (e Employee) GetID() int { return e.ID }
В приведенном выше коде FullName
, IsEngineer
и GetID
— это методы, связанные со структурой Employee
. Их можно вызывать для экземпляров Employee
.
Вот как вы можете создать экземпляр Employee
и вызвать эти методы:
func main() { emp := Employee{ ID: 1, FirstName: "John", LastName: "Doe", Position: "Engineer", } // Call the FullName method fmt.Println(emp.FullName()) // Output: John Doe // Call the IsEngineer method if emp.IsEngineer() { fmt.Println(emp.FirstName, "is an engineer.") } else { fmt.Println(emp.FirstName, "is not an engineer.") } // Call the GetID method fmt.Println("Employee ID is:", emp.GetID()) }
В этом примере мы создаем экземпляр Employee
, а затем вызываем методы FullName
, IsEngineer
и GetID
для этого экземпляра. Вывод будет:
John Doe John is an engineer. Employee ID is: 1
Методы позволяют нам определять поведение, связанное со структурой, инкапсулируя операции, которые можно выполнять с данными, которые содержит структура. Это похоже на то, как методы работают в классах в традиционных объектно-ориентированных языках программирования.
III. Инкапсуляция: экспортированные и неэкспортированные поля
В Go инкапсуляция осуществляется с помощью концепции экспортируемых и неэкспортируемых идентификаторов. Идентификатор (переменная, константа, функция или тип), начинающийся с заглавной буквы, экспортируется, что означает, что он доступен из других пакетов. И наоборот, идентификатор, начинающийся со строчной буквы, не экспортируется, то есть является приватным для своего пакета.
Давайте проиллюстрируем это с помощью структуры Employee
. Допустим, мы хотим добавить приватное поле salary
и публичный метод SetSalary
для безопасной установки этого поля.
В файле employee.go
:
package model type Employee struct { ID int FirstName string LastName string Position string salary float64 // unexported field } // SetSalary is an exported method, so it can be called from other packages. // It safely sets the salary field, encapsulating the logic. func (e *Employee) SetSalary(s float64) { if s < 0 { fmt.Println("Error: salary cannot be negative.") return } e.salary = s } // GetSalary is a public method to get the value of the private field salary func (e Employee) GetSalary() float64 { return e.salary }
Обратите внимание, что мы используем приемник указателя в методе SetSalary
. Это потому, что мы модифицируем структуру Employee
.
В файле main.go
:
package main import ( "fmt" "myapp/model" // Import the model package ) func main() { // Create an instance of Employee emp := model.Employee{ ID: 1, FirstName: "John", LastName: "Doe", Position: "Engineer", } // Call the SetSalary method emp.SetSalary(50000) // Try to access the salary field directly (This will cause a compilation error) // fmt.Println(emp.salary) // Correct way to access the salary field fmt.Println("Employee Salary is: ", emp.GetSalary()) }
Здесь salary
является неэкспортируемым полем, поэтому оно является частным для пакета model
и не может быть напрямую доступно из main.go
. Мы предоставляем методы SetSalary
и GetSalary
для безопасной установки и получения значения поля salary
, инкапсулируя доступ к этому полю.
IV. Наследование и композиция: подход Go
Традиционные объектно-ориентированные языки программирования, такие как Java и C++, часто используют наследование на основе классов как средство повторного использования и организации кода. Однако в Go используется другой подход. Вместо наследования Go поощряет композицию и встраивание.
4.1 Понимание композиции и встраивания
В Go одна структура может быть включена в другую структуру, что приводит к повторному использованию и организации кода. Это делается с помощью концепции, известной как «встраивание». Когда вы встраиваете структуру в другую структуру, методы встроенной структуры напрямую доступны из составной структуры.
Проиллюстрируем это на примере.
package model type Person struct { FirstName string LastName string } func (p Person) FullName() string { return p.FirstName + " " + p.LastName } type Employee struct { Person // embedding Person struct into Employee ID int Position string } func (e Employee) GetID() int { return e.ID }
В файле main.go
:
package main import ( "fmt" "myapp/model" // Import the model package ) func main() { // Create an instance of Employee emp := model.Employee{ Person: model.Person{ FirstName: "John", LastName: "Doe", }, ID: 1, Position: "Engineer", } fmt.Println(emp.FullName()) // Call the FullName method from the embedded Person struct fmt.Println(emp.GetID()) // Call the GetID method from the Employee struct }
Здесь мы определяем структуру Person
с методом FullName
и структуру Employee
, включающую Person
. Структура Employee
теперь может напрямую вызывать метод FullName
, как если бы это был собственный метод.
4.2 Интерфейсы и полиморфизм
Go также поддерживает полиморфизм, основную концепцию объектно-ориентированного программирования, через интерфейсы. Интерфейс — это набор сигнатур методов. Говорят, что любой тип, предоставляющий определения для всех методов интерфейса, удовлетворяет интерфейсу и может быть заменен везде, где ожидается тип интерфейса.
Давайте расширим наш пример интерфейсом.
package model type Worker interface { Work() } func (e Employee) Work() { fmt.Println(e.FullName(), "is working.") }
В файле main.go
:
package main import ( "myapp/model" // Import the model package ) func startWorking(w model.Worker) { w.Work() } func main() { // Create an instance of Employee emp := model.Employee{ Person: model.Person{ FirstName: "John", LastName: "Doe", }, ID: 1, Position: "Engineer", } startWorking(emp) // Output: John Doe is working. }
Здесь Worker
— это интерфейс с методом Work
. Структура Employee
удовлетворяет интерфейсу Worker
, поскольку определяет метод Work
. Следовательно, Employee
можно передать функции startWorking
, которая ожидает Worker
.
Таким образом, Go предоставляет гибкий способ достижения полиморфизма без необходимости наследования. Это помогает писать более несвязанный и модульный код.
Модель параллелизма V. Go: горутины и каналы
Параллелизм — это первоклассный гражданин в Go. Он имеет встроенную поддержку одновременного выполнения функций с использованием горутин и связи между этими функциями с использованием каналов.
5.1 Горутины
Горутина — это легковесный поток выполнения. Это называется «горутиной», потому что существующие термины — потоки, сопрограммы, процессы и т. д. — передают неточные коннотации. Горутина имеет простую модель: это функция, выполняемая одновременно с другими горутинами в том же адресном пространстве.
Напишем простую горутину:
package main import ( "fmt" "time" ) func say(s string) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) } } func main() { go say("world") // this function call is now running concurrently in a separate goroutine say("hello") // this function call is running in the main goroutine }
В этом коде say("world")
выполняется одновременно с say("hello")
благодаря ключевому слову go
.
5.2 Каналы
Каналы — это каналы, которые соединяют одновременно запущенные горутины. Вы можете отправлять значения в каналы из одной горутины и получать эти значения в другую горутину.
Вот простой пример:
package main import "fmt" func sum(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } c <- sum // send sum to c } func main() { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int) go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c) x, y := <-c, <-c // receive from c fmt.Println(x, y, x+y) }
В этом примере sum
— это функция, которая принимает срез целых чисел и канал, вычисляет сумму целых чисел и отправляет результат в канал. Мы создаем две горутины, каждая из которых вычисляет сумму половины среза. Затем мы получаем результаты из канала и печатаем их.
VI. Объектно-ориентированные функции Go: резюме
Давайте вспомним некоторые из ключевых объектно-ориентированных функций Go:
- Структуры. Go использует структуры для группировки данных, аналогичные классам в других языках. Структуры могут иметь связанные с ними методы.
- Методы: Go позволяет вам определять методы для структур. Это позволяет инкапсулировать данные и поведение внутри структуры.
- Инкапсуляция: Go использует экспортированные и неэкспортированные идентификаторы для управления видимостью, аналогично общедоступным и частным в других языках.
- Композиция и встраивание.Вместо наследования Go поощряет композицию и встраивание. Одна структура может включать в себя другую структуру, что позволяет повторно использовать код и организовывать его логически.
- Интерфейсы и полиморфизм. Go поддерживает полиморфизм через интерфейсы. Если тип предоставляет все методы, определенные в интерфейсе, говорят, что он удовлетворяет этому интерфейсу.
- Параллелизм. В Go встроена поддержка параллельного выполнения функций через горутины и каналы. Это упрощает написание многопоточных программ.
Применение принципов ООП в Go: пример из реального мира
Давайте создадим простую систему покупок, чтобы проиллюстрировать эти принципы. У нас будет два типа, Product
и Order
, и интерфейс Notifier
для отправки уведомлений.
Вот как мы могли бы структурировать наш проект:
/myapp /model product.go order.go /service order_service.go /notification notifier.go email_notifier.go sms_notifier.go main.go
In product.go
:
package model // Product represents a product in our store type Product struct { ID int Name string Price float64 }
In order.go
:
package model // Order represents a customer order type Order struct { ID int Product Product Amount int } // TotalPrice calculates the total price of the order func (o Order) TotalPrice() float64 { return o.Product.Price * float64(o.Amount) }
In notifier.go
:
package notification import "myapp/model" // Notifier interface for sending notifications type Notifier interface { Notify(o model.Order) }
In email_notifier.go
:
package notification import ( "fmt" "myapp/model" ) // EmailNotifier notifies about an order via email type EmailNotifier struct { Email string } // Notify sends an email notification func (en EmailNotifier) Notify(o model.Order) { fmt.Printf("Sending email to %s about order %d...\n", en.Email, o.ID) }
In sms_notifier.go
:
package notification import ( "fmt" "myapp/model" ) // SMSNotifier notifies about an order via SMS type SMSNotifier struct { PhoneNumber string } // Notify sends an SMS notification func (sn SMSNotifier) Notify(o model.Order) { fmt.Printf("Sending SMS to %s about order %d...\n", sn.PhoneNumber, o.ID) }
In order_service.go
:
package service import ( "myapp/model" "myapp/notification" ) // OrderService handles order processing type OrderService struct { Notifier notification.Notifier } // ProcessOrder processes the order and sends a notification func (s OrderService) ProcessOrder(o model.Order) { // Here we could add more business logic, such as updating inventory, processing payment, etc. s.Notifier.Notify(o) }
In main.go
:
package main import ( "myapp/model" "myapp/notification" "myapp/service" ) func main() { product := model.Product{ID: 1, Name: "Go Programming Book", Price: 50.0} order := model.Order{ID: 1, Product: product, Amount: 2} emailNotifier := notification.EmailNotifier{Email: "[email protected]"} orderService := service.OrderService{Notifier: emailNotifier} orderService.ProcessOrder(order) smsNotifier := notification.SMSNotifier{PhoneNumber: "123-456-7890"} orderService = service.OrderService{Notifier: smsNotifier} orderService.ProcessOrder(order) }
В этом примере мы создаем структуры Product
и Order
, а также интерфейс Notifier
с двумя реализациями, EmailNotifier
и SMSNotifier
. Это демонстрирует инкапсуляцию, методы, интерфейсы и полиморфизм. Мы также компонуем Product
в Order
, демонстрируя композицию и встраивание.
Заключение
Go предлагает свежий взгляд на объектно-ориентированное программирование, поощряя композицию вместо наследования и предоставляя первоклассную поддержку интерфейсов, параллелизма и многого другого. Простота языка и его мощная стандартная библиотека делают Go отличным выбором для различных приложений, включая веб-серверы, конвейеры данных и даже машинное обучение. Ориентируясь на простоту и прагматизм, Go помогает разработчикам писать понятный и эффективный код, что делает его отличным выбором для современных сложных программных систем.
Спасибо за внимание!
Вы можете получить полный доступ ко всем историям на Medium всего за 5 долларов в месяц, зарегистрировавшись по этой ссылке.