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:

  1. Структуры. Go использует структуры для группировки данных, аналогичные классам в других языках. Структуры могут иметь связанные с ними методы.
  2. Методы: Go позволяет вам определять методы для структур. Это позволяет инкапсулировать данные и поведение внутри структуры.
  3. Инкапсуляция: Go использует экспортированные и неэкспортированные идентификаторы для управления видимостью, аналогично общедоступным и частным в других языках.
  4. Композиция и встраивание.Вместо наследования Go поощряет композицию и встраивание. Одна структура может включать в себя другую структуру, что позволяет повторно использовать код и организовывать его логически.
  5. Интерфейсы и полиморфизм. Go поддерживает полиморфизм через интерфейсы. Если тип предоставляет все методы, определенные в интерфейсе, говорят, что он удовлетворяет этому интерфейсу.
  6. Параллелизм. В 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 долларов в месяц, зарегистрировавшись по этой ссылке.