- Глава 1. Учебник
- Глава 2. Структура программы
- Глава 3. Фундаментальные типы данных
- Глава 4. Составные типы
- Глава 5. Функции
- Глава б. Методы
- Глава 7. Интерфейсы
- 7.1. Интерфейсы как контракты
- 7.2. Типы интерфейсов
- 7.3. Соответствие интерфейсу
- 7.4. Анализ флагов с помощью flag.Value
- 7.5. Значения интерфейсов
- 7.6. Сортировка с помощью sort.Interface
- 7.7. Интерфейс http.Handler
- 7.8. Интерфейс error
- 7.9. Пример: вычислитель выражения
- 7.10. Декларации типов
- 7.11. Распознавание ошибок с помощью деклараций типов
- 7.12. Запрос поведения с помощью деклараций типов
- 7.13. Выбор типа
- 7.14. Пример: XML-декодирование на основе лексем
- 7.15. Несколько советов
- Глава 8. Горутины и каналы
- Глава 9. Параллельность и совместно используемые переменные
- Глава 10. Пакеты и инструменты Go
- Глава 11. Тестирование
- Go -
компилируемый язык
; go run main.go
- компилирует и запускает исходный код;go build main.go
- создает исполняемый файл, который можно запустить позже;- Go поддерживает
Unicode
и может обрабатывать текст с любым набором символов; - Код в Go организован в виде пакетов, которые похожи на модули и библиотеки в других языках программирования;
- Исходный код начинается с объявления пакета и списка импортируемых пакетов;
- Пакет
fmt
содержит функции для форматированного вывода и ввода. ФункцияPrintln()
выводит значения в одну строку с пробелами между ними и символом новой строки в конце; - Пакет
main
определяет отдельную программу, а не библиотеку. Функцияmain
- это точка входа в приложение; - Необходимо импортировать только нужные пакеты, иначе будет ошибка компиляции;
- Функции объявляются с ключевым словом
func
, именем функции, параметрами и возвращаемыми значениями (если есть) и телом функции в фигурных скобках; - Точки с запятой не требуются в конце инструкций или объявлений, кроме случаев, когда несколько инструкций находятся на одной строке;
- Местоположение символов новой строки важно для корректного синтаксического анализа кода Go;
- Инструмент
gofmt
приводит код к стандартному формату, аgoimports
управляет импортом пакетов; - Имена пакетов сортируются в алфавитном порядке с помощью
gofmt
.
- Пакет
os
предоставляет функции для работы с операционной системой кроссплатформенно; - Аргументы командной строки доступны в программе через переменную
os.Args
, которая являетсясрезом
строк; os.Args[0]
- имя команды, остальные элементы - аргументы программы;- Срезы в Go используют полуоткрытые интервалы, включая первый индекс и исключая последний;
- Выражение
s[m:n]
дает срез элементов отm
доn-1
. Если опущено значениеm
илиn
, используются значения по умолчанию0
илиlen(s)
соответственно; - Однострочные комментарии начинаются с
//
, многострочные - с/*...*/
; - Неинициализированные переменные имеют нулевое значение соответствующего типа;
- Оператор
+
для строк выполняет конкатенацию; - Оператор
+=
является присваивающим оператором, который выполняет конкатенацию и присваивание нового значения переменной; - Оператор
:=
используется длякраткого объявления переменных
с присваиванием им типа на основе значения инициализатора; - Операторы i++ и i-- (инкремент\декремент) увеличивают или уменьшают значение переменной на единицу;
- Цикл
for
- единственная инструкция цикла в Go с различными вариантами использования; - Скобки не используются вокруг компонентов цикла for, фигурные скобки обязательны;
Инициализация
выполняется до начала работы цикла и может быть опущена;Условие
вычисляется в начале каждой итерации цикла. Если оно равноtrue
, выполняется тело цикла;Последействие
выполняется после тела цикла, после чего заново вычисляется условие. Если условие равноfalse
, цикл завершается;- Любая из компонентов цикла может быть опущена без использования точек с запятой;
Традиционный
циклwhile
выглядит какfor condition { }
Бесконечный
цикл выглядит какfor { }
и должен быть завершен другим способом, например с помощью инструкцииbreak
илиreturn
;- Цикл
for
сrange
выполняет итерации для диапазона значений для типа данных, например строки или среза; - В каждой итерации цикла
range
возвращаетпару значений
:индекс
изначение элемента
с этим индексом; - Если индекс не нужен, можно использовать пустой идентификатор
_
, который может использоваться везде, где требуется имя переменной, но логике программы он не нужен; - Функция
Join
из пакетаstrings
объединяет элементы с указанным разделителем; - Для вывода значений без форматирования можно использовать функцию
Println
.
- Для условия инструкции
if
скобки не нужны, но для тела инструкции фигурные скобкиобязательны
. Может существовать необязательная частьelse
; Карта
(словарь) содержит набор пар“ключ-значение”
и обеспечиваетконстантное
время выполнения операций хранения, извлечения или проверки наличия элемента;Ключ
может быть любого типа, если значения этого типаможно сравнить
при помощи оператора==
. Значение может быть любого типа;- Если в
карте
(словаре) нет нужного ключа, выражение в правой части присваивает нулевое значение типа новому элементу; Цикл по диапазону
покарте
(словарю) возвращает ключ и значение элемента для конкретного ключа. Порядок обхода карты (словаря) не определен;- Элементы карты (словаря) не отсортированы;
- Пакет
bufio
помогает сделать ввод и вывод эффективным и удобным. ТипScanner
считывает входные данные и разбивает их на строки или слова; - Функция
Scan
возвращаетtrue
, если строка считана и доступна, иfalse
, если входные данные исчерпаны; - Функция
fmt.Printf()
выполняет форматированный вывод на основе списка выражений. Первый аргумент - строка формата, которая указывает, как должны быть отформатированы последующие аргументы; - Функция
Printf()
имеет множество преобразований для форматирования вывода; - Строки могут содержать управляющие последовательности символов, такие как
\n
для новой строки и\t
для табуляции; - По умолчанию
Printf()
не содержит символ новой строки. Функции с окончаниемf
используют такие же правила форматирования какfmt.Printf()
. Функции с окончаниемln
форматируют аргументы как%v
и добавляют\n
; - Функция
os.Open()
возвращаетоткрытый файл
и значение типаerror
. Если ошибка равнаnil
, файл открыт успешно; Если ошибка не равнаnil
, что-то пошло не так; - Функции и другие объекты уровня пакета могут быть объявлены в любом порядке;
- Карта (словарь) является
ссылкой на структуру данных
, созданную функциейmake
. При передаче карты в функцию передаетсякопия ссылки
на объект карты, но сам объект карты не копируется. Изменения в переданной карте будут отражены на исходной карте.
- Объявление
const
дает имена константам - значениям, которыенеизменны
во время компиляции. Константа может быть числом, строкой или булевым значением; - Выражения вида
[]color.Color{...}
иgif.GIF{...}
являютсясоставными литералами
- это компактная запись длясоздания экземпляра составных типов
Go из последовательности значений элементов; - Тип
gif.GIF
- структурный тип. Структура представляет собой группу значений, именуемых полями, собранных в один объект; - {x: 1, y: 2} - структурный литерал - это способ создания и инициализации экземпляра структуры в одной строке кода;
{LoopCount: nframes}
создает значение структуры, поле которого устанавливается равнымnframes
. Все прочие поля имеют нулевое значение своих типов. Обращение к отдельным полям структуры выполняется с помощью записи с точкой.
- Функция
http.Get
из пакетаnet/http
выполняет HTTP-запрос и при отсутствии ошибок возвращает результат в структуру. ПолеBody
этой структуры содержит ответ сервера в виде потока, доступного для чтения; - Метод
io.ReadAll
из пакетаio
считывает поток, результат сохраняется в переменную; - Поток
Body
нужно закрыть для предотвращения утечки ресурсов.
Горутина
- параллельное выполнение функции;Канал
является механизмом связи, который позволяет одной горутинепередавать значения определенного типа
другой горутине. Функцияmain
выполняется в горутине, а инструкцияgo
создает дополнительные горутины;- Функция
io.Copy()
считывает тело ответа и игнорирует его, записывая в выходной потокio.Discard
.Copy
возвращает количество байтов и информацию о произошедших ошибках; - Когда одна горутина пытается отправить или получить информацию по каналу, она блокируется, пока другая горутина пытается выполнить те же действия. После передачи информации обе горутины продолжают работу;
- Весь вывод осуществляется функцией main, что гарантирует, что вывод каждой горутины будет обработан как единое целое без чередования вывода при завершении двух горутин в один и тот же момент времени.
- Запрос представлен структурой типа
http.Request
, которая содержит ряд связанных полей, включаяURL
входящего запроса. Полученный запрос передается функции-обработчику, которая извлекает компонент пути из URL запроса и отправляет его обратно в качестве ответа с помощьюfmt.Fprintf()
; - Чтобы запустить сервер в фоновом режиме в ОС Linux и Mac, нужно ввести символ
&
. Чтобы завершить процесс, нужно найти его с помощью командыps -ef | grep server1.go
и выполнить командуkill PID
. В ОС Windows символ амперсанда не нужен; - Сервер запускает обработчик для каждого входящего запроса в
отдельной горутине
. Однако, если два параллельных запроса попытаются обновить переменную в один и тот же момент времени, может возникнуть ошибка под названиемсостояние гонки ( race condition)
; Состояние гонки (race condition)
- это ошибка, которая может возникнуть в многопоточных или параллельных программах. Она происходит, когда две или более горутинпытаются обновить переменную в один и тот же момент времени
. В результате значение переменной может быть увеличено не согласованно и стать непредсказуемым. Чтобы избежать этой проблемы, нужно гарантировать, что доступ к переменной получает не более одной горутины одновременно. Для этого каждый доступ к переменной должен быть окружен вызовамиmu.Lock()
иmu.Unlock()
;- Go разрешает простым инструкциям предшествовать условию
if
, что особенно полезно при обработке ошибок. Это делает код короче и уменьшает область видимости переменной err, что является хорошей практикой.if err := r.ParseForm(); err != nil {}
.
- Инструкция
switch
представляет собой инструкцию множественного ветвления. Результат вызова функции или значения сравнивается со значением в каждой частиcase
; - Значения проверяются
сверху вниз
, и при первом найденном совпадении выполняется соответствующий код. Необязательный вариантdefault
выполняется, еслинет совпадений
; - Инструкция
switch
может обойтись ибез операнда
и просто перечислять различные инструкцииcase
, каждая из которых представляет собой логическое выражение. Такая инструкция называетсяпереключатель без тегов
; - Инструкции
break
иcontinue
модифицируют поток управления. Инструкцияbreak
передает управление следующей инструкции после наиболее глубоко вложенной инструкцииfor
,switch
илиselect
. Инструкцияcontinue
заставляет наиболее глубоко вложенный циклfor
начать очередную итерацию; - Инструкции могут иметь
метки
, так чтоbreak
иcontinue
могут на них ссылаться. Имеется инструкцияgoto
, она предназначена для машинно-генерируемого кода; - Объявление
type
позволяет присваивать имена существующим типам. Структурные типы зачастую длинны, поэтому они почти всегда именованны; - Go предоставляет
указатели
-значения, содержащие адреса переменных
. Оператор&
дает адрес переменной, а оператор*
позволяет получить значение переменной, на которую указывает указатель. Однако, арифметики указателей в Go нет;* Метод представляет собой функцию, связанную с именованным типом. Методы могут быть связаны почти с любым именованным типом; Интерфейс
представляет собойабстрактные типы
, которые позволяют рассматривать различные конкретные типы одинаково, на основе имеющихся у них методов;- Go поставляется с обширной стандартной библиотекой полезных пакетов. Программирование часто состоит в использовании существующих пакетов;
- Хороший стиль требует написания комментария перед объявлением каждой функции, описывающим ее поведение. Это важно для
инструментов
go doc
иgodoc
, которые используются для поиска и отображения документации. Для многострочных комментариев можно использовать запись/*...*/
.
- Имена функций, переменных, констант, типов, меток инструкций и пакетов в Go должны начинаться с буквы (считается
буквой в
Unicode
) или с подчеркивания и могут содержать любое количество дополнительных букв, цифр и подчеркиваний; - Имена чувствительны к регистру:
heapSort
иHeapsort
- разные имена; - В Go есть 25 ключевых слов, которые могут использоваться только согласно синтаксису языка и 30 предопределенных имен для встроенных констант, типов и функций;
- Имена предопределенных сущностей не являются зарезервированными и могут использоваться в объявлениях;
- Сущности, объявленные внутри функции, являются
локальными
, а объявленные вне функции -глобальными
для пакета; Регистр первой буквы
имени определяет видимость сущности за пределами пакета: имена, начинающиеся с заглавной буквы,экспортируются
;- Имена пакетов состоят только из строчных букв;
- В Go принято выбирать короткие имена, особенно для локальных переменных с небольшой областью видимости, но чем больше область видимости, тем длиннее и значимее имя должно быть;
- Стилистически в Go используется
camelCase
, а сокращения, такие какASCII
иHTML
, должны быть в одном регистре.
- В Go есть 4 основные разновидности объявлений:
var
,const
,type
иfunc
, которые помогают именовать и определять свойства программных сущностей; - Программа на языке Go хранится в файлах с расширением
.go
, состоящих из объявленияpackage
, объявленийimport
и последовательности типов, переменных, констант и функций уровня пакета; - Объявления уровня пакета видимы во всех файлах пакета, в то время как локальные объявления видны только в пределах функции, в которой они объявлены;
- Объявление функции содержит имя, список передаваемых параметров, необязательный список результатов и тело функции с операторами, определяющими ее действия;
- Функции могут инкапсулировать логику и быть использованы в нескольких местах программы, что упрощает разработку, сопровождение и повторное использование кода.
- Объявление переменных в Go осуществляется с помощью ключевого слова
var
, после которого указывается имя переменной, тип и начальное значение; - Тип и начальное значение переменной могут быть
опущены, но не одновременно
. При опущенном типе он определяется из инициализирующего выражения, при опущенном значении - присваивается нулевое значение для данного типа (var s := 1; var s int
, соответственно); - Нулевые значения для разных типов:
0
для чисел,false
для булевых переменных,""
для строк иnil
для интерфейсов и ссылочных типов. Это гарантирует, что переменные в Go всегда хранят определенные значения своего типа, упрощая код и обеспечивая разумное поведение в граничных условиях; - В одном объявлении можно объявить и инициализировать несколько переменных разных типов (
s, v := 1, "hello"
); - Инициализаторы переменных могут быть литеральными значениями или произвольными выражениями. Литералы - это фиксированные значения, которые не могут быть изменены;
- Переменные уровня пакета инициализируются до начала выполнения функции
main
, а локальные переменные инициализируются при встрече их объявления в процессе выполнения функции; - Множество переменных может быть инициализировано с помощью вызова функции, возвращающей несколько значений. Например,
функция
os.Open(name)
возвращает файл и ошибку (var f, err = os.Open(name)
).
- Краткое объявление переменной использует оператор
:=
и позволяет объявить и инициализировать локальные переменные без явного указания их типа (s := i
); - Краткое объявление переменной определяет тип переменной на основе типа выражения справа от оператора
:=
; - Можно использовать краткое объявление переменной для нескольких переменных одновременно, например
i, j := 0, 1
; - Присваивание значений переменным осуществляется через оператор
=
, например,i, j = j, i
для обмена значений переменныхi
иj
; - Если переменная уже была объявлена в том же лексическом блоке, то краткое объявление переменной действует как присваивание;
- Краткое объявление переменной должно объявлять хотя бы одну новую переменную, иначе код не будет компилироваться;
- Краткое объявление переменной упрощает работу с локальными переменными, делает код более читабельным и понятным.
- Переменные в Go представляют собой небольшие блоки памяти, хранящие значение;
- Указатели в Go - это переменные,
хранящие адрес
другой переменной; - Оператор
&
позволяет получить адрес переменной, а оператор*
позволяет получить значение из адреса переменной; - Значение указателя может быть равно
nil
, если указательне указывает ни на одну переменную
; - Указатели могут быть использованы для косвенного изменения значений переменных (через
указатель
s := 1; w := &s; *w = 10; fmt.Println(s==10) // true
); - Передача указателя в функцию позволяет
обновить значение переменной
, косвенно переданной в функцию; - Указатели могут создавать псевдонимы переменных, что делает доступ к переменной возможным без использования её имени;
- Использование указателей является ключом к пакету
flag
, который позволяет управлять аргументами командной строки программы; - Чтобы использовать переменные-флаги, необходимо вызвать функцию
flag.Parse
перед их использованием, доступ к значению происходит по*flag
.
- Встроенная функция
new(T)
в Go позволяет создать неименованную переменную с типомT
, инициализировать ее нулевым значением и вернуть адрес этой переменной в виде указателя типа*T
; - Использование функции
new
удобно, когда нужно создать переменную без необходимости объявлять её имя; - Неименованная переменная, созданная с помощью функции
new
, ничем не отличается от обычной локальной переменной, у которой берется адрес; - После создания неименованной переменной с помощью функции
new
, её значение можно изменять косвенно через указатель; - Возвращаемые значения функции
new
имеют уникальные адреса, за исключением переменных с нулевым размером, которые могут иметь одинаковые адреса в зависимости от реализации; - Функция
new
используется относительно редко, так как часто предпочитают использовать литеральный синтаксис для создания и инициализации структурных типов (person := Person{name: "John", age: 30}
); - Так как
new
является предопределенной функцией, а не ключевым словом, её имя можно переопределить для других целей в рамках области видимости пакета.
- Время жизни переменной - это период, в течение которого она существует в программе;
- Переменные уровня пакета имеют время жизни, равное времени работы всей программы;
- Локальные переменные имеют
динамическое время жизни
и создаются каждый раз при выполнении оператора объявления; - Параметры и результаты функций являются
локальными переменными
и создаются при каждом вызове функции; - Go использует алгоритм сборки мусора, основанный на отслеживании
ссылок на переменные
, что позволяет освободить память, выделенную для ненужных переменных; - Локальная переменная может продолжать существовать даже после возврата значения из охватывающей функции:
func f() *int { i := 10 return &i // возвращаем ссылку на локальную переменную i }
- Компилятор может выбрать, где разместить локальные переменные
(в стеке или куче)
, независимо от того, была ли переменная объявлена с помощьюvar
илиnew
; - Необходимо учитывать время жизни переменных для написания эффективных программ, избегая утечек памяти и ненужного использования ресурсов;
- Сборка мусора облегчает написание корректных программ, но не освобождает от бремени размышлений о памяти и времени жизни переменных.
- Значение переменной обновляется с помощью оператора присваивания
=
; - Оператор присваивания может быть использован для именованных переменных, косвенных переменных, полей структур и элементов массива, среза или карты;
- Арифметические и побитовые бинарные операторы имеют соответствующие присваивающие операторы, которые позволяют упростить запись и избежать повторения выражений;
- Числовые переменные могут быть увеличены или уменьшены с помощью инструкций
++
и--
, которые представляют собой краткую форму операции сложения или вычитания единицы.
- Присваивание кортежу - это метод, который позволяет присваивать значения нескольким переменным одновременно, делая код более компактным и понятным;
- Все выражения справа вычисляются перед присваиванием значений переменным слева, что позволяет обменивать значения
переменных, например, вычислять наибольший общий делитель или числа Фибоначчи:
-
func fib(n int) int { x, y, := 0, 1 for i := 0; i < n; i++ { x, y = y, x+y } return x }
-
- Присваивание кортежу удобно для обмена значений между переменными, такими как
x, y = y, x
илиa[i], a[j] = a[j], a[i]
; - Для функций с несколькими результатами присваивание кортежу используется для того, чтобы присвоить значения каждому из
результатов, например:
f, err = os.Open("foo.txt")
; - Дополнительные результаты функций часто используются для указания на ошибки, возвращая значение
error
или булево значениеok
; - В присваивании кортежу можно использовать пустой идентификатор
_
, чтобы игнорировать ненужные значения, например:_, err = io.Copy(dst, src)
; - Присваивание кортежу повышает читаемость и понятность кода, но нужно избегать его использования при наличии сложных выражений, так как последовательность отдельных инструкций может быть легче для чтения.
- Присваивание в программировании может быть явным и неявным, но в любом случае значение должно быть присваиваемо типу переменной;
- Неявное присваивание происходит, например, при вызове функции, инструкции
return
или литеральных выражениях для составных типов; - Присваивание считается корректным, когда типы переменной и значения точно соответствуют, однако значение
nil
может быть присвоено любой переменной интерфейсного или ссылочного типа; - Правила присваиваемости для констант более гибкие, что позволяет избежать большинства явных преобразований типов;
- Вопрос о возможности сравнения двух значений с помощью операторов
==
и!=
также связан с присваиваемостью: оба операнда должны быть присваиваемы друг другу, то есть иметь одинаковый тип; - Понимание правил присваиваемости помогает писать корректный код и избегать ошибок, связанных с несоответствием типов.
- Тип переменной или выражения определяет характеристики значений: размер, внутреннее представление, возможные операции и связанные методы;
- Разные переменные могут использовать одно и то же внутреннее представление, но быть разными по
смыслу (
var index, count int
); - Объявление
type
определяет новый именованный тип с тем же базовым типом, что и существующий, что обеспечивает возможность различать и контролировать их использование, избегая ошибок; - Именованные типы определенные на уровне пакета видны всему пакету и доступны в других пакетах, если экспортируются;
- Несмотря на связь с одним базовым типом, именованные типы считаются разными типами и для их использования вместе
требуется явное преобразование типа (
Celsius(f)
); - Преобразование типа не изменяет значение и его представление, но меняет его смысл (тип);
- Преобразование значений разрешено, если они имеют один и тот же базовый тип или являются неименованными указателями на переменные с одним базовым типом;
- Именованный тип позволяет определить новое поведение значений этого типа через методы типа, такие как
метод
String()
, который управляет видом значений данного типа при выводе строк.
- Пакеты в Go поддерживают модульность, инкапсуляцию, раздельную компиляцию и повторное использование;
Модульность
- это подход к созданию программных систем, в котором каждая функциональность реализуется в отдельных модулях, связанных между собой через определенные интерфейсы;Инкапсуляция
- это механизм, который позволяет скрыть детали реализации модуля от других компонентов системы, обеспечивая доступ только к определенным элементам модуля;Раздельная компиляция
- это процесс, при котором каждый модуль может редактироваться и компилироваться отдельно, минимизируя время компиляции и облегчая обновление системы;Повторное использование
- это возможность использовать уже написанные и отлаженные модули в новых приложениях, что ускоряет их разработку и повышает надежность.
- Исходный текст пакета располагается в одном или нескольких файлах
.go
, обычно в каталоге с соответствующим именем; - Каждый пакет служит в качестве отдельного пространства имен для своих объявлений;
- Чтобы обратиться к функции за пределами ее пакета, следует квалифицировать идентификатор, например
image.Decode()
илиutf16.Decode()
; - В Go экспортируются идентификаторы, которые начинаются с прописной буквы;
- Каждый файл пакета начинается с объявления
package
, которое определяет имя пакета; - Имена уровня пакета, объявленные в одном файле пакета, являются видимыми для всех других файлов пакета;
- Пакеты могут иметь документирующий комментарий, который предшествует объявлению
package
и начинается с резюмирующего предложения. Обычно только один файл пакета имеет такой комментарий, иногда помещают его в отдельный файлdoc.go
.
- Импорт пакетов в Go происходит с помощью уникального пути импорта, который идентифицирует пакет, и объявления импорта;
- Путь импорта обозначает каталог, содержащий исходные файлы
.go
, которые совместно образуют пакет; - Имя пакета - это краткое (необязательно уникальное) имя, которое соответствует последней части пути импорта;
- Объявление импорта связывает краткое имя импортируемого пакета, которое может использоваться для обращения к его
содержимому в данном файле (
import m "math"
); - Импорт пакета без последующего использования является ошибкой, что позволяет избежать ненужных зависимостей;
- Чтобы автоматически добавлять или удалять пакеты из объявлений импорта, рекомендуется использовать инструмент
golang.org/x/tools/cmd/goimports
; - Использование инструмента
goimports
иgofmt
позволяет приводить исходный код Go к каноническому формату.
- Инициализация пакета начинается с инициализации переменных уровня пакета в порядке их объявления, при этом сначала разрешаются зависимости;
- Если пакет состоит из нескольких
.go
файлов, их инициализация идет в порядке передачи файлов компилятору, инструментgo
сортирует.go
файлы по имени перед вызовом компилятора; - Для инициализации некоторых переменных, таких как таблицы данных, можно использовать функцию
init
, которая выполняется автоматически при запуске программы; - Функции
init
нельзя вызывать или обратиться к ним, они имеют вид:func init() { /*...*/ }
; - Инициализация пакетов выполняется по одному пакету за раз в порядке объявления пакета в программе, сначала инициализируются зависимости, а затем следующий пакет;
- Последним инициализируется пакет
main
, таким образом, все пакеты оказываются полностью инициализированными до начала выполнения функцииmain
приложения.
- Область видимости - часть исходного кода, где объявленное имя ссылается на сущность из этого объявления. Является свойством времени компиляции;
- Время жизни переменной - диапазон времени выполнения, когда к переменной можно обращаться из других частей программы. Является свойством времени выполнения;
- Синтаксический блок - последовательность инструкций, заключенных в фигурные скобки (тело функции, цикла и т.д.);
- Лексические блоки - группы объявлений, определяющие область видимости имени (всеобщий блок, уровень пакета, уровень
файла, блоки конструкций
for
,if
иswitch
,case
вswitch
илиselect
, явные синтаксические блоки); Всеобщий блок
- область видимости встроенных типов, функций и констант (int
,len
,true
и т.д.), доступны на протяжении всей работы программы;Уровень пакета
- область видимости объявлений вне любой функции, доступны из любого файла в том же пакете;Уровень файла
- область видимости импортированных пакетов, доступны только в том же файле;Локальные объявления
- область видимости ограничена пределами функции или ее части;- Область видимости
метки
управления потоком - вся охватывающая функция; - Внутреннее объявление затеняет или
скрывает
внешнее, если имя объявлено и во внутреннем, и во внешнем блоках; - Внутри функции лексические блоки могут быть вложенными с произвольной глубиной вложения, одно локальное объявление может затенять другое;
- Цикл
for
создает два лексических блока:явный блок для тела цикла
инеявный блок, охватывающий переменные, объявленные в инициализации цикла
; - В Go порядок объявления переменных не влияет на их область видимости на уровне пакета, что позволяет создавать рекурсивные и взаимно рекурсивные типы и функции;
- Область видимости переменных, объявленных в циклах
for
, инструкцияхif
иswitch
, ограничивается только их блоками, что может привести к ошибкам компиляции при обращении к ним вне этих блоков; - Часто требуется объявлять переменные до условия, чтобы они были доступны после него, что предотвращает ошибки компиляции из-за использования неправильной области видимости;
- Чтобы избежать ошибок из-за использования краткого объявления переменных, можно объявлять переменные в отдельном блоке
var
, что позволяет правильно настроить их область видимости; - Понимание области видимости переменных в Go позволяет избегать ошибок и создавать корректно работающие программы.
- Числовые типы данных в Go включают целые числа разных размеров, числа с плавающей точкой и комплексные числа, что позволяет использовать их для разнообразных задач и операций;
- Go предоставляет
знаковые
ибеззнаковые
целочисленные арифметические типы (int8, uint8, int16, uint16, int32, uint32, int64, uint64), что позволяет экономить память и оптимизировать работу программы; - типы
int
иuint
имеют естественный или наиболее эффективный размер для знаковых и беззнаковых целых чисел наконкретной платформе
, что облегчает написание кода, не требуя указания размера; rune
иbyte
являются синонимами дляint32
иuint8
соответственно, и используются для работы с символамиUnicode
и фрагментами неформатированных данных;uintptr
- беззнаковый целочисленный тип, используется для низкоуровневого программирования, например, при работе с библиотеками на C;- Для явного преобразования типов при наложении значений разных типов требуется использовать преобразование
типов
T(x)
; - Знаковые целые числа используют формат дополнения до
2
(знак: 1:"-", 0:"+"
), беззнаковые числа хранят только неотрицательные значения, что нужно учитывать при выполнении операций и выбора диапазона значений; - Бинарные операторы Go включают арифметические, логические и операторы сравнения, они имеют различные приоритеты, что может потребовать использования скобок для ясности или корректного выполнения операций;
- Арифметические операторы
+, -, *, /
могут применяться ко всем числовым типам данных в Go, оператор получения остатка%
работает только с целыми числами; - Поведение оператора деления
/
зависит от того, являются ли его операнды целыми числами или числами с плавающей точкой. Если хотя бы один из операндов является числом с плавающей точкой - результат будет числом с плавающей точкой. Если оба целочисленные - остаток будет отброшен; - В случае
переполнения (overflow) типа
результат арифметической операции может быть некорректным, старшие биты, которые не помещаются в результате, сбрасываются без предупреждения; - В Go для работы с целыми числами можно использовать знаковую и беззнаковую арифметику, что позволяет выбирать подходящий тип данных в зависимости от задачи;
- Старший бит определяет знак числа, младшие биты определяют само число, что позволяет точно представлять положительные и отрицательные числа;
- Целые числа и числа с плавающей точкой в Go являются сравниваемыми и упорядочиваемыми, что облегчает работу с ними;
- Унарные операторы
+
и-
применяются для упрощения работы с целыми числами и числами с плавающей точкой; - Битовые операции позволяют работать с числами на уровне битов, что может быть полезным для определенных задач, таких как анализ форматов бинарных файлов или для хеширования и криптографии;
- Важно использовать беззнаковую арифметику при работе с битовыми шаблонами для корректной работы сдвигов влево и вправо;
- Хотя иногда использование беззнаковых чисел кажется более логичным, в Go часто используют знаковые числа, так как они предотвращают ошибки, которые могут возникнуть из-за особенностей беззнаковой арифметики;
- Беззнаковые числа наиболее подходят для решения специализированных задач, где требуется побитовая или нестандартная арифметическая обработка данных;
- В Go для преобразования значения из одного типа в другой требуется явное преобразование типа, это облегчает понимание
программ и устраняет целый класс проблем (например, при неявном преобразовании
float64
вint
, будет теряться дробная часть); - Для преобразования типа используют операцию
T(x)
, гдеT
- тип, аx
- значение для преобразования; - Многие преобразования из целого значения в целое не влекут за собой изменений значения, однако преобразование, сужающее целое число или преобразование целого числа в число с плавающей точкой и обратно, может изменить значение или привести к потере точности;
- При преобразовании значения с плавающей точкой в целое число дробная часть отбрасывается, к усечению по направлению к нулю;
- Следует избегать преобразований, в которых операнд находится вне диапазона целевого типа, так как это может привести к нежелательным результатам;
- Целочисленные литералы могут быть записаны в разных системах счисления: десятичная, восьмеричная (начинается с
0
) и шестнадцатеричная (начинается с0x
или0X
); - Для вывода чисел с использованием пакета
fmt
можно управлять системой счисления и форматом вывода с помощью символов преобразования%d
,%o
,%x
и других; - Литералы рун записываются как символ в одинарных кавычках, это могут быть символы
ASCII
,Unicode
или управляющие последовательности; - Руны выводятся с помощью символов преобразования
%c
или%q
в пакетеfmt
для вывода соответствующего символа или символа в кавычках.
- В Go существует 2 варианта чисел с плавающей точкой:
float32
иfloat64
, их арифметические свойства регулируются стандартомIEEE 754
; - Значения числовых типов с плавающей точкой находятся в диапазоне от очень малых до очень больших и имеют предельные
значения, которые можно найти в пакете
math
; float32
обеспечивает приблизительно6
десятичных цифр точности, типfloat64
- около15
цифр. Рекомендуется использоватьfloat64
, так как при использовании типаfloat32
быстро накапливается ошибка (потеря точности);- Числа с плавающей точкой могут быть записаны буквально с использованием десятичной записи (
.90000001
,1.
), а для очень малых и очень больших чисел лучше использовать научный формат записи (с буквойe
илиE
); - Для вывода значений с плавающей точкой удобно использовать символ преобразования
%g
функцииPrintf
. Также можно использовать символы преобразования%e
(с показателем степени) или%f
(без показателя степени); - Стандарт
IEEE 754
определяет специальные значения, такие как положительная и отрицательная бесконечность, а также значениеNaN (not a number)
. Они могут быть получены при некоторых вычислениях, и с ними можно работать с помощью функций пакетаmath
; - Если в результате вычислений с плавающей точкой может возникнуть ошибка, лучше сообщать об этом отдельным возвращаемым значением в функции.
- Go имеет поддержку комплексных чисел с двумя размерами -
complex64
иcomplex128
, основанными наfloat32
иfloat64
соответственно; - Встроенные функции
complex
,real
иimag
позволяют создавать комплексные числа и извлекать их действительную и мнимую части; - Мнимые литералы, например
3.141592i
или2i
, обозначают комплексное число с нулевым действительным компонентом; - Комплексные числа можно сравнивать на равенство с помощью операторов
==
и!=
; - Пакет
math/cmplx
предоставляет функции для работы с комплексными числами, такие как квадратный корень или возведение в степень; - Арифметика
complex128
может быть использована для генерации множества Мандельброта, что позволяет создавать визуализации математических объектов.
- Булев тип
bool
имеет только два возможных значения:true
иfalse
, используются для управления условными конструкциями и сравнениями; - Унарный оператор
!
используется для логического отрицания:!true
будет равноfalse
; - Чтобы упростить излишние булевы выражения, рекомендуется использовать
x
вместоx == true
; - Операторы
&& (И)
и|| (ИЛИ)
используются для объединения булевых значений, при этом применяется сокращенное вычисление: правый операнд не вычисляется, если ответ определяется значением левого операнда; - Булевы значения не преобразуются неявно в числовые значения или обратно, для этого нужно использовать явные инструкции
или функции преобразования (в Go нет встроенных, придется писать самому, например):
func Btoi(b bool) int { if b { return 1 } return 0 } func Itob(i int) bool { b := i != 0 return b }
- Использование булевых операторов и выражений позволяет создавать логические условия для управления потоком выполнения программы и обработки различных ситуаций.
- Строки в Go являются
неизменяемыми последовательностями байтов
и обычно содержат текстовые символыUnicode
, закодированные вUTF-8
; - Для получения
длины строки в байтах
используется встроенная функцияlen
, и нужно помнить, что она возвращает количество байтов, а не символов; - Индексирование строк в Go происходит по байтам, а не по символам, и обращение к байту с помощью операции
s[i]
, где0 <= i <= len(s)
позволяет получитьi-й
байт строки; - Необходимо избегать индексации за пределами строки, так как это приведет к панике
(panic)
; - Чтобы получить подстроку из строки можно использовать операцию среза
s[i:j]
, гдеi
иj
указывают на начальный и конечный(не включительно)
индексы байтов; - Конкатенация строк происходит с помощью оператора
+
, что позволяет склеивать строки, создавая новую строку; - При конкатенации, создается новый объект строки в памяти, содержащий объединение исходных строк. Исходные строки
остаются неизменными и удаляются из памяти только при выполнении сборки мусора. рекомендуется использовать другие
методы работы со строками, которые не требуют создания новых объектов в памяти, например, метод
Join
; - Сравнение строк возможно с использованием операторов
==
,<
, что делает их удобными для работы в условиях и циклах; - Из-за
неизменяемости
строк в Go, нельзя изменять значения их байтов напрямую, при этом копирование строк и получение подстрок требуют минимальных затрат по памяти, так как они могут разделять одни и те же данные; - Например, при
копировании
строки в новую переменную, никакиеданные не копируются
, а лишьсоздается новая ссылка
на уже существующий кусок памяти со строкой. Таким образом, копирование строк не вызывает дополнительных затрат по памяти. То же самое актуально дляполучения подстроки
; - Работа со строками в Go позволяет эффективно оперировать текстовыми данными, избегая частых выделений памяти и операций изменения строк.
- Строковые литералы в Go - это последовательность байтов, заключенная в двойные кавычки, позволяют записывать текстовые
значения, включая символы
Unicode
; - Управляющие последовательности, начинающиеся с обратной косой черты
'\'
, используются для вставки произвольных значений байтов в строке, обрабатывают управляющие коды ASCII и специальные символы; - Шестнадцатеричные управляющие последовательности записываются как
\xhh
и представляют один байт с указанным значением, ровно с двумя шестнадцатеричными цифрамиh
(в верхнем или нижнем регистре); - Восьмеричные управляющие последовательности записываются как
\ooo
и тоже представляют один байт с указанным значением, с ровно тремя восьмеричными цифрами (от 0 до 7), не превышающими значение\377
; - Неформатированные строковые литералы (
raw string literal
) используют обратные одинарные кавычки `` и позволяют записывать многострочный текст, в котором управляющие последовательности не обрабатываются; - При использовании неформатированных строковых литералов символы возврата каретки
(\r)
удаляются, чтобы значение строки было одинаковым на всех платформах; - Неформатированные строковые литералы удобно использовать для записи регулярных выражений, шаблонов HTML, литералов JSON, сообщений об использовании программы и других текстов, занимающих несколько строк.
ASCII
использует7 бит
для кодирования128
символов, включая английские буквы, цифры, знаки пунктуации и управляющие символы устройств;Unicode
собирает символы всех мировых систем письменности, диакритические знаки, управляющие коды и многое другое для обеспечения совместимости между различными языками и алфавитами;- код символа
Unicode (Unicode code point)
илируна (rune)
в Go представляет собой стандартный номер, назначенный каждому символу; - для хранения
рун
в Go используется тип данныхint32
, являющийся синонимом типаrune
; - последовательность рун может быть представлена в виде последовательности значений
int32
, такое представление называетсяUTF-32
илиUCS-4
; UTF-32
использует32 бита на символ
, что обеспечивает универсальность кодирования, но требует больше памяти, чем обычно необходимо;UTF-8
обеспечивает оптимальное использование памяти, кодирование только символовASCII
в8 битах
или1 байте
, а более широко используемые символы вписываются в16 битов
(W - 1 бит, Ц - 2 бита
).
UTF-8
- это кодировка переменной длины символовUnicode
в виде байтов, изобретенная Кеном Томпсоном и Робом Пайком, является стандартомUnicode
;- Руны в
UTF-8
могут использоватьот 1 до 4 байтов
, но символыASCII
занимают только1 байт
, большинство распространенных рун используют2
или3 байта
; UTF-8
обладает свойством самосинхронизации (можно найти начало символа, просмотрев не более 3 байт) и совместимости сASCII
, что упрощает работу с текстом;- Для работы с отдельными рунами в Go можно использовать пакеты
unicode
иunicode/utf8
, которые предоставляют функции для кодирования, декодирования и обработки рун; - Управляющие последовательности
Unicode
в строковых литералах Go позволяют указывать символы с помощью их числового кода:'\uhhhh'
для 16-разрядных значений и'\uhhhhhhhh'
для 32-разрядных; UTF-8
позволяет проводить некоторые строковые операции без декодирования, например, проверить является ли одна строка префиксом, суффиксом или содержит ли подстроку другой строки;- Если необходимо работать с отдельными символами
Unicode
, нужно использовать другие механизмы, такие как функцияutf8.RuneCountInString()
для определения количества рун в строке; - Для работы с символами
Unicode
в Go необходимо декодированиеUTF-8
, для этого можно использовать пакетunicode/utf8
; - При декодировании с помощью функции
utf8.DecodeRuneInString()
возвращается руна и количество байтов, занятых её кодомUTF-8
; - Цикл по диапазону Go, применённый к строке, выполняет декодирование
UTF-8
неявно (каждый элемент строки, рассматривается какrune
); - Чтобы посчитать количество рун в строке, можно использовать простой цикл
range
или функциюutf8.RuneCountInString(s)
; - Тексты строк в Go интерпретируются как последовательности символов
Unicode
в кодировкеUTF-8
, это необходимо для правильного использования циклов по диапазону; - Если в строке содержатся ошибки кодировки, то при декодировании генерируется специальный замещающий символ Unicode
'\uFFFD'
; - Преобразование
[]rune
примененное к строке с кодировкойUTF-8
возвращает последовательность символовUnicode
, закодированных в этой строке; - Преобразование целочисленного значения в строку рассматривает это число как значение руны и даёт её представление в кодировке UTF-8;
- Если руна некорректна, вместо неё подставляется замещающий символ
�
.
- В пакете
strings
содержатся функции для работы со строками, такие как поиск, замена, сравнение, обрезка, разделение и объединение; - Аналогично пакету
strings
, пакетbytes
предоставляет функции для работы с байтовыми срезами[]byte
, что позволяет эффективнее использовать память при работе со строками, которые могут быть изменены; - Пакет
strconv
содержит функции для преобразования булевых, целочисленных и чисел с плавающей точкой в строковое представление и обратно, что упрощает работу с данными разных типов; - Функции для классификации рун, такие как
IsDigit
,IsLetter
,IsUpper
,IsLower
находятся в пакетеunicode
, а функции преобразования типаToUpper
,ToLower
позволяют изменять регистр рун, если они являются буквами; - Для работы с иерархическими именами файлов используются пакеты
path
иpath/filepath
, причём пакетpath
работает с путями с косой чертой на всех платформах, а пакетpath/filepath
учитывает специфику каждой платформы; - Строки можно преобразовывать в байтовые срезы и обратно, однако стоит учесть, что такое преобразование может привести к выделению памяти для нового массива байтов и копированию данных;
- В пакете
bytes
имеются функции-двойники функций из пакетаstrings
, которые работают с байтовыми срезами, что позволяет избежать излишних преобразований и выделений памяти; - Пакет
bytes
предоставляет типBuffer
для эффективной работы со срезами байтов, который растет по мере записи данных и не требует инициализации, что повышает удобство использования; - Если нужно работать только со строками, то лучше использовать
strings.Builder
. Если нужно работать с байтами или сочетать работу со строками и байтами, то лучше использоватьbytes.Buffer
.
- Для преобразования между строками и числами в Go используется пакет
strconv
; - Чтобы преобразовать целое число в строку, можно использовать функцию
fmt.Sprintf
или функциюstrconv.Itoa
, что позволит получить строковое представление числа; - Для форматирования чисел в другие системы счисления применяются функции
strconv.FormatInt
иstrconv.FormatUint
, что облегчает работу с числами в разных форматах; - Функции
fmt.Printf
с символами преобразования, такими как%b
,%d
,%o
,%x
, удобны при добавлении информации к числам в строковом представлении; - Чтобы преобразовать строку в целое число, можно использовать функции
strconv.Atoi
иstrconv.ParseInt
, что обеспечивает гибкость для работы с числами разных размеров; - Функция
fmt.Scanf
может быть использована для анализа входной информации, состоящей из комбинации строк и чисел, но может быть негибкой при обработке неполного или неправильного ввода.
- Константы в Go - это выражения с известными значениями
на этапе компиляции
, что позволяет компилятору выполнять оптимизации и уменьшать нагрузку на выполнение программы; - Объявление констант начинается с ключевого слова
const
, их значения могут быть строками, числами и логическими значениями; - Инициализация константы может быть
опущена
, в таком случае будут использованы значения и тип предыдущей константы; - Константы обеспечивают защиту от случайного или ошибочного
изменения значения во время выполнения программы
, что повышает надежность кода; - Константные выражения могут использоваться в качестве значений длины массива и других типах данных, что облегчает работу с этими типами данных;
- Использование констант позволяет компилятору обнаруживать ошибки, обычно идентифицируемые во время выполнения программы, уже на стадии компиляции;
iota
- этогенератор последовательных констант
в Go. При объявлении группы констант с использованиемiota
, его значение начинается с0
и увеличивается на1
для каждой последующей константы. Это позволяет удобно создавать группы констант с последовательными значениями, при этом значением каждой константы можно управлять независимо. Также можно использоватьiota
для создания констант с увеличивающимися значениями в определенную арифметическую прогрессию:var Flags uint const ( FlagUp Flags = 1 << iota // 1 FlagBroadcast // 2 FlagLoopback // 4 FlagPointToPoint // 8 FlagMulticast // 16 )
- Генератор констант
iota
в Go используется для создания последовательности связанных значений без явного указания их, начиная с нуля и увеличиваясь на единицу для каждого элемента; - Iota может использоваться для объявления имен для степеней значения 1024, позволяя оперировать с КиБ, МиБ, ГиБ и дальше;
- Однако механизм
iota
имеет свои пределы, например, невозможно генерировать степени 1000 (КБ, МБ и т.д.), так как отсутствует оператор возведения в степень.
Константы
в Go могут иметь любой фундаментальный тип данных, но многие из них не привязаны к определенному типу, что позволяет сохранить высокую точность значений и участвовать в большем количестве выражений без необходимости преобразования;- В Go существует шесть вариантов нетипизированных констант: нетипизированное булево значение, нетипизированное целое число, нетипизированная руна, нетипизированное число с плавающей точкой, нетипизированное комплексное число, нетипизированная строка;
- Нетипизированная константа в языке программирования Go - это значение, которое не имеет явного типа, но может быть автоматически преобразовано в любой тип данных;
- Нетипизированные константы могут преобразовываться в определенный тип при назначении переменной с явным указанием типа или при использовании в выражениях, что обеспечивает большую гибкость и точность;
- В случае литералов, вариант нетипизированной константы определяется синтаксисом: литералы
0
,0.0
,0i
,'\u0000'
обозначают различные варианты констант, такие как нетипизированное целое значение, нетипизированное число с плавающей точкой, нетипизированное комплексное число и нетипизированная руна; - Оператор
/
может представлять как целочисленное деление, так и деление с плавающей точкой в зависимости от операндов, поэтому выбор литерала может повлиять на результат выражения константного деления; - Нетипизированными могут быть только константы, и их преобразование в другой тип, явное или неявное, требует, чтобы целевой тип мог представлять исходное значение, при этом допускается округление для действительных и комплексных чисел с плавающей точкой;
- Если переменной не указан явный тип, нетипизированная константа определит его неявно, а при преобразовании нетипизированной константы в значение интерфейса, ее динамический тип будет определен по умолчанию, что особенно важно для работы с типами данных в Go.
- Массивы в Go - последовательности фиксированной длины из нуля или более элементов определенного типа.
- Пример:
var a [3]int
.
- Пример:
- Фиксированная длина массивов делает их менее гибкими по сравнению со срезами.
- Доступ к элементу массива осуществляется через индекс, начиная с
0
доn-1
длины массива. - Для инициализации массива списком значений можно использовать литерал массива.
- Пример:
q := [...]int{1, 2, 3}
.
- Пример:
- Синтаксис
[...]
объявляет неопределенный массив фиксированной длины, которая определяется автоматически на основе числа элементов, перечисленных в фигурных скобках; - Размер массива является частью его типа, так что типы
[3]int
и[4]int
различны. - Если тип элемента массива является сравниваемым, то таким же является и тип массива.
- Пример сравнения:
fmt.Println(a == b, a == c, b == c)
.
- Пример сравнения:
- При передаче в функцию по значению для каждого значения аргумента, функция получает копию аргумента, а не оригинал.
- Можно использовать указатели на массивы для изменения оригинального массива внутри функции.
- Пример функции с указателем на массив:
func zero(ptr *[32]byte)
.
- Пример функции с указателем на массив:
- Массивы используются редко из-за их негибкости, вместо массивов обычно используют срезы.
- Срезы в Golang представляют собой последовательности переменной длины с элементами одного типа и описываются
как
[]T
, гдеT
- тип элементов среза; - Срез состоит из трех компонентов:
указателя на массив
,длины
иемкости
. Указатель указывает на первый доступный элемент массива, а длина и емкость определяют размер и максимальный размер среза соответственно; - Срезы могут совместно использовать один и тот же базовый массив, что позволяет манипулировать субпоследовательностями элементов массива с минимальными затратами памяти;
- Оператор среза
s[i:j]
создает новый срез, ссылающийся на элементы последовательностиs
сi
доj-1
, при этом0 <= i <= j <= cap(s)
; - Пример объявления массива и создания срезов:
months := [...]string{1: "Январь", 2: "Февраль", /*...*/, 12: "Декабрь"} Q2 := months[4:7] summer := months[6:9]
- Срезы содержат указатель на элемент массива, что позволяет изменять элементы базового массива при передаче среза в функцию;
- При сравнении срезов можно использовать функцию
bytes.Equal
для срезов байтов[]byte
, но для других типов срезов необходимо выполнять сравнение вручную, создавая соответствующую функцию, например:func equal(x, y []string) bool { len(x) != len(y) { return false } for i := range x { if x[i] != y[i] { return false } } return true }
- Использование срезов упрощает манипуляции с данными, сохраняя память и предоставляя простые и понятные методы работы с подпоследовательностями массивов;
- Срезы в Go являются косвенными элементами, что позволяет им содержать самих себя и изменяться при изменении содержимого базового массива;
- Функция
make
создает массив заданного размера, на который ссылается срез, и этот массив доступен только через возвращаемый срезa := make([]string, 0, 3)
; - Длина среза - количество элементов в нем, емкость среза - максимальное число элементов, которые могут быть содержимым среза;
- У срезов имеется нулевое значение
nil
, такой срез ни на что не ссылается, и его длина и емкость равны нулю; - При сравнении срезов с помощью
==
, данные срезы сравниваются не всегда корректно, толькоnil
значения могут быть сравнены. Если хотя бы один элемент или длина срезов отличаются, оператор==
вернетfalse
. Сравнение срезов с помощью оператора==
может быть неэффективно, поскольку сравнение происходит поэлементно; - Если необходимо сравнить содержимое срезов, нужно использовать функцию
reflect.DeepEqual
илиbytes.Equal
; - Если срез имеет длину, равную нулю, нужно проверять его на пустоту с помощью
len(s) == 0
, а неs == nil
; - Для проверки равенства ссылочных типов, таких как указатели и каналы, нужно использовать операторы
==
для проверки ссылочной тождественности; - Если необходимо добавить элементы в срез, и емкость его не достаточна, Go создаст новый массив большего размера и скопирует в него элементы из старого массива;
- В Go есть встроенные функции, такие как
append
, чтобы изменять размер среза; - Срезы являются важными и удобными конструкциями в Go, их использование позволяет сделать программирование более гибким и поддерживаемым.
- Функция
append
в Golang используется для добавления элементов ксрезу (slice)
; это встроенная функция, которая имеет ключевое значение для работы со срезами; - Функция
append
проверяет, имеет ли срездостаточную емкость (capacity)
для добавления элементов; если да, онарасширяет
срез, если нет – выделяет новый массив и копирует в него элементы старого среза и новые элементы; - Для копирования элементов из одного среза в другой можно использовать функцию
copy
; она принимает два аргумента – целевой срез и исходный срез, и возвращает количество фактически скопированных элементов; - Увеличение размера массива при добавлении элементов выполняется путем удвоения его размера, что позволяет снизить количество выделений памяти и гарантировать константное время добавления одного элемента в среднем;
- Встроенная функция
append
может использоваться для добавления одного элемента, нескольких элементов или даже другого среза; например:x = append(x, 1, 2, 3)
илиx = append(x, x...)
; x...
- распаковка среза - используется для передачи каждого элемента, например в функциюappend
, в качестве отдельного аргумента;- Для корректной работы со срезами важно помнить, что хотя элементы базового массива доступны косвенно, указатель среза,
его длина и емкость не являются ссылочными; для обновления их требуется присваивание, например:
runes = append(runes, r)
; - Обновление переменной среза требуется не только при вызове функции
append
, но и для любой функции, которая может изменить длину или емкость среза, или сделать его ссылающимся на другой базовый массив; Вариадическая функция
– функция, которая принимает переменное количество аргументов; объявление вариадической функции выглядит как функция с аргументомy ...int
; например, можно использовать вариадическую функциюappendInt(x []int, y ...int)
для имитации встроенной функции append.
- Изменение элементов среза
"на лету"
позволяет обрабатывать данные, не привлекая дополнительную память; - Срезы можно использовать для реализации стека: добавление элемента -
stack := append(stack, v)
, получение вершины стека -top := stack[len(stack) - 1]
, удаление элемента -stack = stack[:len(stack) - 1]
; - Чтобы удалить элемент из
середины среза
и сохранить порядок элементов, можно использовать функциюcopy
:func remove(slice []int, i int) []int { copy(slice[:i], slice[i+1:]) return slice[:len(slice) - 1] }
- Если порядок элементов не важен, можно просто перенести последний элемент на место удаляемого:
func remove(slice []int, i int) []int { slice[i] = slice[len(slice) - 1] return slice[:len(slice) - 1] }
- Эффективное использование срезов может быть полезным и экономичным с точки зрения использования памяти, хотя требует особой внимательности при работе с данными.
Хеш-таблица (map)
в Go представляет собойнеупорядоченную коллекцию пар ключ-значение
, где все ключи уникальны и значения могут быть найдены, обновлены или удалены с использованием в среднем константного количества сравнений ключей (O(1)
);- Тип карты записывается как
map[K]V
, гдеK
иV
являются типами ключей и значений соответственно; - Для создания
map
используется встроенная функцияmake
, например:ages := make(map[string]int)
; Map
можно создать с использованием литералов:ages := map[string]int{"alice": 31, "charlie": 34}
;- Для удаления элемента из
map
используется встроенная функцияdelete
:delete(ages, "alice")
; - Итерация по элементам
map
варьируется от одного запуска программы к другому, для определенного порядка итерации нужно явно сортировать ключи; - Нулевым значением для типа
map
являетсяnil
, большинство операций с ним выполняются безопасно, но сохранение значений в нулевомmap
вызоветpanic
; - Для проверки наличия элемента в
map
, можно использовать двойное значение переменной, например:age, ok := ages["bob"]
; Map
нельзя сравнивать друг с другом, единственное разрешенное сравнение - сравнение сnil
;- Так как все ключи
map
различны,map
может служить в качестве множества(set)
; - Ключи в
map
должны быть сравниваемыми, поэтому ключами срезы не могут быть напрямую; однако можно использовать вспомогательную функцию для преобразования срезов в строки, сохраняющих условия равенства исходных срезов; - Использование вспомогательной функции для преобразования ключей позволяет применять подходы с использованием
map[string]bool
для любых несравниваемых типов ключей, а также для сравниваемых типов ключей, требующих особого определения равенства, например, сравнение без учета регистра для строк; - Использование
map
для подсчета количества вхождений различных символовUnicode
позволяет легко и эффективно решать задачи в документах с разными наборами символов, поскольку отслеживаются только встречающиеся символы; - Встроенный метод
ReadRune
из пакетаbufio
позволяет декодировать символыUnicode
в тексте, обрабатывая корректные и некорректныеUTF-8
коды; - Вложенные составные типы в значении
map
, такие какmap
или срезы, позволяют создавать гибкую и полезную структуру данных для организации и сохранения связей между строками (map[string][]string{}
).
Структура
в Go - этоагрегированный тип данных
, объединяющий нуль или более именованных произвольных типов в единое целое;- Каждое значение в структуре называется
полем
, и к ним можно получить доступ через запись с точкой (например,dilbert.Name
); - Структуры могут быть переданы функциям, возвращаться из них и храниться в массивах;
- В Go поле структуры экспортируется, если его имя начинается с
прописной буквы
; - Именованный структурный тип
S
может объявить поле с типом указателя*S
, что позволяет создавать рекурсивные структуры данных, такие как связанные списки и деревья; - Нулевое значение для структуры состоит из нулевых значений каждого из ее полей;
- Тип структуры без полей называется пустой структурой
struct{}
и может использоваться в качестве значения для множества с ключами вmap
. - Пример структуры:
type Employee struct { ID int Name string Address string DoB time.Time Position string Salary int ManagerID int }
- Пример использования указателя на структуру для доступа к полям:
position := &dilbert.Position *position = "Senior" + *position
- Структурные литералы в Golang имеют две разновидности: с указанием значений для каждого поля в правильном порядке и с перечислением некоторых или всех имен полей с соответствующими значениями;
- Если поле в структурном литерале опущено, оно получает нулевое значение соответствующего типа;
- Значения структур могут быть переданы как аргументы в функцию и быть возвращены из нее;
- Большие структурные типы обычно передаются в функции или возвращаются из них косвенно с помощью указателя для повышения эффективности;
- Если функция должна модифицировать свой аргумент, передача через указатель становится обязательной;
- Можно использовать сокращенную запись для создания и инициализации структурной переменной и получения ее
адреса (
p := &Person{"John", 30}
); - Примеры:
- Создание структуры с использованием разных видов литералов:
type Person struct { name string age int address struct { street string city string } } person := Person{ name: "John", age: 30, address: struct{ street, city string }{ street: "123 Main St", city: "New York", }, }
- Функция, масштабирующая Point с использованием некоторого коэффициента:
func Scale(p Point, factor int) Point { return Point{p.X * factor, p.Y * factor} }
- Передача структуры в функцию через указатель для модификации аргумента:
func AwardAnnualRaise(e *Employee) { e.Salary = e.Salary * 105 / 100 }
- Сокращенная запись для создания и инициализации структурной переменной и получения ее адреса:
pp := &Point{1, 2}
- Структуры в Go могут быть сравнимы, если все их поля являются сравнимыми (например,
int
,string
,bool
). - Для сравнения двух структур используются операторы
==
или!=
, которые последовательно сравнивают соответствующие поля каждой структуры. - Операция
==
возвращаетtrue
, если все поля двух структур равны, в противном случае возвращаетfalse
. - Сравниваемые структурные типы могут использоваться в
качестве
ключа вmap
.
-
Встраивание структур позволяет использовать именованный структурный тип в качестве анонимного поля другого структурного типа, обеспечивая удобное синтаксическое сокращение для доступа к полям и методам встроенного типа; это упрощает код и делает его более читаемым. Пример:
type Point struct { X, Y int } type Circle struct { Point Radius int }
-
Анонимные поля -
поля структуры
, которые имеют тип, но не имеют имени; тип поля должен быть именованным типом или указателем на именованный тип, а имя поля неявно определяется его типом;
Пример:type Circle struct { Point Radius int }
-
Благодаря встраиванию
анонимных полей
, можно обращаться к полям этих типов без указания промежуточных имен, упрощая обращение к полям вложенных структур;
Пример:var c Circle c.X = 8 c.Y = 8 c.Radius = 5
-
Встраивание структур также позволяет получать
методы встроенного типа
для типа внешней структуры, обеспечиваямеханизм композиции
иобъектно-ориентированного программирования
в Go;
Пример:type Point struct { X, Y int } func (p Point) Distance() float64 {...} type Circle struct { Point Radius int } var c Circle distance := c.Distance() // вызывает Point.Distance() для c.Point
-
Несмотря на возможность использования сокращенной записи для доступа к полям и методам встроенного типа, структурный литерал должен следовать форме объявления типа, поэтому простое указание значений полей без их имен не будет допустимо;
Пример:c := Circle{Point{8, 8}, 5} // верно c := Circle{8, 8, 5} // ошибка компиляции
-
Композиция
в Go - это механизм, который позволяет объединять несколько типов в один тип, подобно наследованию в других языках программирования. Он позволяет одному типу включать в себя другой тип как поле. При использовании композиции, все методы, свойства и функциональность включенного типа становятся доступными для включающего типа, который может дополнительно определять свои собственные методы и свойства; -
В Go
композиция
инаследование
имеют свои отличия:Наследование
- это механизм, который позволяет создавать новый тип на основе существующего типа, который может использовать и переопределить методы базового типа. В то время каккомпозиция
- это механизм, который позволяет включать один тип в другой тип как поле.- В наследовании подтип наследует каждый метод базового типа, включая его состояние. В композиции тип включает в себя другой тип как поле, но не наследует его состояние.
- При использовании композиции, включающий тип может использовать методы включенного типа как свои собственные методы, но не может изменять их поведение. В наследовании подтип может переопределить методы базового типа и изменить их поведение. В целом, композиция и наследование имеют очень разные цели и могут использоваться в разных ситуациях в зависимости от потребностей программы.
JSON (JavaScript Object Notation)
- стандартная запись для передачи и получения структурированной информации; существуют альтернативы, такие какXML
,ANS.1
иGoogle's Protocol Buffers
, ноJSON
является наиболее распространенным из-за простоты, удобочитаемости и всеобщей поддержки;- Go предлагает превосходную поддержку кодирования и декодирования
JSON
с помощью стандартной библиотекиencoding/json
, а также поддерживает другие форматы, такие какencoding/xml
,encoding/asn1
и другие, со схожими API; JSON
представляет собой кодирование значенийJavaScript
(строк, чисел, логических значений, массивов и объектов) в текстовом форматеUnicode
; он эффективен и хорошо читается, что делает его идеальным для представления фундаментальных типов данных и составных типов, таких какмассивы
,срезы
,структуры
и карта"map"
;- Преобразование структуры данных Go (например, movies) в
JSON
называетсямаршалингом
и выполняется с помощью функцииjson.Marshal
:data, err := json.Marshal(movies)
; результат - байтовый срез, содержащий текстовое представление данных в формате JSON; - Чтобы получить более читаемое представление
JSON
для людей, можно использовать функциюjson.MarshalIndent
, которая добавляет отступы и форматирование:data, err := json.MarshalIndent(movies, "", " ")
; Маршалинг
используетимена полей структуры
Go вкачестве имен полей объектов JSON
и маршализуеттолько экспортируемые поля
(с прописной первой буквой);дескрипторы
полей могут быть использованы для указания альтернативных имен полейJSON
и других параметров:Year int json:"released"
;- Противоположная операция маршалингу -
демаршалинг
, заключается в декодированииJSON
и заполнении структуры данных Go; выполняется с помощью функцииjson.Unmarshal
:err = json.Unmarshal(data, &titles)
; позволяет выбирать, какие частиJSON
будут декодированы, а какие отброшены, определяя подходящие структуры данных Go; - Веб-службы часто предоставляют интерфейс
JSON
для обмена данными, и мы можем выполнитьHTTP/S-запрос
для получения нужной информации в форматеJSON
; - Для работы с API сервисов, таких как Github, необходимо создать специальные структуры и константы для представления данных (например, см. файл github.go);
- Важно помнить, что названия полей структуры должны начинаться с
заглавной буквы
, даже если имена полей вJSON
названыстрочными буквами
. Поскольку сопоставление имен полей и JSON не учитывает регистр символов, дескриптор поля нужен только при наличии знака подчеркивания в имени JSON; - Чтобы сформировать корректный запрос с использованием параметров, используйте функцию
url.QueryEscape
, чтобы символы со специальными значениями, такие как?
и&
, сохраняли свое значение; - Для декодирования
JSON
можно использовать функциюjson.Unmarshal
или потоковый декодерjson.Decoder
, который позволяет декодировать несколько последовательных объектов JSON из одного потока; - Для красивого форматирования результатов запроса можно использовать разные подходы: таблицы с фиксированной шириной столбцов или шаблоны (например, см. файл issues.go).
- Пакеты
text/template
иhtml/template
используются для разделения форматирования и кода в Go, позволяя подставлять значения переменных в текстовые или HTML-шаблоны; - Шаблон состоит из строки или файла, содержащего фрагменты в двойных фигурных скобках
{{...}}
, называемые"действиями (action)"
, которые изменяют поведение программы по умолчанию; - В действиях используется язык шаблонов, позволяющий выводить значения, выбирать поля структуры, вызывать функции и методы, управлять потоком и создавать другие шаблоны;
- Текущее значение в действии записывается как точка
.
, которая изначально указывает на параметр шаблона; - Запись
|
в действии передает результат одной операции в другую, аналогично конвейеру оболочки Unix ({{.CreatedAt | daysAgo}}
); - Создание и вывод шаблона происходит в два этапа: сначала нужно выполнить синтаксический анализ шаблона в подходящее внутреннее представление, а затем выполнить его для конкретных входных данных;
- Вспомогательная функция
template.Must
упрощает обработку ошибок (выполняет синтаксический анализ шаблона) и проверяет нет ли ошибок, иначе вызывает панику(panic)
; - Пакет
html/template
автоматически экранирует специальные символы и последовательности в строках HTML, JavaScript, CSS и URL, что помогает избежать атак с помощью инъекций; - Автоматическое экранирование для полей с данными HTML можно подавить с помощью именованного строкового типа
template.HTML
, а для JavaScript, CSS и URL - с использованием аналогичных имен типов. Пример:type Page struct { Title string Content template.HTML } func main() { page := Page{ Title: "My Page", Content: "<h1>Hello, world!</h1>", } tmpl, err := template.New("page").Parse(` <!DOCTYPE html> <html> <head> <title>{{.Title}}</title> </head> <body> {{.Content}} </body> </html> `) if err != nil { panic(err) } err = tmpl.Execute(os.Stdout, page) if err != nil { panic(err) } }
- Функции в Go определяются с помощью ключевого слова
func
, имени функции, списка параметров, списка результатов и тела функции;
Пример объявления функции с двумя параметрами типаint
и одним результатом типаint
:func add(x int, y int) int { return x + y }
- Список параметров функции содержит имена и типы параметров, которые являются локальными переменными функции и инициализируются значениями аргументов, переданными при вызове функции;
- Список результатов функции указывает типы значений, возвращаемых функцией; если функция возвращает одно значение, скобки необязательны и опускаются;
- Функции с именованными результатами автоматически инициализирует локальные переменные с соответствующими типами и
начальными значениями;
Пример функции с именованным результатом:
func sum(x int, y int) (result int) { result = x + y return }
- Аргументы функции
передаются по значению
, поэтому изменения аргументов внутри функции не влияют на исходные значения вызывающей стороны; однако, если аргумент содержитссылку на объект
(указатель, срез, map, функцию, канал), то функция может влиять на его состояние; - Тип функции определяется
сигнатурой
, состоящей из последовательности типов параметров и результатов; имена параметров и результатов не влияют на тип функции; - В Go
отсутствуют
параметры по умолчанию и передача аргументов по имени, что упрощает вызов функции и делает его более предсказуемым; - Возврат значений из функции осуществляется с помощью оператора
return
; если функция имеет список результатов, она должна заканчиваться операторомreturn
, если только исполнение явно не может дойти до конца функции (например, из-за вызоваpanic
или бесконечного цикла безbreak
).
- Рекурсия - это возможность функции вызывать саму себя, что является мощным инструментом для решения многих задач.
- В Go рекурсия используется для обработки деревьев, в том числе для сортировки и обработки HTML-документов.
- Пакет golang.org/x/net/html предоставляет синтаксический анализатор HTML для Go и используется для извлечения информации из HTML-документов.
- Проблему переполнения стека в рекурсии в Go решается использованием стека переменной длины, который позволяет выделять дополнительную память по мере необходимости.
- Функции могут возвращать несколько значений, что полезно для возврата результата и информации об ошибке или статуса
выполнения;
Пример:func Size(rect image.Rectangle) (width, height int)
; - Сборщик мусора Go освобождает память, но не освобождает ресурсы операционной системы, такие как открытые файлы и
сетевые подключения. Они должны быть закрыты явным образом, например
file.Close()
,resp.Body.Close()
; - Результат вызова функции с несколькими возвращаемыми значениями представляет собой кортеж значений. При вызове такой
функции необходимо явно присваивать ее значения переменным или использовать
_
для игнорирования ненужных значений; Пример:links, err := findLinks(url)
,links, _ := findLinks(url)
; - Результат многозначного вызова может быть использован в качестве аргумента для другой функции с несколькими
параметрами, что может быть полезно при отладке;
Пример:
log.Println(findLinks(url))
; - Нужно использовать осознанные имена для возвращаемых значений, особенно если они одного типа, чтобы облегчить
понимание функции;
Пример:
func Split(path string) (dir, file string)
; - Если последнее возвращаемое значение функции типа
bool
,по соглашению
оно указывает на успешное или неуспешное выполнение функции; - В функции с именованными результатами можно использовать
пустой возврат (bare return)
- операторreturn
без операндов. Это может уменьшить дублирование кода, но иногда усложняет понимание функции; Пример:func CountWordsAndImages(url string) (words, images int, err error) { //... if err != nil { return } //... }
- Функции, которые возвращают булево значение (
false
,true
), могут выдать ошибки в случае, например, переполнения стека и нехватки памяти. Такие ошибки необходимо обрабатывать, чтобы избежать непредсказуемого поведения программы. - Обрабатывать ошибки нужно, так как даже в "надежных" программах они могут появиться там, где не ожидаешь. Это связано с несколькими факторами, такими как внешние условия, состояние системы и взаимодействие с другими программами.
- Обработка ошибок важна, для предоставления информации о том, что конкретно пошло не так и было проще разобраться с возникшей проблемой. Это упрощает поиск и исправление ошибок в коде.
- Когда функция возвращает тип
error
, в нее, обычно, передается информация с последней вызванной функции, которая также возвращала типerror
. Это позволяет отслеживать цепочку ошибок и определять их причины. - Тип
error
является интерфейсом, что позволяет определять собственные типы ошибок и использовать их в своих программах.
Пример:type MyError struct { ErrMsg string ErrCode int } func (e *MyError) Error() string { return fmt.Sprintf("Error %d: %s", e.ErrCode, e.ErrMsg) } func myFunction() error { // ... if err != nil { return &MyError{ErrMsg: "Something went wrong", ErrCode: 404} // возвращаем экземпляр MyError вместо обычного error } // ... return nil }
- Ошибка может быть нулевой или ненулевой. Нулевая ошибка (
nil
) обозначает успешное завершение функции, а ненулевая ошибка содержит информацию о проблеме, которая возникла в процессе выполнения функции. - В Go используются конструкции
if
иreturn
для обработки ошибок вместо исключений. Это обеспечивает более точный контроль над обработкой ошибок и предотвращает неопределенное поведение, которое может возникнуть при использовании исключений. - В Go исключения не используются, потому что они могут приводить к неопределенному поведению и усложнять отладку.
Вместо этого используется механизм возврата ошибок через тип
error
. Это позволяет более точно контролировать обработку ошибок и сделать код более понятным и предсказуемым. - Пример работы с ошибками в Go:
func main() { file, err := os.Open("file.txt") defer file.Close() if err != nil { log.Fatal(err) } }
- В случае возникновения ошибки, ее нужно обрабатывать в вызывающей функции;
- Функция
fmt.Errorf
форматирует сообщение об ошибке, используяfmt.Sprintf
, и возвращает новое значениеerror
; - При обработке ошибок в функции main необходимо строить четкую цепочку ошибок от источника до конечного вызова;
- Сообщение об ошибке должно быть осмысленным и подробным, следует избегать начала строки сообщения с прописных букв и символов новой строки;
- Сообщение об ошибке должно быть осмысленным и достаточно подробным (последовательным и согласованным, с другими ошибками цепочки\пакета);
- Для ошибок, которые представляют переходящие или непредсказуемые проблемы (проблемы с сетевым соединением, проблемы с доступностью внешних сервисов, ошибка доступа к диску), имеет смысл повторить сбойную операцию, с задержкой и ограничениями на количество попыток или время.
- Все функции пакета
log
, по умолчанию добавляет в начало сообщения об ошибке текущее время и дату. Такой формат полезен при работе "долгоиграющего" сервера, но менее удобен в интерактивном режиме. Так же, все функции добавляют символ новой строки\n
. - Все функции пакета
log
добавляют в начало сообщения об ошибке текущее время и дату, для изменения этого поведения можно использовать методыlog.SetPrefix
иlog.SetFlags
. - После проверки ошибки сначала обычно обрабатывается ошибка, а затем - код, который следует выполнить, если ошибок нет.
Если ошибка приводит к выходу из функции, успешное продолжение работы выполняется не в блоке
else
, а в теле функции. - Игнорирование ошибок допустимо в случае, если возвращаемое значение ошибки не важно и мы уверены в успешном выполнении операции, а также если мы обрабатываем ошибку в другом месте кода или в другой функции. При этом необходимо сопроводить игнорирование ошибок комментарием, объясняющим причину игнорирования.
- Пакет
io
гарантирует, что ошибка достижения конца файла всегда будет сообщаться, как об отдельной ошибкеio.EOF
; - Вызывающая функция может обнаружить условие конца файла с помощью простого сравнения
==
; - Ошибка
io.EOF
имеет фиксированное сообщение, так как условие конца файла не содержит дополнительной информации.
- Функции - значения первого класса. Они могут быть присвоены переменным или переданы в функцию, а так же возвращены из нее;
- Нулевым значением типа функции является
nil
, и вызов такой функции приводит кpanic
;
Пример:var f func(int) int f(3) // panic: вызов nil-функции
- Значения-функции имеют свой тип;
Пример:func square(n int) int { return n * n } var f func(int) int f = square // Допустимо, так как типы функции square и переменной f совпадают }
- Значения-функции можно сравнить с
nil
, но нельзя сравнивать их между собой напрямую, так как каждая функция в Go имеет уникальный адрес в памяти; Пример:var f func(int) int if f != nil { f(3) } // нет ошибки, так как f не вызывается, если она равна nil
- Значения-функции нельзя использовать в качестве ключей в карте map из-за уникальности их адресов в памяти;
- Значения-функции позволяют параметризовать функции не только данными, но и поведением;
- С помощью значения функции можно разделить логику на модули;
- У
fmt.Printf
есть "трюк", который позволяет добавить переменное количество отступов в строке. С помощью символа*
в%*s
- принимает два аргумента, первый - переменное количество пробелов (ширина вывода, типint
), второй - выводимая строка; - Значения-функции облегчают написание более обобщенных и абстрактных функций, что упрощает их использование и улучшает читаемость кода.
- Детерминированность - это свойство программного кода, при котором он всегда будет давать одинаковый результат при одинаковых входных данных и в одинаковых условиях выполнения. Это означает, что поведение кода предсказуемо и можно точно определить результат его выполнения;
- Инкапсуляция - сокрытие деталей внутренней реализации объекта от других частей программы;
- Функция
append(list, f(item)...)
добавляет все элементы, возвращаемыеf(item)
, вlist
; - Анонимные функции используются для обозначения значений-функций в выражениях без явного имени и могут быть определены
внутри других функций (
strings.Map(func(r rune) rune { return r + 1}, "HAL9000")
); - Можно использовать литерал функции, чтобы обозначить значение-функцию в любом выражении. Такая запись
называется анонимной функцией.
Пример:
// Создаем анонимную функцию, которая принимает два числа и возвращает их сумму sum := func(a, b int) int { return a + b } // Вызываем анонимную функцию и сохраняем результат в переменную result := sum(3, 4)
- Анонимная функция в Go имеет доступ ко всему лексическому выражению, включая переменные их охватывающей
функции. Такая функция называется замыканием.
Пример:
func squares() func() int { x := 0 return func() int { x++ return x * x } }
- Анонимные функции могут возвращать другие функции;
- Значения-функции могут иметь состояние и обновлять локальные переменные охватывающей функции. Поэтому они являются ссылочными типами и не сравниваются;
- Замыкание - это комбинация анонимной функции и ее окружения. Замыкание “запоминает” значения переменных из своего окружения и может использовать их даже после того, как охватывающая функция завершила свое выполнение;
- Время жизни переменной не определяется ее областью видимости. Область видимости определяет, где переменная может быть использована в коде. Время жизни переменной определяется тем, когда память для нее выделена и освобождена;
- Когда анонимная функция требует рекурсии, сначала нужно объявить переменную и присвоить ей анонимную функцию. Затем можно использовать эту переменную внутри анонимной функции для рекурсивного вызова.
- Лексическая область видимости Go может привести к неожиданным результатам при работе с переменными итерации в циклах;
- Все значения-функции (анонимных функций (замыканий)), созданные в цикле, захватывают и совместно используют одну и ту же переменную, а именно, адресуемое место в памяти, а не ее значение в конкретный момент. В результате переменная может быть обновлена несколько раз и сохранить последнее значение в конце цикла;
- Для решения этой проблемы, создается внутренняя переменная с тем же именем, что и у внешней переменной, копией которой
она является.
Пример:
for _, dir := range tempDirs() { dir := dir // Объявление внутренней переменной dir, инициализированной значением внешней переменной dir //... }
- Проблема захвата переменной итерации может возникнуть при использовании инструкции
go
илиdefer
, поскольку обе могут задержать выполнение функции до момента после завершения цикла; - Проблема не является уникальной для Go и может возникнуть в других языках программирования.
- Вариативные функции - это функции, которые могут быть вызваны с разным количеством аргументов; это удобно, например,
для функций форматирования строк, таких как
fmt.Printf
в Go. Пример использования вариативной функции:func sum(nums ...int) int { total := 0 for _, num := range nums { total += num } return total } fmt.Println(sum(1, 2, 3, 4)) // 10
- Чтобы объявить вариативную функцию, перед типом последнего параметра указывается троеточие
...
, которое говорит о том, что функция может быть вызвана с любым количеством аргументов данного типа. Это позволяет создавать более гибкие и универсальные функции; - Вызывающая функция,
неявно
выделяет память для массива, копирует в него аргументы и передает в функцию срез, который представляет весь массив. - Чтобы вызвать вариативную функцию с аргументами, уже находящимися в срезе, следует добавить троеточие после последнего
аргумента. Это удобно для обработки наборов данных, которые уже представлены в виде срезов:
values := []int{1, 2, 3, 4} fmt.Println(sum(values...)) // 10
- Внутри функции параметр
...int
ведет себя каксрез (slice)
. Однако тип вариативной функции отличается от типа функции с параметром типа среза. Пример:func f(...int) {} func g([]int) {} fmt.Printf("%T\n", f) // func(...int) fmt.Printf("%T\n", g) // func([]int)
- Вариативные функции часто используются для форматирования строк;
- Тип
interface{}
может быть использован для создания вариативных функций, которые принимают любые значения в качестве последних аргументов. Это обеспечивает максимальную гибкость и универсальность функций.
- В Go есть механизм отложенного вызова функции или метода
defer
; - Он используется, чтобы избежать дублирования логики очистки и гарантировать освобождение ресурсов.;
- Функция и выражения аргументов вычисляются при выполнении инструкции, но фактический вызов откладывается до завершения
функции, причем независимо от того, как она завершается: обычным способом (void), с помощью оператора
return
или в результатеpanic
; - Любое количество вызовов может быть отложено, и они выполняются в обратном порядке;
- Инструкция
defer
часто используется с парными операциями: открытие\закрытие, подключение\отключение, блокировка\разблокировка - для гарантии освобождения ресурсов во всех случаях; - Правильное место инструкции
defer
, которая освобождает ресурс - сразу же после того, как ресурс был успешно захвачен в случае, если "захват" возвращает ошибку - сразу послеif err != nil
; defer
может использоваться для отладочных записей о входе и выходе из функции;- Анонимная функция в отложенной функции имеет доступ к переменным охватывающей функции, включая именованные результаты;
- Отложенные функции выполняются после того, как инструкция возврата обновляет переменные результатов функции, и могут изменять их значения;
- Отложенные функции не выполняются до конца выполнения функции, поэтому при работе в цикле может возникнуть проблема.
Одним из решений может быть перенос тела цикла, включая инструкцию
defer
в другую функцию, которая вызывается на каждой итерации; - При использовании отложенного вызова для закрытия файлов, следует учитывать особенности работы файловых систем и предпочитать отчет об ошибке операции чтения/записи файла перед отчетом об ошибке закрытия файла, чтобы более точно определить причину возникновения проблем.
- Такие ошибки как, обращение к элементу за границами массива или разыменовывание нулевого указателя требуют проверок в
runtime
. Когда среда выполнения Go обнаруживает эти ошибки, возникаетпаника
(panic
); - Во время паники выполнение программы останавливается, выполняются все отложенные вызовы функций в текущей горутине и программа аварийно завершает работу с записью соответствующего сообщения;
- Журнальное сообщение
паники
содержитзначение паники
(обычно это сообщение об ошибке) итрассировку стека
для каждой горутины. Трассировка стека показывает состояние стека вызовов функций, которые были активны во время паники; - Журнальное сообщение паники содержит много информации о возникшей ошибке, и его следует включать в отчет об ошибке, чтобы разработчик мог проанализировать и исправить ошибку;
- Не все
паники
возникают вruntime
. Встроенная функцияpanic
может вызываться в коде, и это не обязательно связано с ошибками во время выполнения программы. В этом случае паника также приводит к аварийному завершению программы. - В качестве аргумента
паника
принимает любое значение; - Не всегда лучшее решение вызывать
panic
, когда происходит "невозможная" ситуация. В некоторых случаях более целесообразно использовать другие механизмы обработки ошибок, например, возвращать ошибку или использовать специальный тип, который позволяет обрабатывать ошибки; - Если нельзя предоставить более информативное сообщение или обнаружить ошибку заранее, нет смысла в проверке, например,
значения на
nil
и вызовеpanic
, так как среда выполнения все осуществит сама; - Механизм
паник
существенно различается с исключениями из других ЯП. Он используется для грубых ошибок, таких как логическая несогласованность в программе, при этом происходит аварийное завершение программы; - Если ошибка, возникает по причине некорректного ввода, неверной конфигурации или сбоя ввода-вывода, лучше обработать
их с использованием типа
error
; - Префикс
Must
является распространенным соглашением именования для функции такого рода какtemplate.Must
,regexp.MustCompile
- такие функции не возвращают ошибки и они должны выполниться обязательно; - Когда программа сталкивается с
паникой
, все отложенные функции выполняются в порядке, обратном их появлению в коде, начиная с функции на вершине стека и опускаясь до функцииmain
; - Функция может восстановиться после паники так, что программа при этом не будет аварийно завершена;
- Если в Go функция вызывает
panic
, а затем восстанавливается с помощью функцииrecover
, то эта функция возвращает значение, переданное вpanic
; - Для диагностических целей пакет
runtime
позволяет вывести дамп стека; - Функция
runtime.Stack
позволяет вывести информацию о функциях. Однако механизмпаники
в Go запускает отложенные функции до разворачивания стека (вывода его в стандартный поток).
- В некоторых случаях, после возникновения ошибки, может потребоваться восстановление, например, чтобы очистить ресурсы
перед выходом из программы. В этом случае можно использовать функцию
recover
; - Функция recover должна использоваться только внутри отложенной функции, которая вызывается с помощью
оператора
defer
; - Она завершает текущую панику и возвращает ее значение. Функция, которая столкнулась с паникой, продолжается там, где была прервана и выход из нее осуществляется в обычном режиме;
- Если
recover
вызватьвне отложенной функции
или вне функции, котораяне столкнулась с паникой
, то она ничего не будет делать и вернетnil
; - С помощью функции recover можно восстановить функцию после паники и использовать ее (паники) значение для создания сообщения об ошибке и возврата обычной ошибки;
- Не рекомендуется всегда восстанавливаться после паники (особенно необдуманно), потому что состояние переменных в программе после паники может быть неизвестно или не задокументировано. Например, может быть неполное обновление данных или открытый файл, который не закрылся. Если не обрабатывать панику правильно, то можно пропустить ошибку;
- Восстановление из паники в том же пакете может помочь упростить обработку сложных или неожиданных ошибок, но не рекомендуется выполнять восстановление после паники в другом пакете;
- Общедоступные API должны сообщать об ошибках с помощью
error
; - Пример: пакет
net/http
предоставляет веб-сервер. Вместо того чтобы позволить панике в одном из своих обработчиков завершить весь процесс, сервер вызывает функциюrecover
, выводит трассировку стека и продолжает обслуживание. Это может быть удобным, но есть риск утечки ресурсов или возникнуть ситуация, когда паника будет в неопределенном состоянии; - Чтобы восстановление после ошибки было безопасным, необходимо использовать выборочное восстановление только в крайне
редких случаях. Для этого можно создать не экспортируемый тип для значения ошибки и проверять, имеет ли значение,
возвращенное из
recover
, этот тип. Если да - то о панике сообщается как об ошибке, если нет - вызываем панику с тем же значением для восстановления паники; - Из некоторых ситуаций восстановление невозможно. Например, исчерпание памяти приводит к завершению программы с фатальной ошибкой.
- Метод объявляется следующим образом:
func (получатель тип) имя_метода(аргументы) возвращаемое_значение
; - Название "получатель" унаследовано от ранних ООП языков, которые описывали вызов метода как "отправку сообщения объекту";
- В Go для получателя не используется специальное имя, такое как
this
илиself
; - Для имени получателя принято брать первую букву имени типа;
- Вызов метода осуществляется таким же синтаксисом, как доступ к полям структуры с использованием селекторов, например,
p.Distance(q)
вызывает методDistance
у переменнойp
типаPoint
; - Если есть два метода или функции с одинаковым именем, но разными типами, конфликта не будет, так как каждый тип имеет разные пространства имен для методов;
- Выражение
получатель.Метод
называется селектором. Оно выбирает подходящий метод для получателя с его типом. Селекторы также используются для выбора полей структурных типов; - Разрешая связывать методы с любым типом, Go отличается от многих других ООП ЯП. Это позволяет удобно определить дополнительное поведение для простых типов, таких как числа, строки, срезы, карты, иногда даже функции;
- Методы могут быть объявлены для любого именованного типа в том же пакете, за исключением указателей и интерфейсов;
- Компилятор определяет, какой метод должен быть вызван, исходя из имени метода и типа получателя;
- Все методы в конкретном типе должны иметь уникальные имена, но одно и то же имя метода может использоваться разными типами;
- Нет необходимости квалифицировать имена функций (например
PathDistance
) для устранения неоднозначности; - Первое преимущество методов перед функциями: имена методов могут быть короче, особенно, когда мы выполняем их за пределами пакета, так как они могут использовать более короткие имена и опускать имя пакета.
- При вызове функции создается копия каждого значения аргумента. Если нужно обновить переменную или избежать
копирования, нужно передать в функцию адрес переменной с помощью указателя. То же самое относится к методам - их нужно
присоединять к типу указателя
*T
. Пример:func (p *Point) ScaleBy(factor float64) { p.X = factor p.Y = factor }
- Имя метода выглядит так:
(*T).Method
. Скобки необходимы, без них выражение будет трактоваться как*(T).Method
; - По соглашению, если какой-либо метод именованного типа
T
имеет тип получателя указателя*T
, следует использовать указатели в качестве получателей для всех методов этого типа; - Получатель в объявлении метода может быть только именованным типом
T
или указателем на него*T
; - Объявления методов не разрешены для именованных типов, которые сами являются типами указателей
var p &int
; - Если получатель является переменной типа
T
, но методу необходим получатель*T
, можно использовать сокращенную записьp.Method()
. При такой инструкции компилятор выполнит неявное получение адреса из&p
этой переменной. Это работает для только для переменных, включая поля структур, наподобиеp.X
и элементов массивов и срезов, наподобиеperim[0]
; - Нельзя вызвать метод
*T
для не адресуемого получателяT
, так как нет никакого способа получения адреса временного значения; - Если:
r := T{1, 2}; pptr := &r
мы можем вызвать метод типаT
наподобиеT.Method()
с получателем типа*T
, поскольку есть способ получить значение из адреса. Компилятор вставит неявный оператор*
; - В каждом корректном выражении вызова метода истинным является только одно из трех утверждений:
- Либо аргумент получателя имеет тот же тип, что и параметр получателя (оба имеют тип
*T
либоT
); - Либо аргумент получателя является переменной типа
T
, а параметр получателя имеет тип*T
- компилятор неявно получит адрес переменнойr.Method(2) // (&r)
; - Либо аргумент получателя имеет тип
*T
, а параметр получателя имеет типT
- компилятор выполнит разыменовывание получателяpptr.Distance(q) // (*pptr)
.
- Либо аргумент получателя имеет тот же тип, что и параметр получателя (оба имеют тип
- Если все методы именованного типа
T
имеют тип получателяT
(не*T
) - копирование экземпляров этого типа безопасно. Вызов любого из его методов обязательно делает копию. - Если какой-то метод имеет в качестве получателя указатель, следует избегать копирования экземпляров
T
, так как это может нарушать внутренние инварианты. Например, копирование экземпляраbytes.Buffer
может привести к тому, что оригинал и копия будут псевдонимами одного и того же базового массива байтов, последующие вызовы методов будут иметь непредсказуемые результаты. Внутренние инварианты
- это ограничения на значения полей и состояние объекта, которые должны соблюдаться для правильной работы программы или модуля. Например, для структуры, представляющей дерево, внутренний инвариант может заключаться в том, что каждый узел имеет не более двух потомков.
- Некоторые методы могут принимать нулевой указатель в качестве получателя, особенно для ссылочных типов данных, таких как карта или срез;
- Желательно указывать в комментариях, если методы типа допускают нулевое значение получателя;
- Для типа-карты можно использовать встроенные операторы (например, make, литералы срезов, m[key]) или его методы, или и то, и другое вместе;
- Если мы вызовем метод обновления карты, которая является нулевой - это приведет к панике;
- Карты обращаются к своим парам "ключ-значение" косвенно (по ссылке), из-за этого любые обновления и удаления, которые
будут делать вызовы методов с элементами карты, будут видны вызывающей функции. Однако, как и для обычных функций,
любые изменения, которые метод делает с самой ссылкой, например установка ее значения равным
nil
или ее перенаправление на другую карту - не будут видны вызывающей функции.
- Чтобы использовать синтаксические сокращения для всех полей, которые содержит встраиваемая структура, можно создавать типы путем встраивания структур;
- Можно выбрать поля одной структуры из второй структуры, без упоминания ее имени;
- При встраивании структур можно вызывать методы встроенного поля
T
с использованием получателя типаTName
, даже еслиTname
не имеет собственных методов. МетодыT
будут доступны как методыTname
; - Встраивание допускает наличие сложных типов со многими методами, при этом указанные типы создаются путем
композиции
полей, каждое из которых предоставляет несколько методов; - Неверно рассматривать
T
как базовый класс, аTname
- как подкласс при встраивании структур (другие ООП языки); - Типом анонимного поля может быть указатель на именованный тип. В этом случае поля и методы косвенно повышаются из указываемого объекта. Добавление еще одного уровня косвенности позволяет нам совместно использовать общие структуры и динамически изменять взаимоотношение между объектами;
- Тип
Tname
не являетсяT
, хоть и содержит его и имеет его методы, повышенные изT
; - Встроенное поле указывает компилятору на необходимость генерации дополнительных
методов-оберток
, которые делегируют вызов объявленным методам. КогдаTname.T.Method
вызывается первым из этих методов-оберток, значением его получателя являетсяT
а неTname
; - Структурный тип может иметь более одного анонимного поля. В таком случае, значение этого типа будет иметь все методы всех анонимных полей и свои собственные методы;
p.ScaleBy
- компилятор сначала ищет объявленный метод с именемScaleBy
, затем метод, однократно повышенный из встроенных полейTname
, после этого дважды повышенный метод из анонимных полей и т.д. Компилятор сообщит об ошибке, если селектор неоднозначный, т.к. имеются несколько методов с одним именем с одинаковым рангом повышения;- Методы могут быть объявлены только для именованных типов и указателей на них
T
и*T
, но благодаря встраиваниюнеименованные
структурные типы также могут иметь методы.
- Селектор дает значение-метод (функцию), которая связывает метод селектора со значением конкретного получателя;
- Значения-методы полезны, когда API пакета требует значение-функцию, а для клиента желаемым поведением для этой функции является вызов метода для конкретного получателя;
- Синтаксис с использованием значения-метода оказывается более коротким;
- Выражение-метод - это функция, связанная с типом, которая может быть вызвана как обычная функция и принимает первым параметром получателя. Его можно вызвать как обычную функцию;
- Выражения-методы могут быть использованы для выбора нужного метода из нескольких методов, принадлежащих к одному типу, и вызывать выбранный метод для разных получателей.
- Множества в Go реализуются как
map[T]bool
, гдеT
является типом элемента; - Множество, представленное картой, очень гибкое, но для некоторых задач специализированное представление может его
превзойти (например, в анализе потока данных, где элементы множества - небольшие неотрицательные целые числа,
множества имеют много элементов, а распространенными операциями являются объединение и пересечение множеств. Для такой
задачи идеальным решением оказывается
битовый вектор
); Битовый вектор
использует срез беззнаковых целочисленных значений, каждый бит которых, представляет возможный элемент множества. Множество содержитi
, еслиi-й
бит установлен;- Каждое слово может иметь 32, либо 64 бита (в зависимости от архитектуры устройства). Чтобы найти бит для значения
x
, нужно использовать частноеx/64
в качестве индекса значения, а остатокx%64
- как индекс бита внутри этого слова; - Операция объединения использует оператор побитового
ИЛИ (|)
для вычисления объединения 64 элементов за один раз; - Для формирования удобочитаемой строки из структуры данных часто используют
bytes.Buffer
илиstrings.Builder
, а методString
определен в интерфейсе. - Если мы объявляем метод
String
для типа указателя*T
, то значениеT
не будет иметь методаString
. Чтобы вызвать методString
для значенияT
, можно использоватьfmt.Println(x.String())
илиfmt.Println(&x)
. Вывод значенияT
с помощьюfmt.Println(x)
не даст форматированного представления.
- Переменная или метод объекта называется инкапсулированным, если она недоступна для обращения вне этого объекта. Это называется инкапсуляцией или сокрытием информации, и она является важной частью объектно-ориентированного программирования (ООП).
- В языке программирования Go есть только один механизм для управления видимостью имен: идентификаторы с Прописной буквы - экспортируемые, со строчной - нет. Этот механизм ограничивает доступ к переменным и к полям структуры или методам.
- Для инкапсуляции объекта мы должны сделать его структурой.
type T struct
, выражениеs.field
может появиться только в пакете, в котором определенT
; - Если мы определяем тип как срез или другой что-то другое
type T []int
, выражение*s
может быть использовано в любом пакете; - Единицей инкапсуляции является пакет, а не тип, как во многих других ЯП;
- Поля структурного типа являются видимыми для всего кода в том же пакете;
- Инкапсуляция имеет три преимущества:
- Так, как переменные объекта
не могут быть изменены непосредственно
(вне этого объекта), нужно изучать меньше инструкций для понимания возможных значений этих переменных; Сокрытие деталей реализации
устраняет зависимость клиентов от сущностей, которые могут изменяться, что дает проектировщику большую свободу в развитии реализации без нарушения совместимостиAPI
;Инкапсуляция
не позволяет клиентам произвольным образом устанавливать значения переменных объекта. Доступ только черезгеттеры
исеттеры
. Они могут устанавливаться только функциями из одного пакета, автор пакета, может гарантировать, что все функции поддерживают внутренние инварианты объектов;Кратко
: Инкапсуляция имеет три преимущества: она уменьшает сложность кода, устраняет зависимость клиентов от деталей реализации, и предотвращает произвольное изменение значений переменных объекта.
- Так, как переменные объекта
- При именовании метода получения значения обычно префикс
Get
опускается, а при именовании метода установки значения - используется префиксSet
. - В целом, все излишние префиксы должны опускаться в наименовании методов;
- В целом, в Go можно экспортировать поля, но это нужно делать осознанно, учитывая сложность поддержки инвариантов,
вероятность будущих изменений и количество клиентского кода, который будет подвержен внесению изменений.
- Сложность поддержки инвариантов в Go связана с тем, что экспортирование полей может нарушить внутреннее состояние структуры и привести к некорректной работе программы. Инварианты - это условия, которые всегда должны выполняться внутри структуры или объекта, чтобы он работал корректно;
- Когда экспортируются поля, клиентский код может изменять их значения напрямую, что может привести к нарушению инвариантов. При этом, если изменения внесены в экспортированные поля, но не были учтены при обновлении инвариантов, то это может привести к ошибкам в работе программы;
Инварианты
- ограничения на значения переменных, которые должны сохраняться в любой момент выполнения программы.
- Инкапсуляция может быть нежелательной в случаях, когда требуется широкий доступ к переменным объекта, или когда общий доступ к ним более эффективен, чем использование геттеров и сеттеров. Также может быть нежелательно использование инкапсуляции при создании простых типов данных или для объектов, которые не изменяются.
- Конкретные типы имеют точное представление значений и встроенные операции. Интерфейсы же являются абстрактными типами, которые не раскрывают представление значений и операций, но определяют некоторые методы. Методы можно использовать с любым типом, соответствующим интерфейсу. Обертки для функций на основе интерфейсов позволяют избежать дублирования кода;
- Интерфейс
io.Writer
определяет контракт между вызывающим кодом и функцией, которая его реализует. Функция должна иметь методWrite
, соответствующий сигнатуре и поведению интерфейсаio.Writer
. Это позволяет вызывающему коду передавать разные типы данных, которые соответствуют интерфейсуio.Writer
, и гарантирует, что функция выполнит свою работу для любого значения, соответствующего этому интерфейсу; - Одной из ключевых особенностей ООП является взаимозаменяемость - возможность передачи разных типов, которые соответствуют одному интерфейсу. Это позволяет создавать универсальные функции и методы, которые могут работать с различными типами данных;
- Объявление метода
String
позволяет типу соответствовать интерфейсуfmt.Stringer
и определить формат вывода для своих значений. Это полезно при выводе сложных структур данных в удобочитаемом формате.
- Интерфейс определяет множество методов, необходимых для рассматривания типа в качестве экземпляра этого интерфейса;
io.Writer
- один из самых популярных интерфейсов в Go, представляет собой абстракцию всех типов, в которые можно записывать байты;- Пакет
io
включает в себя много других полезных интерфейсов, таких какReader
,Closer
; - Использования встраивания интерфейсов (
внедрения интерфейсов
) позволяет создавать новые интерфейсы, объединяя уже существующие интерфейсы и их методы; - Примерами таких объединенных интерфейсов являются
ReadWriter
иReadWriteCloser
; - Порядок в котором объявлены методы\интерфейсы в интерфейсе, не имеет значения, главное их множество.
- Чтобы тип
удовлетворял
интерфейсу, он должен иметь все методы, которые требует интерфейс; - Чтобы выражение могло быть присвоено интерфейсу, его тип должен соответствовать этому интерфейсу. Это относится к
обеим сторонам
a = b
, если они являются интерфейсами; - Для каждого типа
T
некоторые методы могут быть вызваны с помощью значения типаT
, а другие требуют указателя на тип*T
. Можно вызывать методы типа*T
с помощью переменной типаT
, но это не значит, что значение типаT
обладает всеми методами, которые есть у указателя на*T
. Поэтому, при использовании значения типаT
, может быть доступно меньшее количество методов, чем при использовании указателя на*T
; - Интерфейс скрывает конкретный тип и значения, которые он содержит. Можно использовать только методы, объявленные в интерфейсе, даже если конкретный тип имеет другие методы;
- Тип
interface{}
являетсяпустым интерфейсом
и не накладывает требований на типы, которые ему соответствуют (может быть присвоен любой тип); - Создав значение
interface{}
, с любым значением, мы ничего не можем сделать с ним непосредственно, т.к. интерфейс не имеет методов; - Обычно нет необходимости объявлять отношения между конкретным типом и интерфейсом, которому он соответствует. Однако иногда полезно документировать и проверять эти отношения;
- Объявление
var w io.Writer = new(bytes.Buffer)
проверяет, что значение типа*bytes.Buffer
соответствует интерфейсуio.Writer
во время компиляции. Нет необходимости создавать новую переменную, поскольку любое значение типа*bytes.Buffer
будет подходить, дажеnil
, который можно записать как(*bytes.Buffer)(nil)
; - Непустым типам интерфейсов, таким как
io.Writer
, чаще всегда соответствуют типы указателей, особенно когда один или несколько методов интерфейса подразумевают некоторые изменения получателя, как методWrite
. Особенно часто используется указатель на структуру; - Помимо указателей, можно использовать и другие ссылочные типы для удовлетворения интерфейсов, даже с методами, изменяющими получателя;
- Фундаментальные типы могут соответствовать интерфейсам;
- Тип
time.Duration
соответствует интерфейсуfmt.Stringer
; - Один конкретный тип может соответствовать нескольким интерфейсам;
- Используя интерфейсы, мы можем группировать конкретные типы по их общим аспектам и выражать их общие свойства;
- Каждая группировка конкретных типов может быть выражена в виде интерфейса, и мы можем определять новые интерфейсы, когда в них нуждаемся, не изменяя при этом объявления конкретных типов. Это особенно удобно, когда мы используем конкретный тип из пакета другого автора. Однако все конкретные типы, которые мы хотим группировать с помощью интерфейса, должны удовлетворять базовым общим требованиям.
- Стандартный интерфейс
flag.Value
помогает определить новую запись для флагов командной строки; - Функция
flag.Duration
создает переменную типаtime.Duration
и разрешает пользователю указывать продолжительность в различных удобных для человека форматах, включая вывод методаString
; - Для создания собственного флага нужно определить тип данных, соответствующий интерфейсу
flag.Value
; - Метод
String
из интерфейсаflag.Value
форматирует значение флага для использования в сообщениях справки командной строки. Таким образом, каждыйflag.Value
так же являетсяfmt.Stringer
; - Метод
Set
из интерфейсаflag.Value
анализирует свой строковый аргумент и обновляет значения флага. По сути, методSet
является обратным методом для методаString
, и использовать для них одни и те же обозначения - хорошая практика; - Функция
fmt.Sscanf
получает данные из строки с указанными форматами; - Для использования флага, нужно создать функцию, которая будет возвращать указатель на поле, встроенное в структуру.
Это поле является переменной, которая будет обновляться методом
Set
при обработке флага; - Вызов
Var
вflag.CommandLine.Var
добавляет флаг во множество флагов командной строки приложения, и присваивает значение аргумента параметруflag.Value
, проверяя наличие необходимых методов в структуре типа.
Интерфейс
в Go состоит из двух компонентов -динамического типа
идинамического значения
, которые представляютконкретный тип
иего значение
соответственно;- Для Go, как для статически типизированного языка, типы являются понятием времени компиляции, поэтому тип не является значением;
Дескрипторы типов
предоставляют информацию о каждом типе, такую как его имя и методы;- В
значении интерфейса
компонент типа представлен дескриптором соответствующего типа; - Переменные в Go всегда инициализируются точным значением, интерфейсы не являются исключением;
- Нулевое значение для интерфейса имеет и тип, и значение, равные
nil
; - Значение интерфейса можно описать как нулевое или ненулевое, в зависимости от его динамического типа;
- Для проверки, является ли значение интерфейса нулевым, можно использовать инструкции
w==nil
иw!=nil
; - Вызов любого метода нулевого интерфейса приводит к
панике
w.Write([]byte("hello")) // panic: разыменовывание нулевого указателя
; w = os.Stdout
- эта инструкция присваиваетw
значение типа*os.File
и включает неявное преобразование из конкретного типа в тип интерфейса, которое эквивалентно явному преобразованиюio.Writer(os.Stdout)
. Такие преобразования охватывают тип и значение своего операнда. Динамический тип значения интерфейса устанавливается равным дескриптору для типа указателя*os.File
, а его динамическое значение хранит копиюos.Stdout
, которая является указателем на переменнуюos.File
, представляющую стандартный вывод процесса;- Во время компиляции мы не можем знать, каким будет динамический тип значения интерфейса, поэтому вызов
с помощью интерфейса должен использовать
динамическую диспетчеризацию
; - Вместо непосредственного вызова компилятор должен генерировать код для получения адреса метода с именем
Write
из дескриптора типа и выполнить косвенный вызов по этому адресу. Аргументом получателя для вызова является копия динамического значения интерфейсаos.Stdout
. Когда мы используем интерфейсы в Go, то мы можем вызвать методы, которые не определены в интерфейсе, но определены в типе, который реализует этот интерфейс. Вместо того чтобы прямо вызывать метод, компилятор создает код для получения адреса этого метода из типа и вызывает его через адрес; w = new(bytes.Buffer)
- присваивает значение типаbytes.Buffer
значению интерфейса. Теперьдинамический тип
представляет собой*bytes.Buffer
, а динамическое значение представляет собой указатель на вновь выделенный буфер;- Значения интерфейсов в Go могут хранить динамические значения любого размера, что позволяет нам создавать обобщенные функции и методы;
- Значения интерфейсов можно сравнивать с использованием операторов
==
и!=
. Два значения интерфейсов равны, если оба равныnil
или если их динамические типы одинаковы, а динамические значения равны согласно результату сравнения с помощью оператора==
с обычным поведением для данного типа; - Поскольку значения интерфейсов сравнимы, они могут использоваться в качестве ключей карт или операндов
инструкции
switch
. Но, если сравниваются два значения интерфейсов, имеющих одинаковые динамические типы и эти типы не сравнимы (например, срезы), то сравнение заканчивается паникой; - В этом отношении типы интерфейсов в Go отличаются от других типов. Другие типы безопасно сравниваемы (такие, как фундаментальные типы или указатели) или не сравниваемы вообще (срезы, карты, функции), но при сравнении значений интерфейсов или составных типов, содержащих значения интерфейсов, мы должны учитывать потенциальную возможность возникновения паники;
- Значения интерфейсов можно сравнивать только в том случае, если мы уверены, что они содержат динамические значения сравниваемых типов;
- Аналогичный риск существует при использовании интерфейсов в качестве ключей карт или операндов
switch
. Значения интерфейсов можно сравнивать только в том случае, если мы уверены, что они содержат динамические значения сравниваемых типов; - При обработке ошибок или при отладке часто оказывается полезной информация о динамическом типе значения интерфейса.
Для этого можно использовать символы преобразования
%T
пакетаfmt
.
- Нулевое значение интерфейса, которое не содержит значения как такового, не совпадает со значением интерфейса, содержащим нулевой указатель;
- Если значение
debug
равноtrue
, функцияmain
накапливает вывод функцииf
вbytes.Buffer
, и при изменении значенияdebug
наfalse
накопление вывода отключается, что приводит к панике во время вызоваout.Write
; - Динамическое значение
out
равноnil
, но динамический типout
является*bytes.Buffer
, что пройдет защитную проверкуout != nil
, но вызов(*bytes.Buffer).Write
со значением получателя, равнымnil
, приводит к панике; - Изменение типа
buf
в функцииmain
наio.Writer
избегает присваивания дисфункционального значения интерфейсу и решает проблему с некорректным вызовомout.Write
.
- Пакет
sort
предоставляет сортировку "на лету", т.е. без привлечения дополнительной памяти любой последовательности в соответствии с любой функцией упорядочивания; - Функция
sort.Sort
ничего не предполагает о представлении последовательности или ее элементах. Вместо этого она использует интерфейс,sort.Interface
, чтобы задать контракт между обобщенным алгоритмом сортировки и всеми типами последовательностей, которые могут быть отсортированы. Реализация этого интерфейса определяет как конкретное представление последовательности, которая часто является срезом, так и желаемый порядок его элементов. - Алгоритм сортировки "на лету" требует трех вещей, которые являются методами
sort.Interface
:- Длины последовательности;
- Средства сравнения двух элементов;
- Способа обмена двух элементов местами.
- Для сортировки любой последовательности нужно определить тип, который реализует три указанных метода, а затем
применить
sort.Sort
к экземпляру этого типаsort.Sort(StringSlice(names))
, гдеStringSlice
- тип, аnames
- срез строк; - Преобразование типа дает значение среза с теми же длиной, емкостью и базовым массивом, что и у исходного среза, но типом, который имеет три метода, необходимые для сортировки;
- Пакет
sort
предоставляет типStringSlice
и функциюStrings
для сортировки среза строк, так что вызов выше можно сократить доsort.Sort(names)
; - Пакет
text/tabwriter
генерирует таблицы.*tabwriter.Writer
соответствует интерфейсуio.Writer
. Он накапливает все данные. МетодFlush
этого типа форматирует всю таблицу и выводит результат; - Чтобы выполнить сортировку в обратном порядке, не нужно определять новый тип с методом
Less
. Пакетsort
предоставляет функциюReverse
, которая преобразует любой порядок сортировки в обратный; - Функция
Reverse
используеткомпозицию
. Пакетsort
определяет неэкспортируемый типreverse
, являющийся структурой, в которую встроенsort.Interface
. МетодLess
дляreverse
вызывает методLess
встроенного значенияsort.Interface
, но с индексами в обратном порядке, что приводит к обращению результата сортировки; - Два других метода
reverse
,Len
иSwap
, неявно предоставляются исходным значениемsort.Interface
, которое является встроенным полем; - Экспортируемая функция
Reverse
возвращает экземпляр типаreverse
, который содержит исходное значениеsort.Interface
; - Чтобы определить новый порядок сортировки (по разным полям сразу), нужно определить конкретный тип и функцию многоуровневого упорядочивания, где есть первичный, вторичный, третичный ключи. Это требует использования анонимной функции;
- Конкретные типы, реализующие интерфейс
sort.Interface
не всегда являются срезами (можно использовать структурный тип). - Чтобы отсортировать последовательность, нужно выполнить
O(n log n)
операций сравнения. Чтобы проверить, отсортирована ли последовательность, нужно выполнить не болееn-1
сравнения. ФункцияIsSorted
из пакетаsort
проверяет, отсортирована ли последовательность. Она использует функцию упорядочения иsort.Interface
, но не вызывает методSwap
; - Для удобства, пакет
sort
предоставляет версии своих функций и типов, специализированные для[]int
,[]string
,[]float64
с использованием их естественного порядка. Для других типов, таких как[]int64
,[]uint
, нужно писать собственную реализацию.
- Функция
ListenAndServe
требует адрес сервера и экземпляр интерфейсаHandler
, которому направляются все запросы. Он работает бесконечно, если только не происходит ошибка (или при запуске сервера происходит сбой), в таком случае он возвращает ненулевую ошибку; - Интерфейс
http.Handler
имеет единственный методServeHTTP(w ResponseWriter, *Requests)
, который позволяет отвечать наHTTP-запросы
; ResponseWriter
- еще один интерфейс, он дополняет интерфейсio.Writer
методами для отправки HTTP-заголовков ответа, аhttp.Requests
- структура, которая содержит данные, соответствующие HTTP-запросу, такие, как URL, заголовки, тело ответа и т.д.;- Если мы создадим тип и реализуем для него метод
ServeHTTP
, он будет соответствовать интерфейсуhttp.Handler
; - Чтобы сообщить клиенту об ошибке HTTP, если она произошла, нужно вызвать
w.WriteHeader(http.Status*...)
. Это должно быть сделано до записи любого текста вw
. Так же можно использовать вспомогательную функциюhttp.Error(w, msg, status)
; r.URL.Query()
- выполняет запрос преобразования параметров HTTP-запроса в мультикарту типаurl.Values
Например:vals := r.URL.Query() val1 := vals.Get("key1") val2 := vals.Get("key2");
- Если мы реализуем для нашего типа метод
ServeHTTP
, нам нужно использоватьr.URL.Path
и инструкциюswitch
для определения адреса из запроса. Это несколько неудобно. Удобнее будет определить логику для каждого случая в виде отдельной функции или метода. Плюс к этому, связанным URL может потребоваться схожая логика, например, несколько изображений могут иметьURL
видаimages/*.png
; - Чтобы удобно добавлять разные варианты действий, и избежать вышеописанной ситуации, пакет
net/http
предоставляетServeMux
-мультиплексор запросов
, упрощающий связь между URL и обработчиками (Handler
); ServeMux
собирает целое множество обработчиковhttp.Handler
в единыйhttp.Handler
. Различные типы, соответствующие одному и тому же интерфейсу, являютсявзаимозаменяемыми
. Веб-сервер может диспетчеризовать запросы к любомуhttp.Handler
, независимо от того, какой конкретный тип скрывается за ним;- В более сложных приложениях могут использоваться несколько
ServeMux
и объединятся; - В Go нет канонического веб-фреймворка, аналогичного Ruby on Rails или Django. Но это не значит, что такого фреймворка не может быть. Просто стандартная библиотека Go является настолько гибкой, что конкретный фреймворк просто не нужен. Тем более, что наличие фреймворка удобно на ранних этапах проекта, но связанные с ним дополнительные сложности могут усложнить дальнейшую поддержку проекта;
mux := http.NewServeMux(); mux.Handle("/list", http.HandlerFunc(db.list))
- создаем новыйServeMux
и используем его для сопоставления URL с соответствующим обработчиком. После этого используемServeMux
как основной обработчик в вызовеlog.Fatal(http.ListenAndServe(":8000", mux))
;db.list
вmux.Hanlde
представляет собой значение-метод, т.е. значение типаfunc(w http.ResponseWriter, r *http.Requests)
, которое при вызове вызывает методdatabase.list
, со значением получателяdb
. Проще говоря,db.list
является функцией, которая реализует поведение обработчика, но, так как у этой функции нет методов, она не может соответствовать интерфейсуhttp.Hanlder
и не может быть передана непосредственноmux.Handle
;http.HandlerFunc(db.list)
- это преобразование типа, а не вызов функции, посколькуhttp.HandlerFunc
является типом;HandlerFunc
демонстрирует некоторые необычные возможности механизма интерфейсов Go. Это тип функции, который имеет методы и соответствует интерфейсуhttp.Handler
. Поведением его методаServeHTTP
является вызов базовой функции. Таким образом,HandlerFunc
является адаптером, который позволяет значению-функции соответствовать интерфейсу, когда функция и единственный метод интерфейса имеют одинаковую сигнатуру;- Этот трюк, позволяет типу
type database map[string]dollar
соответствовать интерфейсуhttp.Handler
различными способами - его методами, которые имеют ту же сигнатуру как и интерфейс; ServeMux
имеет удобный методHandleFunc
, который приводит типdatabase
к соответствию интерфейсуhttp.Handler
. Поэтому можно упростить код регистрации обработчика доmux.HandleFunc("/list", db.list)
;- Чтобы создать два разных веб сервера, которые будут прослушивать разные порты, определять разные URL и выполнять
диспетчеризацию разными обработчиками - нужно создать еще один
ServeMux
и выполнить еще один вызовListenAndServe
; - Пакет
net/http
предоставляет глобальный экземплярServeMux
с именемDefaultServeMux
и функциями уровня пакетаhttp.Hanlde
иhttp.HanldeFunc
; - Для использования
DefaultServeMux
в качестве основного обработчика сервера вListenAndServe
нужно передатьnil
; - Веб-сервер вызывает каждый обработчик в новой горутине, так что обработчики должны принимать меры предосторожности, такие как блокировки при доступе к переменным, к которым могут обращаться другие горутины (включая другие запросы того же обработчика).
error
- это тип интерфейса в Go, который используется для представления ошибок. Он имеет один методError
, который возвращает сообщение об ошибке.- Чтобы создать новое значение
error
, можно использовать функциюerrors.New(msg)
, которая принимает сообщение об ошибке и возвращает новое значениеerror
; - Внутри пакета
errors
есть типerrorString
, который является структурой и используется для хранения сообщения об ошибке. Он не может быть изменен после создания (инкапсуляция
); - Каждый вызов
errors.New
создает новый экземплярerror
, который не равен никакому другому. Это означает, что две ошибки с одинаковым сообщением об ошибке будут разными значениямиerror
; - Есть также функция
fmt.Errorf
, которая позволяет форматировать сообщение об ошибке и возвращает новое значениеerror
. Она часто используется вместоerrors.New
- Пакет
syscall
также предоставляет типы ошибок, которые соответствуют интерфейсуerror
. Они используются для представления ошибок системных вызовов. Он предоставляетAPI
низкоуровневых системных вызовов Go. На многих платформах он предоставляет числовой типErrno
.
Декларация типа (type assertion)
- это операция, которая проверяет, что значение интерфейса имеет определенный тип. Синтаксически она выглядит, какx.(T)
;- Если декларация типа успешна, то результатом операции будет значение этого типа. Если нет, то операция вызовет панику;
- Декларация типа может использоваться для извлечения значения из интерфейса;
- Если значение интерфейса равно
nil
, то декларация типа не будет успешной; - Можно использовать декларацию типа с двумя результатами, чтобы избежать паники. В этом случае второй результат будет
иметь тип
bool
и указывать на успех или неудачу операции.f, ok := w.(*os.File)
; - Если декларация типа используется в присваивании с двумя результатами, то второй результат можно использовать для
принятия решения о последующих действиях.
if f, ok := w.(*os.File); ok {...}
; - Иногда имя переменной может быть повторно использовано в декларации типа, чтобы затенить (переприсвоить) оригинальную
переменную.
w, ok := w.(*os.File)
; - В общем, декларация типа - это способ проверить и извлечь значение из интерфейса в Go. Она может использоваться для обработки ошибок и принятия решений в зависимости от типа значения интерфейса.
- Операции ввода-вывода могут завершиться ошибкой по разным причинам. В Go есть пакет
os
, который помогает обрабатывать эти ошибки. Он предоставляет три функции для классификации ошибок:- Файл уже существует (для операций создания файла);
- Файл не найден (для операций чтения);
- Отсутствие прав доступа.
- Вместо того чтобы проверять сообщение об ошибке на наличие определенной подстроки, пакет
os
использует специальный типPathError
для описания ошибок, связанных с операциями над файлами (Open
,Delete
). Также есть типLinkError
(Symlink
,Rename
) для ошибок, связанных с операциями над двумя файлами. - Большинство программистов не обращают внимание на
PathError
и обрабатывают все ошибки одинаково, путем вызова их методовError
. Хотя методError
ошибкиPathError
формирует сообщение с использованием простой конкатенации полей, структураPathError
сохраняет базовые компоненты ошибки; - Если нам нужно отличать один тип ошибки от другого, можно использовать декларации типа для обнаружения определенного типа ошибки; Это даст больше информации, чем просто строка;
- Если сообщение об ошибке объединяется в более крупную строку, например с помощью вызова
fmt.Errorf
, то структураPathError
теряется; - Обычно распознавание ошибки должно выполняться сразу после сбоя, прежде чем ошибка будет передана вызывающей функции.
- Метод
Write
интерфейсаio.Writer
принимает байтовый срез в качестве аргумента. Если нужно записать строку, необходимо преобразовать ее в байтовый срез с помощью преобразования типа[]byte(string)
. Это преобразование выделяет память и создает копию строки; - Некоторые типы, соответствующие интерфейсу
io.Writer
, также имеют методWriteString
, который позволяет эффективно записывать строки без создания временной копии. Примерами таких типов являются*os.File
,*bytes.Buffer
и*bufio.Writer
- Нельзя утверждать, что произвольный
io.Writer w
также имеет методWriteString
. Но, можно определить новый интерфейс, который имеет только методWriteString
, и использовать декларацию типа для проверки, соответствует ли динамический тип объекта этому новому интерфейсу. Если это так, то можно вызвать методWriteString
для эффективной записи строки без создания временной копии; - Стандартная библиотека Go предоставляет функцию
io.WriteString
, которая использует этот подход для эффективной записи строки в объект, соответствующий интерфейсуio.Writer
; io.WriteString
документирует предположение о том, что конкретный тип соответствует интерфейсуstringWriter
, но функции которые его вызывают, должны документировать, что они делают такое предположение;- Определение метода у некоторого типа является неявным согласием на определенный поведенческий контракт. Например, если
тип имеет метод
WriteString
, то предполагается, что этот метод позволяет эффективно записывать строки без создания временной копии; - Декларацию типа можно использовать для проверки типа во время выполнения. Это позволяет выяснить, соответствует ли
значение общего типа интерфейса более конкретному типу интерфейса, и если это так, то она использует поведение
последнего. Эта методика может использоваться независимо от того, является ли запрашиваемый интерфейс стандартным,
как
io.ReadWriter
, или пользовательским, какstringWriter
; - Эта методика может использоваться независимо от того, является ли запрашиваемый интерфейс стандартным или
пользовательским. Например, функция
fmt.Fprintf
использует этот подход для определения, соответствует ли значение интерфейсамerror
илиfmt.Stringer
, и изменяет форматирование значения в зависимости от этого; - Если значение не соответствует ни одному из этих двух интерфейсов (или другим, которые в нем определены), то выполняется унифицированная обработка всех прочих типов с использованием рефлексии.
- Интерфейсы в Go могут использоваться двумя способами: для выражения подобия типов и для объединения типов:
- Первый способ - это когда интерфейс определяет методы, которые должны быть реализованы типами. Это похоже на
полиморфизм подтипов, когда объекты разных типов могут использоваться одинаково благодаря общему
интерфейсу.
type Expr interface { String() string }
; - Второй способ - это когда интерфейс используется как объединение типов. Это позволяет хранить значения разных
типов в одной переменной и обрабатывать их по-разному в зависимости от типа. Это похоже на перегрузку, когда
функция может принимать аргументы разных типов и обрабатывать их
по-разному;
func getType(x interface{}) string {/*...*/}
- Первый способ - это когда интерфейс определяет методы, которые должны быть реализованы типами. Это похоже на
полиморфизм подтипов, когда объекты разных типов могут использоваться одинаково благодаря общему
интерфейсу.
- API Go для работы с базами данных SQL позволяет безопасно создавать запросы, заменяя символы
?
на значения аргументов; - Построение запросов таким образом позволяет избежать атак SQL-инъекций. Метод
Exec
преобразует значение каждого аргумента в его SQL-запись в виде литерала; - Инструкция
switch
в Go упрощает написание цепочекif-else
. Аналогично,type switch
упрощает написание цепочекif-else
для проверки типов; Type switch
- это инструкцияswitch
, которая проверяет динамический тип значения интерфейса;- Она выглядит как обычная инструкция
switch
, но вместо значения используетсяx.(type)
, гдеx
- это переменная интерфейса,type
представляет собой ключевое слово; - Каждый
case
указывает один или несколько типов. Если тип значенияx
соответствует типу вcase
, то выполняется тело этогоcase
; - Если ни один
case
не соответствует типу значенияx
, то выполняется телоdefault
(если оно есть). - Все инструкции
case
рассматриваются по порядку, и когда соответствие найдено, выполняется тело соответствующей инструкцииcase
. Однако, порядокcase
имеет значение вtype switch
, если несколькоcase
могут соответствовать типу значенияx
. Например, если у нас есть интерфейсinterface{}
и дваcase
:case int
иcase interface{int}
. Если значениеx
имеет типint
, то обаcase
могут соответствовать этому типу. В этом случае будет выполненпервый case
в порядке следования. Поэтому порядокcase
важен при написанииtype switch
; - Использование
fallthrough
запрещено вtype switch
; - Можно использовать расширенную форму
type switch
, чтобы связать извлеченное значение с новой переменной в каждомcase
. Это позволяет получить доступ к значению, извлеченному декларацией типа.switch x := x.(type)
. Эта новая переменная может иметь то же имя, что и переменная интерфейсаx
; - В каждом
case
с единственным типом новая переменная имеет этот тип.case int: x // x.(type) == int
; - В
type switch
можно объединить несколькоcase
, если требуется выполнить одно и то же действие для нескольких типов (case int, uint:
); - Хотя типом переменной
x
являетсяinterface{}
, ее можно рассматривать какобъединение типов
, которые могут соответствовать ее значению. Например, если мы знаем, что значениеx
может бытьint, uint, bool, string или nil
, то можем рассматриватьx
как объединение этих типов.
- Пакет
encoding/xml
в Go предоставляет API для работы с документами XML. Он позволяет декодировать документы XML в структуры Go и кодировать структуры Go обратно в XML. Также он предоставляет низкоуровневый API для декодирования XML на основе лексем; - Стиль на основе лексем означает, что синтаксический анализатор получает входные данные и генерирует поток лексем.
Лексема
- это единица информации, которая генерируется синтаксическим анализатором. Каждый вызов(*xml.Decoder).Token
возвращает одну лексему; - Интерфейс Token является примером распознаваемого объединения. Это означает, что он позволяет работать с фиксированным набором типов, которые изначально определены и не скрыты;
- Типы распознаваемого объединения обрабатываются с помощью
type switch
, где каждыйcase
имеет свою логику; - API гарантирует, что лексемы
StartElement
иEndElement
будут соответствовать друг другу даже в неверно сформированных документах.
- При проектировании нового пакета в Go нужно избегать создания нескольких интерфейсов до определения соответствующих им типов. Это поможет избежать ненужных абстракций с затратами на выполнение. Иначе, этот подход даст несколько интерфейсов, каждый из которых имеет только одну реализацию. Не делайте этого;
- Такие интерфейсы являются ненужными абстракциями, которые к тому же имеют свою ненулевую стоимость во время выполнения. Ограничить методы типа или поля структуры, видимые извне пакета, можно с помощью механизма экспорта;
- Интерфейсы нужны только в том случае, если есть несколько конкретных типов, работа с которыми должна выполниться единообразно.
- Исключением из этого правила является ситуация, когда интерфейс соответствует единственному конкретному типу, но этот тип не может находиться в том же пакете из-за зависимостей. В этом случае интерфейс может помочь развязать два пакета;
- Поскольку интерфейсы в Go абстрагируются от деталей реализации, они обычно маленькие с простыми методами, часто с
одним методом, как
io.Writer
илиfmt.Stringer
; - Новым типам проще соответствовать небольшим интерфейсам. При проектировании интерфейса стоит следовать
правилу
запрашивать только самое необходимое
; - Go поддерживает объектно-ориентированное программирование, но не обязательно использовать его исключительно. Не все должно быть объектом. Автономные функции и неинкапсулированные типы данных имеют свое место.
Горутины
- это способ выполнения нескольких задачодновременно
в Go, сходные спотоками
в других языках, но более легковесные и эффективные;Главная горутина
создается и выполняется автоматически при запуске программы, она вызывает функциюmain
;- Для создания
новых горутин
используется инструкцияgo
, за которой следуетвызов функции или метода
, который будет выполняться параллельно; Горутины
прекращают выполнение, когдаглавная горутина завершается
или когда программа явно завершает свою работу;- Нет прямых способов остановить одну
горутину
из другой, но имеются способы обмена информацией междугорутинами
, с помощью которых можно попроситьгорутину
остановиться самостоятельно; Горутины
способствуют написанию чистых, композитных программ, состоящих изавтономных процессов
, работающих одновременно.Композитная программа
- это программа, состоящая изнескольких отдельных модулей или компонентов
, которые взаимодействуют друг с другом для выполнения задачи целиком. Каждыйкомпонент
может быть автономным процессом, который работает одновременно с другими. Это позволяет более эффективно использовать ресурсы компьютера и ускоряет выполнение задачи. Такой подход также упрощает разработку и тестирование программы, поскольку каждыйкомпонент
может быть разработан, проверен и оптимизирован отдельно от других. Использованиекомпозитных программ
способствует написанию более гибких и модульных приложений, которые могут быть проще поддерживать и расширять в будущем.
- Сети являются естественной областью применения параллелизма, так как серверы обычно обрабатывают много клиентских подключений одновременно;
- Пакет
net
предоставляет компоненты для построения сетевых клиентов и серверов, работающих посредствомTCP
,UDP
или сокетовUnix
. Пакетnet/http
является надстройкой над функциями пакетаnet
; - В качестве примера рассмотрен последовательный сервер часов, который выводит текущее время клиенту один раз в секунду;
- При создании сервера используются функция
net.Listen
для прослушивания входящих соединений и методAccept
для обработки входящих запросов на подключение (возвращает значение только в тот момент, когда устанавливается подключение); - Функция
handleConn
обрабатывает одно клиентское соединение, в цикле выводит текущее время клиенту с использованием методаtime.Now().Format
; - Для подключения к серверу можно использовать стандартную вспомогательную программу
nc (netcat)
или аналогичную Go-версию (netcat1.go
); - При использовании
последовательного сервера
, второй клиент вынужден ждать завершения работы первого, поскольку сервер обрабатывает только одно соединение за раз; - Для создания
параллельного сервера
, можно добавить ключевое словоgo
перед вызовом функцииhandleConn
, что позволит каждому вызову осуществляться в собственной горутине и обрабатывать несколько клиентов одновременно.
- Горутины в Golang позволяют выполнять параллельные вычисления в одном соединении;
- Добавление ключевого слова go перед вызовом функции echo позволяет горутинам работать параллельно и обрабатывать запросы одновременно;
- Использование горутин позволяет параллельно отправлять сообщения на сервер и выводить ответ сервера на экран;
go mustCopy(os.Stdout, conn) mustCopy(conn, os.Stdin)
- Важно убедиться, что одновременные вызовы методов объектов языка Golang безопасны, для чего необходимо обеспечить безопасность параллелизма.
- Горутины представляют собой
процессы
в рамках параллельной программы Go, а каналы являютсясоединениями между ними
; Канал
представляет собой механизм связи, который позволяет одной горутине отправлять значения другой горутине;- Каждый канал имеет
тип элементов канала
, записывается какchan <тип_элементов>
, напримерchan int
; - Канал создается с использованием встроенной функции
make
. Пример:ch := make(chan int)
; - Копируя канал или передавая его функции в качестве аргумента, копируется
ссылка
на структуру данных; - Каналы поддерживают три операции:
отправление
,получение
изакрытие
канала; - Отправление значения через канал:
ch <- value
; пример:ch <- 3
; - Получение значения из канала:
value = <-ch
; пример:x := <-ch
; - Закрытие канала с помощью встроенной функции
close
:close(ch)
; - Каналы могут быть
небуферизованными
(не иметь емкости) илибуферизованными
(иметь емкость). Создание буферизованного канала:ch = make(chan int, 3)
; - Небуферизованные каналы
блокируют отправляющую горутину
до тех пор, пока получающая горутина не заберет значение. Буферизованные каналыимеют очередь для хранения значений
до получения их другими горутинами.
-
Операция отправления в небуферизованный канал блокирует горутину до тех пор, пока другая горутина не выполнит соответствующее получение из того же канала; это обеспечивает синхронизацию отправления и получения данных между горутинами. Пример:
ch := make(chan int) go func() { ch <- 42 // Отправление значения в канал; горутина блокируется, пока другая горутина не выполнит получение }() val := <-ch // Получение значения из канала; обе горутины продолжают работу
-
Небуферизованные каналы иногда называют
синхронными
, поскольку они обеспечивают синхронизацию операций отправления и получения. Это может быть полезно для упорядочивания выполнения горутин и избегания проблем с параллельным доступом к переменным; -
Если нужно передать событие без значения, можно использовать канал с типом элементов
struct{}
; это подчеркивает, что важен только факт передачи сообщения. Пример:done := make(chan struct{}) go func() { // Работа в фоновой горутине done <- struct{}{} // Сигнал главной горутине об окончании работы }() <-done // Ожидание завершения фоновой горутины
-
Использование каналов для синхронизации горутин обеспечивает явное взаимодействие и гарантирует, что определенные действия завершены до продолжения работы других горутин; это помогает избежать ошибок, связанных с параллельным доступом к общим данным и состоянию.
-
При реализации клиент-серверной связи с использованием горутин и каналов важно использовать синхронизацию для корректного завершения работы программы и предотвращения потери данных. Пример кода:
func main() { conn, err := net.Dial("tcp", "localhost:8000") if err != nil { log.Fatal(err) } done := make(chan struct{}) go func() { io.Copy(os.Stdout, conn) // Примечание: игнорируем ошибки log.Println("done") done <- struct{}{} // Сигнал главной горутине }() mustCopy(conn, os.Stdin) conn.Close() <-done // Ожидание завершения фоновой горутины }
Конвейер (pipeline)
в Go - это способ подключения горутин друг к другу с использованием каналов так, чтобы выход одной горутины становился входом для другой;- Закрытие каналов при помощи функции
Close
используется для сообщения получающей горутине о том, что больше не будет отправляться значений; - После закрытия канала, все последующие операции отправления вызовут панику, а операции получения будут возвращать нулевые значения без блокировки;
- Для определения закрытия канала используется вариант операции получения с двумя результатами: полученный элемент и
логическое значение
ok
, которое равноtrue
, если получено значение, иfalse
, если канал закрыт и опустошен:x, ok := <-naturals if !ok { break // Канал закрыт и опустошен }
- Цикл по диапазону используется для получения всех значений из канала и автоматического завершения работы после
получения последнего значения:
for x := range naturals { squares <- x * x }
- Закрытие каналов не обязательно после завершения работы с ними, но используется при необходимости сообщить принимающей горутине о завершении отправки значений;
- Попытка закрыть уже закрытый или нулевой канал вызывает панику;
- Закрытие каналов может использоваться как механизм оповещения о завершении работы.
-
Каналы в Go могут быть
двунаправленными
(для отправления и получения данных) илиоднонаправленными
(только для отправления или только для получения данных);chan int // двунаправленный канал chan<- int // канал только для отправления данных <-chan int // канал только для получения данных
-
Однонаправленные каналы используются для документирования намерений разработчика и предотвращения неправильного использования каналов в функциях, ограничения доступа к каналам только для чтения или только для записи, что может повысить безопасность, уменьшить сложность кода и улучшить производительность в некоторых случаях.
-
Положение стрелки
<-
относительно ключевого словаchan
является мнемоническим. Нарушения использования однонаправленных каналов обнаруживаются на этапе компиляции, что позволяет предотвратить ошибки в реализации функций; -
Операция
Close
для каналов должна вызываться только отправляющей горутиной, потому что она утверждает, что больше не будет отправления данных в канал. Попытка закрыть канал только для получения приводит к ошибке времени компиляции; -
Двунаправленные каналы могут быть неявно преобразованы в однонаправленные, но обратное преобразование невозможно;
naturals := make(chan int) go counter(naturals) // naturals преобразуется в chan<- int go printer(squares) // squares преобразуется в <-chan int
-
Использование однонаправленных каналов и правильная организация горутин позволяют создавать четкую и понятную структуру программы, что улучшает ее читаемость и облегчает поддержку.
-
Буферизованный канал имеет очередь элементов, максимальный размер которой определяется аргументом
capacity
функцииmake
; это позволяет хранить определенное количество элементов в канале, а не только одно значение, как в небуферизованных каналах. Пример создания буферизованного канала с емкостью 3:ch := make(chan string, 3)
-
Операция отправления в буферизованный канал вставляет отправляемый элемент в конец очереди, а операция получения удаляет первый элемент из очереди; если канал заполнен, операция отправления блокирует горутину, и наоборот, если канал пуст, операция получения блокирует горутину. Это обеспечивает синхронизацию между горутинами, работающими с каналом;
-
Встроенная функция
cap
позволяет получить емкость буфера канала, а функцияlen
возвращает количество элементов, находящихся в настоящее время в буфере. Их использование может быть полезно для оптимизации производительности или при отладке:fmt.Println(cap(ch)) // 3 fmt.Println(len(ch)) // 2
-
Не рекомендуется использовать буферизованные каналы в пределах одной горутины в качестве очереди, так как каналы глубоко связаны с планированием горутин и могут привести к блокировке всей программы; вместо этого используйте срезы для создания простой очереди;
-
Буферизованные каналы могут быть использованы для ускорения обработки параллельных запросов, как в примере функции
mirroredQuery
, которая отправляет запросы на три сервера и возвращает результат самого быстрого первого ответа, игнорируя остальные медленные результаты; -
Важно предотвратить утечки горутин, так как, в отличие от переменных, они не собираются сборщиком мусора автоматически; нужно гарантировать, что горутины прекратятся, когда они больше не нужны, особенно когда происходит работа с буферизованными каналами;
-
Выбор между буферизованными и небуферизованными каналами влияет на корректность работы программы; небуферизованные каналы обеспечивают более надежную синхронизацию, так как каждая операция отправления синхронизируется с соответствующей операцией получения. В случае буферизованных каналов эти операции разделены;
-
В некоторых случаях можно создавать буферизованный канал определенного размера и совершать все отправления значений до получения первого значения из канала, особенно когда заранее известна верхняя граница отправляемых значений;
-
Буферизация каналов может повышать производительность программы, если операции, выполняемые разными горутинами, требуют разного времени на выполнение. Буферы позволяют сглаживать временные различия и обеспечивают более плавное выполнение программы;
-
Буферизация каналов может быть неэффективной, если одна стадия выполнения работает быстрее другой, и буфер большую часть времени либо заполняется, либо остается пустым;
-
Если определенная стадия выполнения программы сильно сложнее и медленнее остальных, можно использовать дополнительные горутины для выполнения этой стадии. Это позволяет более равномерно распределить нагрузку и повысить производительность программы.
-
Примеры:
- Небуферизованный канал:
ch := make(chan int)
- Буферизованный канал:
ch := make(chan int, 5)
- Отправка и получение данных через канал в разных горутинах:
go func() { ch <- someValue }() go func() { receivedValue := <-ch }()
- При работе с параллельными циклами в Golang можно использовать
горутины
для выполнения независимых операций в параллельных потоках, что увеличивает производительность и эффективность программы;go func()
- В задачах с
чрезвычайной параллельностью
, когда все подзадачи полностью независимы друг от друга, можно реализовать параллельное выполнение и получить линейное увеличение производительности с увеличением степени параллелизма; - Для ожидания завершения горутины можно использовать
канал
, в который каждая горутина отправляетсобытие
о своем завершении, аглавная горутина
подсчитывает количество полученных событий и завершается после обработки всех подзадач;var ch = make(chan struct{}); /*...*/ ch <- struct{}{}
в горутине; - При использовании
анонимных функций внутри цикла
стоит быть осторожным с захватом переменных цикла и передавать их какявные аргументы функции
, чтобы избежать ошибок и неожиданного поведения программы;var i = make(chan int) for j:=0; j<10; j++ { go func(a int) { i <- a * 2 }(j) }
- Если нужно возвращать значения от рабочих горутин в главную горутину, можно использовать
каналы
для передачи результатов или ошибок; - Важно избегать утечек горутин, когда горутина
навсегда заблокирована
в ожидании отправки значения в канал, так как это может привести к остановке программы или нехватке памяти, для этого рекомендуется использоватьбуферизованные каналы
с достаточной емкостью, что предотвращает блокировку рабочих горутин при отправке сообщений; - Использование типа
sync.WaitGroup
позволяет обеспечить безопасное ожидание завершения работы всех горутин перед закрытием канала, а также позволяет работать спеременной-счетчиком
, доступной для нескольких горутин одновременно;var wg sync.WaitGroup
- Для корректного использования
sync.WaitGroup
следует вызывать методAdd
перед началом рабочей горутины, а методDone
(илиAdd(-1)
) внутри рабочей горутины, что гарантирует правильное состояние счетчика в процессе работы программы; - Использование оператора
defer
внутри рабочей горутины с методомDone()
обеспечивает уменьшение значения счетчика и завершение работы горутины даже в случае возникновения ошибки; - Создание параллельной горутины для ожидания завершения всех рабочих горутин и последующем закрытии канала обеспечивает
правильное выполнение программы без блокировок и зависаний;
// Ожидание счетчика go func() { wg.Wait() close(sizes) }()
- При использовании параллельных циклов в Go стоит следить за последовательностью вызовов и взаимодействиями между горутинами и счетчиками, что позволит предотвратить возможные блокировки, ошибки или некорректное выполнение программы;
- Польза использования параллельных циклов заключается в улучшении производительности программы, эффективном использовании ресурсов и возможности обеспечения безопасной работы с общими переменными при помощи каналов и синхронизации работы горутин.
- Для избежания проблемы захвата переменной цикла, горутина сканирования принимает параметр ссылки, а не использует переменную цикла напрямую;
- Передача аргументов командной строки в рабочий список осуществляется в отдельной горутине, чтобы избежать взаимоблокировки между основной горутиной и горутинами сканирования;
- Слишком высокая параллельность может привести к проблемам, таким как превышение ограничения на количество одновременно открытых файлов и сбоев сетевых операций;
- Ограничение количества параллельных обращений к ресурсам может быть достигнуто путем контроля числа одновременно выполняемых функций с использованием буферизованного канала и подсчитывающего семафора;
- Каждый свободный слот в буфере канала представляет маркер, который занимается при выполнении функции и освобождается при завершении, что обеспечивает ограничение параллелизма;
- Использование подсчитывающего семафора для ограничения параллельных запросов: ограничивает количество одновременно выполняющихся HTTP-запросов, что позволяет достичь баланса между производительностью и нагрузкой на сервер;
- Включение счетчика для отслеживания количества отправок в рабочий список (var n int): позволяет определить, работает ли программа и следует ли ожидать завершения работы. Это используется для корректного завершения программы после обнаружения всех достижимых ссылок из начального URL;
- Создание основного цикла для удаления дубликатов ссылок и отправки непросмотренных ссылок сканерам: уменьшает количество одинаковых запросов и делает код более эффективным;
- Использование замкнутой переменной в главной горутине для обеспечения сокрытия информации: делает программу более безопасной и корректной, защищая переменные от несанкционированного доступа другими частями программы.
- Мультиплексирование с
select
позволяет ожидать событий от нескольких каналов одновременно; - Можно использовать инструкцию
select
для выполнения операций отправления или получения на каналах без блокировки; - Если готовы (отправка &&|| получение) несколько вариантов,
select
выбирает один из них случайным образом; - Инструкция
select
может иметь вариант по умолчанию (default
) для обозначения операции, которую необходимо выполнять, когда другие связи не готовы к обработке, и для обеспечения неблокирующей связи; - Возможно использовать
time.Tick
для запуска периодических действий, однако это может привести к утечке горутин, если отсчеты времени не нужны на протяжении всего жизненного цикла приложения. В таких случаях нужно использоватьtime.NewTicker
иticker.Stop()
; - Нулевое значение для канала (
nil
) может быть полезно, поскольку операции отправления и получения с нулевым каналом блокируются навсегда. В таких случаях варианты с нулевыми каналами в инструкцииselect
никогда не выбираются, что позволяет включить или отключить варианты в зависимости от других входных событий или генерации вывода.
- Функция
walkDir
обходит все файлы и подкаталоги в каталоге, отправляет размеры файлов в канал и вызывает сама себя для подкаталогов; - Главная горутина считывает данные из канала и выводит результаты, второстепенная горутина вызывает walkDir для каждого каталога и закрывает канал после завершения;
- Вариант программы, который выводит промежуточные результаты об использовании диска, если установлен флаг "-v" и использует таймер для генерации событий каждые 500 мс;
- Параллелизм с использованием sync.WaitGroup и создание новой горутины для каждого вызова walkDir: Третья версия программы использует WaitGroup для подсчета вызовов walkDir, создает новые горутины для каждого вызова walkDir, и закрывает канал fileSizes, когда счетчик достигает нуля;
- Можно использовать семафор для ограничения количества одновременно открытых файлов и предотвращения создания слишком большого количества горутин, что может привести к исчерпанию системных ресурсов.
- Отмена горутин важна: Иногда необходимо указать горутине на прекращение выполнения, например, в случае разрыва соединения с клиентом на веб-сервере. Однако, непосредственное прекращение другой горутины невозможно, так как это может привести к неопределенному состоянию совместно используемых переменных;
- Решение с каналом прерывания: Вместо отправки значения в канал для отмены нескольких горутин, закрываем его. Это создает механизм широковещательной передачи, поскольку закрытый канал немедленно возвращает нулевые значения для всех последующих операций получения;
- Создание канала отмены: Создаем канал отмены, в который не передаются значения, но его закрытие означает, что программа должна прекратить работу. Добавляем вспомогательную функцию cancelled() , которая проверяет состояние отмены при вызове;
- Чтение из стандартного ввода для отмены: Создаем горутину, которая считывает данные из стандартного ввода. При считывании любого ввода (например, нажатии клавиши пользователем), горутина уведомляет об этом все остальные, закрывая канал отмены;
- Реагирование горутин на сигнал отмены: В главной горутине добавляем в инструкцию
select
вариант, который проверяет получение от канала отмены. При выборе этого варианта происходит возврат из функции с опустошением каналаfileSizes
, игнорируя все значения и предотвращая зависание при отправке в каналfileSizes
; - Проверка состояния отмены: Горутины могут опрашивать состояние отмены, и если оно установлено, возвращаться без выполнения действий. Это помогает избежать создания новых горутин после отмены;
- Оптимизация отмены: Важно проверять состояние отмены в ключевых местах кода, чтобы уменьшить задержку отмены и обеспечить возможность быстрого завершения всех горутин при отмене. Это может потребовать изменений в логике программы, но в результате достигается большая гибкость и контроль над выполнением горутин;
- Тестирование отмены: Можно использовать вызов
panic
вместо возврата из функцииmain
при отмене, чтобы среда выполнения создала дамп стека всех горутин в программе. Это помогает определить, были ли все горутины корректно отменены или требуют дополнительного расследования.
- В примере чат-сервера используются четыре вида горутин:
main
,broadcaster
,handleConn
иclientWriter
. Это позволяет организовать общение между несколькими пользователями и обрабатывать подключение и отключение клиентов; - Главная горутина
main
отвечает за прослушивание входящих сетевых подключений от клиентов, а для каждого из них создается новая горутинаhandleConn
, которая обрабатывает подключение клиента; - Использование
select
в горутинеbroadcaster
помогает организовать обработку различных видов сообщений от клиентов и событий подключения/отключения клиентов; - Горутина
broadcaster
отвечает за широковещание сообщений от одного клиента ко всем остальным и обработку событий подключения/отключения клиентов; - Горутина
handleConn
отвечает за создание нового канала исходящих сообщений для клиента, считывание текста от клиента и отправку сообщений широковещателю; - Горутина
clientWriter
отвечает за отправку широковещательных сообщений клиентам; - Использование переменных, безопасных с точки зрения параллелизма, таких как каналы и экземпляры
net.Conn
, исключает необходимость явных операций блокировки.
- В программе с двумя или более горутинами все этапы каждой горутины выполняются в знакомом порядке, но в общем случае
мы не знаем, предшествует ли событие
x
в одной горутине событиюy
в другой горутине, происходит ли оно послех
или одновременно с ним; это делает параллельное программирование более сложным, но позволяет достичь высокой производительности и эффективности в использовании ресурсов. - Функция считается безопасной с точки зрения параллельности, если продолжает работать правильно даже при параллельном вызове, т.е. при вызове из двух или более горутин без дополнительной синхронизации; это важно для написания стабильных и надежных программ, которые можно масштабировать и оптимизировать.
- Тип является безопасным с точки зрения параллельности, если все доступные методы и операции являются таковыми; однако, безопасные типы являются скорее исключением, чем правилом, поэтому необходимо внимательно следить за документацией и использовать синхронизацию, когда это нужно.
- От экспортируемых функций уровня пакета обычно ожидается безопасность с точки зрения параллельности, так как они могут быть вызваны из разных горутин одновременно и должны обеспечивать взаимное исключение.
Состояние гонки
— это ситуация, в которой программа не дает правильный результат для некоторого чередования операций нескольких горутин; состояния гонки сложны для воспроизведения и диагностики, поэтому необходимо избегать их появления, применяя механизмы синхронизации и обеспечивая параллельную безопасность функций и типов.- Гонка данных возникает, когда две горутины одновременно обращаются к одной и той же переменной, и по крайней мере одно из обращений является записью; гонки данных могут привести к неопределенному поведению и сложным для отладки ошибкам, поэтому их предотвращение является крайне важным аспектом параллельного программирования на языке Golang.
- Не существует такого понятия, как доброкачественная гонка данных, поэтому всегда стоит избегать их возникновения для стабильности и надежности программ;
- Способы избежания гонки данных на языке Golang:
- Не записывать переменную: инициализировать все необходимые записи перед созданием дополнительных горутин и не изменять их впоследствии; это позволяет обеспечить безопасность в параллельном выполнении кода и упрощает синхронизацию;
- Ограничить доступ к переменной одной горутиной: другие горутины должны использовать каналы для запросов получения или изменения значения ограниченной переменной; в этом случае используется мантра Go "не связывайтесь путем совместного использования памяти; совместно используйте память путем связи", что помогает избегать гонок данных и упрощает работу с переменными;
- Использовать взаимное исключение, позволяя многим горутинам обращаться к переменной, но только по одной за раз;
это известно как
механизм мьютексов (mutex)
и является одним из основных инструментов синхронизации в Golang, который обеспечивает правильное взаимодействие горутин с общим ресурсом;
- Польза использования этих подходов и механизмов состоит в обеспечении корректной работы программы в условиях параллельного выполнения, снижении вероятности возникновения ошибок и упрощении процесса синхронизации между горутинами;
Бинарный семафор
(канал емкостью 1) используется для гарантии одновременного доступа к совместно используемой переменной только одной горутиной;- Для более удобного механизма взаимного исключения можно использовать тип
Mutex
из пакетаsync
с методамиLock
иUnlock
для захвата и освобождения токена блокировки соответственно; Мьютекс
охраняет совместно используемые переменные, и важно соблюдать соглашение об объявлении этих переменных сразу после объявления самого мьютекса;Критический раздел
- область кода междуLock
иUnlock
, в которой горутина может безопасно читать и модифицировать совместно используемые переменные;Монитор
- механизм блокировок, функций и переменных, который позволяет одной или нескольким функциям инкапсулировать переменные и обеспечивать последовательный доступ к ним для нескольких горутин;- Использование инструкции
defer
позволяет отложить вызовUnlock
и автоматически продлить критический раздел до конца функции, облегчая обработку освобождения блокировки на всех путях выполнения, включая пути ошибок; - Отложенный вызов
Unlock
будет выполняться даже припанике
во время выполнения критического раздела, что может быть важным в программах, использующихrecover
; - Важно обеспечить
атомарность
операций (либо операция выполняется полностью, либо вообще не выполняется), чтобы избежать побочных эффектов, вызванных неправильной обработкой конкурентных операций, таких как временное опускание значения переменной ниже нуля или ложное отклонение транзакции; - В Golang следует предпочитать ясность и последовательность кода, избегая преждевременной оптимизации и активно
использовать
defer
имьютексы
для обеспечения безопасности при параллельном программировании; - Функция
Deposit
может вызватьвзаимоблокировку (deadlock)
из-за попытки захватить мьютекс дважды; это происходит из-занереентерабельности мьютексов
, то есть они не позволяют блокировать уже заблокированный мьютекс; Мьютексы
используются для обеспечения сохранения инвариантов совместно используемых переменных в критических точках во время выполнения программы, что позволяет избежать ошибок и несогласованности данных;- Распространенное
решение
проблемыреентерабельности мьютексов
- разделение функций на две: неэкспортируемую функцию, которая делает реальную работу, и экспортируемую функцию, которая выполняет блокировку до вызова первой; - Инкапсуляция помогает поддерживать инварианты структур данных и параллелизма, а также уменьшает неожиданные взаимодействия в программе, что приводит к более стабильному и безопасному коду;
- Важно не экспортировать мьютексы и переменные, которые они защищают, независимо от того, являются ли они переменными уровня пакета или полями структуры, для соблюдения инкапсуляции и поддержания порядка в коде;
- Использование мьютексов и принципов инкапсуляции в коде на языке Golang способствует созданию надежных и производительных приложений, так как минимизирует возможность ошибок из-за совместного доступа к переменным и параллельного выполнения.
- Бывает ситуация, когда операции
чтения
безопасны параллельно, а операциизаписи
требуют исключительного доступа; для такого сценария подходит блокировка"несколько читателей, один писатель"
, которая в Go реализуется черезsync.RWMutex
; RWMutex
отличается от обычного мьютекса тем, что разделяет блокировки начитателей (RLock)
иписателей (Lock)
, что позволяет параллельно выполнять операций чтения и ускоряет выполнение программы;RLock
следует использовать только тогда, когда в критическом разделе кода не происходитзаписи
совместно используемых переменных;- Если в функции не только
чтение
, но иобновление переменных
(например, счетчиков или кеша), лучше использоватьисключительную блокировку (Lock)
для безопасности; RWMutex
рационально применять, когда большинству горутин требуется блокировкачитателей
, и за блокировку ведется состязание; такая блокировка работает медленнее обычных мьютексов из-за более сложной внутренней бухгалтерии, но обеспечивает параллельное выполнение операций чтения и ускоряет выполнение программы;sync.RWMutex
- позволяетнескольким горутинам одновременно читать данные
из общей переменной, но толькоодной горутине изменять
их. Это означает, что если одна горутина получаетблокировку для записи (используя метод Lock)
, все остальные горутины, которые пытаются получить блокировку длячтения (используя метод RLock)
или для записи, будут заблокированы до тех пор, пока первая горутинане разблокирует RWMutex (используя метод Unlock)
. Однако если несколько горутин получают блокировку для чтения, они могут одновременно читать данные без блокировки друг друга.
- Синхронизация памяти играет важную роль в многопоточном программировании, так как определяет порядок выполнения операций и взаимодействие переменных между горутинами;
- В современных компьютерах может быть несколько процессоров, каждый с собственным кешем оперативной памяти, что может привести к непредсказуемому поведению в случае отсутствия правильной синхронизации между горутинами;
- Без использования примитивов синхронизации, таких как
каналы
илимьютексы
, нет гарантии, что результаты выполнения одной горутины будут видимыми для других горутин, работающих на других процессорах; - Примитивы синхронизации (каналы и мьютексы) гарантируют, что все накопленные записи выполняющихся горутин будут сброшены и зафиксированы, делая их видимыми для других горутин;
- В случае параллельного доступа к совместно используемым переменным без использования взаимного исключения возникает гонка данных, что может привести к неожиданным и недетерминированным результатам;
- Чтобы избежать проблем параллелизма, важно использовать простые и проверенные шаблоны: ограничивать доступ к переменным одной горутиной или использовать взаимные исключения для совместно используемых переменных.
Отложенная инициализация
- хорошая практика для уменьшения времени запуска программы и экономии ресурсов, поскольку она происходит только тогда, когда это действительно необходимо;- Использование отложенной инициализации с одиночной переменной доступной только одной горутине может быть
достигнуто с помощью простого условия проверки на
nil
, но такой подход не является безопасным для параллельного доступа; - Для корректной синхронизации между горутинами необходимо использовать мьютексы, такие, как
sync.Mutex
илиsync.RWMutex
, которые предотвращают одновременный доступ к общим данным, исключая возможность состояний гонки; - Использование
sync.RWMutex
позволяет обеспечить более высокую степень параллелизма, но требует более сложной реализации; - Для упрощения синхронизации и решения задачи однократной инициализации можно использовать специализированный
инструмент -
sync.Once
, который гарантирует, что функция инициализации вызывается только один раз; - Метод
sync.Once.Do()
принимает функцию инициализации в качестве аргумента, а выполнение инициализации осуществляется только при первом вызове, одновременно гарантируя видимость изменений в памяти для всех горутин; - Использование
sync.Once
позволяет избежать преждевременного доступа к переменным и обеспечивает корректную синхронизацию и инициализацию данных в параллельных средах.
- Даже при максимальной аккуратности в программировании, существует риск допущения ошибок параллелизма;
- В Go существует инструмент динамического анализа -
детектор гонки
, который помогает обнаруживать эти ошибки; - Для активации детектора гонки, достаточно добавить флаг
-race
к командамgo build
,go run
илиgo test
; - Этот флаг приведет к созданию модифицированной версии приложения или теста с дополнительным инструментарием для анализа обращений к совместно используемым переменным и событиями синхронизации;
Детектор гонки
изучает поток событий и определяет случаи возможных гонок данных между горутинами;- В результате анализа, детектор гонки выдает отчет, содержащий идентификатор переменной и стеки вызовов активных функций для лучшего понимания и исправления проблемы;
- Детектор гонки показывает только фактически выполненные гонки данных во время выполнения, но не может доказать отсутствие других гонок;
- При использовании детектора гонки программа требует больше памяти и времени на выполнение, но это оправдано при редко встречающихся состояниях гонки, так как помогает сэкономить время на отладку;
- Использование детектора гонки в Golang полезно для обеспечения корректной работы приложений, предотвращения ошибок параллелизма и ускорения процесса разработки и отладки программ.
- Создание
параллельного неблокирующего кеша
может решить проблему функций с запоминанием в параллельных программах, позволяя кешировать результаты функций и избежать излишнего повторения дорогостоящих операций; - Пример функции с запоминанием -
httpGetBody
, делающая HTTP GET запрос и читающая тело ответа. Хранение результатов этой функции может существенно ускорить программу; Memo
, (memo2) использующий функциюf
и кеш на основе карты строк, позволяет кешировать результаты выполнения функций и предоставлять их быстро при повторных запросах;- Использование параллелизма в программе может значительно ускорить выполнение операций ввода-вывода, особенно при
наличии дубликатов
URL-адресов
; - Простой способ сделать кеш безопасным с точки зрения параллельности - использовать синхронизацию на
основе монитора ( мьютекс)
, однако это может сериализовать операции ввода-вывода и уменьшить преимущества параллелизма; - Для решения этой проблемы и обеспечения корректной работы кеша в условиях параллелизма, можно использовать другие
подходы, например,
разделение блокировок
илиалгоритмы без блокировок
; - В реализации
Get
(memo3) горутина выполняет блокировку дважды:для поиска
идля обновления
; это позволяет улучшить производительность и предотвращает блокировки при использовании кеша. - Некоторые URL могут выбираться дважды при одновременных вызовах
Get
: это приводит к лишней работе, поэтому следует использовать подавление повторений для более эффективной работы с кешем. - В версии 4
Memo
(memo4) каждый элемент карты является указателем на структуруentry
, которая содержит результат вызова функцииf
и канал с именемready
; закрытие канала оповещает другие горутины о готовности результата. Get
включает захват мьютекса, поиск существующей записиentry
, выделение памяти, вставку новой записи и освобождение мьютекса; это обеспечивает параллельность и неблокирование кеша.- Если существующая запись
entry
не готова, горутина должна ожидать оповещения о готовности перед чтением значения; это достигается с помощью чтения из каналаready
. - Получение оповещения о готовности и чтение результатов в других горутинах происходит после записи данных в первой горутине, поэтому не требуется мьютекс и нет гонок данных.
- Можно использовать альтернативный дизайн кеша с
ограниченной управляющей горутиной и каналами
для отправки запросов и получения результатов, что также обеспечит безопасность доступа к данным и параллельность работы. - Управляющая горутина обрабатывает запросы, вызывает функцию
f
, сохраняет результат и оповещает о готовности с помощью методовcall
иdeliver
; эти методы вызываются в отдельных горутинах для предотвращения блокировки обработки новых запросов. - Разные подходы к параллельности и синхронизации данных (с совместно используемыми переменными и блокировками или * со взаимодействующими последовательными процессами*) могут быть использованы без чрезмерной сложности, и иногда переход от одного подхода к другому упрощает код.
- Знание обоих подходов помогает выбрать наиболее подходящий для конкретной ситуации и создать эффективные параллельные алгоритмы и структуры данных на языке Golang.
- Потоки операционной системы имеют блок памяти фиксированного размера для стека, обычно около
2 Мбайт
; это область для хранения локальных переменных вызовов функций, находящихся в работе или приостановленных; - Фиксированный размер стека может быть как
слишком большим для простых задач
, так ислишком малым для сложных и глубоко рекурсивных функций
; это создает проблемы с использованием памяти и ограничивает количество одновременных потоков; - В Go используются горутины, которые начинают работу с небольшим стеком, обычно около
2 Кбайт
, что экономит память и позволяет создавать большее количество горутин; - Стек горутины может расти и уменьшаться в зависимости от необходимости, что обеспечивает более эффективное использование памяти, больше одновременных потоков и возможность работы с рекурсивными функциями большей глубины;
- Максимальный размер стека горутины в Go может достигать
1 Гбайта
, что обеспечивает гораздо большую гибкость по сравнению со стеками фиксированного размера; - Использование горутин и динамически изменяемых стеков в Go позволяет повысить производительность и эффективность программ, делая их масштабируемыми и адаптивными к различным задачам и условиям выполнения.
Потоки операционной системы планируются в ядре
, что требует переключения контекста и является медленной операцией из-за слабой локальности и обращений к памяти;- Go имеет собственный планировщик, который использует
m:n-планирование
, мультиплексирование (планирование)m
горутин наn
потоках операционной системы, чтобы сделать планирование быстрее и эффективнее; - Планировщик Go вызывается не периодически аппаратным таймером, а
неявно некоторыми конструкциями языка Go
, такими какtime.Sleep
,каналы
имьютексы
; - Когда горутина
блокируется на задаче
, планировщик переводит ее в спящий режим и запускает другую горутину, что позволяет эффективнее использовать ресурсы; - Планирование горутин
значительно дешевле
, чем планирование потоков, поскольку не требуется переключение контекста ядра; - Использование планировщика и горутин в Golang делает многопоточное программирование более доступным и быстрым.
- Планировщик Go использует параметр
GOMAXPROCS
для определения количества потоков операционной системы, которые могут одновременно активно выполнять код Go; это позволяет оптимально распределить ресурсы процессора для одновременного выполнения задач; - Значение
GOMAXPROCS
по умолчанию равно количеству процессоров компьютера (логических), что обеспечивает оптимальное использование мощности машины для выполняемых программ; Спящие
,заблокированные в коммуникации
илизаблокированные на операциях ввода-вывода
горутины не требуют самих потоков, что снижает нагрузку на систему при работе с большим количеством горутин;- Управление параметром
GOMAXPROCS
возможно через переменную среды или функциюruntime.GOMAXPROCS
, что позволяет адаптировать его значение для конкретных условий и оптимизировать процессы в различных ситуациях; - Изменение значения
GOMAXPROCS
может привести к изменению поведения программы, исходя из количества доступных потоков и одновременно работающих горутин; это позволяет более точно контролировать процесс выполнения кода и рассчитывать заранее требуемые ресурсы системы; - Планирование горутин зависит от множества факторов и среды выполнения, что делает каждую ситуацию уникальной и может привести к различным результатам при разных условиях.
- В большинстве операционных систем и языков программирования с поддержкой многопоточности существует идентификация потоков, которая помогает создавать абстракцию "локальной памяти потока";
- Горутины в Go не предоставляют идентификацию потока, программисту: это специальное архитектурное решение, направленное на преодоление злоупотребления локальной памятью потока и проблем, связанных с "связями на расстоянии";
- Локальная память потока может привести к сложно отлавливаемым ошибкам, вызванными неправильным поведением функции, определяемым не только аргументами, но и идентификатором потока;
- Go поощряет простой стиль программирования, где все параметры, влияющие на поведение функции, являются явными, что делает код более читаемым и позволяет распределять подзадачи по многим горутинам без проблем идентификации;
- Использование явных параметров и значения вместо локальной памяти потока позволяет легче структурировать проекты, тестировать, профилировать и документировать пакеты, а также делиться ими с другими программистами.
Системы пакетов
облегчают разработку и поддержку больших программ путем группировки связанных функций в модули, что делает их легче понимать и изменять независимо друг от друга;Модульность
позволяет использовать пакеты совместно в разных проектах, они могут быть распространены внутри организации или доступны всему миру;Пакеты
определяют уникальное пространство имен для всех своих идентификаторов, что избегает конфликтов имен в разных частях программы;Инкапсуляция пакетов
управляет видимостью имен и позволяет скрыть вспомогательные функции и типы за API пакета, облегчая его разработку и поддержку;Ограничение видимости переменных пакета
заставляет пользователей обращаться к ним только через экспортированные функции, что обеспечивает сохранение внутренних инвариантов и взаимоисключение в параллельных программах;- При изменении файла в пакете, необходимо перекомпилировать его и все зависимые пакеты;
- Компиляция Go быстрая благодаря трём основным причинам:
явное указание импортируемых пакетов
,отсутствие циклов в зависимостях
ииспользование объектных файлов с экспортируемой информацией для всех зависимостей
.
- Пути импорта в Go идентифицируют пакеты и используются в объявлениях
import
; - Спецификация языка Go не определяет смысл и правила нахождения путей импорта, поэтому разные инструменты могут
иметь свои соглашения; наиболее распространенным инструментом является
go
, который определяет стандартные правила для большинства программистов Go; - Для пакетов, предназначенных для совместного использования или публикации, пути импорта должны быть глобально уникальными; это предотвращает конфликты и способствует легкому нахождению и использованию пакетов;
- Пути импорта пакетов, не входящих в стандартную библиотеку, должны начинаться с доменного имени организации-владельца; это делает пути импорта более понятными и способствует их уникальности;
- В объявлениях
import
можно использовать пути импорта как стандартных библиотек (например,"fmt"
,"math/rand"
), так и сторонних пакетов (например,"golang.org/x/net/html"
,"github.com/gosqldriver/mysql"
); - Использование путей импорта соответствующим образом упрощает подключение и использование различных пакетов, что способствует ускорению разработки и улучшению качества кода на языке Golang.
- Объявление
package
определяет идентификатор по умолчанию для пакета при его импорте в другой пакет; это позволяет упростить обращение к членам пакета через идентификатор, например,rand.Int
для пакетаmath/rand
; - Имя пакета по соглашению
является последней частью пути импорта
, что позволяет иметь разные пакеты с одинаковыми именами, но разными путями импорта, упрощая их использование; - Пакет с именем
main
определяет команду(выполнимую программу Go)
, что сигнализирует инструментам Go о необходимости создания исполняемого файла; - Файлы с суффиксом
_test
в имени пакета определяют пакеты для внешнего тестирования, что позволяет избежать циклов в графе импорта и облегчает тестирование кода; Инструменты управления зависимостями
могут добавитьсуффикс с номером версии в путь импорта пакета
, и этот суффикс следует исключать из имени пакета для упрощения использования.
- Исходный файл Go может содержать нуль или более объявлений импорта после объявления
package
; это позволяет загружать код из других пакетов и использовать его в текущем файле; - Объявления импорта могут быть сгруппированы с помощью добавления пустых строк для указания различных предметных областей; это облегчает чтение кода;
- Импортированные пакеты могут использовать
альтернативные имена
, чтобы избегать конфликтов с именами других пакетов или локальных переменных; - Альтернативное имя влияет только на импортирующий файл, что обеспечивает гибкость при импорте пакетов;
- Пример импорта с использованием альтернативного имени:
import ( "crypto/rand" mrand "math/rand" // Альтернативное имя mrand устраняет конфликт )
- При наличии громоздких имен пакетов, использование альтернативных имен может облегчить чтение кода;
- Инструмент
go build
проверяет зависимости и выдает сообщение об ошибке, если они образуют цикл, что предотвращает возникновение проблем с зависимостями; - Использование последовательных и однозначных импортированных имен облегчает понимание и поддержку кода.
- Пустой импорт используется только для запуска кода, который не возвращает какие-либо значения, например, для
инициализации переменных и функций (
init()
). - Для подавления ошибки "неиспользуемый импорт" используется переименование импорта с альтернативным
названием
_ (пустой идентификатор)
; Пустой импорт
применяется при реализации механизма времени компиляции, позволяющем основной программе включать необязательные возможности с помощью пустого импорта дополнительных пакетов (например, декодеры изображений);- Пример использования пустого импорта - подключение декодировщика PNG:
import _ "image/png"
, после чего функцияimage.Decode
сможет распознавать и обрабатывать изображения в данном формате; - Работа со стандартной библиотекой
image
и пустыми импортами позволяет легко создавать преобразователи изображений, которые считывают изображение в одном формате и записывают его в другом; - Без пустого импорта необходимого формата выполнимый файл будет компилироваться и компоноваться, однако функция
image.Decode
не сможет распознать данный формат и выдаст ошибку; - Аналогичный механизм с пустыми импортами используется в пакете
database/sql
для установки только тех драйверов баз данных, в которых пользователь нуждается, например, для поддержкиPostgreSQL
илиMySQL
; - Преимущества использования пустых импортов включают меньший размер выполняемого файла (за счет исключения неиспользуемых функций), гибкость и возможность расширения функционала приложения при необходимости.
- Имена пакетов должны быть короткими и понятными; например, стандартные библиотеки Go используют
имена
bufio
,bytes
,flag
,fmt
,http
,io
,json
,os
,sort
,sync
иtime
; - Лучше использовать описательные и недвусмысленные имена для пакетов, чтобы облегчить понимание их функций,
например
imageutil
илиioutil
вместо простоutil
; - Обычно имена пакетов являются словами в единственном числе, хотя есть исключения, например,
bytes
,errors
иstrings
; - Нужно избегать дублирования или конфликта имен пакетов с другими смыслами, чтобы не возникло путаницы;
- При именовании членов пакета нужно учитывать, что их имена будут использоваться совместно с именами пакетов, поэтому они должны быть гармоничными и логичными;
- Для функций и структур внутри пакетов лучше использовать краткие и лаконичные имена, которые отражают их назначение,
например:
bytes.Equal
,flag.Int
,http.Get
,json.Marshal
; - В некоторых пакетах, таких как
strings
, имена функций и структур не содержат слово"string"
, поскольку оно подразумевается в названии пакета; - Пакеты, предоставляющие основной тип данных и его методы, иногда имеют короткие имена, чтобы избежать стилистического
повторения, например:
template.Template
илиrand.Rand
; - Для сложных пакетов, таких как
net/http
, можно использовать простые и основные имена для наиболее важных членов пакета, например:Get
,Post
,Handle
,Error
,Client
,Server
.
- Инструмент
go
является универсальным инструментом для работы с кодом на языке Go, объединяющим в себе функции менеджера пакетов, системы сборки и тестировщика; это упрощает процесс разработки и управления зависимостями; - Инструмент go использует стиль "складного ножа" с множеством подкоманд, таких как get, run, build и fmt, для выполнения разных действий с кодом и пакетами;
- Инструмент go опирается на соглашения для упрощения конфигурации, такие как один пакет на каталог, и путь импорта пакета соответствует иерархии каталогов в рабочей области, что позволяет инструменту легко находить нужные файлы, объектные файлы и URL-серверы;
- Использование инструмента go позволяет разработчикам сосредоточиться на написании кода, облегчает управление проектом и его зависимостями, автоматизирует ряд рутинных задач и улучшает качество кода благодаря встроенным командам форматирования и тестирования.
- Переменная
GOPATH
- это корневой каталог рабочей области, который определяет места расположения исходного кода, скомпилированных пакетов и выполнимых программ; GOPATH
имеет три подкаталога -src
,pkg
иbin
, каждый из которых хранит определенный тип файлов;- В
GOPATH
, каталогsrc
содержит исходный код всех пакетов, каждый из которых импортируется по отношению к этому каталогу; - В
GOPATH/src
могут быть несколько репозиториев управления версиями (для каждого пакета); - В подкаталоге
bin
хранятся выполнимые программы, такие какhelloworld
; - Вторая переменная среды -
GOROOT
- указывает корневой каталог дистрибутива Go; - Команда
go env
выводит действующие значения переменных среды, имеющих отношение к инструментарию, включая значения по умолчанию для отсутствующих; GOOS
иGOARCH
- переменные, которые определяют целевую операционную систему и архитектуру целевого процессора соответственно.
- Команда
go get
позволяет загрузить и обновить пакеты из Интернета, а также их зависимости; это упрощает управление кодом и синхронизацию с другими разработчиками; go get
может загружать как одиночные пакеты, так и всё поддерево репозитория, что делает его универсальным инструментом;- После загрузки пакетов команда
go get
автоматически выполняет сборку и установку библиотек и команд, что облегчает работу с новыми пакетами; - Команда
go get
поддерживает популярные системы управления версиями, такие как Git или Mercurial, и популярные сайты хостинга кода, такие как GitHub, Bitbucket и Launchpad; - Путь импорта указывает не только, где найти пакет в локальной рабочей области, но и где его найти в Интернете, что
позволяет
go get
обрабатывать различные доменные имена и пути импорта; - Если указан флаг
-u
,go get
будет обновлять все посещенные пакеты и их зависимости до последней версии, что гарантирует их актуальность; - Начиная с версии Go 1.6, поддерживается
"вендоризация"
– использование локальных копий внешних зависимостей, которые хранятся в каталоге"vendor"
внутри проекта для легкого доступа и управления; Вендоризация
упрощает управление зависимостями, т.к. зависимости разных проектов могут иметь разные версии и будут храниться локально, что позволяет легко переключать версии и избегать конфликтов.
- Команда
go build
используется для компиляции пакетов, указанных в качестве аргументов командной строки; это позволяет проверить наличие ошибок компиляции и обеспечивает корректную работу кода; - Если пакет имеет имя
main
,go build
вызывает компоновщик для создания выполнимого файла (строит запрошенный пакет и его зависимости, после этого отбрасывает скомпилируемый код, оставляя только бинарник), что позволяет запускать программы и обеспечивает удобство работы; - Команда
go install
сохраняет скомпилированный код каждого пакета и команды, что делает последующие построения гораздо более быстрыми; - Возможность
кросс-компиляции
в Go позволяет легко создавать выполнимые файлы для работы с разными операционными системами или процессорами, что обеспечивает универсальность кода; - Go поддерживает использование специальных комментариев, таких
как
дескрипторы построения
(// +build linux darwin
), что позволяет тонко управлять компиляцией файлов для разных платформ или процессоров, что облегчает работу с низкоуровневой переносимостью или оптимизацией версий важных процедур.
- Стиль Go настоятельно рекомендует тщательно документировать API пакетов; она помогает пользователям легче разобраться в предназначении и использовании пакетов;
- Каждое объявление экспортируемого члена пакета и самого объявления пакета должны сопровождаться комментарием, объясняющим их цель и использование;
- Документирующие комментарии Go являются полными предложениями, начинающимися с имени объявления, объясняющим его назначение;
- Пакетное объявление должно иметь только один документирующий комментарий, который может находиться в любом файле или в
отдельном файле под названием
doc.go
; - Важно стремиться к краткости и простоте в документации, поскольку как и код, она также требует обслуживания;
- Инструмент
go doc
выводит документирующие комментарии для указанных объектов, пакетов или членов пакета, что облегчает работу с документацией; - Инструмент
godoc
предоставляет HTML-страницы с документацией, которые можно просматривать через браузер или запустить на локальном сервере для просмотра собственных пакетов; - Польза от использования тщательной документации заключается в упрощении процесса обучения для новичков, быстром разборе в коде для опытных разработчиков и улучшении общего качества кода.
- Пакеты являются важным механизмом
инкапсуляции
в Golang. Неэкспортируемые идентификаторы
видимы только в пределах одного пакета, аэкспортируемые
- видимы всем.- Для определения идентификаторов, которые являются видимыми только для небольшого набора доверенных пакетов, можно
использовать
внутренние пакеты
. Внутренний пакет
(internal
) может быть импортирован только другим пакетом, находящимся в дереве с корнем в родительском по отношению кinternal
каталоге.- Разбивая большой пакет на более управляемые меньшие части, можно не захотеть раскрывать интерфейсы между этими частями для других пакетов, для этой задачи подойдет применение внутреннего пакета.
- Совместное использование вспомогательных функций несколькими пакетами проекта без их общедоступности может быть
достигнуто через
внутренние пакеты
. Внутренние пакеты
могут использоваться для экспериментирования с новым пакетом без преждевременной фиксации его API, предоставив его "на испытательный срок" узкому кругу клиентов.- Использование
внутренних пакетов
позволяет более гибко и безопасно управлять доступом к коду проекта. Внутренние пакеты
помогают избежать конфликтов имен и запутанности в коде проекта.Внутренние пакеты
- это один из способов обеспечения безопасности и структурирования кода в больших проектах.
- Инструмент
go list
предоставляет информацию о доступных пакетах в рабочем пространстве; go list
может проверять наличие пакета и выводить его путь импорта, что упрощает поиск и использование пакетов для разработчиков;- Использование символов
...
в аргументеgo list
позволяет перечислять все пакеты в рабочем пространстве, в определенном поддереве или связанные с конкретной темой (...xml...
), упрощая навигацию и организацию кода; - Команда
go list
предоставляет полные метаданные для каждого пакета, что дает возможность получить более детальную информацию для пользователей или других инструментов; - Флаг
-json
выводит всю информацию о пакете в форматеJSON
, что облегчает автоматическую обработку данных; - Флаг
-f
позволяет настраивать формат вывода с использованием языка шаблоновtext/template
, что даёт возможность выделить определенную информацию о пакете, учитывая индивидуальные требования разработчика; - Команда
go list
полезна для интерактивных запросов и автоматизации процесса построения и тестирования кода; - Использование
go list
улучшает организацию и структурирование кода, а также ускоряет поиск нужных пакетов и анализ их содержимого.
- Команда
go test
является тест-драйвером для пакетов Go, организованных согласно определенным соглашениям; go test
позволяет проверять программную логику (Test), производительность(Benchmark) и машинно-проверяемую документацию (Example)*;- Файлы
*_test.go
представляют собой часть временного пакетаmain
, который создается и запускаетсяgo test
; go test
сканирует файлы*_test.go
на предмет наличия тестовых функций, функций производительности и функций-примеров.
- Каждый тестовый файл в Go должен импортировать пакет
testing
; это необходимо для корректной работы инструментов тестирования Go; - Имена тестовых функций должны начинаться с
Test
, и далее следует необязательный суффикс, начинающийся с прописной буквы; это соглашение упрощает поиск тестовых функций при использовании командыgo test
; - Параметр
t
в тестовых функциях предоставляет методы для сообщения о не пройденных тестах и для протоколирования дополнительной информации; это помогает лучше отслеживать поведение тестов и выявлять проблемы в коде; go test
используют для компиляции и выполнения тестов;- Сначала нужно написать тест, который воспроизводит проблему, чтобы убедиться в ее наличии; это поможет быстрее определить и исправить источник ошибки;
Табличное тестирование (table-driven testing)
- хороший подход для написания тестовых примеров в Go, так как он позволяет легко добавлять новые записи в таблицу и избегает дублирования проверок и вывода сообщений об ошибках;- Тесты в Go
не зависят друг от друга
; использование методовt.Error
илиt.Errorf
не приводит кпанике
илиостановке выполнения теста
, что позволяет узнать о нескольких сбоях при одном запуске теста; - В случае, если необходимо остановить тестирование после определенной ошибки, нужно использовать методы
t.Fatal
илиt.Fatalf
, но вызывать их нужно только из той же горутины, что и функция тестирования; - Формат сообщений об ошибках в тестах должен быть понятным и информативным, включая информацию о выполняемой операции, фактический результат и ожидаемый результат; такой подход сэкономит время при диагностике ошибок и поможет быстрее исправить проблемы;
Рандомизированное тестирование
используется для проверки функций на более широком спектре входных данных, создавая их случайным образом;- Для проверки результатов рандомизированного тестирования можно использовать две стратегии: написать альтернативную реализацию функции с более простым алгоритмом или создавать входные значения по определенному шаблону, для которого известен ожидаемый результат;
- Рандомизированные тесты
недетерминированны
, поэтому нужно записывать достаточно информации для воспроизведения сбоев, например, инициализирующее значение генератора псевдослучайных чисел; - Использование текущего времени в качестве источника случайности обеспечивает тестирование с новыми наборами входных данных при каждом запуске, что особенно полезно при автоматизированном выполнении тестов;
- Рандомизированное тестирование может осуществляться с помощью функций, генерирующих случайные входные
данные, и обработки результатов с использованием стандартного пакета "
testing
"; - Использование рандомизированного тестирования позволяет охватить больше потенциальных случаев, на которых функция может работать некорректно, улучшая таким образом надежность кода и уменьшая вероятность ошибок в будущем.
- Пакет с именем
main
может быть импортирован как библиотека, что позволяет использовать его функции для тестирования; - Разделение программы на две функции, одна из которых выполняет основную работу, а другая обрабатывает входные данные, облегчает тестирование и делает программу более модульной;
- Добавление параметров в функцию может уменьшить зависимость от глобальных переменных и упростить тестирование;
- Использовать
io.Writer
для записи результата функции позволяет подставить другую реализацию во время тестирования, упрощая проверку результатов работы функций; - Тестовый код находится в том же пакете, что и основной рабочий код, что позволяет использовать функции и переменные пакета для тестирования;
- Создание таблицы с тестовыми данными упрощает добавление новых тестовых примеров и улучшает структуру тестового кода;
- Ошибка в тесте должна описывать неудачную операцию, фактическое и ожидаемое поведение, что облегчает понимание причины проблемы;
- Тестируемый код не должен вызывать функции, такие как
log.Fatal
илиos.Exit
, чтобы избежать непредвиденного завершения тестирования; сообщать об ожидаемых ошибках можно, возвращая ненулевое значение ошибки; - В случае паники тест-драйвер выполнит восстановление, и тест будет считаться не пройденным.
- Тестирование черного ящика подразумевает, что доступна только информация о предоставляемом API и документации, а внутренние компоненты пакета закрыты от взгляда (непрозрачны). Этот подход позволяет тестировать пакеты без знания их внутренней структуры, что делает его более универсальным и применимым к различным пакетам;
- Тестирование белого ящика - подход к тестированию, при котором тест имеет привилегированный доступ к внутренним компонентам и структурам данных пакета;
- Тестирование белого ящика дополняет тестирование черного ящика, обеспечивая более детальное покрытие сложных частей реализации;
- В Go тесты белого ящика могут проверять инварианты типов данных пакета и иметь доступ к неэкспортируемым функциям и переменным;
- Можно заменять части рабочего кода на легко тестируемые "поддельные"
реализации (
var notifyUser = func(username string, msg string) {/*...*/}
), которые проще настраивать, более предсказуемы, надежны и изучаемы; - В Go для временного сохранения и восстановления глобальных переменных можно использовать
defer
; - Использование глобальных переменных безопасно при тестировании, так как
go test
обычно не запускает несколько тестов одновременно; - Тестирование белого ящика в Go позволяет лучше понять и контролировать поведение внутренних компонентов программы и улучшить степень покрытия тестами различных случаев использования.
- Внешние тестовые пакеты позволяют обойти циклические импорты и проводить тесты между пакетами разного уровня;
это
полезно для
интеграционных тестов
, которые проверяют взаимодействие между компонентами; Внешние тестовые пакеты
объявляются в отдельном файле с суффиксом_test
в имени пакета, что позволяетgo test
идентифицировать этот пакет для тестирования;- Можно использовать инструмент
go list
для определения типов файлов Go в каталоге пакета, например, объявления рабочего кода и внешних тестов; - В некоторых случаях
внешним тестовым пакетам
может потребоваться привилегированный доступ к внутреннему представлению тестируемого пакета; для этого нужно использовать файл с суффиксом_test.go
внутри тестируемого пакета, что позволяет "через черный ход" предоставить доступ к некоторым внутренним функциям; - Файл
export_test.go
может использоваться для экспорта некоторых функций и переменных, доступных только для внешних тестов, тем самым обеспечивая согласованность поведения внутри и вне пакета; - Использование внешних тестовых пакетов делает код проекта на языке Golang более модульным, упрощает процесс тестирования и повышает уровень контроля над доступом к внутренним объектам и функциям.
- Хороший тест должен предоставлять четкое и сжатое описание симптомов проблемы и, возможно, информацию о контексте, в котором произошел сбой; это помогает разработчикам быстрее идентифицировать и исправить ошибки;
- Вместо использования абстрактных вспомогательных функций в тестах, стоит начать с реализации конкретного требуемого поведения и добавлять функции для упрощения кода и избавления от повторений только после этого;
- Хороший тест должен продолжать выполняться, даже если проверка указывает на наличие сбоя, чтобы предоставить разработчикам общую картину сбоев и помочь выявлению проблемы;
- Табличный подход к тестированию, при котором тест выполняется в цикле с разными входными данными, может быть полезен для проверки функций с множеством вариантов;
- При написании тестов на языке Go нужно уделять особое внимание предоставлению информативных сообщений об ошибках, которые помогут разработчикам быстрее и точнее идентифицировать и исправить проблемы.
Хрупкие тесты
— это тесты, которые дают ложные сбои при внесении корректных изменений в программу.- Эти тесты часто называют
детекторами изменений
илитестами статус-кво
, которые сбоят при почти любом изменении рабочего кода. - Время, затраченное на работу с такими тестами, может нивелировать все преимущества, которые они предоставляют.
Хрупкие тесты
возникают из-за сложных входных или выходных данных тестируемой функции.- По мере развития программы части выходных данных, скорее всего, будут изменяться, что может вызвать ложные срабатывания тестов. Это касается и сложных входных данных, которые могут перестать быть корректными.
- Чтобы избежать
хрупких тестов
, следует проверять только нужные свойства и быть избирательным в проверках. - Нужно тестировать более простые и стабильные интерфейсы программы, а не внутренние функции.
- Большие функции могут быть полезными для извлечения из сложных выводов тестируемой функции самой сути вывода и более точной проверки.
// TestCurrentTime проверяет точное совпадение времени, что не является
// практичным, так как время изменяется каждую секунду. Этот тест будет давать
// ложные сбои при каждом запуске.
func TestCurrentTime(t *testing.T) {
// Хрупкий тест: проверяет точное совпадение времени
expected := "2022-01-01 15:04:05"
result := currentTime()
if result != expected {
t.Fatalf("Expected %s but got %s", expected, result)
}
}
// TestCurrentTimeGood проверяет только формат времени, не требуя точного
// совпадения. Это делает его менее хрупким и надежным при внесении изменений
// или при повторных запусках.
func TestCurrentTimeGood(t *testing.T) {
// Нормальный тест: проверяет только формат времени
result := currentTime()
_, err := time.Parse("2006-01-02 15:04:05", result)
if err != nil {
t.Fatalf("Time format is incorrect: %s", err)
}
}
// currentTime() возвращает текущее время в формате строки.
func currentTime() string {
return fmt.Sprintf(time.Now().Format("2006-01-02 15:04:05"))
}
- Тестирование никогда не является полным и может только увеличивать уверенность в работоспособности кода;
Охват теста (покрытие)
описывает степень исследования тестируемого кода тестами, но не может быть выражен в строгих количественных значениях;Эвристика охвата инструкций
– наиболее распространенный способ оценки охвата тестов, показывает долю инструкций, выполняемых во время тестирования;- Инструмент Go
cover
встроен вgo test
и используется для измерения охвата инструкций; - Для сбора данных об охвате нужно использовать флаг
-coverprofile
при запускеgo test
, что позволяет получить информацию о доли выполненных инструкций и создает журнал данных; - Использование флага
-covermode=count
увеличивает счетчик выполнения инструкций, позволяя определить, какие блоки выполняются чаще, а какие - реже; - 100% охват инструкций не всегда возможен или практичен, так как некоторые инструкции могут быть недостижимы или обрабатывать редкие ошибки;
- Тестирование является прагматичной деятельностью, связанной с компромиссом между стоимостью написания тестов и стоимостью возможных сбоев;
- Инструменты для работы с охватом помогают определить слабые места и направить усилия разработчика в нужном направлении, однако разработка качественных тестов требует тщательных размышлений.
- Функции производительности в Go используются для измерения производительности программы на разных входных данных;
- Префикс
Benchmark
и параметр*testing.B
используются для создания функции производительности; - Поле
N
в*testing.B
указывает количество итераций, которое будет выполнена измеряемая операция; - Измерить производительность можно с помощью команды
go test -bench='.'
, где регулярное выражение после флага-bench
определяет, какие функцииBenchmark
будут запущены; - Настройка
GOMAXPROCS
влияет на параллельные тесты производительности; - При разработке быстрой программы, оптимизация часто сводится к минимизации выделения памяти;
- Флаг
-benchmem
используется для отображения статистики распределений памяти;
- Преждевременная оптимизация является корнем всех зол: программисты тратят слишком много времени на оптимизацию некритических частей кода, что вредит отладке и поддержке программ.
- Критический код следует оптимизировать только после его обнаружения, а не априорно, поскольку интуитивные оценки часто оказываются неверными;
Профилирование
- наилучший способ определения критического кода: это автоматизированный подход к измерению производительности, основанный на сборе статистики событий во время выполнения программы и анализе полученных данных;- Go поддерживает множество видов профилирования, включая
профиль процессора
,профиль памяти
ипрофиль блокировок
, позволяя определять различные аспекты производительности кода; Профиль процессора
идентифицирует функции, запрашивающие наибольшее процессорное время, записывая события припрерываниях
, вызванных операционной системой;Профиль памяти
определяет операции, ответственные за выделение наибольшего количества памяти, и опирается на события, записываемые для каждых512 Кбайт
выделенной памяти;Профиль блокировок
позволяет выявлять операции, вызывающие самые длительные блокировки горутин, такие, каксистемные вызовы
,операции с каналами
изахват блокировок
;- Для сбора информации профилей в Go используется инструмент
go test
с соответствующими флагами, однако следует избегать использования нескольких типов профилирования одновременно, так как это может исказить результаты; - Использование профилирования в Go позволяет оптимизировать критические части кода, улучшая производительность и качество разработанных программ;
- Профилирование полезно для длительно работающих приложений и может быть включено в программе с помощью
API runtime
; - Инструмент
pprof
используется для анализа собранной информации о профилировании, он является стандартной частью дистрибутива Go и вызывается с помощью командыgo tool pprof
; - Для анализа данных с помощью
pprof
требуются два аргумента:выполняемый файл
, который создал профиль, ижурнал профиля
; - Имена функций не включены в журнал профилирования для экономии памяти, вместо этого функции идентифицируются по адресам, поэтому выполнимый файл требуется для корректного анализа данных;
- Во время профилирования,
go test
сохраняет выполняемый файл какfоо.test
, гдеfоо
— имя тестируемого пакета; - Рекомендуется профилировать конкретные функции, которые репрезентативны для нагрузки рабочей программы, используя флаг
-run=NONE
для отключения сравнительного анализа тестовых случаев; - Флаг
-text
отображает текстовую таблицу профилирования с отсортированными функциями в порядке их "горячести", таким образом, упрощая выявление проблем производительности; - Флаг
-nodecount=10
ограничивает вывод результатов профилирования до 10 строк для упрощения анализа; - Для более тонкого анализа проблем могут использоваться графические выводы
pprof
, требующие установки пакетаGraphViz
и использования флага-web
, который создает аннотированный граф функций программы.
Функции-примеры
в Go называютсяExample
и не имеют параметров и результатов; их основная роль - служить документацией для библиотечных функций и продемонстрировать взаимодействие между несколькими типами и функциями API;- Примеры функций облегчают понимание работы функций, так как они являются реальным кодом Go, проверяемым во время компиляции, что обеспечивает их актуальность и предотвращает устаревание;
- Используя суффикс
Example
, веб-сервер документацииgodoc
связывает функции-примеры с функцией или пакетом, который они иллюстрируют, облегчая поиск и понимание соответствующей информации; Функции-примеры
являются выполнимыми тестами, запускаемыми командойgo test
, которая проверяет правильность вывода стандартного потока описанных в комментарии// Output:
.