golang

Книга «Golang для профи: Создаем профессиональные утилиты, параллельные серверы и сервисы, 3-е изд.…

  • пятница, 15 сентября 2023 г. в 00:00:22
https://habr.com/ru/companies/piter/articles/761096/
image Привет, Хаброжители!

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

Третье издание «Golang для профи» исследует практические возможности Go и описывает такие продвинутые темы, как параллелизм и работа сборщика мусора Go, использование Go с Docker, разработка мощных утилит командной строки, обработка данных в формате JSON (JavaScript Object Notation) и взаимодействие с базами данных. Кроме того, книга дает дополнительные сведения о работе внутренних механизмов Go, знание которых позволит оптимизировать код на Go и использовать типы и структуры данных новыми и необычными способами.

Также охватываются некоторые нюансы и идиомы языка Go, предлагаются упражнения и приводятся ссылки на ресурсы для закрепления полученных знаний.

Станьте опытным программистом на Go, создавая системы и внедряя передовые методы программирования на Go в свои проекты!

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

Рефлексия


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

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

Вот два главных вопроса, которые я задал себе, когда впервые познакомился с рефлексией.

  • Почему рефлексия была включена в Go?
  • Когда я должен использовать рефлексию?

Отвечу на первый вопрос: рефлексия позволяет динамически выяснять тип произвольного объекта вместе с информацией о его структуре. Для работы с рефлексией в Go представлен пакет reflect. Помните, в предыдущей главе мы упоминали, что fmt.Println() достаточно сообразителен, чтобы понимать типы данных своих параметров и действовать соответствующе? Так вот, «под капотом» пакет fmt использует для этого рефлексию.

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

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

Появление дженериков в Go может в ряде случаев снизить частоту использования рефлексии, поскольку с их помощью можно легко работать с различными типами данных, не зная заранее их точно. Однако ничто не сравнится с рефлексией, когда необходимо полностью изучить структуру и типы данных переменной. Мы сравним рефлексию с дженериками в главе 13.

Наиболее полезными частями пакета reflect являются два типа данных: reflect.Value и reflect.Type. В частности‚ reflect.Value используется для хранения значений любого типа, тогда как reflect.Type служит для представления Go-типов. Существуют две функции: reflect.TypeOf() и reflect.valueOf(), которые возвращают reflect.Type и reflect.Value соответственно. Обратите внимание, что reflect.TypeOf() возвращает фактический тип переменной‚ и если мы исследуем структуру, то она вернет имя структуры.

Поскольку структуры играют ключевую роль в Go, пакет reflect содержит метод reflect.NumField(), предназначенный для перечисления количества полей в структуре, а также метод Field(), позволяющий получать значение reflect.Value определенного поля структуры.

Пакет reflect также определяет тип данных reflect.Kind, который используется для представления определенного типа данных переменной: int, struct и т. д. В документации к пакету reflect перечислены все возможные значения типа данных reflect.Kind. Функция Kind() возвращает вид переменной.

Наконец, методы Int() и String() возвращают целое и строковое значения reflect.Value соответственно.

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

Изучение внутренней структуры Go-структуры


Следующая утилита показывает, как использовать рефлексию для обнаружения внутренней структуры и полей переменной Go-структуры. Введите ее и сохраните как reflection.go:

package main

import (
     "fmt"
     "reflect"
)

type Secret struct {
     Username string
     Password string
}

type Record struct {
     Field1 string
     Field2 float64
     Field3 Secret
}

