Skip to content

Latest commit

 

History

History
3619 lines (3175 loc) · 394 KB

Summary.md

File metadata and controls

3619 lines (3175 loc) · 394 KB

Выводы к разделам



  • 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 композиция и наследование имеют свои отличия:

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

  • 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, которая отправляет запросы на три сервера и возвращает результат самого быстрого первого ответа, игнорируя остальные медленные результаты;

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

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

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

  • Буферизация каналов может повышать производительность программы, если операции, выполняемые разными горутинами, требуют разного времени на выполнение. Буферы позволяют сглаживать временные различия и обеспечивают более плавное выполнение программы;

  • Буферизация каналов может быть неэффективной, если одна стадия выполнения работает быстрее другой, и буфер большую часть времени либо заполняется, либо остается пустым;

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

  • Примеры:

    1. Небуферизованный канал:
    ch := make(chan int)
    1. Буферизованный канал:
     ch := make(chan int, 5)
    1. Отправка и получение данных через канал в разных горутинах:
    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:
    1. Не записывать переменную: инициализировать все необходимые записи перед созданием дополнительных горутин и не изменять их впоследствии; это позволяет обеспечить безопасность в параллельном выполнении кода и упрощает синхронизацию;
    2. Ограничить доступ к переменной одной горутиной: другие горутины должны использовать каналы для запросов получения или изменения значения ограниченной переменной; в этом случае используется мантра Go "не связывайтесь путем совместного использования памяти; совместно используйте память путем связи", что помогает избегать гонок данных и упрощает работу с переменными;
    3. Использовать взаимное исключение, позволяя многим горутинам обращаться к переменной, но только по одной за раз; это известно как механизм мьютексов (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:.