Ознакомление
Go - Компилируемый многопоточный язык программирования, разработанный Google. Данный язык программирования
используется для написания бэкенд-приложений, а также десктопного софта.
Go получил свою популярность благодаря легкости в освоении, хорошей экосистемы, а также обширной стандартной библиотеки
методов.
Установка
Я использую
asdf
- это утилита для управления версий компиляторов, REPL'ов и всего прочего. Устанавливается asdf
следующим образом:brew install asdf
Затем следующее нужно добавить в
.zshrc
:if command -v asdf &> /dev/null; then
. /opt/homebrew/opt/asdf/libexec/asdf.sh
fi
Данная команда позволяет проверить наличие
asdf
в системе. Если asdf
существует, то он подключает
автодополнение к команде.Установка Go
Далее нам нужно установить сам Go, для этого добавляем плагин для GoLang в asdf:
asdf plugin add golang
Затем смотрим все версии Go (с помощью команды
asdf list all golang
) и выбираем нужную нам (я остановился на 1.20.2):# Устанавливаем нужную нам версию
asdf install golang 1.20.2
# Делаем ее глобальной для системы
asdf global golang 1.20.2
Первая программа на Go
Начнем с чего-то легкого и напишем "Hello, World" на Go.
В отличие от других языков программирования (привет, Rust!), тут все достаточно тривиально:
main.go
package main
import "fmt"
func main() {
fmt.Println("Hello, World") // Hello, World
}
Каждый файл в Go описывает пакет. Главный пакет, с которого начинается исполнение программы называется
main
.
Как уже можно понять - мы можем объявить пакет c помощью ключевого слова package
.Далее с помощью ключевого слова
import
мы импортируем пакет fmt
, который нужен для форматирования строк, а также
вывода в стандартный вывод.Стартовая функция в Go называется
main
. С нее будет начинаться выполнение программы. В данном случае внутри main
мы
используем экспортированный метод из пакета fmt
- Println
для того чтобы вывести "Hello, World"
в стандартный
вывод.Объявление переменных
Переменные в Go могут задаваться двумя способами:
variable.go
// Первый способ
var hello_world string = "Hello, World!"
// Второй способ (shorthand syntax)
hi := "Hi!"
- Способ с
var
работает внутри функций и вне их. Мы можем задавать переменные в глобальном скоупе с помощью данного способа. - Способ с
:=
называется shorthand syntax, такой способ можно использовать только внутри функций, так как мы не задаем явного типа.
global-variable.go
hello := "Hello!" // Not OK
func main() {
hi := "Hi!" // OK
}
В случае когда нам нужна переменная в глобальном скоупе мы должны использовать
var
:global-variable.go
import "fmt"
var hello string = "Hello!" // OK!
func main() {
fmt.Println(hello)
}
Автоматическое приведение типа
Нам не обязательно указывать тип переменной явно при инициализации. Go сделает это за нас:
type.go
var hello_world = "Hello, World!" // type: string
Константы
Мы можем задавать константы с помощью способа с
var
:constant.go
const PI float32 = 3.14
ИнформацияВажно отметить, что мы должны явно указать тип при использовании ключевого словаconst
.
Типизация
Go - строготипизированный язык, это значит что у каждого значения есть свой тип данных.
Сейчас мы рассмотрим основные типы данных для данного языка программирования, которые идут
вместе со встроенной стандартной библиотекой.
Числа
Числа в Go бывают разных типов, далее идет таблица с перечислением типов и их описанием:
Тип | Описание |
---|---|
int8 | Целое число [-128 / 127] (8 бит) |
uint8 | Беззнаковое целое число [0 / 255] (8 бит) |
byte | Синоним типа uint8 , представляет целое число [0 / 255] (8 бит) |
int16 | Целое число [-32_768 / 32_767] (16 бит) |
uint16 | Беззнаковое целое число [0 / 65_535] (16 бит) |
int32 | Целое число [-2_147_483_648 / 2_147_483_647] (32 бита) |
uint32 | Беззнаковое целое число [0 / 4_294_967_295] (32 бита) |
rune | Синоним типа int32 , представляет целое число [-2_147_483_648 / 2_147_483_647] (32 бита) |
int64 | Целое число [–9_223_372_036_854_775_808 / 9_223_372_036_854_775_807] (64 бита) |
uint64 | Беззнаковое целое число [0 / 18_446_744_073_709_551_615] (64 бита) |
int | Знаковое целое число, которое в зависимости от платформы может занимать либо 4 байта (32 бита), либо 8 байт (64 бита). То есть соответствовать либо int32 , либо int64 . |
uint | Беззнаковое целое число, которое, аналогично типу int , в зависимости о платформы может занимать либо 4 байта (32 бита), либо 8 байт (64 бита). То есть соответствовать либо uint32 , либо uint64 . |
numbers.go
// Минимально возможный знаковый тип
var smallest int8 = -8
// Минимально возможный беззнаковый тип
var smallest_unsigned uint8 = 8
// Аналог int8 (алиас к int8)
var smallest_analogue byte = 32
// 16-битный тип числа
var small int16 =
// 16-битный беззнаковый тип
var small_unsigned f uint16 = 5
// 32-битный тип числа
var medium g int32 = -6
// 32-битный беззнаковый тип
var medium_unsigned j uint32 = 8
// Алиас к int32
var medium_analogue h rune = -7
// 64-битный тип числа
var large k int64 = -9
// 64-битный беззнаковый тип
var large_unsigned l uint64 = 10
// Платформозависимый тип
var platform m int = 102
// Платформозависимый беззнаковый тип
var platform_unsigned n uint = 105
Для чисел с плавающей точкой (рациональных чисел) существуют два типа -
float32
и float64
, их описание в виде
таблицы представлено внизу:Тип | Описание |
---|---|
float32 | Представляет число с плавающей точкой от [1.4*10 - 45 / 3.4*1038 (для положительных)] . Занимает в памяти 4 байта (32 бита) |
float64 | Представляет число с плавающей точкой от [4.9*10 - 324 / 1.8*10308 (для положительных)] . Занимает в памяти 8 байт (64 бита). |
float.go
var float_medium float32 = -1.23232;
var float_large float64 = 2.323409230;
В Go также существуют типы для комплексных чисел, они перечислены внизу:
Тип | Описание |
---|---|
complex64 | Комплексное число, где вещественная и мнимая части представляют числа float32 |
complex128 | Комплексное число, где вещественная и мнимая части представляют числа float64 |
complex.go
var complex_medium complex64 = 1+5i
var complex_large complex128 = 4+i
Булевый тип
Как и в других языках программирования в Go существует булев тип, выглядит он следующим образом:
boolean.go
var b1 bool = true
var b2 bool = false
Строки
Строки в Go имеют один тип (снова привет, Rust!) -
string
. В отличие от JavaScript строки должны быть заключены в
двойные кавычки (прим. "Hello!"
), также в строках могут быть следующие Escape-последовательности:Последовательность | Описание |
---|---|
\n | Переход на новую строку |
\r | Возврат каретки |
\t | Табуляция |
\" | Двойная кавычка внутри строк |
\\ | Обратный слеш |
string.go
var s string = "Hello, World!\n"
Контроль выполнения
К контролю выполнения относятся такие структуры как:
- Циклы (
for
) - Условия (
if / else if / if
) - Оператор перехода к метке (
goto
) - Оператор условного переключения (
switch
)
Данные структуры нужны для того чтобы управлять потоком выполнения.
Условия
Условия в Go реализуются через
if
, else if
и else
:conditions.go
package main
import "fmt"
var amount_of_people = 255
func main() {
// Условие
// Если количество людей четное - то условие выполнится
if amount_of_people % 2 == 0 {
fmt.Println("Amount of people is even!")
} else {
fmt.Println("Amount of people isn't even!")
}
}
// Amount of people isn't even!
Задание выражения перед условием
В большинстве конструкций Go мы можем задавать выражение перед выполнением конструкции, которая отвечает за управление
потоком выполнения.
if
в данном случае - не исключение, задание выражения перед выполнением if
выглядит следующим
образом:condition-statement.go
package main
import "fmt"
func main() {
if amount_of_people := 255; amount_of_people % 2 == 0 {
fmt.Println("Amount of people is even!")
} else {
fmt.Println("Amount of people isn't even!")
}
}
ИнформацияВажно: Для инициализации переменной используется shorthand syntax. Оно используется всегда.
ИнформацияТакже важно отметить что переменная инициализированная перед выполнением конструкции доступна только внутри данной конструкции, то есть скоупом данной переменной является скоуп конструкции.
Циклы
В отличие от других языков программирования в Go есть только один цикл -
for
. Он выполняет функции loop
, while
и for
соответственно.Loop
Loop - вечный цикл, который не имеет условия. Его можно прервать с помощью оператора
break
или просто запустить, чтобы
он выполнялся покуда программа не завершит свое выполнение💁🏻loop.go
package main
import "fmt"
func main() {
// Вечный цикл
for {
fmt.Println("Hello!")
}
}
While
While - цикл с внешним итератором. Такие циклы используют в случаях когда итератор уже был инициализирован и его
изменение может зависеть от других внешних факторов.
while.go
package main
import "fmt"
func main() {
// Цикл с внешним операндом
var i = 0;
for i < 10 {
fmt.Println(i) // Циклично: 0 1 2 3 4 5 6 7 8 9
i += 1
}
}
Важно отметить что мы можем задавать выражение перед началом цикла, как мы делали это для
if
.
Выглядит задание такого выражения следующим образом:while.go
package main
import "fmt"
func main() {
// Цикл с внешним операндом, который инициализирован перед условием цикла
for i := 0; i < 10; {
fmt.Println(i) // Циклично: 0 1 2 3 4 5 6 7 8 9
i += 1
}
}
For
For - популярный формат задания цикла, где при инициализации цикла заданы сразу три составляющие:
- Итератор
- Условие для итератора
- Инкрементор/декрементор
for.go
package main
import "fmt"
func main() {
// Стандартный цикл for
for i := 0; i < 10; i += 1 {
fmt.Println(i) // Циклично: 0 1 2 3 4 5 6 7 8 9
}
}
Switch
Switch - специальная конструкция, которая используется вместо связок
if / else if / else
.Синтаксис у
switch
следующий:switch.go
package main
import "fmt"
var amount_of_people = 255
func main() {
switch amount_of_people {
case 0: fmt.Println("There's no people")
case 100: fmt.Println("There's a hundred")
case 200: fmt.Println("There's a 2 hundred")
default: fmt.Println("Unknown Number")
}
}
Также
switch
может использоваться без аргумента, такой switch
всегда будет работать как switch true {}
.
Такой switch
будет искать первый кейс, который будет равен true
:switch-true.go
package main
import "fmt"
var amount_of_people = 255
func main() {
switch {
case amount_of_people < 0: fmt.Println("There's no people")
case amount_of_people <= 100: fmt.Println("There's a hundred or less people")
case amount_of_people <= 200: fmt.Println("There's a two hundred or less people")
case amount_of_people == 255: fmt.Println("There's exactly 255 people")
default: fmt.Println("Unknown number!")
}
}
Также, как уже можно было увидеть внутри кейсов можно использовать условия, но на этом Go не ограничивается,
мы также можем вызывать в условиях кейсов функции. Функции будут выполняться по мере достижения кейсов:
switch-true.go
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17: // Метод t.Hour() здесь не выполнится, если switch прервался на кейсе t.Hour() < 12
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
Коллекции
В Go существует три вида стандартных коллекций:
- Массив
- Слайс
- Хэшмапа
Стоит сразу же уточнить, что массивы в Go статические, то есть мы не можем изменять их вместимость или задавать
их размер динамически.
Массивы
Массивы задаются следующим образом:
array.go
package main
import "fmt"
func main() {
var arr [2]string = [2]string {"Hello", "World"}
fmt.Println(arr) // [Hello World]
}
Типы для массива формируются следующим образом:
[<количество ячеек>]<тип>
, как уже можно понять количество элементов
в массиве
является частью типа,
то есть [2]string
и [3]string
- являются разными типами данных.Если мы объявим массив и не инициализируем значения в нем, то массив инициализируется с дефолтными значениями, таким
образом разработчики Go попытались избежать ситуации, когда массив объявлен, но не инициализирован.
По умолчанию дефолтные значения для стандартных типов следующие:
Тип | Стандартное значение |
---|---|
Число (целое и с дробное) | 0 |
Комплексные числа | 0+0i |
Строка | "" |
Булево значение | false |
default-value.go
package main
import "fmt"
func main() {
var arr [2]int32
fmt.Println(arr[1]) // 0
}
Мы также можем сказать компилятору, чтобы он сам посчитал количество элементов в новосозданном массиве с помощью
следующего синтаксиса:
autolength.go
package main
import "fmt"
func main() {
arr := [...]string {"Hello", "World"}
fmt.Println(arr) // [Hello World]
}
Основное предназначение массивов - содержать элементы для слайсов.
Оператор range
С помощью оператора
range
можно удобно итерироваться по массивам:range.go
package main
import "fmt"
func main() {
arr := [...]int {1,2,3,4,5}
for index, value := range arr {
fmt.Println("Index: ", index, "\nValue: ", value, "\n")
}
}
// Output:
/*
Index: 0
Value: 1
Index: 1
Value: 2
Index: 2
Value: 3
Index: 3
Value: 4
Index: 4
Value: 5
*/
Если нам (допустим) не нужен индекс, то мы можем использовать специальное зарезервированное имя -
_
.
По умолчанию компилятор Go жалуется, когда мы объявили и не используем какую-то переменную, однако, компилятор
никогда не будет жаловаться, если мы не будем использовать _
:range-for.go
package main
import "fmt"
func main() {
arr := [...]int {1,2,3,4,5}
for _, value := range arr {
fmt.Println("Value: ", value)
}
}
// Output:
/*
Value: 1
Value: 2
Value: 3
Value: 4
Value: 5
*/
Слайсы
Слайсы - это указатели для массивов. Они не имеют статической "длины" и не содержат значений, они лишь ссылаются
на массивы.
Вот схематическая диаграмма как слайсы ссылаются на массивы:
Slice from 0th element to 3rd (not inclusive)
┌─────────────────┐ ┌─────────────────┐
│0:3 │ │0 │
│*pointer to array│ │ │
│ ├─────────────►│ 127 │
│ │ │ │
│ │ │ │
└───────────┬─────┘ ├─────────────────┤
│ │1 │
│ │ │
│ │ 355 │
│ │ │
│ │ │
│ ├─────────────────┤
│ │2 │
│ │ │
│ │ 899 │
└───────────────────►│ │
│ │
├─────────────────┤
│3 │
│ │
│ 417 │
│ │
│ │
└─────────────────┘
Мы можем создать слайсы с помощью нотации скобок
[первый индекс включительно: последний индекс невключительно]
:slice-link.go
package main
import "fmt"
func main() {
var x = [10]int {};
var y = x[0:1]
var z = x[0:1]
fmt.Println("First Output: ", x, y, z) // [0 0 0 0 0 0 0 0 0 0] [0] [0]
// Прямое изменение
x[0] = 1
fmt.Println("Second Output:", x, y, z) // [1 0 0 0 0 0 0 0 0 0] [1] [1]
// Изменение через ссылку
y[0] = 2
fmt.Println("Third Output: ", x, y, z) // [2 0 0 0 0 0 0 0 0 0] [2] [2]
}
/*
First Output: [0 0 0 0 0 0 0 0 0 0] [0] [0]
Second Output: [1 0 0 0 0 0 0 0 0 0] [1] [1]
Third Output: [2 0 0 0 0 0 0 0 0 0] [2] [2]
*/
В данном куске кода мы выполняем следующее:
- Объявляем массив величиной в 10 ячеек;
- Затем мы объявляем ссылку на первый элемент массива;
- Объявляем еще одну ссылку на первый элемент массива;
- Выводим массив и ссылки;
- Меняем первый элемент массива через прямое обращение (
x[0]
); - Выводим массив и ссылки;
- Меняем первый элемент массива через ссылку;
- Выводим массив и ссылки;
Как мы можем увидеть если мы изменим данные в массиве, то они обновляются и в слайсе.
Если же мы изменим данные через слайс, то они обновятся и в массиве и соответственно в других слайсах тоже.
Слайсы, которые аллоцируют массив
В предыдущих примерах мы использовали слайсы для того чтобы они ссылались на уже существующий массив, однако это
не всегда удобно. Иногда, нам нужен просто динамический список однотипных элементов и тут слайсы начинают действовать
в полную силу.
Слайсы по сути это структура, которая ссылается на массив, содержит его длину и вместимость,
а также ссылку на первый элемент:
type-struct.go
type Slice struct {
first_element: Pointer,
length: int,
capacity: int,
}
Мы можем использовать слайсы для динамического выделения памяти, "под капотом" они будут создавать массив в куче
и ссылаться на него, причем мы можем увеличивать количество элементов массива (или уменьшать его) и слайсы динамически
будут помогать нам выделять память:
dynamic-slice.go
package main
import "fmt"
func main() {
// В данном случае слайс создает массив под капотом, нам не нужно ничего указывать
arr := []string{"Hello", "World"}
fmt.Println(arr) // [ Hello World ]
// В данном случае массив не выделяется (то есть слайс ссылается на nil - нулеовой указатель)
empty := []string {}
// Мы можем добавить элемент к пустому слайсу и тогда Go автоматически выделит нам память под новый слайс
empty = append(empty, "Hello!")
fmt.Println(empty) // [ Hello! ]
}
make, copy, append
Существуют три метода, которые позволяют нам взаимодействовать со срезами:
make
- позволяет выделить память для слайса, создавая новый массив, где можно указать длину массива и его вместительность;copy
- используется для того чтобы скопировать срез в другую переменную;append
- используется для того чтобы смерджить два слайса или добавить к слайсу какой-то элемент.
slice-utility.go
package main
import "fmt"
func main() {
// Выделяем память под новый массив с помощью make
var slice = make([]string, 4)
fmt.Println(slice) // [ "" "" "" "" ]
slice[0] = "Hello"
slice[1] = ","
slice[2] = " "
slice[3] = "World"
fmt.Println(slice) // [ "Hello" "," " " "World" ]
// Копируем слайс в другой слайс
copied_slice := make([]string, len(slice) + 1)
copy(copied_slice, slice)
fmt.Println(copied_slice) // [ "Hello" "," " " "World" ]
// Дополняем слайс
copied_slice = append(copied_slice, "!")
fmt.Println(copied_slice) // [ "Hello" "," " " "World" "!" ]
}
Выделение памяти для слайса
В Go, когда мы создаем новый слайс, среда выполнения Go выделяет память как для заголовка слайса,
так и для базового массива, на который ссылается слайс. Размер выделяемой памяти зависит от длины и емкости слайса.
Вот обзор того, как выделяется память при создании нового слайса:
- Заголовок слайса размещается в стеке или куче в зависимости от контекста, в котором создается слайс. Заголовок содержит информацию о длине, емкости и адресе базового массива, а также указатель на первый элемент среза;
- Если длина и емкость среза равны нулю, то базовый массив не выделяется, а указатель среза устанавливается
равным
nil
; - Если длина среза больше нуля, тогда среда выполнения Go выделяет новый базовый массив в куче с размером, равным произведению длины среза и размера его типа элемента. Начальное содержимое массива устанавливается равным нулю для типа элемента;
- Если емкость среза больше длины, то среда выполнения Go резервирует дополнительную память для базового массива в пределах емкости среза. Это позволяет срезу расти, не требуя перераспределения базового массива.
Когда мы изменяем срез, добавляя к нему элементы, среде выполнения Go может потребоваться выделить новый базовый массив
и скопировать существующие элементы в новый массив. Это происходит, когда длина среза достигает емкости базового
массива. Новый массив обычно больше старого, чтобы обеспечить дальнейшее увеличение среза без перераспределения.
Вы также можете дополнительно почитать по теме выделения памяти на данных сайтах:
- Golang.org: The Go Programming Language Specification: Memory Allocation
- Blog Golang: Go Slices: usage and internals
- Povilasv.me: Go Memory Management
- Making.pusher: Understanding Go's Memory Management
- Ardanlabs.com: Memory Allocation in Go
Хэшмапы
Хэшмапы - это коллекция значений типа
[ключ: значение]
, данная структура данных удобна тем, что у нас есть мгновенный
доступ к данным (сложность нахождения нужных данных - O(1)).Разница между make, обычной инициализацией и объявлением
С помощью
make
мы можем инициализировать структуры и задавать им исходную длину:make-length.go
package main
import "fmt"
func main() {
arr := make([]int, 2)
fmt.Println(arr) // [ 0 0 ]
}
Мы также можем создать "пустышку" с помощью обычной инициализации:
empty-length.go
package main
import "fmt"
func main() {
arr := []int{}
fmt.Println(arr) // [ ]
}
Однако если мы просто объявим массив и попробуем назначить ему первый элемент, то получим ошибку во время исполнения
программы:
nil-arr.go
package main
import "fmt"
func main() {
var arr []int
fmt.Println(arr) // [ ]
arr[0] = 0; // ERROR
}
panic: runtime error: index out of range [0] with length 0
goroutine 1 [running]:
main.main()
/tmp/sandbox4123608687/prog.go:9 +0x65
Не смотря на то, что
fmt.Println(arr)
вывел нам [ ]
, массив до сих пор не инициализирован, а сам срез указывает
на nil
.ИнформацияСтоит запомнить, что если мы хотим изменять длину структуры (добавлять/удалять элементы), то нам всегда нужно инициализировать данную структуру с помощью "пустышки" или задавать исходную длину с помощьюmake
Создание хэшмапы с помощью
map
Мы можем инициализировать хэшмапу двумя способами:
- С помощью
make
; - С помощью "пустышки".
hashmap.go
package main
import "fmt"
func main() {
// Инициализация с помощью метода пустышки
empty_hashmap := map[string]int{}
// Инициализация с помощью make
make_hashmap := make(map[string]int)
fmt.Println(empty_hashmap, make_hashmap) // map[] map[]
}
Далее мы можем добавить новые ключи и значения для хэшмапы или прямо внутри
{}
(в случае метода с "пустышкой"),
или с помощью отдельного объявления с помощью hashmap["ключ"] = "значение"
:hashmap_key.go
package main
import "fmt"
func main() {
// Инициализация с помощью метода пустышки
empty_hashmap := map[string]int{
"Daniil": 20,
}
empty_hashmap["Slava"] = 21
// Инициализация с помощью make
make_hashmap := make(map[string]int)
make_hashmap["Daniil"] = 20
fmt.Println(empty_hashmap, make_hashmap) // map[Daniil:20 Slava:21] map[Daniil:20]
}
Удаление элементов из хэшмапы
Мы можем удалить поле из хэш-таблицы с помощью встроенного метода
delete
:delete-map.go
package main
import "fmt"
func main() {
ages := map[string]int {
"Daniil": 20,
}
fmt.Println(ages) // map[ Daniil:20 ]
delete(ages, "Daniil")
fmt.Println(ages) // map[]
}
Случай, когда ключа не существует в хэшмапе
Бывают случаи, когда мы не знаем есть ли ключ в хэшмапе. По умолчанию Go вернет нам нулевое значение на несуществующий
ключ:
existing-map-key.go
package main
import "fmt"
func main() {
ages := map[string]int {
"Daniil": 20,
}
fmt.Println(ages["Lena"]) // 0
}
В Go есть способ проверить есть ли значение лучше, нежели сравнение с нулевым значением, оно представлено внизу:
empty-map-value.go
package main
import "fmt"
func main() {
ages := map[string]int {
"Daniil": 20,
}
if age, is_exist := ages["Lena"]; is_exist {
fmt.Println(ages["Lena"])
} else {
fmt.Println("No value")
}
}
// Output: No value
Функции
Функции - своеобразные черные ящики, в которые передаются входные данные, а на выходе получаем обработанные выходные
данные. Функция также может не получать никаких входных данных и отдавать какие-то выходные данные, или наоборот,
получать входные данные и ничего не отдавать.
Функции в Go объявляются точно таким же образом, как мы все время до этого объявляли функцию
main
:function.go
func pretty_output(title string, description string) {
fmt.Println("Title: ", title)
fmt.Println("Description: ", description)
}
Рядом с каждым параметром в данном случае мы написали тип
string
(однако это делать для одних
и тех же типов не обязательно).Объединенные типы для аргументов
Если мы передаем в функцию несколько аргументов с одним и тем же типом, то мы можем указать тип после всех параметров
при инициализации, выглядит это вот так:
combined-arg.go
package main
import "fmt"
func pretty_output(title, description string) { // В данном случае и title, и description будут с типом string
fmt.Println("Title: ", title)
fmt.Println("Description: ", description)
}
Возвращаемые данные
Функция может возвращать данные, для этого нам достаточно просто указать тип, после скобок с параметрами.
Внизу предоставлен пример функции, которая складывает два числа:
return.go
package main
func sum(a, b int) int {
return a + b
}
Мы также можем вернуть несколько значений из функции с помощью специальной нотации:
package main
import "fmt"
func divideWithReminder(a int, b int) (int, int) {
var divide int = a / b
var reminder int = a % b
return divide, reminder
}
func main() {
d, r := divideWithReminder(11, 5)
fmt.Println(d, r) // 2 1
}
Возвращение данных по имени переменной
Мы можем вернуть переменную, которая была объявлена в скоупе функции, с помощью следующего синтаксиса:
return-var.go
package main
import "fmt"
func divideWithReminder(a int, b int) (divide int, reminder int) {
divide = a / b
reminder = a % b
return
}
func main() {
d, r := divideWithReminder(11, 5)
fmt.Println(d, r) // 2 1
}
ИнформацияВажно заметить, что мы не объявили переменные внутри функции, мы объявили их еще когда использовали нотациюfunc <название>(<параметры>) (<название выходных данных>)
Мы также можем использовать нотацию общего типа для возвращаемых данных:
package main
import "fmt"
func divideWithReminder(a int, b int) (divide, reminder int) {
divide = a / b
reminder = a % b
return
}
func main() {
d, r := divideWithReminder(11, 5)
fmt.Println(d, r) // 2 1
}
Spread-оператор
Подобно JavaScript (ES6), в Go тоже есть spread-оператор, который работает абсолютно так же:
spread.go
func arguments(args ...string) {
for _, v := range args {
fmt.Println(v)
}
}
func main() {
arguments("Hello", "World", "!")
}
// Output:
/*
Hello
World
!
*/
Замыкания
Как и в JavaScript - в Go есть замыкания. Замыкания это инициализация функции внутри другой функции. У внутренней
функции есть доступ к переменным внешней функции.
Мы можем вернуть функцию, которая замкнута внутри другой функции, тем самым при выполнении у замкнутой функции
(вложенной) будет доступ к переменным функции-родителя:
closure.go
func main() {
modifier := 5
add := func(x, y int) int {
return x + y + modifier
}
fmt.Println(add(1,1)) // 7
}
Вот пример с возвращением функции:
closure-return.go
package main
import "fmt"
func generate_add(modifier int) func(a int, b int) int {
// Объявляем новую функцию внутри другой функции (замыкание)
add_callback := func(a, b int) int {
return a + b + modifier
}
// Возвращаем внутренний коллбэк
return add_callback
}
func main() {
var add = generate_add(10)
fmt.Println(add(5, 7)) // 22
}
Как мы можем видеть функция
generate_add
возвращает другую функцию, тип которой мы явно прописали:
func (a int, b int) int
Defer, panic, recover
defer
- специальное ключевое слово, которое откладывает выполнение функции в самый конец.Отложенные функции образуют стек дополнительных вызовов, вызовы функции будут выполняться в обратном порядке,
так как стек работает по принципу "первый вошел - последний вышел".
Вот что будет, если вывести числа от 1 до 9 с помощью
defer
:defer.go
package main
import "fmt"
func main() {
for i, _ := range make([]int, 9) {
defer fmt.Println(i + 1)
}
}
// Output:
/*
9
8
7
6
5
4
3
2
1
*/
defer
всегда выполняется по завершению функции, даже если функция не завершилась (был вызван panic
из-за ошибки во
выполнения программы).defer
особенно полезен, когда нам нужно выполнить действие, которое понадобится сделать в конце функции. Например,
мы открыли файл и нам нужно будет закрыть его в конце:open-file.go
f, _ := os.Open(filename)
defer f.Close() // Закроет файл в конце выполнения функции
defer
также полезен когда мы используем его в связке с recover
:recover.go
package main
import "fmt"
func main() {
defer func() {
str := recover()
fmt.Println(str)
}()
panic("PANIC")
}
recover
нужна для того чтобы восстанавливать выполнение программы. Если программа запаникует
(вызовется метод panic
), то единственный способ восстановить ее работоспособность - метод recover
,
который при выполнении продолжит выполнение функций из стека.Проблема состоит в том, что после выполнения
panic
- ход выполнения программы останавливается, а значит
recover
никак не сможет выполниться. Тут к нам на помощь приходит defer
. Напомню, что функции вызванные с помощью
defer
выполняются в конце функции, даже если она запаниковала.В листинге вверху мы делаем следующее:
- Используем
defer
для того чтобы выполнить анонимную функцию; - В конце выполнения
main
вызываетсяpanic
, которое прекращает ход выполнения программы; - Затем выполняется анонимная функция, которую мы инициализировали и вызвали вместе с
defer
; - Внутри данной анонимной функции выполняется
recover
, который восстанавливает ход выполнения программы и возвращает строку, которая была использована при вызовеpanic
; - Выполняется вывод строки, которая была передана при
panic
.
ИнформацияАнонимные функции, которые инициализируются и сразу же выполняются - называются IIFE (Immediately Invoked Functional Expression). Подробнее о них можно почитать здесь
Указатели
Мы подобрались к одной из самых неприятных тем во всех языках программирования - указатели.
На самом деле указатели в Go устроены точно так же, как и в C/C++, с одним "но" - в Go нет арифметики для
указателей, то есть нам не придется ничего считать. Создатели Go посчитали что арифметика для указателей - довольно
опасная штука и не стали использовать ее в своем языке.
Передача аргумента == Копирование
По умолчанию в Go все переданные значения в функцию - копируются. Это значит что мы не можем их изменить внутри функции.
Это позволяет делать "чистые функции" (функции, которые не изменяют внешних состояний, а также отдают одни и те же
выходные данные для одних и тех же входных данных) намного проще.
Давайте попробуем поменять последний элемент массива на цифру
2
и увидим что из этого получится:pointer.go
package main
import "fmt"
func main() {
arr := [...]int {1, 2, 3}
change_last_element(arr)
fmt.Println(arr) // [ 1 2 3 ]
}
func change_last_element(arr [3]int) {
// Изменяет данные в копии массива
arr[2] = 2
}
Как мы можем увидеть Go вывел нам
[ 1 2 3 ]
, не смотря на то, что внутри функции change_last_element
мы попытались
изменить третий элемент (который равен 3
) на 2
.Изменения не отобразились потому что массив при передаче в
change_last_element
скопировался, а не передался по ссылке.Ссылки и указатели
По умолчанию у всех данных есть ссылка в памяти, так как все данные должны где-то хранится. Мы легко можем взять данную
ссылку - для этого нам достаточно просто написать
&
перед именем переменной:see-address.go
package main
import "fmt"
func main() {
arr := [...]int {1, 2, 3}
fmt.Printf("Address of array = %v: %p\n", arr, &arr) // Address of array = [1 2 3]: 0xc00001a018
}
ИнформацияОбратите внимание, что в листинге вверху мы используем методPrintf
для того чтобы вывести адрес переменной
Теперь когда мы знаем как достать адрес переменной - мы можем создать указатель.
Указатель
Указатель - специальный тип данных, который содержит в себе адрес памяти какой-либо переменной.
Отличия ссылки и указателя
- Ссылки в Go нужны для того чтобы передавать адрес памяти и создавать указатели.
- Указатели же нужны для того чтобы хранить адреса, которые они получили с помощью ссылок и изменять значения которые хранятся по ссылке, которую мы передали
link-vs-pointer.go
package main
import "fmt"
func main() {
// Мы создаем массив данных, мы можем получить ссылку на адрес памяти с помощью &arr
arr := [...]int {1, 2, 3}
// Мы создаем указатель, обратите внимание на тип
// Он всегда будет следующего синтакисиса: *<исходный тип данных>
var pointer_to_arr *[3]int = &arr
// Теперь мы можем использовать pointer_to_arr чтобы с помощью обращения по памяти
// посмотреть что лежит внутри переданного адреса:
fmt.Println(*pointer_to_arr) // [ 1 2 3 ]
}
Теперь последовательно:
- Ссылка нужна для того чтобы достать адрес для переменной и не более;
- Мы не можем доставать значения с помощью ссылки и тем более изменять их. Ссылка нужна для того чтобы
просто достать адрес памяти; - Мы можем достать значение по переданному адресу и изменить его с помощью указателя, так как мы можем
поставить перед ним оператор дереференсинга (переадресации) -*pointer
- Указатель объявляется с помощью ссылки;
- Тип указателя -
*<исходный тип данных>
; - Мы можем достучаться до значения, которое лежит в памяти с помощью
*
перед указателем
В случае вверху это:*pointer_to_arr
; - Если мы просто выведем указатель без
*
, то выведется ссылка, которая содержится в указателе.
Если вы еще не поняли отличия указателей от ссылок, то вот таблица:
Характеристика | Указатели (pointers) | Ссылки (references) |
---|---|---|
Объявление | Используют знак * перед типом данных, чтобы объявить указатель | Используют знак & перед переменной, чтобы получить ее адрес |
Наличие значения по умолчанию | Могут быть nil , что указывает на отсутствие значения | Не могут быть nil , так как они всегда указывают на существующую переменную |
Разыменование | Могут быть разыменованы с помощью знака * , чтобы получить значение, на которое они указывают | Не могут быть разыменованы, так как они представляют адрес для переменной |
Использование в передаче аргументов | Используются для передачи значений по ссылке | Используются для получения адреса переменной и передачи этого адреса в качестве аргумента функции |
Безопасность при работе с памятью | Не гарантируют безопасность при работе с памятью и могут привести к ошибкам во время выполнения программы | Обеспечивают безопасность при работе с памятью, так как они не позволяют производить операции над невалидными адресами |
Использование для создания структур данных | Могут быть использованы для создания сложных структур данных, таких как связные списки или деревья | Используются в Go для создания «алиасов» на переменные, что позволяет иметь несколько имен для одной и той же переменной |
Передача указателей
Теперь мы можем написать функцию, которая будет изменять элементы в нашем массиве,
мы можем даже сделать это двумя способами:
- С помощью объявления указателя:
package main
import "fmt"
func main() {
arr := [...]int {1, 2, 3}
pointer_arr := &arr
change_last_element(pointer_arr)
fmt.Println(arr) // [ 1 2 2 ]
}
func change_last_element(arr *[3]int) {
// Изменяет данные в переданном массиве через указатель
arr[2] = 2
}
- С помощью передачи ссылки в массив:
package main
import "fmt"
func main() {
arr := [...]int {1, 2, 3}
change_last_element(&arr)
fmt.Println(arr) // [ 1 2 2 ]
}
func change_last_element(arr *[3]int) {
// Изменяет данные в переданном массиве через указатель
arr[2] = 2
}
В случае вверху ссылка трансформируется в указатель автоматически. Мы передаем ссылку как аргумент, компилятор
видит, что аргументом является не ссылка, а указатель и превращает ссылку в указатель.
Структуры и интерфейсы
Структуры и интерфейсы это хороший способ упорядочить информацию.
Структуры
Допустим, что нам нужна структура, для того чтобы обозначить прямоугольник. У каждого прямоугольника есть высота и ширина.
Вот как будет выглядеть структура для прямоугольника:
square.go
package main
import "fmt"
type Rectangle struct {
width: uint
height: uint
}
func main() {
var first_rectangle Rectangle
}
В данном случае мы просто задали тип для переменной first_rectangle. Мы также можем сразу же инициализировать структуру:
square.go
package main
import "fmt"
type Rectangle struct {
width: uint
height: uint
}
func main() {
first_rectangle := Rectangle {
width: 20,
height: 30
}
}
Мы также можем использовать синтаксис без указания полей, в таком случае поля будут определяться по очередности объявления:
square.go
package main
import "fmt"
type Rectangle struct {
width: uint
height: uint
}
func main() {
first_rectangle := Rectangle {
20, // width
30 // height
}
}
Также есть еще один способ объявить объект структуры — с помощью make:
square.go
package main
import "fmt"
type Rectangle struct {
width: uint
height: uint
}
func main() {
first_rectangle := make(Rectangle)
first_rectangle.width = 20
fmt.Println(first_rectangle)
}
// Output:
/*
Rectangle {
width: 20,
height: 0
}
*/
Можно достать данные или изменить данные полей структуры с помощью нотации через точку,
как это показано вверху (
first_rectangle.width
)ИнформацияНужно обратить внимание, что при использовании make поля инициализируются с нулевыми значениями.
Если все данные в структуре одного и того же типа, то можно использовать сокращение для определения типов,
как это делается при объявлении параметров в функциях:
type HexColor struct {
r, g, b uint8
}
Методы
Мы конечно же можем использовать внешние функции для того чтобы взаимодействовать со структурой:
struct-func.go
package main
import "fmt"
type Rectangle struct {
width: uint
height: uint
}
func area(rect Rectangle) {
return rect.width * rect.height
}
func main() {
first_rectangle := Rectangle {
20, // width
30 // height
}
fmt.Println(area(first_rectangle)) // 600
}
Однако, есть более красивый способ объявить функции, которые связаны со структурой - методы.
Методы привязаны к структуре, поэтому мы тоже можем использовать их через нотацию с точкой.
Вот как выглядит объявление метода:
struct-method.go
type Rectangle struct {
width: uint
height: uint
}
func (rect *Rectangle) area() uint {
return rect.width * rect.height
}
ИнформацияНам не нужно использовать нотацию (*rect).width для того чтобы достать значение rect, а затем достать поле width, Go разименовывает переменную за нас, поэтому нам не нужно указывать оператор разименовывания *.
Как можно понять из примера выше мы передаем в метод
area
указатель на структуру Rectangle
.
Если у вас возник вопрос почему мы передали указатель, то ответ на него все тот же, что и раньше:
при передаче аргумента в функцию он не передается по ссылке, а копируется. Мы можем передать Rectangle
без указателя, но тогда в теле функции мы будем изменять копию нашей структуры, а не саму структуру.Встраивание структуры (наследование)
Есть случаи когда мы можем объявить абстрактную структуру, для того чтобы использовать поля и методы из нее:
package main
import "fmt"
type Human struct {
Name string
}
func (h *Human) talk(replic string) {
fmt.Println(h.Name, ": ", replic)
}
type Developer struct {
Human
Main_language string
}
func main() {
d := new(Developer)
d.Name = "Hannah"
fmt.Println(d)
d.talk("Hi!")
}
Данный подход называется наследованием (подобно наследованию из ООП). Следует обратить внимание что мы использовали
структуру Person без имени для поля, то есть написали следующее:
type Developer struct {
Human
}
Также в первом листиннге следует обратить внимание на ключевое слово
new
. Оно используется для того чтобы
создать указатель на новую структуру, где все поля будут равны нулевым значениям.Давайте сразу уясним, что следующие две записи абсолютно идентичны по смыслу и различаются только синтаксисом:
new.go
package main
import "fmt"
type Person struct {
name string
}
func main() {
person1 := new(Person)
person2 := &Person{}
fmt.Printf("Person1 type: %T\n", person1) // Person1 type: *main.Person
fmt.Printf("Person2 type: %T", person2) // Person2 type: *main.Person
}
Опциональные поля
Нередко нам требуются опциональные поля. Проблема Go в том, что при инициализации структуры все поля берут
нулевое значение, если им не были назначены другие значения во время инициализации:
nullish.go
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
p := Person{}
fmt.Println(p.Age) // 0
}
Бывают случаи, когда в опциональных полях должны хранится нулевые значения (
false
, 0
, ""
).
Понять было ли поле инициализировано с таким значением или пришел ли ответ от (условного) сервера - невозможно.
В этом случае для того чтобы избежать путанницы для всех опциональных полей - мы будем использовать указатели.
При инициализации, если для поля не было передано значение, а типизация поля содержит указатель - нулевое значение
для такого поля будет nil
(нулевой указатель/пустой указатель):optional.go
package main
import "fmt"
type Person struct {
Name *string
Age *int
}
func main() {
var name string = "Daniil"
p := Person{
Name: &name
}
fmt.Println(p) // {0xc000014270 <nil>}
fmt.Println(p.Age) // <nil>
}
В первом выводе программы нам вывелось два поля. Так как мы инициализировали поле
name
при инициализации -
нам вывелся адрес данного указателя. Вторым значением является <nil>
, так как мы явно не указали второе поле.Интерфейсы
Структура решает проблему с наличием у структур полей, интерфейсы же решают проблему с наличием у структур методов.
Интерфейс выглядит следующим образом:
type Shape interface {
area() int
}
Теперь мы можем использовать данный интерфейс для того чтобы структура могла принимать любую другую структуру,
которая имеет метод
area
в качестве поля:type Shape interface {
area() int
}
type Multiplier struct {
Shape_with_area Shape
}
Мы также можем передавать данный интерфейс как тип для параметра функции:
type Shape interface {
area() int
}
func some_calculation(s Shape) {
// ...
}
Параллелизм
Помните я говорил, что указатели одна из самых сложных тем? Так вот, данная тема тоже является одной из сложных.
Не спешите, закрывать статью и оставлять все на потом, сейчас мы рассмотрим параллелизм так, будто бы его объясняли 5-и летнему
ребенку.
Go осуществляет параллелизм с помощью goroutine и каналов. Рассмотрим данные термины подробнее.
Goroutine
Goroutine - это обычная функция, которая была вызвана с ключевым словом
go
. После указания ключевого слова go
- функция
стала асинхронной. Go не будет ждать выполнения данной функции, а сразу перейдет к следующей строке.ИнформацияИнтересно, что если последняя строка программы выполнится быстрее чем сама рутина (тут и далее будем называть goroutine - рутина), то рутина не выполнится вовсе - она просто не успеет, Go не станет ее дожидаться.
Внизу приведен пример простейшей рутины. Мы специально использовали Scanln, который читает пользовательский ввод
для того чтобы приостановить выполнение синхронного потока и наша рутина успела выполнится:
async.go
package main
import "fmt"
func f(n int) {
for i := 0; i < 10; i++ {
fmt.Println(n, ":", i)
}
}
func main() {
go f(0)
var input string
fmt.Scanln(&input)
}
/*
0 : 0
0 : 1
0 : 2
0 : 3
0 : 4
0 : 5
0 : 6
0 : 7
0 : 8
0 : 9
<пользовательский ввод>
*/
Если мы не поставим Scanln, то Go постарается провести столько итераций и выводов строки, сколько сможет до окончания
выполнения программы.
Если убрать
Scanln
и попробовать запустить программу, то вывод будет каждый раз разный:0 : 0
0 : 1
0 : 0
0 : 1
0 : 2
0 : 3
0 : 4
0 : 0
0 : 0
0 : 0
0 : 1
0 : 2
0 : 3
0 : 4
0 : 5
0 : 6
0 : 7
0 : 8
Каналы
Допустим, что у нас есть две рутины. Они абсолютно асинхронны. Читать данные вне этих рутин опасно, так как данные
вне рутин будут изменять вне зависимости от их выполнения, если мы так захотим.
Для того чтобы две рутины могли общаться придумали каналы.
Каналы представляют собой паттерн наблюдатель (его еще знают как PubSub). Это паттерн, где есть объекты, которые раздают
ивенты, а есть те, которые подписываются и слушают ивенты. Это можно представить как email рассылку. Вы зарегистрировались
на сайте и подписались на рассылку, теперь вам будут приходить письма, когда сайт захочет их вам отправить.
channel.go
package main
import (
"fmt"
"time"
)
func pinger(c chan string) {
for i := 0; ; i++ {
c <- "ping"
}
}
func printer(c chan string) {
for {
msg := <- c
fmt.Println(msg)
time.Sleep(time.Second * 1)
}
}
func main() {
var c chan string = make(chan string)
go pinger(c)
go printer(c)
var input string
fmt.Scanln(&input)
}
/*
Будет выводить "ping" каждую секунду
*/
Вверху мы используем пакет
time
и метод Sleep
для того чтобы сообщение в канал отправлялось каждую секунду, а не спамилось
вечно.ИнформацияЖелательно скомпилировать и выполнить листинг вверху для лучшего понимания работы каналов.
В данном листинге
printer
является подписчиком, а pinger
- отправителем. Они общаются через канал c
, который мы объявили
с помощью следующего синтаксиса:channel.go
var c chan string = make(chan string)
chan string
по сути является двойным типом. chan
- это непосредственно тип, который описывает канал, а string
в
данном случае описывает какого типа будут сообщения.Синтаксис
<-
используется для того чтобы отправить что-то в канал:c <- "ping"
И получить данные из канала:
var a string = <- c
// или
fmt.Println(<- c)
Блокирование
Одна из интереснейших тем - блокирование. Блокирование это механизм выполнения рутин, который отправляют что-то в канал.
Рутины которые отправляют данные в канал не будут выполняться, покуда принимающая рутина не будет готова,
то есть полностью не выполнится.
К слову, все рутины выполняются в том порядке, в котором мы их запустили. Давайте добавим еще одну функцию
postping
,
которая по идее должна будет выполниться сразу после ping
, но не тут то было:package main
import (
"fmt"
"time"
)
func pinger(c chan string) {
for i := 0; ; i++ {
c <- "ping"
}
}
func postping(c chan string) {
for i := 0; ; i++ {
c <- "postping"
}
}
func printer(c chan string) {
for {
msg := <- c
fmt.Println(msg)
time.Sleep(time.Second * 1)
}
}
func main() {
var c chan string = make(chan string)
go pinger(c)
go postping(c)
go printer(c)
var input string
fmt.Scanln(&input)
}
/*
Порядок вывода будет следующим:
Вывелся "ping"
** Прошла секунда **
Вывелся "postping"
** Прошла секунда **
НО НИКАК НЕ СЛЕДУЮЩИМ:
Вывелся "ping"
Вывелся "postping"
** Прошла секунда **
Вывелся "ping"
Вывелся "postping"
** Прошла секунда **
...
*/
Рекомендую к прочтению (референсы)