func main() {
     A := Record{"String value", -12.123, Secret{"Mihalis", "Tsoukalos"}}

Мы начинаем с определения структурной переменной Record, которая содержит другое структурное значение (Secret{«Mihalis», «Tsoukalos»}).

     r := reflect.ValueOf(A)
     fmt.Println("String value:", r.String())

Здесь возвращается значение reflect.Value переменной A.

     iType := r.Type()

Используя Type(), мы получаем тип данных переменной — в данном случае переменной A.

     fmt.Printf("i Type: %s\n", iType)
     fmt.Printf("The %d fields of %s are\n", r.NumField(), iType)

     for i := 0; i < r.NumField(); i++ {

Цикл for выше позволяет посетить все поля структуры и изучить их характеристики.

          fmt.Printf("\t%s ", iType.Field(i).Name)
          fmt.Printf("\twith type: %s ", r.Field(i).Type())
          fmt.Printf("\tand value _%v_\n", r.Field(i).Interface())

Оператор fmt.Printf() выше возвращает имя, тип данных и значение полей.

          // проверьте, есть ли другие структуры, встроенные в запись
          k := reflect.TypeOf(r.Field(i).Interface()).Kind()
          // нужно преобразование в строку, чтобы можно было сравнить
          if k.String() == "struct" {

Чтобы проверить тип данных переменной с помощью строки, нам нужно сначала преобразовать тип данных в string.

               fmt.Println(r.Field(i).Type())
          }

          // то же, что и раньше, но с использованием внутреннего значения
          if k == reflect.Struct {

Во время проверки вы также можете использовать внутреннее представление типа данных. Однако смысла в этом меньше, чем в использовании значения string.

               fmt.Println(r.Field(i).Type())
          }
     }
}

При выполнении reflection.go мы получаем такой вывод:

$ go run reflection.go
String value: <main.Record Value>
i Type: main.Record
The 3 fields of main.Record are
          Field1 with type: string           and value _String value_
          Field2 with type: float64          and value _-12.123_
          Field3 with type: main.Secret      and value _{Mihalis Tsoukalos}_
main.Secret
main.Secret

Итак‚ main.Record — полное уникальное имя структуры, определенное Go‚ main — имя пакета, а Record — имя структуры. Это позволяет Go различать элементы разных пакетов.

Представленный код не изменяет никаких значений структуры. Если бы требовалось внести изменения в значения ее полей, то пришлось бы использовать метод Elem() и передавать ее в виде указателя в valueOf() — помните, что указатели позволяют вносить изменения в фактическую переменную. Есть методы, благодаря которым можно изменять существующее значение. В нашем случае мы собираемся с помощью setString()и SetInt() изменять поля string и int соответственно.

Данный метод описан в следующем подразделе.

Изменение значений структуры с использованием рефлексии


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

Введите следующий код Go и сохраните его как setValues.go. Его также можно найти в репозитории книги на GitHub:

package main

import (
     "fmt"
     "reflect"
)

type T struct {
     F1 int
     F2 string
     F3 float64
}

func main() {
     A := T{1, "F2", 3.0}

A — это переменная, которая исследуется в данной программе.

     fmt.Println("A:", A)

     r := reflect.ValueOf(&A).Elem()

С помощью Elem() и указателя на переменную A она может быть изменена при необходимости.

     fmt.Println("String value:", r.String())
     typeOfA := r.Type()
     for i := 0; i < r.NumField(); i++ {
          f := r.Field(i)
          tOfA := typeOfA.Field(i).Name
          fmt.Printf("%d: %s %s = %v\n", i, tOfA, f.Type(), f.Interface())

          k := reflect.TypeOf(r.Field(i).Interface()).Kind()
          if k == reflect.Int {
               r.Field(i).SetInt(-100)
          } else if k == reflect.String {
               r.Field(i).SetString("Changed!")
          }
     }

Мы используем SetInt() для изменения целочисленного значения и setString() для изменения значения string. Целочисленные значения устанавливаются равными -100, а строковые значения — Changed!..

          fmt.Println("A:", A)
     }

При выполнении setValues.go мы получаем такой вывод:

     $ go run setValues.go
     A: {1 F2 3}
     String value: <main.T Value>
     0: F1 int = 1
     1: F2 string = F2
     2: F3 float64 = 3
     A: {-100 Changed! 3}

Первая строка выходных данных показывает начальную версию A, тогда как последняя — окончательную версию A с измененными полями. Основное применение такого кода заключается в динамическом изменении значений полей структуры.

Три недостатка рефлексии


