Go: Полное руководство по языку программирования
11 мая 2022 г.Привет, добро пожаловать на курс, и спасибо за изучение Go. Я надеюсь, что этот курс даст вам отличный опыт обучения!
Содержание
- Начиная
- Что такое Го?
- Зачем учиться Го?
- Установка и настройка
- Глава I
- Привет, мир
- Переменные и типы данных
- Форматирование строк
- Управление потоком
- Функции
- Модули
- Рабочие пространства
- Пакеты
- Полезные команды
- Строить
- Глава II
- Указатели
- Структуры
- Методы
- Массивы и срезы
- Карты
- Глава III
- Интерфейсы
- Ошибки
- Паника и восстановление
- Тестирование
- Дженерики
- Глава IV
- Параллелизм
- Горутины
- Каналы
- Выбирать
- Группы ожидания
- Мьютексы
- Приложение
- Следующие шаги
Что такое Go?
Go (также известный как Golang) — это язык программирования, разработанный в Google в 2007 году и открытый в 2009 году.
Он ориентирован на простоту, надежность и эффективность. Он был разработан, чтобы объединить эффективность, скорость и безопасность статически типизированного и скомпилированного языка с простотой программирования динамического языка, чтобы снова сделать программирование более увлекательным.
В каком-то смысле они хотели объединить лучшие стороны Python и C++, чтобы создавать надежные системы, способные использовать преимущества многоядерных процессоров.
Зачем изучать Go?
Прежде чем мы начнем этот курс, давайте поговорим о том, почему мы должны изучать Go
1. Легко учиться
Go довольно прост в освоении и имеет поддерживающее и активное сообщество.
А поскольку это многоцелевой язык, вы можете использовать его для таких вещей, как бэкенд-разработка, облачные вычисления и, в последнее время, наука о данных.
2. Быстро и надежно
Это делает его очень подходящим для распределенных систем. Такие проекты, как Kubernetes и Docker, написаны на Go.
3. Простой, но мощный
В Go всего 25 ключевых слов, поэтому его легко читать, писать и поддерживать. Сам язык лаконичен.
Но не обманывайтесь простотой, у Go есть несколько мощных функций, которые мы позже изучим в ходе курса.
4. Карьерные возможности
Go быстро развивается и внедряется компаниями любого размера. И вместе с этим появляются новые возможности высокооплачиваемой работы.
Я надеюсь, что это заставило вас заинтересоваться Go. Давайте начнем этот курс.
В этом уроке мы установим Go и настроим наш редактор кода.
Установка и настройка
Скачать
Мы можем установить Go из раздела загрузки.
Установка
Эти инструкции взяты с официального сайта.
macOS
- Откройте загруженный файл пакета и следуйте инструкциям по установке Go.
Пакет устанавливает дистрибутив Go в /usr/local/go
. Пакет должен поместить каталог /usr/local/go/bin
в вашу переменную окружения PATH. Вам может потребоваться перезапустить все открытые сеансы терминала, чтобы изменения вступили в силу.
- Убедитесь, что вы установили Go, открыв командную строку и введя следующую команду:
$ идти версия
- Убедитесь, что команда печатает установленную версию Go.
Линукс
- Удалите любую предыдущую установку Go, удалив папку
/usr/local/go
(если она существует), затем извлеките архив, который вы только что скачали, в/usr/local
, создав новое дерево Go в/usr/ местный/идти
:
$ rm -rf /usr/local/go && tar -C /usr/local -xzf go1.18.1.linux-amd64.tar.gz
(Вам может понадобиться запустить команду от имени пользователя root или через sudo)
Не распаковывайте архив в существующее дерево /usr/local/go
. Известно, что это приводит к неработающим установкам Go.
- Добавьте
/usr/local/go/bin
в переменную окружения PATH.
Вы можете сделать это, добавив следующую строку в ваш $HOME/.profile
или /etc/profile
(для общесистемной установки):
экспорт PATH=$PATH:/usr/local/go/bin
Примечание. Изменения, внесенные в файл профиля, могут не применяться до тех пор, пока вы в следующий раз не войдете в свой компьютер. Чтобы немедленно применить изменения, просто запустите команды оболочки напрямую или выполните их из профиля с помощью такой команды, как source $HOME/.profile
.
- Убедитесь, что вы установили Go, открыв командную строку и введя следующую команду:
$ идти версия
- Убедитесь, что команда печатает установленную версию Go.
Окна
- Откройте загруженный файл MSI и следуйте инструкциям по установке Go.
По умолчанию установщик установит Go to Program Files или Program Files (x86).
Вы можете изменить местоположение по мере необходимости.
После установки вам нужно будет закрыть и снова открыть все открытые командные строки, чтобы изменения в среде, сделанные установщиком, отражались в командной строке.
- Убедитесь, что вы установили Go.
- В Windows откройте меню «Пуск».
- В поле поиска меню введите cmd, затем нажмите клавишу Enter.
- В появившемся окне командной строки введите следующую команду:
$ идти версия
- Убедитесь, что команда печатает установленную версию Go.
Код VS
В этом курсе я буду использовать [VS Code] (https://code.visualstudio.com), и вы можете скачать его [здесь] (https://code.visualstudio.com/download).
Вы можете использовать любой другой редактор кода, который вам нравится.
Расширение
Обязательно установите [расширение Go] (https://code.visualstudio.com/docs/languages/go), которое упрощает работу с Go в VS Code.
Это все, что касается установки и настройки Go, давайте начнем курс и напишем наш первый привет, мир!
Привет, мир
Давайте напишем нашу первую программу hello world, мы можем начать с инициализации модуля. Для этого мы можем использовать команду go mod.
``` ударить
Пример инициализации $go mod
Но подождите... что такое "модуль"? Не волнуйтесь, мы обсудим это в ближайшее время! Но пока предположим, что модуль представляет собой набор пакетов Go.
Двигаясь дальше, давайте теперь создадим файл main.go
и напишем программу, которая просто печатает hello world.
```иди
основной пакет
импортировать "фмт"
основная функция () {
fmt.Println("Привет, мир!")
Если вам интересно, fmt
является частью стандартной библиотеки Go, которая представляет собой набор основных пакетов, предоставляемых языком.
Теперь давайте быстро разберем, что мы здесь сделали, или, точнее, структуру программы go.
Во-первых, мы определили пакет, такой как main
```иди
основной пакет
Затем у нас есть импорт.
```иди
импортировать "фмт"
И наконец, что не менее важно, это наша функция main, которая действует как точка входа для нашего приложения, как и в других языках, таких как C, Java или C#.
```иди
основная функция () {
Помните, цель здесь состоит в том, чтобы сохранить мысленную пометку, а позже в курсе мы подробно узнаем о «функциях», «импорте» и других вещах!
Наконец, чтобы запустить наш код, мы можем просто использовать команду go run.
``` ударить
$ запустить main.go
Привет, мир!
Поздравляем, вы только что написали свою первую программу на Go!
С этим давайте перейдем к следующей теме.
Переменные и типы данных
В этом уроке мы узнаем о переменных. Мы также узнаем о различных типах данных, которые нам предоставляет Go.
Переменные
Начнем с объявления переменной.
Это также известно как объявление без инициализации:
```иди
var foo строка
Декларация с инициализацией:
```иди
var foo string = "Go - это круто"
Несколько объявлений:
```иди
var foo, bar string = "Привет", "Мир"
// ИЛИ
вар (
строка foo = "Привет"
строка строки = "Мир"
Тип опущен, но будет выведен:
```иди
var foo = "Какой у меня тип?"
Сокращенно, здесь мы опускаем ключевое слово var
, а тип всегда подразумевается. Вот как мы будем видеть переменные, объявляемые большую часть времени. Мы также используем :=
для объявления плюс присваивание:
```иди
foo := "В сокращении!"
Примечание: сокращение работает только внутри тела функции
.
Константы
Мы также можем объявить константы с помощью ключевого слова const
. Которые, как следует из названия, являются фиксированными значениями, которые нельзя переназначить.
```иди
const константа = "Это константа"
Типы данных
Идеальный! Теперь давайте рассмотрим некоторые основные типы данных, доступные в Go. Начиная со строки.
Нить
В Go строка — это последовательность байтов.
Они объявляются с помощью двойных кавычек или обратных кавычек, которые могут занимать несколько строк.
```иди
var name string = "Меня зовут Go"
var bio string = `Я статически типизирован.
Я был разработан в Google».
Бул
Далее идет bool
, который используется для хранения логических значений. Он может иметь два возможных значения — «истина» или «ложь».
```иди
логическое значение переменной = ложь
var isItTrue bool = истина
Операторы
Мы можем использовать следующие операторы для логических типов:
| | | |
| Логический | && | ! |
| Равенство | == | != |
Числовые типы
Теперь поговорим о числовых типах, начиная с
Целые числа со знаком и без знака
В Go есть несколько встроенных целочисленных типов разного размера для хранения целых чисел со знаком и без знака.
Размер универсального типа int
и uint
зависит от платформы. Это означает, что он имеет ширину 32 бита в 32-битной системе и 64 бита в 64-битной системе.
```иди
var i int = 404 // зависит от платформы
var i8 int8 = 127 // от -128 до 127
var i16 int16 = 32767 // от -2^15 до 2^15 - 1
var i32 int32 = -2147483647 // от -2^31 до 2^31 - 1
var i64 int64 = 9223372036854775807 // от -2^63 до 2^63 - 1
Подобно целым числам со знаком, у нас есть целые числа без знака.
```иди
var ui uint = 404 // зависит от платформы
var ui8 uint8 = 255 // от 0 до 255
var ui16 uint16 = 65535 // от 0 до 2^16
var ui32 uint32 = 2147483647 // от 0 до 2^32
var ui64 uint64 = 9223372036854775807 // от 0 до 2^64
var uiptr uintptr // Целочисленное представление адреса памяти
Если вы заметили, существует также беззнаковый целочисленный тип указателя uintptr, который представляет собой целочисленное представление адреса памяти. Не рекомендуется использовать это, так что нам не о чем беспокоиться.
Так какой из них мы должны использовать?
Рекомендуется, чтобы всякий раз, когда нам нужно целочисленное значение, мы просто использовали int
, если у нас нет особой причины использовать размерный или беззнаковый целочисленный тип.
Целочисленные псевдонимы
Далее давайте обсудим целочисленные типы псевдонимов.
Байт и руна
В Golang есть два дополнительных целочисленных типа, называемых byte и rune, которые являются псевдонимами для типов данных uint8 и int32 соответственно.
```иди
тип байт = uint8
тип руна = int32
A руна
представляет кодовую точку Unicode.
```иди
вар б байт = 'а'
вар р руна = '🍕'
Плавающая точка
Далее у нас есть типы с плавающей запятой, которые используются для хранения чисел с десятичной составляющей.
В Go есть два типа чисел с плавающей запятой: float32 и float64. Оба типа соответствуют стандарту IEEE-754.
Тип по умолчанию для значений с плавающей запятой — float64.
```иди
var f32 float32 = 1,7812 // 32-битный IEEE-754
var f64 float64 = 3,1415 // IEEE-754 64-бит
Операторы
Go предоставляет несколько операторов для выполнения операций над числовыми типами.
Сложный
В Go есть 2 сложных типа. комплекс128, где и действительная, и мнимая части являются плавающими64, и комплекс64, где действительная и мнимая части являются плавающими32.
Мы можем определять комплексные числа либо с помощью встроенной комплексной функции, либо как литералы.
```иди
var c1 комплекс128 = комплекс (10, 1)
var c2 комплекс64 = 12 + 4i
Нулевые значения
Теперь давайте обсудим нулевые значения. Итак, в Go любой переменной, объявленной без явного начального значения, присваивается нулевое значение. Например, давайте объявим некоторые переменные и увидим:
```иди
переменная я int
переменная с плавающей запятой64
переменная b логическое значение
var s строка
fmt.Printf("%v %v %v %q
", i, f, b, s)
``` ударить
$ запустить main.go
0 0 ложь ""
Итак, как мы видим, int
и float
назначаются как 0, bool
как false, а string
как пустая строка. Это сильно отличается от того, как это делают другие языки. Например, большинство языков инициализируют неприсвоенные переменные как null или undefined.
Это здорово, но что это за символы процента в нашей функции Printf
? Как вы уже догадались, они используются для форматирования и о них мы узнаем в следующем видео.
Преобразование типов
Двигаемся дальше, теперь, когда мы увидели, как работают типы данных, давайте посмотрим, как выполнить преобразование типов.
```иди
я := 42
f := float64(i)
u := uint(f)
fmt.Printf("%T %T", f, u)
``` ударить
$ запустить main.go
float64 uint
И как мы видим, он печатает тип как float64
и uint
.
Обратите внимание, что это отличается от синтаксического анализа.
Типы псевдонимов
Псевдонимы были введены в Go 1.9.
Они позволяют разработчикам предоставлять альтернативное имя для существующего типа и использовать его взаимозаменяемо с базовым типом.
```иди
основной пакет
импортировать "фмт"
введите MyAlias = строка
основная функция () {
var str MyAlias = "Я псевдоним"
fmt.Printf("%T - %s", str, str) // Вывод: я псевдоним
Определенные типы
Наконец, мы определили типы, которые, в отличие от псевдонимов, не используют знак равенства.
```иди
основной пакет
импортировать "фмт"
введите строку MyDefined
основная функция () {
var str MyDefined = "Я определен"
fmt.Printf("%T - %s", str, str) // Вывод: main.MyDefined - я определен
Но подождите... Какая разница?
Таким образом, определенные типы делают больше, чем просто дают имя типу.
Сначала он определяет новый именованный тип с базовым типом. Однако этот определенный тип отличается от любого другого типа, включая тип подчеркивания.
Следовательно, его нельзя использовать взаимозаменяемо с базовым типом, таким как псевдонимы.
Сначала это немного сбивает с толку, надеюсь, этот пример прояснит ситуацию.
```иди
основной пакет
импортировать "фмт"
введите MyAlias = строка
введите строку MyDefined
основная функция () {
var псевдоним MyAlias
переменная дефиниция MyDefined
// ✅ Работает
строка var copy1 = псевдоним
// ❌ нельзя использовать str (переменную типа MyDefined) в качестве строкового значения в переменной
строка var copy2 = защита
fmt.Println (копия1, копия2)
Как мы видим, мы не можем использовать определенный тип взаимозаменяемо с базовым типом, в отличие от псевдонимов.
Что ж, это почти все для переменных и типов данных в Go.
Форматирование строки
Мы узнаем о форматировании строк или о шаблонах.
Пакет fmt
содержит множество функций. Итак, чтобы сэкономить время, мы обсудим наиболее часто используемые функции... начнем с fmt.Print
внутри нашей основной функции.
```иди
fmt.Print("Что", "есть", "ваше", "имя?")
fmt.Print("Мой", "имя", "есть", "голанг")
``` ударить
$ запустить main.go
Как тебя зовут? Меня зовут голанг
Как мы видим, Print
ничего не форматирует, а просто берет строку и печатает ее.
Затем у нас есть Println
, который аналогичен Print
, но добавляет новую строку в конце, а также вставляет пробел между аргументами.
```иди
fmt.Println("Что", "такое", "ваше", "имя?")
fmt.Println("Мой", "имя", "есть", "голанг")
``` ударить
$ запустить main.go
Как вас зовут?
Меня зовут Голанг
Это намного лучше!
Затем у нас есть Printf
, также известный как «Форматировщик печати», который позволяет нам форматировать числа, строки, логические значения и многое другое.
Давайте посмотрим на пример:
```иди
имя := "голанг"
fmt.Println("Как тебя зовут?")
fmt.Printf("Меня зовут %s", имя)
``` ударить
$ запустить main.go
Как вас зовут?
Меня зовут Голанг
Как мы видим, %s
был заменен нашей переменной name
.
Но вопрос в том, что такое %s
и что это значит?
Итак, они называются глаголами аннотации, и они сообщают функции о том, как форматировать аргументы. С их помощью мы можем контролировать такие вещи, как ширина, тип и точность, и их много. Вот [шпаргалка] (https://pkg.go.dev/fmt).
Теперь давайте быстро рассмотрим еще несколько примеров. Здесь мы попробуем вычислить процент и вывести его в консоль:
```иди
процент := (3/5) * 100
fmt.Printf("%f", проценты)
``` ударить
$ запустить main.go
58.181818
Допустим, мы хотим просто 58,18
, что соответствует точности в 2 пункта, мы также можем сделать это, используя .2f
Кроме того, чтобы добавить фактический знак процента, нам нужно его экранировать.
```иди
процент := (3/5) * 100
fmt.Printf("%.2f %%", проценты)
``` ударить
$ запустить main.go
58,18 %
Это подводит нас к «Sprint», «Sprintln» и «Sprintf». В основном это то же самое, что и функции печати, с той лишь разницей, что они возвращают строку, а не печатают ее.
Давайте рассмотрим пример:
```иди
s := fmt.Sprintf("hex:%x bin:%b", 10,10)
fmt.Println(s)
``` ударить
$ запустить main.go
шестнадцатеричный: корзина: 1010
Итак, как мы видим, Sprintf
форматирует наше целое число как шестнадцатеричное или двоичное и возвращает его как строку.
Наконец, у нас есть многострочные строковые литералы, которые можно использовать следующим образом:
```иди
сообщение := `
Привет из
многострочный
fmt.Println(msg)
Большой! Но это только вершина айсберга... так что обязательно ознакомьтесь с документацией по пакету fmt
.
Для тех, кто работает с C/C++, это должно показаться естественным; но если вы исходите, скажем, из Python или JavaScript, поначалу это может показаться немного странным. Но он очень мощный, и вы увидите, что эта функциональность используется довольно широко.
Управление потоком
Давайте поговорим об управлении потоком, начнем с if/else.
если еще
Это работает почти так же, как вы ожидаете, но выражение не нужно заключать в круглые скобки ()
```иди
основная функция () {
х := 10
если х > 5 {
fmt.Println("x равно gt 5")
} иначе, если х > 10 {
fmt.Println("x больше 10")
} еще {
fmt.Println("в противном случае")
``` ударить
$ запустить main.go
х это gt 5
Компактно, если
Мы также можем сжать наши операторы if:
```иди
основная функция () {
если х := 10; х > 5 {
fmt.Println("x равно gt 5")
Примечание: этот шаблон довольно распространен.
выключатель
Затем у нас есть оператор switch
, который часто является более коротким способом написания условной логики.
В Go case switch запускает только первый case, значение которого равно выражению условия, а не все последующие case. Следовательно, в отличие от других языков, оператор break автоматически добавляется в конце каждого случая.
Это означает, что он оценивает дела сверху вниз, останавливаясь, когда дело удается. Давайте рассмотрим пример:
```иди
основная функция () {
день := "понедельник"
день переключения {
случай "понедельник":
fmt.Println("Время работать!")
случай "пятница":
fmt.Println("давайте устроим вечеринку")
По умолчанию:
fmt.Println("просмотреть мемы")
``` ударить
$ запустить main.go
время работать!
Switch также поддерживает сокращенное объявление, подобное этому:
```иди
день переключения := "понедельник"; день {
случай "понедельник":
fmt.Println("Время работать!")
случай "пятница":
fmt.Println("давайте устроим вечеринку")
По умолчанию:
fmt.Println("просмотреть мемы")
Мы также можем использовать ключевое слово fallthrough для передачи управления следующему кейсу, даже если текущий кейс совпал.
```иди
день переключения := "понедельник"; день {
случай "понедельник":
fmt.Println("Время работать!")
Проваливаться
случай "пятница":
fmt.Println("давайте устроим вечеринку")
По умолчанию:
fmt.Println("просмотреть мемы")
И если мы запустим это, мы увидим, что после первого совпадения case оператор switch переходит к следующему case из-за ключевого слова fallthrough
:
``` ударить
$ запустить main.go
время работать!
Давайте веселиться
Мы также можем использовать его без каких-либо условий, что то же самое, что и «switch true».
```иди
х := 10
выключатель {
случай х > 5:
fmt.Println("x больше")
По умолчанию:
fmt.Println("x не больше")
Циклы
Теперь давайте обратим внимание на петли.
Итак, в Go у нас есть только один тип цикла — цикл for.
Но он невероятно универсален. То же, что и оператор if, для цикла, не нуждается в круглых скобках ()
в отличие от других языков.
Начнем с основного цикла for.
```иди
основная функция () {
для я := 0; я < 10; я++ {
fmt.Println(i)
Базовый цикл for состоит из трех компонентов, разделенных точкой с запятой:
- оператор инициализации: который выполняется перед первой итерацией
- условное выражение: вычисляется перед каждой итерацией
- оператор post: который выполняется в конце каждой итерации
Перерыв и продолжение
Как и ожидалось, Go также поддерживает операторы break и continue для управления циклом. Давайте попробуем быстрый пример:
```иди
основная функция () {
для я := 0; я < 10; я++ {
если я < 2 {
Продолжать
fmt.Println(i)
если я > 5 {
перемена
fmt.Println("Мы прорвались!")
Таким образом, оператор «continue» используется, когда мы хотим пропустить оставшуюся часть цикла, а оператор «break» используется, когда мы хотим выйти из цикла.
Кроме того, операторы Init и post являются необязательными, поэтому мы можем заставить наш цикл for вести себя как цикл while.
```иди
основная функция () {
я: = 0
для ;i < 10; {
я += 1
Примечание: мы также можем удалить дополнительные точки с запятой, чтобы сделать его немного чище.
Вечный цикл
Наконец, если мы опустим условие цикла, он зациклится навсегда, поэтому бесконечный цикл можно выразить компактно. Это также известно как бесконечный цикл.
```иди
основная функция () {
за {
// делаем что-то здесь
Ну, это почти все, что касается управления потоком!
Функции
Теперь мы обсудим, как мы работаем с функциями в Go. Итак, начнем с простого объявления функции.
Простое объявление
```иди
функция myFunction() {}
И мы можем вызвать или выполнить его следующим образом:
```иди
мояФункция()
Передадим ему некоторые параметры:
```иди
основная функция () {
МояФункция("Привет")
func myFunction (строка p1) {
fmt.Printtln(p1)
```jsx
$ запустить main.go
Как мы видим, он печатает наше сообщение.
Мы также можем сделать сокращенное объявление, если последовательные параметры имеют один и тот же тип. Например:
```иди
func myNextFunction (строка p1, p2) {}
Возврат значения
Теперь давайте также вернем значение:
```иди
основная функция () {
s := myFunction("Привет")
fmt.Println(s)
func myFunction (строка p1) строка {
msg := fmt.Sprintf("функция%s", p1)
вернуть сообщение
Несколько возвратов
Зачем возвращать по одному значению за раз, когда можно сделать больше? Go также поддерживает несколько возвратов!
```иди
основная функция () {
с, я := myFunction("Привет")
fmt.Println(s, i)
func myFunction (строка p1) (строка, целое число) {
msg := fmt.Sprintf("функция%s", p1)
вернуть сообщение, 10
Именованные возвраты
Еще одна интересная функция — [именованные возвраты] (https://go.dev/tour/basics/7), где возвращаемым значениям можно присваивать имена и обрабатывать их как собственные переменные:
```иди
func myFunction (строка p1) (строка s, i int) {
s = fmt.Sprintf("функция%s", p1)
я = 10
возврат
Обратите внимание, как мы добавили оператор return без каких-либо аргументов, он также известен как голый возврат.
Я скажу, что, хотя эта функция интересна, пожалуйста, используйте ее с осторожностью, так как это может снизить читабельность больших функций.
Функции как значения
Далее, давайте поговорим о функциях как о значениях, в Go функции первого класса, и мы можем использовать их как значения. Итак, давайте очистим нашу функцию и попробуем!
```иди
функция myFunction () {
фн := функция() {
fmt.Println("внутри фн")
фн()
Мы также можем упростить это, сделав fn
анонимной функцией.
```иди
функция myFunction () {
функция () {
fmt.Println("внутри фн")
Обратите внимание, как мы выполняем его, используя скобкиesis в конце.
Замыкания
Зачем останавливаться на достигнутом? Давайте также вернем функцию и, следовательно, создадим нечто, называемое замыканием. Простым определением может быть то, что замыкание — это значение функции, которое ссылается на переменные вне своего тела.
Замыкания имеют лексическую область видимости, что означает, что функции могут получить доступ к значениям в области видимости при определении функции.
```иди
func myFunction() func(int) int {
сумма := 0
return func(v int) int {
сумма += v
возвращаемая сумма
```иди
добавить := myFunction()
добавить(5)
fmt.Println (добавить (10))
Как мы видим, мы получаем результат 15, так как переменная sum
привязана к функции. Это очень мощная концепция, и ее обязательно нужно знать.
Функции с переменным числом аргументов
Теперь давайте рассмотрим функции с переменным числом аргументов, то есть функции, которые могут принимать ноль или несколько аргументов, используя оператор многоточия ...
.
Примером здесь может быть функция, которая может добавить кучу значений:
```иди
основная функция () {
сумма := добавить (1, 2, 3, 5)
fmt.Println(сумма)
func add (значения ... int) int {
сумма := 0
для _, v := значения диапазона {
сумма += v
возвращаемая сумма
Довольно круто, да? Кроме того, не беспокойтесь о ключевом слове range
, мы обсудим его позже в курсе.
Забавный факт*: fmt.Println
— это функция с переменным числом переменных, поэтому мы смогли передать ей несколько значений.*
отложить
Наконец, давайте обсудим ключевое слово defer
, которое позволяет нам отложить выполнение функции до тех пор, пока не вернется окружающая функция.
```иди
основная функция () {
отложить fmt.Println("Я закончил")
fmt.Println("Делаю какую-то работу...")
Можем ли мы использовать несколько функций отсрочки? Безусловно, это подводит нас к тому, что известно как стек отложений, давайте возьмем пример:
```иди
основная функция () {
отложить fmt.Println("Я закончил")
отложить fmt.Prinlnt("Вы?")
fmt.Println("Делаю какую-то работу...")
``` ударить
$ запустить main.go
Делаю какую-то работу...
Ты?
я закончил
Как мы видим, операторы отсрочки складываются и выполняются по принципу последним пришел — первым обслужен.
Таким образом, Defer невероятно полезен и обычно используется для очистки или обработки ошибок.
Функции также можно использовать с дженериками, но мы обсудим их позже в курсе.
Итак, это все о функциях в ходу!
Модули
Теперь мы узнаем о модулях.
Так что же такое модули?
Проще говоря, модуль представляет собой набор [пакетов Go] (https://go.dev/ref/spec#Packages), хранящихся в дереве файлов с файлом go.mod
в корне, при условии, что каталог * снаружи* $GOPATH/src
Модули Go были представлены в Go 1.11, что обеспечивает встроенную поддержку версий и модулей. Ранее нам требовался флаг GO111MODULE=on, чтобы включить функциональность модуля, когда он был экспериментальным. Но теперь, после Go 1.13, режим модулей используется по умолчанию для всей разработки.
Но подождите, что такое GOPATH
?
Что ж, GOPATH
— это переменная, которая определяет корень вашего рабочего пространства и содержит следующие папки:
- src: содержит исходный код Go, организованный в иерархию
- pkg: содержит скомпилированный код пакета
- bin: содержит скомпилированные бинарные и исполняемые файлы.
Как и ранее, давайте создадим новый модуль с помощью команды go mod init, которая создает новый модуль и инициализирует файл go.mod, описывающий его.
``` ударить
Пример инициализации $go mod
- Здесь важно отметить, что модуль Go также может соответствовать репозиторию Github, если вы планируете опубликовать этот модуль. Например:*
``` ударить
$ go mod init **github.com/example/repo
Теперь давайте рассмотрим файл go.mod
, который определяет путь к модулю модуля, а также путь импорта, используемый для корневого каталога, и его требования к зависимостям.
```иди
модуль <имя>
перейти <версия>
требовать (
И если мы хотим добавить новую зависимость, мы будем использовать команду «go install»:
``` ударить
$ установить github.com/rs/zerolog
Как мы видим, также был создан файл go.sum. Этот файл содержит ожидаемые хэши содержимого новых модулей.
Мы можем перечислить все зависимости с помощью команды go list следующим образом:
``` ударить
$ перейти список -m все
Если зависимость не используется, мы можем просто удалить ее с помощью команды go mod tidy.
``` ударить
$ иди мод аккуратно
Завершая обсуждение модулей, давайте также обсудим поставщиков.
Продажа — это создание вашей собственной копии сторонних пакетов, которые использует ваш проект. Эти копии традиционно размещаются внутри каждого проекта, а затем сохраняются в репозитории проекта.
Это можно сделать с помощью команды go mod vendor
.
Итак, давайте переустановим удаленный модуль с помощью go mod tidy
```иди
основной пакет
импортировать "github.com/rs/zerolog/log"
основная функция () {
log.Info().Msg("Привет")
``` ударить
$ иди мод аккуратно
go: поиск модуля для пакета github.com/rs/zerolog/log
go: нашел github.com/rs/zerolog/log в github.com/rs/zerolog v1.26.1
``` ударить
$ идти поставщик модов
├── go.mod
├── go.sum
├── иди работай
├── main.go
└── продавец
├── github.com
│ └── рс
│ └── зерлог
│ └── ...
└── modules.txt
Итак, это в значительной степени все для модулей.
Рабочие области
Далее мы узнаем о многомодульных рабочих пространствах, которые были представлены в Go 1.18.
Рабочие области позволяют нам работать с несколькими модулями одновременно без необходимости редактировать файлы go.mod
для каждого модуля. Каждый модуль в рабочей области рассматривается как корневой модуль при разрешении зависимостей.
Чтобы лучше понять это, давайте начнем с создания модуля hello
:
``` ударить
$ рабочие пространства mkdir && рабочие пространства cd
$ mkdir привет && cd привет
$ go mod init привет
В демонстрационных целях я добавлю простой main.go
и установлю пример пакета.
```иди
основной пакет
импорт (
"ФМТ"
"golang.org/x/example/stringutil"
основная функция () {
результат: = stringutil.Reverse («Привет, рабочая область»)
fmt.Println(результат)
``` ударить
$ перейти на golang.org/x/example
перейти: скачать golang.org/x/example v0.0.0-20220412213650-2e68773dfca0
перейти: добавлено golang.org/x/example v0.0.0-20220412213650-2e68773dfca0
И если мы запустим это, мы должны увидеть наш вывод в обратном порядке.
``` ударить
$ запустить main.go
ecapskrow olleh
Это здорово, но что, если мы хотим изменить модуль stringutil
, от которого зависит наш код?
До сих пор нам приходилось делать это с помощью директивы replace в файле go.mod, но теперь давайте посмотрим, как мы можем использовать здесь рабочие пространства.
Итак, давайте создадим наше рабочее пространство в каталоге workspace
:
``` ударить
$ иди работай инициализируй
Это создаст файл [go.work](http://go.work)
:
``` ударить
$ кошка идти.работа
перейти 1.18
Мы также добавим наш модуль «hello» в рабочую область.
``` ударить
$ иди работай используй ./привет
Это должно обновить файл [go.work](http://go.work)
со ссылкой на наш модуль hello
:
```иди
перейти 1.18
используйте ./привет
Теперь давайте загрузим и изменим пакет stringutil
и обновим реализацию функции Reverse
:
``` ударить
$ git клон https://go.googlesource.com/example
Клонирование в "пример"...
удаленный: всего 204 (дельта 39), повторно используется 204 (дельта 39)
Получение объектов: 100% (204/204), 467,53 КиБ | 363,00 КиБ/с, готово.
Разрешение дельт: 100% (39/39), выполнено.
пример/stringutil/reverse.go
```иди
func Reverse(s string) string {
return fmt.Sprintf("Я могу делать что угодно!! %s", s)
Наконец, давайте добавим пакет example
в нашу рабочую область:
``` ударить
$ идти работать использовать ./пример
$ кошка идти.работа
перейти 1.18
использовать (
./пример
./привет
Отлично, теперь, если мы запустим наш модуль «hello», мы заметим, что функция «Reverse» была изменена.
``` ударить
$ беги привет
Я могу сделать что угодно!! Привет рабочая область
Это очень недооцененная функция Go 1.18, но в определенных обстоятельствах она весьма полезна.
Итак, это почти все для рабочих пространств в Go.
Пакеты
Теперь поговорим о пакетах.
Так что же такое пакеты?
Пакет — это не что иное, как каталог, содержащий один или несколько исходных файлов Go или другие пакеты Go.
Это означает, что каждый исходный файл Go должен принадлежать пакету, а объявление пакета выполняется поверх каждого исходного файла следующим образом:
```иди
пакет <имя_пакета>
До сих пор мы делали все внутри package main
. По соглашению, исполняемые программы (я имею в виду программы с пакетом main
) называются Команды, другие просто называются Пакеты.*
Пакет main
также должен содержать функцию main()
, которая представляет собой специальную функцию, действующую как точка входа в исполняемую программу.
Давайте возьмем пример, создав наш собственный пакет «custom» и добавив в него некоторые исходные файлы, такие как «code.go».
```иди
пакет на заказ
Прежде чем двигаться дальше, мы должны поговорить об импорте и экспорте. Как и в других языках, в Go также есть концепция импорта и экспорта, но она очень элегантна.
По сути, любое значение (например, переменная или функция) может быть экспортировано и видно из других пакетов, если они были определены с идентификатором в верхнем регистре.
Давайте попробуем пример в нашем custom
пакете:
```иди
пакет на заказ
var value int = 10 // Не будет экспортироваться
var Value int = 20 // Будет экспортировано
Как мы видим, идентификаторы в нижнем регистре не будут экспортированы и будут закрыты для пакета, в котором они определены. В нашем случае это «пользовательский» пакет.
Это здорово, но как нам импортировать или получить к нему доступ? Ну, то же самое, что мы делали до сих пор неосознанно. Давайте перейдем к нашему файлу main.go
и импортируем наш custom
пакет.
Здесь мы можем обратиться к нему, используя «модуль», который мы инициализировали в нашем файле «go.mod» ранее:
```иди
---go.mod---
пример модуля
перейти 1.18
---main.go--
основной пакет
импортировать "пример/обычай"
основная функция () {
обычай.Значение
Обратите внимание, что имя пакета является последним именем пути импорта.
Мы также можем импортировать несколько пакетов, например:
```иди
основной пакет
импорт (
"ФМТ"
"пример/обычай"
основная функция () {
fmt.Println(Пользовательское.Значение)
Мы также можем использовать псевдоним для нашего импорта, чтобы избежать таких коллизий:
```иди
основной пакет
импорт (
"ФМТ"
abcd "пример/обычай"
основная функция () {
fmt.Println(abcd.Value)
Внешние зависимости
В Go мы не только ограничены работой с локальными пакетами, мы также можем устанавливать внешние пакеты с помощью команды «go install», как мы видели ранее.
Итак, давайте загрузим простой пакет логов github.com/rs/zerolog/log
``` ударить
$ установить github.com/rs/zerolog
```иди
основной пакет
импорт (
"github.com/rs/zerolog/журнал"
abcd "пример/обычай"
основная функция () {
log.Print(abcd.Value)
Кроме того, обязательно ознакомьтесь с документацией по устанавливаемым вами пакетам, которая обычно находится в файле readme проекта. go doc анализирует исходный код и создает документацию в формате HTML.
Ссылка на него обычно находится в файлах readme.
Наконец, я добавлю, что в Go нет особого соглашения «структура папок», поэтому всегда старайтесь организовывать свои пакеты простым и интуитивно понятным способом.
Итак, это в значительной степени все для пакетов!
Полезные команды
Во время обсуждения нашего модуля мы обсудили некоторые команды go, связанные с модулями go, а теперь давайте обсудим некоторые другие важные команды.
Начиная с go fmt
, который форматирует исходный код и применяется этим языком, чтобы мы могли сосредоточиться на том, как наш код должен работать, а не на том, как он должен выглядеть.
``` ударить
$ идти вперед
Поначалу это может показаться немного странным, особенно если вы, как и я, знакомы с javascript или python, но, честно говоря, довольно приятно не беспокоиться о правилах линтинга.
Затем у нас есть go vet
, который сообщает о возможных ошибках в наших пакетах.
Итак, если я продолжу и сделаю ошибку в синтаксисе, а затем запущу go vet
Он должен уведомлять меня об ошибках.
``` ударить
$ иди к ветеринару
Затем у нас есть go env
, который просто выводит всю информацию о среде go; мы узнаем о некоторых из этих переменных времени сборки в следующем уроке.
Наконец, у нас есть go doc
, который показывает документацию для пакета или символа; вот пример пакета формата:
``` ударить
$ go doc -src fmt Printf
Давайте воспользуемся командой go help
, чтобы посмотреть, какие другие команды доступны.
``` ударить
$ иди помоги
Как видим, имеем:
go fix
находит программы Go, которые используют старые API, и переписывает их, чтобы использовать новые.
go generate
обычно используется для генерации кода
go install
компилирует и устанавливает пакеты и зависимости
go clean
используется для очистки файлов, созданных компиляторами.
Некоторые другие очень важные команды — «go build» и «go test», но мы подробно узнаем о них позже в курсе.
Это практически все команды go, не стесняйтесь экспериментировать с ними!
Строить
Создание статических двоичных файлов — одна из лучших функций Go, которая позволяет нам эффективно поставлять наш код.
Мы можем сделать это очень легко, используя команду «go build»:
```иди
основной пакет
импортировать "фмт"
основная функция () {
fmt.Println("Я бинарный файл!")
``` ударить
$ иди строй
Это должно создать двоичный файл с именем нашего модуля. Например, здесь у нас есть пример
Мы также можем указать вывод:
``` ударить
$ go build -o приложение
Теперь, чтобы запустить это, нам просто нужно выполнить его:
``` ударить
$ ./приложение
Я бинарник!
Да, это так просто!
Теперь давайте поговорим о некоторых важных переменных времени сборки, начиная с
GOOS
иGOARCH
Эти переменные среды помогают использовать программы сборки для разных операционных систем
и базовый процессор [архитектуры] (https://en.wikipedia.org/wiki/Microarchitecture).
Мы можем перечислить всю поддерживаемую архитектуру с помощью команды go tool:
``` ударить
$ go инструмент список расстояний
андроид/амд64
iOS/AMD64
js/васм
линукс/амд64
окна/рука64
Вот пример сборки исполняемого файла окна из macOS!
``` ударить
$ GOOS=windows GOARCH=amd64 go build -o app.exe
CGO_ENABLED
Эта переменная позволяет нам настроить [CGO] (https://go.dev/blog/cgo), который в Go используется для вызова кода C.
Это помогает нам создать статически связанный двоичный файл, который работает без каких-либо внешних зависимостей.
Это очень полезно, скажем, когда мы хотим запустить наши бинарные файлы go в контейнере докера с минимальными внешними зависимостями.
Вот пример того, как его использовать:
``` ударить
$ CGO_ENABLED=0 go build -o приложение
Указатели
В этом уроке мы обсудим указатели. Так что же такое указатели?
Проще говоря, указатель — это переменная, которая используется для хранения адреса памяти другой переменной.
Его можно использовать следующим образом:
```иди
вар х *Т
Где T — это тип, такой как «int», «string», «float» и т. д.
Давайте попробуем простой пример и посмотрим на это в действии:
```иди
основной пакет
импортировать "фмт"
основная функция () {
вар п *инт
fmt.Println(p)
``` ударить
$ запустить main.go
ноль
Хм, это печатает nil
, но что такое nil
?
Таким образом, nil — это предварительно объявленный идентификатор в Go, который представляет нулевое значение для указателей, интерфейсов, каналов, карт и срезов.
Это похоже на то, что мы узнали в разделе переменных и типов данных, где мы видели, что неинициализированный int
имеет нулевое значение 0, bool
имеет значение false и так далее.
Хорошо, теперь давайте присвоим значение указателю:
```иди
основной пакет
импортировать "фмт"
основная функция () {
а := 10
var p *int = &a
fmt.Println("адрес:", p)
Мы используем оператор амперсанда &
для ссылки на адрес памяти переменной.
``` ударить
$ запустить main.go
0xc0000b8000
Это должно быть значение адреса памяти переменной a
Разыменование
Мы также можем использовать оператор звездочки *
для значения, хранящегося в переменной, на которую указывает указатель.
Это называется разыменованием.
Например, мы можем получить доступ к значению переменной a
через указатель p
, используя этот оператор звездочки *
.
```иди
основной пакет
импортировать "фмт"
основная функция () {
а := 10
var p *int = &a
fmt.Println("адрес:", p)
fmt.Println("значение:", *p)
``` ударить
$ запустить main.go
адрес: 0xc000018030
значение: 10
Мы можем не только получить к нему доступ, но и изменить его через указатель:
```иди
основной пакет
импортировать "фмт"
основная функция () {
а := 10
var p *int = &a
fmt.Println ("до", а)
fmt.Println("адрес:", p)
*р = 20
fmt.Println("после:", а)
``` ударить
$ запустить main.go
до 10
адрес: 0xc000192000
после: 20
Я думаю, что это довольно аккуратно!
Указатели как аргументы функции
Указатели также можно использовать в качестве аргументов для функции, когда нам нужно передать некоторые данные по ссылке.
Вот пример:
```иди
мояФункция(&а)
func myFunction(ptr *int) {}
Новая функция
Есть и другой способ инициализации указателя. Мы можем использовать функцию new
, которая принимает тип в качестве аргумента, выделяет достаточно памяти для размещения значения этого типа и возвращает указатель на него.
Вот пример:
```иди
основной пакет
импортировать "фмт"
основная функция () {
р: = новый (целое число)
*р = 100
fmt.Println("значение", *p)
fmt.Println ("адрес", р)
``` ударить
$ запустить main.go
значение 100
адрес 0xc000018030
Указатель на указатель
Вот интересная идея... можем ли мы создать указатель на указатель. И ответ - да!
Да мы можем.
```иди
основной пакет
импортировать "фмт"
основная функция () {
р: = новый (целое число)
*р = 100
р1 := &р
fmt.Println("Значение P", *p, "адрес", p)
fmt.Println("Значение P1", *p1, "адрес", p)
fmt.Println("Разыменованное значение", **p1)
``` ударить
$ запустить main.go
Значение P 100 адрес 0xc0000be000
Значение P1 0xc0000be000 адрес 0xc0000be000
Разыменованное значение 100
Обратите внимание, как значение p1
соответствует адресу p
Кроме того, важно знать, что указатели в Go не поддерживают арифметику указателей, как в C или C++.
```иди
p1 := p * 2 // Ошибка компилятора: недопустимая операция
Однако мы можем сравнить два указателя одного типа на равенство, используя оператор ==
:
```иди
р := &а
р1 := &а
fmt.Println(p == p1)
Но почему?
Это подводит нас к вопросу на миллион долларов: зачем нам нужны указатели?
Что ж, на этот вопрос нет однозначного ответа, а указатели — это просто еще одна полезная функция, которая помогает нам эффективно изменять наши данные без копирования большого объема данных.
И может быть применен к множеству вариантов использования.
Наконец, я добавлю, что если вы переходите с языка, в котором нет понятия указателей, не паникуйте и попытайтесь сформировать мысленную модель того, как работают указатели.
Идеальный! Мы узнали об указателях и вариантах их использования, теперь давайте перейдем к следующей теме.
Структуры
В этом уроке мы узнаем о структурах.
Таким образом, структура — это пользовательский тип, содержащий набор именованных полей. По сути, он используется для группировки связанных данных в единое целое.
Если вы исходите из объектно-ориентированного опыта, подумайте о структурах как о легковесных классах, которые поддерживают композицию, но не наследование.
Определение
Мы можем определить структуру следующим образом:
```иди
введите структуру человека {}
Мы используем ключевое слово type
, чтобы ввести новый тип, за которым следует имя, а затем ключевое слово struct
, чтобы указать, что мы определяем структуру.
Теперь давайте добавим ему несколько полей:
```иди
тип Человек структура {
Строка имени
Строка фамилии
Возраст
И, если поля имеют одинаковый тип, мы также можем свернуть их:
```иди
тип Человек структура {
Строка Имя, Фамилия
Возраст
Объявление и инициализация
Теперь, когда у нас есть структура, мы можем объявить ее так же, как и другие типы данных:
```иди
основная функция () {
var p1 Человек
fmt.Println("Человек 1:", p1)
``` ударить
$ запустить main.go
Человек 1: { 0}
Как мы видим, все поля структуры инициализированы своими нулевыми значениями. Итак, FirstName
и LastName
установлены в пустую строку “”
, а Age
установлен в 0.
Мы также можем просто инициализировать его как "struct literal".
```иди
основная функция () {
var p1 Человек
fmt.Println("Человек 1:", p1)
var p2 = Человек{Имя: "Каран", Фамилия: "Пратап Сингх", Возраст: 22}
fmt.Println("Человек 2:", p2)
Для удобства чтения мы можем разделить на новую строку, но это также потребует запятой в конце:
```иди
var p2 = человек{
Имя: "Каран",
Фамилия: «Пратап Сингх»,
Возраст: 22,
``` Баш
$ запустить main.go
Человек 1: { 0}
Человек 2: {Каран Пратап Сингх, 22}
Мы также можем инициализировать только подмножество полей:
```иди
основная функция () {
var p1 Человек
fmt.Println("Человек 1:", p1)
var p2 = человек{
Имя: "Каран",
Фамилия: «Пратап Сингх»,
Возраст: 22,
fmt.Println("Человек 2:", p2)
var p3 = человек{
Имя: "Тони",
Фамилия: «Старк»,
fmt.Println("Человек 3:", стр.3)
``` ударить
$ запустить main.go
Человек 1: { 0}
Человек 2: {Каран Пратап Сингх, 22}
Человек 3: {Тони Старк 0}
Как мы видим, поле возраста человека 3 по умолчанию имеет нулевое значение.
Без имени поля
Структуры Go также поддерживают инициализацию без имен полей:
```иди
основная функция () {
var p1 Человек
fmt.Println("Человек 1:", p1)
var p2 = человек{
Имя: "Каран",
Фамилия: «Пратап Сингх»,
Возраст: 22,
fmt.Println("Человек 2:", p2)
var p3 = человек{
Имя: "Тони",
Фамилия: «Старк»,
fmt.Println("Человек 3:", стр.3)
var p4 = Человек{"Брюс", "Уэйн"}
fmt.Println("Человек 4:", стр.4)
Но вот в чем загвоздка, нам нужно будет указать все значения во время инициализации, иначе произойдет сбой:
``` ударить
$ запустить main.go
аргументы командной строки
./main.go:30:27: слишком мало значений в Person{...}
```иди
var p4 = Человек{"Брюс", "Уэйн", 40}
fmt.Println("Человек 4:", стр.4)
Мы также можем объявить анонимную структуру:
```иди
основная функция () {
var p1 Человек
fmt.Println("Человек 1:", p1)
var p2 = человек{
Имя: "Каран",
Фамилия: «Пратап Сингх»,
Возраст: 22,
fmt.Println("Человек 2:", p2)
var p3 = человек{
Имя: "Тони",
Фамилия: «Старк»,
fmt.Println("Человек 3:", стр.3)
var p4 = Человек{"Брюс", "Уэйн", 40}
fmt.Println("Человек 4:", стр.4)
вар а = структура {
Строка имени
}{"Голанг"}
fmt.Println("Аноним:", а)
Доступ к полям
Давайте очистим наш пример от нашего бита и посмотрим, как мы можем получить доступ к отдельным полям:
```иди
основная функция () {
вар р = человек {
Имя: "Каран",
Фамилия: «Пратап Сингх»,
Возраст: 22,
fmt.Println("Имя", p.Имя)
Мы также можем создать указатель на структуру:
```иди
основная функция () {
вар р = человек {
Имя: "Каран",
Фамилия: «Пратап Сингх»,
Возраст: 22,
указатель := &p
fmt.Println((*ptr).Имя)
fmt.Println(ptr.FirstName)
Оба утверждения равны, так как в Go нам не нужно явно разыменовывать указатель.
Мы также можем использовать встроенную функцию new
:
```иди
основная функция () {
p := новый(человек)
p.FirstName = "Каран"
p.LastName = "Пратап Сингх"
р.Возраст = 22
fmt.Println("Человек", p)
``` ударить
$ запустить main.go
Человек и {Каран Пратап Сингх 22}
В качестве примечания, две структуры равны, если все их соответствующие поля также равны:
```иди
основная функция () {
var p1 = человек{"а", "б", 20}
var p2 = человек{"а", "б", 20}
fmt.Println(p1 == p2)
``` ударить
$ запустить main.go
истинный
Экспортируемые поля
Теперь давайте узнаем, что такое экспортируемые и неэкспортируемые поля в структуре. Так же, как и правила для переменных и функций, если поле структуры объявлено с идентификатором нижнего регистра, оно не будет экспортировано и будет видно только пакету, в котором оно определено.
```иди
тип Человек структура {
Строка Имя, Фамилия
Возраст
строка почтового кода
Таким образом, поле zipCode не будет экспортировано. Кроме того, то же самое касается структуры «Person», если мы переименуем ее в «person», она также не будет экспортирована.
```иди
введите структуру человека {
Строка Имя, Фамилия
Возраст
строка почтового кода
Встраивание и композиция
Как мы обсуждали ранее, Go не обязательно поддерживает наследование, но мы можем сделать что-то подобное с встраиванием.
```иди
тип Человек структура {
Строка Имя, Фамилия
Возраст
введите структуру SuperHero {
Человек
Мощность внутр.
Итак, наша новая структура будет иметь все свойства исходной структуры. И это должно вести себя так же, как наша обычная структура.
```иди
основная функция () {
с := Супергерой{}
s.FirstName = "Брюс"
s.LastName = "Уэйн"
с.Возраст = 40
s. Псевдоним = "Бэтмен"
fmt.Println(s)
``` ударить
$ запустить main.go
{{Брюс Уэйн 40} Бэтмен}
Однако это обычно не рекомендуется, и в большинстве случаев предпочтение отдается композиции. Поэтому вместо встраивания мы просто определим его как обычное поле.
```иди
тип Человек структура {
Строка Имя, Фамилия
Возраст
введите структуру SuperHero {
человек человек
Строка псевдонима
Следовательно, мы можем переписать наш пример и с композицией:
```иди
основная функция () {
p := Person{"Брюс", "Уэйн", 40}
s:= SuperHero{p, "бэтмен"}
fmt.Println(s)
``` ударить
$ запустить main.go
{{Брюс Уэйн 40} Бэтмен}
Опять же, здесь нет правильного или неправильного, но, тем не менее, встраивание иногда бывает кстати.
Структурные теги
Тег структуры — это всего лишь тег, который позволяет нам прикреплять метаданные к полю, которые можно использовать для пользовательского поведения с помощью пакета reflect
.
Давайте узнаем, как мы можем определить теги структуры.
```иди
введите структуру животного {
Строка имени key:"value1"
Возраст int key:"value2"
Вы часто найдете теги в пакетах кодирования, таких как XML, JSON, YAML, ORM и управление конфигурацией.
Вот пример тегов для кодировщика JSON:
```иди
введите структуру животного {
Строка имени json:"имя"
Возраст int json:"возраст"
Характеристики
Наконец, давайте обсудим свойства структур.
Структуры являются типами значений. Когда мы присваиваем одну переменную struct другой, создается и назначается новая копия struct.
Точно так же, когда мы передаем структуру другой функции, функция получает свою собственную копию структуры.
```иди
основной пакет
импортировать "фмт"
тип точки структура {
X, Y с плавающей запятой64
основная функция () {
p1 := Точка{1, 2}
p2 := p1 // Копия p1 присваивается p2
р2.Х = 2
fmt.Println(p1) // Вывод: {1 2}
fmt.Println(p2) // Вывод: {2 2}
Пустая структура занимает ноль байт памяти:
```иди
основной пакет
импорт (
"ФМТ"
"небезопасно"
основная функция () {
структура переменной {}
fmt.Println(unsafe.Sizeof(s)) // Вывод: 0
Что ж, на этом мы завершаем обсуждение структур. Далее мы узнаем, как расширять наши структуры с помощью методов.
Методы
Давайте поговорим о методах, иногда также называемых приемниками функций.
Технически Go не является объектно-ориентированным языком программирования. В нем нет классов, объектов и наследования.
Однако в Go есть типы. И вы можете определить методы для типов.
Метод — это не что иное, как функция со специальным аргументом receiver. Давайте посмотрим, как мы можем объявить методы:
```иди
func (переменная T) Name(params) (returnTypes) {}
Аргумент receiver имеет имя и тип. Он появляется между ключевым словом func и именем метода.
Например, давайте определим структуру Car
:
```иди
тип автомобиля структура {
Строка имени
Год инт
Теперь давайте определим такой метод, как IsLatest
, который сообщит нам, был ли автомобиль произведен в течение последних 5 лет:
```иди
func (c Car) IsLatest() bool {
вернуть c.Year >= 2017
Как видите, мы можем получить доступ к экземпляру Car
, используя переменную приемника c
. Мне нравится думать об этом как о ключевом слове this из объектно-ориентированного мира.
Теперь мы должны иметь возможность вызывать этот метод после инициализации нашей структуры, точно так же, как мы это делаем с классами в других языках:
```иди
основная функция () {
c := Автомобиль{"Tesla", 2021}
fmt.Println("Последний", c.Последний())
Методы с приемниками Pointer
Во всех примерах, которые мы видели ранее, был приемник значения.
При использовании получателя значения метод работает с копией переданного ему значения. Таким образом, любые изменения, сделанные для получателя внутри методов, невидимы для вызывающего.
Например, давайте создадим еще один метод под названием «UpdateName», который будет обновлять имя «Автомобиля».
```иди
func (c Car) UpdateName(строка имени) {
c.Имя = имя
Теперь давайте запустим это:
```иди
основная функция () {
c := Автомобиль{"Tesla", 2021}
c.ИмяОбновления("Тойота")
fmt.Println("Автомобиль:", c)
``` ударить
$ запустить main.go
Автомобиль: {Tesla 2021}
Похоже, имя не было обновлено, поэтому теперь давайте переключим наш приемник на тип указателя и попробуем еще раз:
```иди
func (c *Car) UpdateName(строка имени) {
c.Имя = имя
``` ударить
$ запустить main.go
Автомобиль: {Тойота 2021}
Как и ожидалось, методы с получателями указателей могут изменять значение, на которое указывает получатель.
Такие модификации видны и вызывающему методу.
Характеристики
Давайте также посмотрим на некоторые свойства методов!
- Go достаточно умен, чтобы правильно интерпретировать наш вызов функции, и, следовательно, вызовы методов получателя указателя — это просто синтаксический сахар, предоставленный Go для удобства.
```иди
(&c).ИмяОбновления(...)
- Мы также можем опустить переменную часть приемника, если мы ее не используем:
```иди
func (автомобиль) UpdateName(...) {}
- Методы не ограничиваются структурами, но также могут использоваться и с неструктурными типами:
```иди
основной пакет
импортировать "фмт"
введите MyInt int
func (i MyInt) isGreater (значение int) bool {
вернуть i > MyInt (значение)
основная функция () {
я := MyInt(10)
fmt.Println(i.isGreater(5))
Почему методы вместо функций?
Итак, вопрос в том, почему методы вместо функций?
Как всегда, конкретного ответа на этот вопрос нет, и ни в коем случае одно не лучше другого. Вместо этого их следует использовать надлежащим образом, когда возникает ситуация.
Одна вещь, о которой я могу думать прямо сейчас, это то, что методы могут помочь нам избежать конфликтов имен.
Поскольку метод привязан к определенному типу, у нас могут быть одинаковые имена методов для нескольких получателей.
Но вообще, может быть, это просто сводится к предпочтениям? Например, «вызовы методов намного легче читать и понимать, чем вызовы функций» или наоборот.
Итак, на этом мы завершаем обсуждение методов.
Массивы и срезы
В этом уроке мы узнаем о массивах и срезах в Go.
Массивы
Так что же такое массив?
Массив представляет собой набор фиксированного размера элементов одного типа. Элементы массива хранятся последовательно, и к ним можно получить доступ, используя их «индекс».
Декларация
Мы можем объявить массив следующим образом:
```иди
вар [n]T
Здесь n
— это длина, а T
может быть любого типа, например целого числа, строки или определяемых пользователем структур.
Теперь давайте объявим массив целых чисел длиной 4 и выведем его.
```иди
основная функция () {
переменная обр[4]целое
fmt.Println(обр)
``` ударить
$ запустить main.go
[0 0 0 0]
По умолчанию все элементы массива инициализируются нулевым значением соответствующего типа массива.
Инициализация
Мы также можем инициализировать массив, используя литерал массива:
```иди
var a [n]T = [n]T{V1, V2, ... Vn}
```иди
основная функция () {
переменная обр = [4] целое {1, 2, 3, 4}
fmt.Println(обр)
``` ударить
$ запустить main.go
[1 2 3 4]
Мы можем даже сделать сокращенное объявление:
```иди
обр: = [4] целое {1, 2, 3, 4}
Доступ
Как и в других языках, мы можем получить доступ к элементам, используя «индекс», поскольку они хранятся последовательно:
```иди
основная функция () {
обр: = [4] целое {1, 2, 3, 4}
fmt.Println (обр[0])
``` ударить
$ запустить main.go
Итерация
Теперь поговорим об итерации.
Таким образом, существует несколько способов перебора массивов.
Первый использует цикл for с функцией len
, которая дает нам длину массива:
```иди
основная функция () {
обр: = [4] целое {1, 2, 3, 4}
для я := 0; я < длин (обр); я++ {
fmt.Printf("Индекс: %d, Элемент: %d
", i, arr[i])
``` ударить
$ запустить main.go
Индекс: 0, Элемент: 1
Индекс: 1, Элемент: 2
Индекс: 2, Элемент: 3
Индекс: 3, Элемент: 4
Другой способ — использовать ключевое слово range
с циклом for:
```иди
основная функция () {
обр: = [4] целое {1, 2, 3, 4}
для i, e := диапазон обр {
fmt.Printf("Индекс: %d, Элемент: %d
", i, e)
``` ударить
$ запустить main.go
Индекс: 0, Элемент: 1
Индекс: 1, Элемент: 2
Индекс: 2, Элемент: 3
Индекс: 3, Элемент: 4
Как видим, наш пример работает так же, как и раньше.
Но ключевое слово range довольно универсально и может использоваться разными способами.
```иди
for i, e := range arr {} // Обычное использование диапазона
for _, e := range arr {} // Опустить индекс с _ и использовать элемент
for i := range arr {} // Использовать только индекс
for range arr {} // Просто цикл по массиву
Многомерный
Все массивы, которые мы создали до сих пор, являются одномерными. Мы также можем создавать многомерные массивы в Go.
Например:
```иди
основная функция () {
обр := [2][4]целое{
{1, 2, 3, 4},
{5, 6, 7, 8},
для i, e := диапазон обр {
fmt.Printf("Индекс: %d, Элемент: %d
", i, e)
``` ударить
$ запустить main.go
Индекс: 0, Элемент: [1 2 3 4]
Индекс: 1, Элемент: [5 6 7 8]
Мы также можем позволить компилятору определить длину массива, используя многоточие ...
вместо длины:
```иди
основная функция () {
обр := [...][4]int{
{1, 2, 3, 4},
{5, 6, 7, 8},
для i, e := диапазон обр {
fmt.Printf("Индекс: %d, Элемент: %d
", i, e)
``` ударить
$ запустить main.go
Индекс: 0, Элемент: [1 2 3 4]
Индекс: 1, Элемент: [5 6 7 8]
Характеристики
Теперь поговорим о некоторых свойствах массивов.
Длина массива является частью его типа. Итак, массивы a
и b
— совершенно разные типы, и мы не можем присвоить один другому.
Это также означает, что мы не можем изменить размер массива, потому что изменение размера массива означало бы изменение его типа.
```иди
основной пакет
основная функция () {
переменная a = [4]целое{1, 2, 3, 4}
var b [2]int = a // Ошибка, нельзя использовать a (type [4]int) как тип [2]int в присваивании
Массивы в Go — это типы значений. В отличие от других языков, таких как C, C++ и Java, где массивы являются ссылочными типами.
Это означает, что когда мы присваиваем массив новой переменной или передаем массив в функцию, копируется весь массив.
Итак, если мы внесем какие-либо изменения в этот скопированный массив, исходный массив не будет затронут и останется неизменным.
```иди
основной пакет
импортировать "фмт"
основная функция () {
var a = [7]string{"Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"}
var b = a // Копия a присваивается b
б[0] = "Понедельник"
fmt.Println(a) // Вывод: [Пн Вт Ср Чт Пт Сб Вс]
fmt.Println(b) // Вывод: [понедельник вт ср чт пт сб вс]
Срезы
Я знаю, что вы думаете, массивы полезны, но немного негибки из-за ограничений, вызванных их фиксированным размером.
Это подводит нас к срезу, так что же такое срез?
Срез — это сегмент массива. Срезы основаны на массивах и обеспечивают большую мощность, гибкость и удобство.
Срез состоит из трех вещей:
- Ссылка-указатель на базовый массив.
- Длина сегмента массива, который содержит срез.
- И емкость, которая является максимальным размером, до которого сегмент может вырасти.
Как и в случае с функцией len, мы можем определить емкость среза с помощью встроенной функции cap.
Вот пример:
```иди
основной пакет
импортировать "фмт"
основная функция () {
а := [5]целое{20, 15, 5, 30, 25}
с:= а[1:4]
// Вывод: массив: [20 15 5 30 25], длина: 5, емкость: 5
fmt.Printf("Массив: %v, длина: %d, емкость: %d
", a, len(a), cap(a))
// Вывод: Срез [15 5], Длина: 3, Емкость: 4
fmt.Printf("Разрез: %v, Длина: %d, Емкость: %d", с, длина(ы), крышка(и))
Не волнуйтесь, мы собираемся подробно обсудить все, что здесь показано.
Декларация
Давайте посмотрим, как мы можем объявить срез:
```иди
переменная []T
Как мы видим, нам не нужно указывать длину.
Давайте объявим срез целых чисел и посмотрим, как это работает:
```иди
основная функция () {
переменная [] строка
fmt.Println(s)
fmt.Println(s == ноль)
``` ударить
$ запустить main.go
истинный
Таким образом, в отличие от массивов, нулевое значение среза равно nil
.
Инициализация
Есть несколько способов инициализировать наш слайс. Один из способов — использовать встроенную функцию make.
```иди
make([]T, len, cap) []T
```иди
основная функция () {
var s = сделать ([] строка, 0, 0)
fmt.Println(s)
``` ударить
$ запустить main.go
Подобно массивам, мы можем использовать литерал среза для инициализации нашего среза.
```иди
основная функция () {
var s = []string{"Go", "TypeScript"}
fmt.Println(s)
``` ударить
$ запустить main.go
[Перейти на TypeScript]
Другой способ — создать срез из массива. Поскольку срез — это сегмент массива, мы можем создать срез от индекса «низкий» до «высокий» следующим образом.
```иди
а[низкий:высокий]
```иди
основная функция () {
вар а = [4] строка {
"С++",
"Идти",
"Джава",
"Машинопись",
s1 := a[0:2] // Выбор от 0 до 2
s2 := a[:3] // Выбираем первые 3
s3 := a[2:] // Выбираем последние 2
fmt.Println("Массив:", а)
fmt.Println("Срез 1:", s1)
fmt.Println("Срез 2:", s2)
fmt.Println("Срез 3:", s3)
``` ударить
$ запустить main.go
Массив: [C++ Go Java TypeScript]
Фрагмент 1: [C++ Go]
Фрагмент 2: [C++ Go Java]
Фрагмент 3: [Java TypeScript]
- Отсутствие нижнего индекса подразумевает 0, а отсутствие верхнего индекса означает *
len(a)
Здесь следует отметить, что мы можем создать срез из других срезов, а не только из массивов:
```иди
вар а = [] строка {
"С++",
"Идти",
"Джава",
"Машинопись",
Итерация
Мы можем перебирать срез так же, как вы перебираете массив, используя цикл for либо с функцией len
, либо с ключевым словом range
.
функции
Итак, теперь давайте поговорим о встроенных функциях слайсов, предоставляемых в Go.
- копировать
Функция copy()
копирует элементы из одного слайса в другой. Требуется 2 среза, пункт назначения и источник. Он также возвращает количество скопированных элементов.
```иди
func copy(dst, src []T) int
Давайте посмотрим, как мы можем его использовать:
```иди
основная функция () {
s1 := [] строка {"а", "б", "в", "г"}
s2 := сделать([]строка, 0)
е := копия (s2, s1)
fmt.Println("Источник:", s1)
fmt.Println("Dst:", s2)
fmt.Println("Элементы:", e)
``` ударить
$ запустить main.go
Источник: [а б в г]
Dst: [a b c d]
Элементы: 4
Как и ожидалось, наши 4 элемента из исходного слайса были скопированы в целевой слайс:
- добавить
Теперь давайте посмотрим, как мы можем добавить данные в наш слайс, используя встроенную функцию append
, которая добавляет новые элементы в конец данного слайса.
Он принимает срез и переменное количество аргументов. Затем он возвращает новый фрагмент, содержащий все элементы.
```иди
добавить(слайс []T, элементы ...T) []T
Давайте попробуем это на примере, добавив элементы в наш слайс:
```иди
основная функция () {
s1 := [] строка {"а", "б", "в", "г"}
s2 := добавить (a1, "e", "f")
fmt.Println("a1:", a1)
fmt.Println("a2:", a2)
``` ударить
$ запустить main.go
а1: [а б в г]
a2: [a b c d e f]
Как мы видим, новые элементы были добавлены, и был возвращен новый срез.
Но если данный срез не имеет достаточной емкости для новых элементов, то выделяется новый базовый массив с большей емкостью.
Все элементы из базового массива существующего среза копируются в этот новый массив, а затем добавляются новые элементы.
Характеристики
Наконец, давайте обсудим некоторые свойства срезов.
Срезы являются ссылочными типами, в отличие от массивов.
Это означает, что изменение элементов среза приведет к изменению соответствующих элементов в указанном массиве.
```иди
основной пакет
импортировать "фмт"
основная функция () {
a := [7]string{"Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"}
с := а[0:2]
с[0] = "Солнце"
fmt.Println(a) // Вывод: [Вс Вт Ср Чт Пт Сб Вс]
fmt.Println(s) // Вывод: [Вс Вт]
Срезы также можно использовать с вариативными типами.
```иди
основной пакет
импортировать "фмт"
основная функция () {
значения: = []int{1, 2, 3}
сумма := добавить (значения...)
fmt.Println(сумма)
func add (значения ... int) int {
сумма := 0
для _, v := значения диапазона {
сумма += v
сумма возврата*
Вот и все, что касается массивов и срезов в Go!
Карты
Итак, Go предоставляет встроенный тип карты, и мы узнаем, как его использовать.
Но вопрос в том, что такое карты? А зачем они нам нужны?
Итак, карта — это неупорядоченный набор пар ключ-значение. Он сопоставляет ключи значениям. Ключи уникальны в пределах карты, а значения могут быть разными.
Он используется для быстрого поиска, извлечения и удаления данных на основе ключей. Это одна из наиболее часто используемых структур данных.
Декларация
Начнем с декларации.
Карта объявляется с использованием следующего синтаксиса:
```иди
вар м карта[K]V
Где «K» — тип ключа, а «V» — тип значения.
Например, вот как мы можем объявить сопоставление ключей string
со значениями int
:
```иди
основная функция () {
карта var m[string]int
fmt.Println(m)
``` ударить
$ запустить main.go
ноль
Как мы видим, нулевое значение карты равно nil
.
Карта nil
не имеет ключей. Более того, любая попытка добавить ключи в nil
map приведет к ошибке во время выполнения.
Инициализация
Существует несколько способов инициализации карты.
задействовать
Мы можем использовать встроенную функцию make, которая выделяет память для ссылочных типов данных и инициализирует их базовые структуры данных.
```иди
основная функция () {
var m = сделать (карта [строка] целое)
fmt.Println(m)
``` ударить
$ запустить main.go
карта[]
литерал карты
Другой способ — использовать литерал карты.
```иди
основная функция () {
вар м = карта [строка] int {
"а": 0,
"б": 1,
fmt.Println(m)
Обратите внимание, что последняя запятая обязательна.
``` ударить
$ запустить main.go
карта [а: 0 б: 1]
Как всегда, мы также можем использовать наши пользовательские типы:
```иди
тип Пользовательская структура {
Строка имени
основная функция () {
var m = карта[строка]Пользователь{
"a": Пользователь {"Питер"},
"b": Пользователь {"Сет"},
fmt.Println(m)
Мы можем даже удалить тип значения, и Go разберется с этим!
```иди
var m = карта[строка]Пользователь{
"а": {"Питер"},
"б": {"Сет"},
``` ударить
$ запустить main.go
карта[a:{Питер} b:{Сет}]
Добавлять
Теперь давайте посмотрим, как мы можем добавить значение на нашу карту.
```иди
основная функция () {
var m = карта[строка]Пользователь{
"а": {"Питер"},
"б": {"Сет"},
m["c"] = Пользователь{"Стив"}
fmt.Println(m)
``` ударить
$ запустить main.go
map[a:{Питер} b:{Сет} c:{Стив}]
Забрать
Мы также можем получить наши значения с карты с помощью ключа.
```иди
с:= м["с"]
fmt.Println("Ключ c:", c)
``` ударить
$ запустить main.go
клавиша c: {Стив}
Что, если мы используем ключ, которого нет в карте?
```иди
д := м["д"]
fmt.Println("Ключ d:", d)
Да, вы угадали! Мы получим нулевое значение типа значения карты.
``` ударить
$ запустить main.go
Ключ c: {Стив}
Ключ д: {}
Существуют
Когда вы извлекаете значение, присвоенное данному ключу, оно также возвращает дополнительное логическое значение.
Логическая переменная будет иметь значение «истина», если ключ существует, и «ложь» в противном случае.
Давайте попробуем это на примере:
```иди
с, хорошо := м["с"]
fmt.Println("Ключ c:", c, ок)
д, хорошо := м["д"]
fmt.Println("Ключ d:", d, ок)
``` ударить
$ запустить main.go
Ключ c: {Стив} Присутствует: правда
Ключ d: {} Присутствует: false
Обновление
Мы также можем обновить значение ключа, просто переназначив ключ.
```иди
м["а"] = "Роджер"
``` ударить
$ запустить main.go
map[a:{Роджер} b:{Сет} c:{Стив}]
Удаление
Или мы можем удалить ключ, используя встроенную функцию «удалить».
Вот как выглядит синтаксис:
```иди
удалить (м,
Первый аргумент — это карта, а второй — ключ, который мы хотим удалить.
Функция delete()
не возвращает никакого значения. Кроме того, он ничего не делает, если ключ не существует на карте.
``` ударить
$ запустить main.go
карта[a:{Роджер} c:{Стив}]
Итерация
Подобно массивам или срезам, мы можем перебирать карты с помощью ключевого слова range
.
```иди
основной пакет
импортировать "фмт"
основная функция () {
var m = карта[строка]Пользователь{
"а": {"Питер"},
"б": {"Сет"},
m["c"] = Пользователь{"Стив"}
для ключа, значение: = диапазон m {
fmt.Println("Ключ: %s, Значение: %v", ключ, значение)
``` ударить
$ запустить main.go
Ключ: c, значение: {Стив}
Ключ: а, значение: {Питер}
Ключ: b, значение: {Сет}
Обратите внимание, что карта — это неупорядоченная коллекция, и поэтому порядок итерации карты не обязательно будет одинаковым каждый раз, когда мы итерируем ее.
Характеристики
Наконец, давайте поговорим о свойствах карты.
Карты являются ссылочными типами, что означает, что когда мы назначаем карту новой переменной, они обе ссылаются на одну и ту же базовую структуру данных.
Следовательно, изменения, сделанные одной переменной, будут видны другой.
```иди
основной пакет
импортировать "фмт"
тип Пользовательская структура {
Строка имени
основная функция () {
var m1 = карта [строка] Пользователь {
"а": {"Питер"},
"б": {"Сет"},
м2 := м1
m2["c"] = Пользователь{"Стив"}
fmt.Println(m1) // Вывод: map[a:{Питер} b:{Сет} c:{Стив}]
fmt.Println(m2) // Вывод: map[a:{Питер} b:{Сет} c:{Стив}]
Что ж, на этом мы завершаем наш разговор о картах!
Интерфейсы
В этом разделе поговорим об интерфейсах.
Что такое интерфейс?
Итак, интерфейс в Go — это абстрактный тип, который определяется с помощью набора сигнатур методов. Интерфейс определяет поведение для подобных типов объектов.
Здесь поведение является ключевым термином, который мы вскоре обсудим.
Давайте возьмем пример, чтобы понять это лучше.
Одним из лучших реальных примеров интерфейсов является розетка. Представим, что нам нужно подключить к розетке разные устройства.
Попробуем это реализовать. Вот типы устройств, которые мы будем использовать:
```иди
введите мобильную структуру {
фирменная строка
введите структуру ноутбука {
строка процессора
введите структуру тостера {
сумма в
тип чайник структура {
строка количества
введите структуру сокета{}
Теперь давайте определим метод Draw
для типа, скажем, mobile
. Здесь мы просто напечатаем свойства типа:
```иди
func (m mobile) Draw (power int) {
fmt.Printf("%T -> марка: %s, мощность: %d", m, m.brand, мощность)
Отлично, теперь мы определим метод Plug
для типа socket
, который принимает наш тип mobile
в качестве аргумента:
```иди
func (гнездо) Вилка (мобильное устройство, внутренняя мощность) {
device.Draw(мощность)
Попробуем "подключить" или "подключить" тип mobile
к нашему типу socket
в функции main
:
```иди
основной пакет
импортировать "фмт"
основная функция () {
м := мобильный {"Apple"}
с := сокет{}
с.Вилка(м, 10)
И если мы запустим это, мы увидим следующее:
``` ударить
$ запустить main.go
main.mobile -> бренд: Apple, мощность: 10
Это интересно, но скажем, теперь мы хотим подключить **наш тип ноутбука
.
```иди
основной пакет
импортировать "фмт"
основная функция () {
м := мобильный {"Apple"}
л:= ноутбук{"Intel i9"}
с := сокет{}
с.Вилка(м, 10)
s.Plug(l, 50) // Ошибка: нельзя использовать l в качестве мобильного значения в аргументе
Как мы видим, это вызовет ошибку.
Что нам теперь делать? Определить другой метод? Такие как PlugLaptop
?
Конечно, но потом каждый раз мы добавляем новый тип устройства. нам также нужно будет добавить новый метод к типу сокета, и это не идеально.
Здесь вступает в действие «интерфейс». По сути, мы хотим определить контракт, который в будущем должен быть реализован.
Мы можем просто определить интерфейс, такой как PowerDrawer
, и использовать его в нашей функции Plug
, чтобы разрешить любое устройство, которое удовлетворяет критериям, а именно: тип должен иметь метод Draw
, соответствующий сигнатуре, которую требует интерфейс.
И вообще, сокету ничего не нужно знать о нашем устройстве, и он может просто вызвать метод Draw
.
Теперь давайте попробуем реализовать наш интерфейс PowerDrawer
. Вот как это будет выглядеть.
В качестве суффикса в имени принято использовать «-er». И, как мы обсуждали ранее, интерфейс должен описывать только ожидаемое поведение. В нашем случае это метод Draw.
```иди
введите интерфейс PowerDrawer {
Ничья (мощность)
Теперь нам нужно обновить наш метод Plug, чтобы он принимал в качестве аргумента устройство, реализующее интерфейс PowerDrawer.
```иди
func (гнездо) Plug(устройство PowerDrawer, питание int) {
device.Draw(мощность)
И чтобы удовлетворить интерфейс, мы можем просто добавить методы Draw
ко всем типам устройств.
```иди
введите мобильную структуру {
фирменная строка
func (m mobile) Draw (power int) {
fmt.Printf("%T -> марка: %s, мощность: %d
", m, m.brand, мощность)
введите структуру ноутбука {
строка процессора
func (лэптоп) Draw(power int) {
fmt.Printf("%T -> процессор: %s, мощность: %d
", l, l.cpu, мощность)
введите структуру тостера {
сумма в
func (t тостер) Draw(power int) {
fmt.Printf("%T -> количество: %d, мощность: %d
", t, t.amount, мощность)
тип чайник структура {
строка количества
func (k чайник) Draw(power int) {
fmt.Printf("%T -> количество: %s, мощность: %d
", k, k.количество, мощность)
Теперь мы можем подключить все наши устройства к розетке с помощью нашего интерфейса!
```иди
основная функция () {
м := мобильный {"Apple"}
л:= ноутбук{"Intel i9"}
т := тостер{4}
k := чайник{"50%"}
с := сокет{}
с.Вилка(м, 10)
с.Вилка(l, 50)
с.Plug(t, 30)
с.Вилка(k, 25)
И, как мы и ожидали, это работает.
``` ударить
$ запустить main.go
main.mobile -> бренд: Apple, мощность: 10
main.laptop -> процессор: Intel i9, мощность: 50
main.toaster -> количество: 4, мощность: 30
main.kettle -> количество: Half Empty, мощность: 25
Но почему это считается такой мощной концепцией?
Что ж, интерфейс может помочь нам отделить наши типы. Например, поскольку у нас есть интерфейс, нам не нужно обновлять нашу реализацию socket
. Мы можем просто определить новый тип устройства с помощью метода Draw.
В отличие от других языков, интерфейсы Go реализованы неявно, поэтому нам не нужно что-то вроде ключевого слова implements
. Это означает, что тип удовлетворяет интерфейсу автоматически, когда он имеет «все методы»* интерфейса.
Пустой интерфейс
Далее поговорим о пустом интерфейсе. Пустой интерфейс может принимать значение любого типа.
Вот как мы это декларируем.
```иди
интерфейс var x {}
Но зачем нам это нужно?
Пустые интерфейсы можно использовать для обработки значений неизвестных типов.
Некоторые примеры:
- Чтение разнородных данных из API
- Переменные неизвестного типа, как в функции
fmt.Prinln
Чтобы использовать значение пустого типа interface{}
, мы можем использовать утверждение типа или переключатель типа для определения типа значения.
Утверждение типа
Утверждение типа обеспечивает доступ к базовому конкретному значению значения интерфейса.
Например:
```иди
основная функция () {
интерфейс var i{} = "привет"
с := я.(строка)
fmt.Println(s)
Этот оператор утверждает, что значение интерфейса содержит конкретный тип, и присваивает значение базового типа переменной.
Мы также можем проверить, содержит ли значение интерфейса определенный тип.
Утверждение типа может возвращать два значения:
- Первое — это базовое значение
- Второй — это логическое значение, которое сообщает, успешно ли выполнено утверждение.
```иди
с, хорошо := я.(строка)
fmt.Println(s, ок)
Это может помочь нам проверить, имеет ли значение интерфейса определенный тип или нет.
В некотором смысле это похоже на то, как мы считываем значения с карты.
А если это не так, то ok
будет ложным, а значением будет нулевое значение типа, и никакой паники не произойдет.
```иди
f, хорошо := i.(float64)
fmt.Println(f, ок)
Но если интерфейс не поддерживает тип, оператор вызовет панику.
```иди
f = i.(float64)
fmt.Println(f) // Паника!
``` ударить
$ запустить main.go
привет
привет правда
0 ложь
паника: преобразование интерфейса: интерфейс {} является строкой, а не float64
Тип Переключатель
Здесь оператор switch
может использоваться для определения типа переменной типа empty interface{}
.
```иди
интерфейс var t{}
т = "привет"
переключатель т := т. (тип) {
строка случая:
fmt.Printf("строка: %s
", т)
случай логический:
fmt.Printf("логическое значение: %v
", т)
случай инт:
fmt.Printf("целое число: %d
", т)
По умолчанию:
fmt.Printf("неожиданно: %T
", t)
И если мы запустим это, мы сможем убедиться, что у нас есть тип string
.
``` ударить
$ запустить main.go
строка: привет
Характеристики
Давайте обсудим некоторые свойства интерфейсов.
Нулевое значение
Нулевое значение интерфейса равно nil
```иди
основной пакет
импортировать "фмт"
введите интерфейс MyInterface {
Метод()
основная функция () {
переменная MyInterface
fmt.Println(i) // Вывод:
Встраивание
Мы можем встраивать интерфейсы, такие как структуры.
Например:
```иди
тип интерфейс1 интерфейс {
Метод1()
тип интерфейс2 интерфейс {
Метод2()
тип интерфейс3 интерфейс {
интерфейс1
интерфейс2
Ценности
Значения интерфейса сопоставимы.
```иди
основной пакет
импортировать "фмт"
введите интерфейс MyInterface {
Метод()
введите структуру MyType {}
метод func (MyType) () {}
основная функция () {
т := MyType{}
переменная MyInterface = MyType{}
fmt.Println(t == i)
Значения интерфейса
Под капотом значение интерфейса можно рассматривать как кортеж, состоящий из значения и конкретного типа.
```иди
основной пакет
импортировать "фмт"
введите интерфейс MyInterface {
Метод()
введите структуру MyType {
собственность в
метод func (MyType) () {}
основная функция () {
переменная MyInterface
я = мой тип{10}
fmt.Printf("(%v, %T)
", i, i) // Вывод: ({10}, main.MyType)
На этом мы рассмотрели интерфейсы в Go.
Это действительно мощная функция, но помните: «Чем больше интерфейс, тем слабее абстракция» — Роб Пайк.
Ошибки
В этом уроке поговорим об обработке ошибок.
Обратите внимание, я сказал об ошибках, а не об исключениях, поскольку в Go нет обработки исключений.
Вместо этого мы можем просто вернуть встроенный тип error
, который является типом интерфейса.
```иди
тип интерфейса ошибки {
Строка с ошибкой()
Мы вернемся к этому в ближайшее время. Во-первых, давайте попробуем понять основы.
Итак, давайте объявим простую функцию Divide
, которая, как следует из названия, будет делить целое число a
на b
```иди
func Divide(a, b int) int {
возврат а/б
Отлично. Теперь мы хотим вернуть ошибку, скажем, чтобы предотвратить деление на ноль. Это приводит нас к построению ошибки.
Ошибки построения
Есть несколько способов сделать это, но мы рассмотрим два наиболее распространенных.
пакет ошибки
Во-первых, с помощью функции «Создать», предоставляемой пакетом «ошибки».
```иди
основной пакет
импортировать "ошибки"
основная функция () {}
func Divide(a, b int) (int, ошибка) {
если б == 0 {
вернуть 0, errors.New("невозможно разделить на ноль")
возврат a/b, ноль
Обратите внимание, как мы возвращаем ошибку вместе с результатом. И, если ошибки нет, мы просто возвращаем nil
как есть, нулевое значение ошибки, потому что, в конце концов, это интерфейс.
Но как мы справляемся с этим? Итак, для этого давайте вызовем функцию Divide
в нашей main
функции.
```иди
основной пакет
импорт (
"ошибки"
"ФМТ"
основная функция () {
результат, ошибка := Разделить (4, 0)
если ошибка != ноль {
fmt.Println(ошибка)
// Что-то делаем с ошибкой
возврат
fmt.Println(результат)
// Используем результат
func Divide(a, b int) (int, error) {...}
``` ударить
$ запустить main.go
нельзя делить на ноль
Как видите, мы просто проверяем, равна ли ошибка nil
, и соответствующим образом строим нашу логику. Это считается довольно идиоматичным в Go, и вы увидите, что это часто используется.
Другой способ создания наших ошибок — использование функции fmt.Errorf
.
Эта функция похожа на fmt.Sprintf
, и она позволяет нам отформатировать нашу ошибку. Но вместо строки возвращает ошибку.
Он часто используется, чтобы добавить контекст или детали к нашим ошибкам.
```иди
func Divide(a, b int) (int, ошибка) {
если б == 0 {
вернуть 0, fmt.Errorf("невозможно разделить %d на ноль", а)
возврат a/b, ноль
И это должно работать аналогично:
``` ударить
$ запустить main.go
нельзя делить 4 на ноль
Ошибки Sentinel
Еще одна важная техника в Go — определение ожидаемых ошибок, чтобы их можно было явно проверить в других частях кода. Их иногда называют сигнальными ошибками.
```иди
основной пакет
импорт (
"ошибки"
"ФМТ"
var ErrDivideByZero = errors.New("не может делить на ноль")
функция main() {...}
func Divide(a, b int) (int, ошибка) {
если б == 0 {
вернуть 0, ErrDivideByZero
возврат a/b, ноль
В Go считается обычным ставить перед переменной префикс Err.
Например, ErrNotFound
Но в чем смысл?
Таким образом, это становится полезным, когда нам нужно выполнить другую ветвь кода, если обнаружена ошибка определенного типа.
Например, теперь мы можем явно проверить, какая ошибка произошла, используя функцию [errors.Is](http://errors.Is)
.
```иди
основной пакет
импорт (
"ошибки"
"ФМТ"
основная функция () {
результат, ошибка := Разделить (4, 0)
если ошибка != ноль {
выключатель {
case error.Is(err, ErrDivideByZero):
fmt.Println(ошибка)
// Что-то делаем с ошибкой
По умолчанию:
fmt.Println("понятия не имею!")
возврат
fmt.Println(результат)
// Используем результат
func Divide(a, b int) (int, error) {...}
``` ударить
$ запустить main.go
нельзя делить на ноль
Пользовательские ошибки
Эта стратегия охватывает большинство вариантов использования обработки ошибок. Но иногда нам нужны дополнительные функции, такие как динамические значения внутри наших ошибок.
Ранее мы видели, что error — это просто интерфейс. Таким образом, что угодно может быть ошибкой, если оно реализует метод Error(), возвращающий сообщение об ошибке в виде строки.
Итак, давайте определим нашу пользовательскую структуру DivisionError
, которая будет содержать код ошибки и сообщение.
```иди
основной пакет
импорт (
"ошибки"
"ФМТ"
тип DivisionError структура {
Код внутр.
Строка сообщения
func (d DivisionError) Ошибка () строка {
return fmt.Sprintf("код %d: %s", d.Code, d.Msg)
функция main() {...}
func Divide(a, b int) (int, ошибка) {
если б == 0 {
вернуть 0, DivisionError {
Код: 2000,
Msg: "нельзя делить на ноль",
возврат a/b, ноль
Здесь мы будем использовать функцию errors.As
вместо errors.Is
, чтобы преобразовать ошибку в правильный тип.
```иди
основная функция () {
результат, ошибка := Разделить (4, 0)
если ошибка != ноль {
вар divErr DivisionError
выключатель {
регистр ошибок. Как (ошибка, & divErr):
fmt.Println(divErr)
// Что-то делаем с ошибкой
По умолчанию:
fmt.Println("понятия не имею!")
возврат
fmt.Println(результат)
// Используем результат
func Divide(a, b int) (int, error) {...}
``` ударить
$ иди беги man.go
код 2000: нельзя делить на ноль
Но какая разница между errors.Is
и errors.As
?
Разница в том, что эта функция проверяет, имеет ли ошибка определенный тип, в отличие от Is()
, которая проверяет, является ли ошибка конкретным объектом ошибки.
Мы также можем использовать утверждения типов, но это нежелательно.
```иди
основная функция () {
результат, ошибка := Разделить (4, 0)
если e, хорошо := err.(DivisionError); в порядке {
fmt.Println(e.Code, e.Msg) // Вывод: 2000 нельзя делить на ноль
возврат
fmt.Println(результат)
Наконец, я скажу, что обработка ошибок в Go сильно отличается от традиционной идиомы try/catch в других языках. Но он очень мощный, поскольку побуждает разработчика фактически обрабатывать ошибку явным образом, что также улучшает читабельность.
Я надеюсь, что это руководство помогло вам узнать об ошибках в Go и о том, как с ними справляться.
Паника и восстановление
Итак, ранее мы узнали, что идиоматический способ обработки ненормальных условий в программе Go — это использование ошибок. Хотя в большинстве случаев ошибок достаточно, в некоторых случаях программа не может продолжать работу.
В этих случаях мы можем использовать встроенную функцию паники.
```иди
функция паники (интерфейс {})
Паника — это встроенная функция, которая останавливает нормальное выполнение текущей горутины. Когда функция вызывает panic
, нормальное выполнение функции немедленно останавливается, и управление возвращается вызывающей стороне. Это повторяется до тех пор, пока программа не завершится с сообщением о панике и трассировкой стека.
Примечание: мы обсудим горутины
позже в курсе.
Итак, давайте посмотрим, как мы можем использовать функцию паники:
```иди
основной пакет
основная функция () {
БудетПаника()
функция WillPanic() {
паника ("Вау")
И если мы запустим это, мы увидим панику в действии:
``` ударить
$ запустить main.go
паника: вау
горутина 1 [работает]:
main.WillPanic(...)
.../main.go:8
основной.главный()
.../main.go:4 +0x38
статус выхода 2
Как и ожидалось, наша программа напечатала сообщение о панике, за которым следовала трассировка стека, а затем она была завершена.
Итак, вопрос в том, что делать, когда случается неожиданная паника?
Что ж, можно восстановить контроль над паникующей программой, используя встроенную функцию recover
вместе с ключевым словом defer
.
```иди
интерфейс функции восстановления(){}
Давайте попробуем пример, создав функцию handlePanic
. И затем мы можем вызвать его с помощью defer
```иди
основной пакет
импортировать "фмт"
основная функция () {
БудетПаника()
функция handlePanic() {
данные: = восстановить ()
fmt.Println("Восстановлено:", данные)
функция WillPanic() {
отложить обработчик паники()
паника ("Вау")
``` ударить
$ запустить main.go
Восстановлено: Вау
Как мы видим, наша паника была устранена, и теперь наша программа может продолжать выполнение.
Наконец, я упомяну, что «паника» и «восстановление» могут считаться похожими на идиому «попробовать/поймать» в других языках. Одним из важных факторов является то, что мы должны избегать паники и восстанавливать и использовать ошибки, когда это возможно.
Если да, то это подводит нас к вопросу, когда мы должны использовать слово «паника»?
Есть два допустимых варианта использования слова «паника»:
- Неисправимая ошибка
Это может быть ситуация, когда программа не может просто продолжить свое выполнение.
Например, чтение конфигурационного файла, который важен для запуска программы, так как в случае сбоя самого чтения файла делать больше нечего.
- Ошибка разработчика
Это самая распространенная ситуация.
Например, разыменование указателя, когда значение равно nil
, вызовет панику.
Я надеюсь, что это руководство помогло вам понять, как использовать панику и восстановление в Go.
Тестирование
В этом уроке мы поговорим о тестировании в Go. Итак, начнем с простого примера.
Мы создали пакет math
, который содержит функцию Add
, которая, как следует из названия, складывает два целых числа.
```иди
пакетная математика
func Add(a, b int) int {
вернуть а + б
Он используется в нашем пакете main следующим образом:
```иди
основной пакет
импорт (
"пример/математика"
"ФМТ"
основная функция () {
результат := math.Add(2, 2)
fmt.Println(результат)
И, если мы запустим это, мы должны увидеть результат:
``` ударить
$ запустить main.go
Теперь мы хотим протестировать нашу функцию «Добавить». Итак, в Go мы объявляем файлы тестов с суффиксом _test
в имени файла. Итак, для нашего add.go
мы создадим тест как add_test.go
Структура нашего проекта должна выглядеть так:
``` ударить
├── go.mod
├── main.go
└── математика
├── add.go
└── add_test.go
Мы начнем с использования пакета math_test и импортируем пакет testing из стандартной библиотеки. Это верно! Тестирование встроено в Go, в отличие от многих других языков.
Но подождите... зачем нам использовать math_test
в качестве нашего пакета, разве мы не можем просто использовать тот же пакет math
?
Ну да, мы можем написать наш тест в том же пакете, если захотим, но я лично считаю, что выполнение этого в отдельном пакете помогает нам писать тесты более несвязанным образом.
Теперь мы можем создать нашу функцию TestAdd. Он примет аргумент типа testing.T
, который предоставит нам полезные методы.
```иди
пакет math_test
импортировать "тестирование"
func TestAdd(t *testing.T) {}
Прежде чем мы добавим какую-либо логику тестирования, давайте попробуем ее запустить. Но на этот раз мы не можем использовать команду «go run», вместо этого мы будем использовать команду «go test».
``` ударить
$ пройти тест ./математика
хорошо, пример/математика 0,429 с
Здесь у нас будет имя нашего пакета «math», но мы также можем использовать относительный путь «./...» для тестирования всех пакетов.
``` ударить
$ пройти тест ./...
? пример [без тестовых файлов]
хорошо, пример/математика 0,348 с
И если Go не найдет ни одного теста в пакете, он сообщит нам об этом.
Отлично, давайте напишем тестовый код. Для этого мы сверим наш результат с ожидаемым значением, и если они не совпадают, мы можем использовать метод t.Fail
, чтобы не пройти тест.
```иди
пакет math_test
импортировать "тестирование"
функция TestAdd(t *testing.T) {
получил := math.Добавить(1, 1)
ожидается: = 2
если есть != ожидается {
t.Ошибка()
Большой! Кажется, наш тест пройден.
``` ударить
$ пройти тест по математике
хорошо, пример/математика 0,412 с
Давайте также посмотрим, что произойдет, если мы провалим тест, поэтому для этого мы можем изменить наш ожидаемый результат:
```иди
пакет math_test
импортировать "тестирование"
функция TestAdd(t *testing.T) {
получил := math.Добавить(1, 1)
ожидается: = 3
если есть != ожидается {
t.Ошибка()
``` ударить
$ пройти тест ./математика
ок пример/математика (кешируется)
Если вы видите это, не волнуйтесь. Для оптимизации наши тесты кэшируются. Мы можем использовать команду «go clean», чтобы очистить кеш, а затем повторно запустить тест.
``` ударить
$ очистить -testcache
$ пройти тест ./математика
--- FAIL: TestAdd (0,00 с)
НЕУДАЧА
FAIL пример/математика 0,354 с
НЕУДАЧА
Итак, вот как будет выглядеть провал теста.
Табличные тесты
Это подводит нас к табличным тестам. Но что именно они собой представляют?
Итак, ранее у нас были аргументы функций и ожидаемые переменные, которые мы сравнивали, чтобы определить, прошли ли наши тесты или нет. Но что, если мы определим все это в срезе и пройдемся по нему? Это сделает наши тесты немного более гибкими и поможет нам легко запускать несколько случаев.
Не волнуйтесь, мы научимся этому на примере. Итак, мы начнем с определения нашей структуры addTestCase:
```иди
пакет math_test
импорт (
"пример/математика"
"тестирование"
введите структуру addTestCase {
а, б, ожидаемый интервал
var testCases = []addTestCase{
{1, 1, 3},
{25, 25, 50},
{2, 1, 3},
{1, 10, 11},
func TestAdd(t *testing.T) {
для _, tc := диапазон testCases {
получил := math.Add(tc.a, tc.b)
если есть != tc.expected {
t.Errorf("Ожидали %d, но получили %d", tc.expected, получили)
Обратите внимание, как мы объявили addTestCase в нижнем регистре. Правильно, мы не хотим его экспортировать, так как он бесполезен вне нашей логики тестирования.
Запустим наш тест:
``` ударить
$ запустить main.go
--- FAIL: TestAdd (0,00 с)
add_test.go:25: Ожидал 3, а получил 2
НЕУДАЧА
FAIL пример/математика 0,334 с
НЕУДАЧА
Похоже, наши тесты сломались, давайте исправим их, обновив тестовые примеры.
```иди
var testCases = []addTestCase{
{1, 1, 2},
{25, 25, 50},
{2, 1, 3},
{1, 10, 11},
Отлично, работает!
``` ударить
$ запустить main.go
хорошо, пример/математика 0,589 с
Покрытие кода
Наконец, давайте поговорим о покрытии кода. При написании тестов часто важно знать, какую часть кода они покрывают. Обычно это называется покрытием кода.
Чтобы рассчитать и экспортировать покрытие для нашего теста, мы можем просто использовать аргумент -coverprofile
с командой go test
.
``` ударить
$ пройти тест ./math -coverprofile=coverage.out
хорошо, пример/математика 0,385 с охватом: 100,0% утверждений
Похоже, у нас отличное покрытие. Давайте также проверим отчет, используя команду go tool cover
, которая дает нам подробный отчет.
``` Баш
$ go обложка инструмента -html=coverage.out
Как мы видим, это гораздо более читаемый формат. И самое главное, он встроен прямо в стандартный инструментарий.
Фазз-тестирование
Наконец, давайте посмотрим на нечеткое тестирование, которое было представлено в Go версии 1.18.
Фаззинг — это тип автоматизированного тестирования, при котором непрерывно манипулируют входными данными программы для поиска ошибок.
Go fuzzing использует руководство по покрытию для интеллектуального обхода кода, подвергаемого фаззингу, для поиска и сообщения об ошибках пользователю.
Поскольку оно может достигать крайних случаев, которые люди часто пропускают, фазз-тестирование может быть особенно ценным для поиска ошибок и эксплойтов безопасности.
Давайте попробуем пример:
```иди
func FuzzTestAdd(f *testing.F) {
f.Fuzz(func(t *testing.T, a, b int) {
math.Добавить(а, б)
Если мы запустим это, мы увидим, что он автоматически создаст тестовые случаи. Поскольку наша функция «Добавить» довольно проста, тесты пройдут.
``` ударить
$ go test -fuzz FuzzTestДобавить пример/математику
fuzz: истекло: 0 с, сбор базового покрытия: 0/192 завершено
фаззинг: истекло: 0 с, сбор базового покрытия: 192/192 завершено, сейчас фаззинг с 8 работниками
fuzz: истекло: 3 с, execs: 325017 (108336/сек), новых интересных: 11 (всего: 202)
fuzz: истекло: 6 с, execs: 680218 (118402/сек), новых интересных: 12 (всего: 203)
fuzz: истекло: 9 с, execs: 1039901 (119895/сек), новых интересных: 19 (всего: 210)
fuzz: пройдено: 12 с, execs: 1386684 (115594/сек), новое интересное: 21 (всего: 212)
ПРОХОДЯТ
ок фу 12.692с
Но если мы обновим нашу функцию «Добавить» случайным граничным случаем, когда программа будет паниковать, если «b + 10» больше, чем «а»:
```иди
func Add(a, b int) int {
если а > б + 10 {
panic("В должно быть больше, чем А")
вернуть а + б
И если мы повторно запустим тест, этот пограничный случай будет обнаружен фазз-тестированием:
``` ударить
$ go test -fuzz FuzzTestДобавить пример/математику
предупреждение: начиная с пустого корпуса
fuzz: истекло: 0 с, execs: 0 (0/сек), новое интересное: 0 (всего: 0)
fuzz: прошло: 0 с, execs: 1 (25/сек), новое интересное: 0 (всего: 0)
--- FAIL: FuzzTestAdd (0,04 с)
--- FAIL: FuzzTestAdd (0,00 с)
testing.go:1349: паника: B больше, чем A
Я думаю, что это действительно классная функция Go 1.18. Вы можете узнать больше о фазз-тестировании в официальном блоге Go.
Отлично, так что это почти все для этого урока.
Дженерики
В этом разделе мы узнаем о Generics, долгожданной функции, которая была выпущена в Go версии 1.18.
Что такое дженерики?
Дженерики означают параметризованные типы. Проще говоря, дженерики позволяют программистам писать код, в котором тип может быть указан позже, потому что тип не имеет непосредственного значения.
Давайте возьмем пример, чтобы понять это лучше.
В нашем примере у нас есть простые функции суммирования для разных типов, таких как int
, float64
и string
. Поскольку переопределение методов в Go запрещено, нам обычно приходится создавать новые функции.
```иди
основной пакет
импортировать "фмт"
func sumInt(a, b int) int {
вернуть а + б
func sumFloat(a, b float64) float64 {
вернуть а + б
func sumString(строка a, b) строка {
вернуть а + б
основная функция () {
fmt.Println (суммаInt (1, 2))
fmt.Println (сумма с плавающей запятой (4.0, 2.0))
fmt.Println(sumString("a", "b"))
Как мы видим, помимо типов, эти функции очень похожи.
Давайте посмотрим, как мы можем определить общую функцию.
```иди
func fnNameT ограничение {
Здесь T
— это параметр нашего типа, а **constraint
будет интерфейсом, позволяющим любому типу **реализовать интерфейс**.
Я знаю, я знаю, это сбивает с толку. Итак, давайте начнем создавать нашу общую функцию sum
.
Здесь мы будем использовать T
в качестве параметра типа с пустым interface{}
в качестве ограничения.
```иди
func sumT interface{} T {
fmt.Println(a, b)
Кроме того, начиная с Go 1.18, мы можем использовать any, что в значительной степени эквивалентно пустому интерфейсу.
```иди
func sumT any T {
fmt.Println(a, b)
С параметрами типа возникает необходимость передавать аргументы типа, что может сделать наш код многословным.
```иди
sumint // аргумент явного типа
сумма [поплавок64] (4.0, 2.0)
суммастрока
К счастью, Go 1.18 поставляется с выводом типов, который помогает нам писать код, вызывающий универсальные функции без явных типов.
```иди
сумма(1, 2)
сумма(4.0, 2.0)
сумма("а", "б")
Давайте запустим это и посмотрим, работает ли это:
``` ударить
$ запустить main.go
1 2
4 2
а б
Теперь давайте обновим функцию sum
, чтобы добавить наши переменные:
```иди
func sumT any T {
вернуть а + б
```иди
fmt.Println (сумма (1, 2))
fmt.Println (сумма (4.0, 2.0))
fmt.Println (сумма («а», «б»))
Но теперь, если мы запустим это, мы получим ошибку, что оператор +
не определен в ограничении.
``` ударить
$ запустить main.go
./main.go:6:9: недопустимая операция: оператор + не определен в a (переменная типа T ограничена любым)
Хотя ограничение типа any обычно работает, оно не поддерживает операторы.
Итак, давайте определим наше собственное ограничение с помощью интерфейса. Наш интерфейс должен определять набор символов, содержащий int
, float
и string
.
Вот как выглядит наш интерфейс SumConstraint
:
```иди
введите интерфейс SumConstraint {
инт | поплавок64 | нить
func sumT SumConstraint T {
вернуть а + б
основная функция () {
fmt.Println (сумма (1, 2))
fmt.Println (сумма (4.0, 2.0))
fmt.Println (сумма («а», «б»))
И это должно работать так, как ожидалось:
``` ударить
$ запустить main.go
аб
Мы также можем использовать пакет constraints
, который определяет набор полезных ограничений, которые будут использоваться с параметрами типа.
Для этого нам нужно будет установить пакет constraints
:
``` Баш
$ перейти на golang.org/x/exp/constraints
перейти: добавлено golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd
```иди
импорт (
"ФМТ"
"golang.org/x/exp/ограничения"
func sumT ограничения.Ordered T {
вернуть а + б
основная функция () {
fmt.Println (сумма (1, 2))
fmt.Println (сумма (4.0, 2.0))
fmt.Println (сумма («а», «б»))
Здесь мы используем ограничение Ordered:
```иди
тип Упорядоченный интерфейс {
Целое | Поплавок | ~ строка
~
— это новый токен, добавленный в Go, а выражение ~string
означает набор всех типов, базовым типом которых является string
И он по-прежнему работает, как ожидалось.
``` ударить
$ запустить main.go
аб
Обобщения — замечательная функция, поскольку она позволяет писать абстрактные функции, которые в некоторых случаях могут значительно уменьшить дублирование кода.
Когда использовать дженерики
Итак, когда использовать дженерики? Мы можем взять следующие варианты использования в качестве примера
- Функции, работающие с массивами, срезами, картами и каналами
- Структуры данных общего назначения, такие как стек или связанный список
- Чтобы уменьшить дублирование кода
Наконец, я добавлю, что, хотя дженерики являются отличным дополнением к языку, их следует использовать с осторожностью.
И рекомендуется начинать с простого и писать общий код только после того, как мы написали очень похожий код как минимум 2 или 3 раза.
Отлично, на этом мы завершаем обсуждение дженериков!
параллелизм
В этом уроке мы узнаем о параллелизме, который является одной из самых мощных функций Go.
Итак, давайте начнем с вопроса, что такое «параллелизм»*?
Что такое параллелизм
Параллелизм по определению — это способность разбивать компьютерную программу или алгоритм на отдельные части, которые могут выполняться независимо.
Конечный результат параллельной программы такой же, как у программы, которая выполнялась последовательно.
Используя параллелизм, мы можем достичь тех же результатов за меньшее время, тем самым повысив общую производительность и эффективность наших программ.
Параллелизм против параллелизма
Многие люди путают параллелизм с параллелизмом, потому что оба они в некоторой степени подразумевают одновременное выполнение кода, но это две совершенно разные концепции.
Параллелизм — это задача одновременного выполнения нескольких вычислений и управления ими, а параллелизм — это задача одновременного выполнения нескольких вычислений.
Простая цитата Роба Пайка в значительной степени подводит итог:
- «Параллелизм — это работа с большим количеством вещей одновременно. Параллелизм — это делать много вещей одновременно»*
Но параллелизм в Go — это больше, чем просто синтаксис. Чтобы использовать мощь Go, нам нужно сначала понять, как Go подходит к параллельному выполнению кода. Go опирается на модель параллелизма, называемую CSP (Communicating Sequential Processes).
Взаимодействие последовательных процессов (CSP)
[Communicating Sequential Processes] (https://dl.acm.org/doi/10.1145/359576.359585) (CSP) — это модель, предложенная Тони Хоаром в 1978 году и описывающая взаимодействие между параллельными процессами. Он совершил прорыв в компьютерных науках, особенно в области параллелизма.
Такие языки, как Go и Erlang, были сильно вдохновлены концепцией взаимодействия последовательных процессов (CSP).
Параллелизм — это сложно, но CSP позволяет нам лучше структурировать наш параллельный код и предоставляет модель для понимания параллелизма, которая немного упрощает его. Здесь процессы независимы, и они общаются, используя общие каналы между собой.
Мы узнаем, как Golang реализует это с помощью горутин и каналов позже в курсе.
Базовые концепции
Теперь давайте познакомимся с некоторыми основными концепциями параллелизма.
Гонка данных
Гонка данных возникает, когда процессам приходится одновременно обращаться к одной и той же переменной.
Например, один процесс читает, а другой одновременно записывает в одну и ту же переменную.
Условия гонки
Состояние гонки возникает, когда время или порядок событий влияет на правильность фрагмента кода.
Тупики
Взаимная блокировка возникает, когда все процессы заблокированы, ожидая друг друга, и программа не может продолжать работу.
Условия Коффмана
Есть четыре условия, известные как условия Коффмана, все они должны быть выполнены, чтобы возникла тупиковая ситуация.
- Взаимное исключение
Параллельный процесс одновременно содержит по крайней мере один ресурс, что делает его недоступным для совместного использования.
На диаграмме ниже показан единственный экземпляр Ресурса 1, и он принадлежит только Процессу 1.
- Подожди и подожди
Параллельный процесс удерживает ресурс и ожидает дополнительного ресурса.
На приведенной ниже диаграмме процесс 2 удерживает ресурсы 2 и 3 и запрашивает ресурс 1, которым владеет процесс 1.
- Без приоритета
Ресурс, удерживаемый параллельным процессом, не может быть отнят системой. Его можно освободить только в процессе удержания.
На диаграмме ниже процесс 2 не может вытеснить ресурс 1 из процесса 1. Он будет освобожден только тогда, когда процесс 1 добровольно откажется от него после завершения его выполнения.
- Круговое ожидание
Процесс ожидает ресурса, удерживаемого вторым процессом, который ожидает ресурса, удерживаемого третьим процессом, и так далее, пока последний процесс не будет ожидать ресурса, удерживаемого первым процессом. Таким образом, образуя круговую цепочку.
На приведенной ниже диаграмме процессу 1 выделяется Ресурс 2, и он запрашивает Ресурс 1. Точно так же Процессу 2 выделяется Ресурс 1, и он запрашивает Ресурс 2. Это формирует круговой цикл ожидания.
Голод
Голодание происходит, когда процесс лишен необходимых ресурсов и не может выполнить свою функцию.
Голодание может произойти из-за взаимоблокировок или неэффективных алгоритмов планирования процессов. Чтобы решить проблему голода, нам нужно использовать лучшие алгоритмы распределения ресурсов, которые гарантируют, что каждый процесс получит свою справедливую долю ресурсов.
На этом мы завершаем обсуждение основ параллелизма в Go.
Горутины
В этом уроке мы познакомимся с горутинами.
Но прежде чем мы начнем наше обсуждение, я хотел бы поделиться важной пословицей Go.
- «Не общайтесь, делясь памятью, делитесь памятью, общаясь».* — Роб Пайк
Что такое горутина?
Горутина — это облегченный поток выполнения, который управляется средой выполнения Go и, по сути, позволяет нам писать асинхронный код синхронным образом.
Важно знать, что они не являются реальными потоками ОС, а сама основная функция работает как горутина.
Один поток может запускать в них тысячи горутин с помощью планировщика времени выполнения Go, который использует совместное планирование. Это означает, что если текущая горутина заблокирована или завершена, планировщик переместит другие горутины в другой поток ОС.
Следовательно, мы достигаем эффективности в планировании, когда ни одна рутина не блокируется навсегда.
Мы можем превратить любую функцию в горутину, просто используя ключевое слово go:
```иди
перейти fn(x, y, z)
Прежде чем мы напишем какой-либо код, важно кратко обсудить модель fork-join.
Модель разветвления
Go использует идею модели параллелизма fork-join за горутинами. Модель fork-join по существу подразумевает, что дочерний процесс отделяется от своего родительского процесса, чтобы работать одновременно с родительским процессом.
После завершения своего выполнения дочерний процесс снова сливается с родительским процессом. Точка, в которой он соединяется, называется точкой соединения.
Теперь давайте напишем код и создадим собственную горутину:
```иди
основной пакет
импортировать "фмт"
функция говорить (строка аргумента) {
fmt.Println(аргумент)
основная функция () {
иди говори ("Привет, мир")
Здесь перед вызовом функции «speak» стоит ключевое слово «go». Это позволит ему работать как отдельная горутина. Вот и все, мы только что создали нашу первую горутину. Это так просто!
Отлично, давайте запустим это:
``` Баш
$ запустить main.go
Интересно, похоже, что наша программа не запустилась полностью, так как в ней отсутствует какой-то вывод. Это связано с тем, что наша основная горутина завершила работу и не дождалась созданной нами горутины.
Что, если мы заставим нашу программу ждать с помощью функции time.Sleep
?
```иди
основная функция () {
время.Сон(1 * время.Секунда)
А теперь, если мы запустим это:
``` ударить
$ запустить main.go
Привет, мир
Вот и все, теперь мы можем видеть наш полный вывод.
Хорошо, это работает, но не идеально. Так как же нам улучшить это?
Что ж, самое сложное в использовании горутин — это знать, когда они остановятся. Важно знать, что горутины работают в одном и том же адресном пространстве, поэтому доступ к общей памяти должен быть синхронизирован.
Это подводит нас к каналам, которые мы обсудим в следующем.
Каналы
В этом уроке мы узнаем о каналах.
Так что же такое каналы?
Ну, просто определил.
Канал — это канал связи между горутинами. Вещи идут в один конец и выходят из другого в том же порядке, пока канал не закроется.
Как мы узнали ранее, каналы в Go основаны на коммуникативных последовательных процессах (CSP).
Создание канала
Теперь, когда мы понимаем, что такое каналы, давайте посмотрим, как мы можем их объявить:
```иди
вар чан т
Здесь мы ставим префикс нашего типа T
, который является типом данных значения, которое мы хотим отправить и получить, с ключевым словом chan
, которое обозначает канал.
Давайте попробуем напечатать значение нашего канала c
типа string
.
```иди
основная функция () {
вар чан строка
fmt.Println(c)
``` ударить
$ запустить main.go
<ноль>
Как мы видим, нулевое значение канала равно nil
, и если мы попытаемся отправить данные по каналу, наша программа запаникует.
Итак, как и в случае со слайсами, мы можем инициализировать наш канал с помощью встроенной функции make.
```иди
основная функция () {
ch := make(строка chan)
fmt.Println(c)
И если мы запустим это, мы увидим, что наш канал был инициализирован:
``` ударить
$ запустить main.go
0x1400010e060
Отправка и получение данных
Теперь, когда у нас есть общее представление о каналах, давайте реализуем наш предыдущий пример, используя каналы, чтобы узнать, как мы можем использовать их для связи между нашими горутинами:
```иди
основной пакет
импортировать "фмт"
функция говорить (строка аргументов, строка ch chan) {
ch <- arg // Отправить
основная функция () {
ch := make(строка chan)
иди говори ("Hello World", ch)
данные := <-ch // Получение
fmt.Println(данные)
Обратите внимание, как мы можем отправлять данные, используя* channel<-data
, и получать данные, используя синтаксис data := <-channel
.
И если мы запустим это:
``` ударить
$ запустить main.go
Привет, мир
Отлично, наша программа работала, как мы и ожидали.
Буферизованные каналы
У нас также есть буферизованные каналы, которые принимают ограниченное количество значений без соответствующего приемника для этих значений.
Эта длина буфера или емкость может быть указана с помощью второго аргумента функции make.
```иди
основная функция () {
ch := make(строка chan, 2)
иди говори ("Hello World", ch)
иди говори ("Привет снова", ch)
данные1: = <-ch
fmt.Println (данные1)
данные2: = <-ch
fmt.Println (данные2)
Поскольку этот канал буферизован, мы можем отправлять эти значения в канал без соответствующего параллельного приема.
По умолчанию канал не буферизован и имеет емкость 0, поэтому мы опускаем второй аргумент функции make.
Далее у нас есть направленные каналы.
Направленные каналы
При использовании каналов в качестве параметров функции мы можем указать, предназначен ли канал только для отправки или получения значений. Это повышает безопасность типов нашей программы, так как по умолчанию канал может как отправлять, так и получать значения.
В нашем примере мы можем обновить второй аргумент нашей функции speak
, чтобы она могла отправлять только значение.
```иди
функция говорить (строка аргументов, ch chan <- строка) {
ch <- arg // Только отправка
Здесь chan<-
может использоваться только для отправки значений и вызовет панику, если мы попытаемся получить значения.
Закрытие каналов
Кроме того, как и любой другой ресурс, когда мы закончим с нашим каналом, нам нужно его закрыть. Этого можно добиться с помощью встроенной функции close.
Здесь мы можем просто передать наш канал функции close.
```иди
основная функция () {
ch := make(строка chan, 2)
иди говори ("Hello World", ch)
иди говори ("Привет снова", ch)
данные1: = <-ch
fmt.Println (данные1)
данные2: = <-ch
fmt.Println (данные2)
закрыть (ч)
Дополнительно получатели могут проверить, закрыт ли канал, назначив второй параметр выражению приема.
```иди
основная функция () {
ch := make(строка chan, 2)
иди говори ("Hello World", ch)
иди говори ("Привет снова", ch)
данные1: = <-ch
fmt.Println (данные1)
данные2, ок := <-ch
fmt.Println(данные2, ок)
закрыть (ч)
Если ok
равно false
, то больше нет значений для приема и канал закрывается.
В некотором смысле, это похоже на то, как мы проверяем, существует ли ключ на карте или нет.
Характеристики
Наконец, давайте обсудим некоторые свойства каналов.
- Отправка на нулевой канал блокируется навсегда:
```иди
var c chan строка
c <- "Привет, мир!" // Паника: все горутины спят - тупик!
- Получение от нулевого канала блокируется навсегда:
```иди
var c chan строка
fmt.Println(<-c) // Паника: все горутины спят - тупик!
- Паника посыла на закрытый канал:
```иди
var c = make(строка chan, 1)
c <- "Привет, мир!"
близко (с)
c <- "Привет, Паника!" // Паника: отправить по закрытому каналу
- Прием из закрытого канала немедленно возвращает нулевое значение:
```иди
var c = make(chan int, 2)
с <- 5
с <- 4
близко (с)
для я := 0; я < 4; я++ {
fmt.Printf("%d ", <-c) // Вывод: 5 4 0 0
- Диапазон по каналам
Мы также можем использовать for
и range
для перебора значений, полученных из канала:
```иди
основной пакет
импортировать "фмт"
основная функция () {
ch := make(строка chan, 2)
ч <- "Привет"
ч <- "Мир"
закрыть (ч)
для данных: = диапазон ch {
fmt.Println(данные)
Вместе с этим мы узнали, как горутины и каналы работают в Go. Я надеюсь, что это было полезно.
Выбирать
В этом уроке мы узнаем об операторе select в Go.
Оператор select
блокирует код и ожидает одновременного выполнения нескольких операций канала.
select
блокируется до тех пор, пока не сможет запуститься один из его кейсов, затем он выполняет этот кейс. Он выбирает один случайным образом, если готовы несколько.
```иди
основной пакет
импорт (
"ФМТ"
"время"
основная функция () {
один: = make (строка chan)
два := make(строка chan)
иди функ () {
время.Сон(время.Секунда * 2)
один <- "Один"
иди функ () {
время.Сон(время.Секунда * 1)
два <- "Два"
Выбрать {
случай результат := <-one:
fmt.Println("Получено:", результат)
случай результат := <-два:
fmt.Println("Получено:", результат)
Близкий)
близко (два)
Подобно switch
, select
также имеет случай по умолчанию, который запускается, если нет другого готового случая. Это поможет нам отправлять или получать без блокировки.
```иди
основная функция () {
один: = make (строка chan)
два := make(строка chan)
для х := 0; х < 10; х++ {
иди функ () {
время.Сон(время.Секунда * 2)
один <- "Один"
иди функ () {
время.Сон(время.Секунда * 1)
два <- "Два"
для х := 0; х < 10; х++ {
Выбрать {
случай результат := <-one:
fmt.Println("Получено:", результат)
случай результат := <-два:
fmt.Println("Получено:", результат)
По умолчанию:
fmt.Println("По умолчанию...")
time.Sleep(200 * time.Millisecond)
Близкий)
близко (два)
Также важно знать, что пустой select {}
блокируется навсегда.
```иди
основная функция () {
Выбрать {}
Близкий)
близко (два)
Это почти все, что касается оператора select
в Go.
группы ожидания
Как мы узнали ранее, горутины работают в одном и том же адресном пространстве, поэтому доступ к общей памяти должен быть синхронизирован.
Пакет sync
предоставляет полезные примитивы.
Итак, в этом уроке мы узнаем о группах ожидания.
По сути, WaitGroup помогает нам дождаться завершения нескольких горутин.
Мы можем использовать группу ожидания, используя следующие функции:
- Функция
Add(int)
принимает целочисленное значение, которое, по сути, представляет собой количество горутин, которых должна ожидать группа ожидания. Эта функция должна быть вызвана до того, как мы выполним горутину.
- Функция
Done()
вызывается внутри горутины, чтобы сигнализировать об успешном выполнении горутины.
- Функция
Wait()
блокирует программу до тех пор, пока все горутины, указанные вAdd()
, не вызовут изнутриDone()
.
Давайте рассмотрим пример:
```иди
основной пакет
импорт (
"ФМТ"
"синхронизировать"
функция работы () {
fmt.Println("работает...")
основная функция () {
var wg sync.WaitGroup
WG.Добавить(1)
иди функ () {
отложить wg.Done()
Работа()
wg.Подождите()
Если мы запустим это, мы увидим, что наша программа работает так, как ожидалось:
``` ударить
$ запустить main.go
работающий...
Мы также можем напрямую передать весовую группу в функцию:
```иди
функция работы (wg *sync.WaitGroup) {
отложить wg.Done()
fmt.Println("работает...")
основная функция () {
var wg sync.WaitGroup
WG.Добавить(1)
иди работай(&wg)
wg.Подождите()
Но важно знать, что группу ожидания нельзя копировать. И если это явно передается в функции, это должно быть сделано по указателю. Это потому, что это может повлиять на наш счетчик, что нарушит логику нашей программы.
Давайте также увеличим количество горутин и обновим функцию «Добавить» нашей группы ожидания, чтобы она ждала 4 горутины:
```иди
основная функция () {
var wg sync.WaitGroup
WG.Добавить(4)
иди работай(&wg)
иди работай(&wg)
иди работай(&wg)
иди работай(&wg)
wg.Подождите()
И, как и ожидалось, все наши горутины были выполнены:
``` ударить
$ запустить main.go
работающий...
работающий...
работающий...
работающий...
Это все для этого урока!
Мьютексы
В этом уроке мы узнаем о мьютексе.
Что такое мьютекс?
Мьютекс не позволяет другим процессам вводить критический раздел данных, пока процесс занимает его, чтобы предотвратить возникновение условий гонки.
Что такое критический раздел?
Таким образом, критическая секция может быть фрагментом кода, который не должен выполняться несколькими потоками одновременно, поскольку код содержит общие ресурсы.
Мы можем использовать Mutex, используя следующие функции:
- Функция
Lock()
получает или удерживает блокировку
- Функция
Unlock()
снимает блокировку.
- Функция
TryLock()
пытается заблокировать и сообщает, удалось ли это.
Давайте посмотрим на пример, мы создадим структуру Counter
и добавим метод Update
, который будет обновлять внутреннее значение:
```иди
основной пакет
импорт (
"ФМТ"
"синхронизировать"
тип Счетчик структура {
значение целое
func (c Counter) Update(n int, wg sync.WaitGroup) {
отложить wg.Done()
fmt.Printf("Добавление %d к %d
", n, c.value)
c.value += n
основная функция () {
var wg sync.WaitGroup
c := Счетчик{}
WG.Добавить(4)
перейти c.Update(10, &wg)
перейти c.Update(-5, &wg)
перейти c.Update(25, &wg)
перейти c.Update(19, &wg)
wg.Подождите()
fmt.Println(c.value)
Давайте запустим это и посмотрим, что произойдет:
``` ударить
$ запустить main.go
Добавление -5 к 0
Добавление 10 к 0
Добавление 19 к 0
Добавление 25 к 0
Результат 49
Это не выглядит точным, кажется, что наше значение всегда равно нулю, но каким-то образом мы получили правильный ответ.
Ну, это потому, что в нашем примере несколько горутин обновляют переменную value
. И, как вы, должно быть, догадались, это не идеально.
Это идеальный вариант использования Mutex. Итак, давайте начнем с использования sync.Mutex и поместим нашу критическую секцию между функциями Lock() и Unlock():
```иди
основной пакет
импорт (
"ФМТ"
"синхронизировать"
тип Счетчик структура {
m sync.Mutex
значение целое
func (c Counter) Update(n int, wg sync.WaitGroup) {
c.m.Lock()
отложить wg.Done()
fmt.Printf("Добавление %d к %d
", n, c.value)
c.value += n
см Разблокировать ()
основная функция () {
var wg sync.WaitGroup
c := Счетчик{}
WG.Добавить(4)
перейти c.Update(10, &wg)
перейти c.Update(-5, &wg)
перейти c.Update(25, &wg)
перейти c.Update(19, &wg)
wg.Подождите()
``` ударить
$ запустить main.go
Добавление -5 к 0
Добавление 19 к -5
Добавляем 25 к 14
Добавление 10 к 39
Результат 49
Похоже, мы решили нашу проблему, и вывод также выглядит правильно.
Пакет sync
До сих пор мы обсуждали sync.WaitGroup
и sync.Mutex
, но в пакете sync
доступно множество других функций, которые могут пригодиться при написании параллельного кода.
RWMutex
означает взаимное исключение чтения/записи и, по сути, то же самое, что иMutex
, но блокирует более одного процесса чтения или только процесс записи. Это также дает нам больший контроль над памятью.
- «Пул» — это набор временных объектов, к которым могут обращаться и сохранять многие горутины одновременно.
Once
— это объект, который выполняет действие только один раз.
Cond
реализует переменную условия, которая указывает горутины, ожидающие события или желающие объявить о событии.
Это все для этого урока, увидимся в следующем!
Следующие шаги
Надеюсь, что этот курс был отличным опытом обучения. Я хотел бы услышать отзывы от вас. Желаю вам всего наилучшего для дальнейшего обучения!
Также опубликовано [Здесь] (https://karanpratapsingh.com/blog/learn-go-the-complete-course)
Оригинал