Без сомнения, рефлексия — эффективная функция Go. Однако, как и все инструменты, ее следует использовать осторожно по трем основным причинам.
  • Первая заключается в том, что широкое использование рефлексии затрудняет чтение и поддержку ваших программ. Потенциальным решением этой проблемы служит хорошее документирование, но разработчики печально известны тем, что не находят времени на написание надлежащей документации.
  • Вторая причина состоит в том, что Go-код, использующий рефлексию, замедляет ваши программы. Вообще говоря, Go-код, который работает с определенным типом данных, всегда быстрее, чем Go-код, который использует рефлексию для динамической работы с любым типом данных Go. Кроме того, такой динамический код затрудняет рефакторинг или анализ вашего кода с помощью специальных инструментов.
  • Последняя причина заключается в том, что ошибки рефлексии не могут быть обнаружены во время сборки и появляются в виде сообщений об ошибке (panic) уже во время выполнения. Это означает, что ошибки рефлексии потенциально могут привести к аварийному завершению ваших программ. Это может произойти через месяцы или даже годы после разработки Go-программы! Одним из решений проблемы будут тщательные проверки перед вызовом опасной функции. Однако это добавляет еще больше Go-кода в ваши программы, что делает их еще медленнее.

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

Методы типа


Метод типа — это функция, которая привязана к определенному типу данных. Методы типов (или методы на типах) на самом деле являются функциями, однако определяются и используются немного по-другому.

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

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

Создание методов типа


Итак, представьте, что вы хотите выполнить вычисления с помощью матриц 2 × 2. Самым естественным способом реализации этого станет определение нового типа данных и методов типа для сложения, вычитания и умножения матриц 2 × 2 с использованием этого нового типа данных. Чтобы сделать его еще более интересным и универсальным, мы создадим утилиту командной строки, которая принимает элементы двух матриц 2 × 2 в качестве аргументов командной строки (в общей сложности восемь целых значений) и выполняет все три вычисления, используя определенные методы типа.

Имея тип данных ar2x2, вы можете создать для него метод типа functionName следующим образом:

     func (a ar2x2) FunctionName(parameters) <return values> {
          ...
     }

Часть (a ar2x2) — это то, что делает функцию functionName() методом типа, поскольку связывает ее с типом данных ar2x2. Никакой другой тип данных не сможет использовать эту функцию. Однако вы можете без проблем реализовать functionName() для других типов данных или в виде обычной функции. Если у вас есть переменная ar2x2 с именем varAr, вы можете вызвать functionName() как varAr.functionName(...), что выглядит как выбор поля структуры.

Вы не обязаны использовать методы типа, если не хотите этого. Фактически любой метод типа может быть переписан как обычная функция. Следовательно, functionName() можно переписать так:

     func FunctionName(a ar2x2, parameters...) <return values> {
          ...
     }

Имейте в виду, что «под капотом» компилятор Go действительно превращает методы в обычные вызовы функций со значением self в качестве первого параметра. Однако для работы интерфейсов требуется использование методов типа.

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

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

Бо́льшую часть времени и когда есть такая необходимость, результаты метода типа сохраняются в переменной, которая вызвала метод типа. Для реализации этого в случае типа данных ar2x2 мы передаем указатель на массив, который вызвал метод типа, например‚ так: func (a *ar2x2).

В следующем подразделе показаны методы типа в действии.

Использование методов типа


В этом подразделе показано использование методов типа с помощью типа данных ar2x2 в качестве примера. Функция Add() и метод Add() применяют один и тот же алгоритм для сложения двух матриц. Единственное различие заключается в способе вызова и в том факте, что функция возвращает массив, тогда как метод сохраняет результат в вызывающей переменной.

Сложение и вычитание матриц — простая задача (вы просто поэлементно складываете или вычитаете элементы первой и второй матрицы‚ расположенные в одинаковой позиции), но умножение матриц — уже более сложный процесс.

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

Если вы определяете методы типа для структуры, то должны убедиться, что имена методов типа не конфликтуют с каким-либо именем поля структуры, поскольку компилятор Go отклонит такие двусмысленности.

Введите следующий код и сохраните его как methods.go:

package main

import (
     "fmt"
     "os"
     "strconv"
)

type ar2x2 [2][2]int

// традиционная функция Add()
func Add(a, b ar2x2) ar2x2 {
     c := ar2x2{}
     for i := 0; i < 2; i++ {
          for j := 0; j < 2; j++ {
               c[i][j] = a[i][j] + b[i][j]
          }
     }
     return c
}

Здесь у нас есть традиционная функция, которая складывает две переменные ar2x2 и возвращает результат.

// метод типа Add()
func (a *ar2x2) Add(b ar2x2) {
     for i := 0; i < 2; i++ {
          for j := 0; j < 2; j++ {
               a[i][j] = a[i][j] + b[i][j]
          }
     }
}

А здесь метод типа Add(), который привязан к типу данных ar2x2. Результат сложения не возвращается. В данном случае переменная ar2x2, которая вызвала метод Add(), будет изменена и сохранит результат — это причина использования указателя при определении метода типа. Если такое поведение вам не нужно, придется изменить сигнатуру и реализацию метода типа в соответствии с вашими потребностями.

// метод типа Substract()
func (a *ar2x2) Subtract(b ar2x2) {
     for i := 0; i < 2; i++ {
          for j := 0; j < 2; j++ {
               a[i][j] = a[i][j] - b[i][j]
          }
     }
}

Представленный выше метод вычитает ar2x2 b из ar2x2 a, а результат сохраняется в a.

// метод типа Multiply()
func (a *ar2x2) Multiply(b ar2x2) {
     a[0][0] = a[0][0]*b[0][0] + a[0][1]*b[1][0]
     a[1][0] = a[1][0]*b[0][0] + a[1][1]*b[1][0]
     a[0][1] = a[0][0]*b[0][1] + a[0][1]*b[1][1]
     a[1][1] = a[1][0]*b[0][1] + a[1][1]*b[1][1]
}

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

func main() {
     if len(os.Args) != 9 {
          fmt.Println("Need 8 integers")
          return
     }

     k := [8]int{}
     for index, i := range os.Args[1:] {
          v, err := strconv.Atoi(i)
          if err != nil {
               fmt.Println(err)
               return
          }
          k[index] = v
     }
     a := ar2x2{{k[0], k[1]}, {k[2], k[3]}}
     b := ar2x2{{k[4], k[5]}, {k[6], k[7]}}

Функция main() получает входные данные и создает две матрицы 2 × 2. После этого выполняются нужные вычисления с этими двумя матрицами.

     fmt.Println("Traditional a+b:", Add(a, b))
     a.Add(b)
     fmt.Println("a+b:", a)
     a.Subtract(a)
     fmt.Println("a-a:", a)

     a = ar2x2{{k[0], k[1]}, {k[2], k[3]}}

Мы вычисляем a + b двумя различными способами: с помощью обычной функции и с помощью метода типа. Поскольку и a.Add(b), и a.Subtract(a) изменяют значение a, мы должны инициализировать a, прежде чем использовать его повторно.

     a.Multiply(b)
     fmt.Println("a*b:", a)
     
     a = ar2x2{{k[0], k[1]}, {k[2], k[3]}}
     b.Multiply(a)
     fmt.Println("b*a:", b)
}

Наконец, мы вычисляем a * b и b * a, чтобы показать их различие, поскольку свойство коммутативности не применяется к умножению матриц.

При выполнении methods.go мы получаем такой вывод:

$ go run methods.go 1 2 0 0 2 1 1 1
Traditional a+b: [[3 3] [1 1]]
a+b: [[3 3] [1 1]]
a-a: [[0 0] [0 0]]
a*b: [[4 6] [0 0]]
b*a: [[2 4] [1 2]]

Входными данными здесь служат две матрицы 2 × 2, [[1 2] [0 0]] и [[2 1] [1 1]], а результат — это их расчеты.

Теперь, когда мы знаем о методах типов, пришло время перейти к интерфейсам, поскольку они не могут быть реализованы без методов типа.

Об авторе
Михалис Цукалос — системный инженер UNIX, который увлекается написанием технических текстов. Автор книг Go Systems Programming и Mastering Go1, как первого, так и второго изданий. Получил степень бакалавра математики в Университете Патр и степень магистра информационных технологий в Университетском колледже Лондона. Написал более 300 технических статей для различных журналов, включая Sys Admin, MacTech, Linux User and Developer, Usenix ;login:, Linux Format и Linux Journal. В круг научных интересов Михалиса входят временные ряды, базы данных и индексирование.

Вы можете связаться с автором по адресу www.mtsoukalos.eu и @mactsouk.
О научном редакторе
Дерек Паркер — инженер-программист в Red Hat. Создатель отладчика Delve для Go и автор компилятора Go, компоновщика и стандартной библиотеки. Является автором проектов с открытым исходным кодом и специалистом по сопровождению ПО‚ работал над множеством проектов — от интерфейсного JavaScript до низкоуровневого ассемблерного кода.

Более подробно с книгой можно ознакомиться на сайте издательства:

» Оглавление
» Отрывок

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — Golang