[In one Paper]: Go
Ознакомление
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"
Контроль выполнения
К контролю выполнения относятся такие структуры как:
  1. Циклы (for)
  2. Условия (if / else if / if)
  3. Оператор перехода к метке (goto)
  4. Оператор условного переключения (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 - популярный формат задания цикла, где при инициализации цикла заданы сразу три составляющие:
  1. Итератор
  2. Условие для итератора
  3. Инкрементор/декрементор
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 существует три вида стандартных коллекций:
  1. Массив
  2. Слайс
  3. Хэшмапа
Стоит сразу же уточнить, что массивы в 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]
*/
В данном куске кода мы выполняем следующее:
  1. Объявляем массив величиной в 10 ячеек;
  2. Затем мы объявляем ссылку на первый элемент массива;
  3. Объявляем еще одну ссылку на первый элемент массива;
  4. Выводим массив и ссылки;
  5. Меняем первый элемент массива через прямое обращение (x[0]);
  6. Выводим массив и ссылки;
  7. Меняем первый элемент массива через ссылку;
  8. Выводим массив и ссылки;
Как мы можем увидеть если мы изменим данные в массиве, то они обновляются и в слайсе.
Если же мы изменим данные через слайс, то они обновятся и в массиве и соответственно в других слайсах тоже.
Слайсы, которые аллоцируют массив
В предыдущих примерах мы использовали слайсы для того чтобы они ссылались на уже существующий массив, однако это не всегда удобно. Иногда, нам нужен просто динамический список однотипных элементов и тут слайсы начинают действовать в полную силу.
Слайсы по сути это структура, которая ссылается на массив, содержит его длину и вместимость, а также ссылку на первый элемент:
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 выделяет память как для заголовка слайса, так и для базового массива, на который ссылается слайс. Размер выделяемой памяти зависит от длины и емкости слайса.
Вот обзор того, как выделяется память при создании нового слайса:
  1. Заголовок слайса размещается в стеке или куче в зависимости от контекста, в котором создается слайс. Заголовок содержит информацию о длине, емкости и адресе базового массива, а также указатель на первый элемент среза;
  2. Если длина и емкость среза равны нулю, то базовый массив не выделяется, а указатель среза устанавливается равным nil;
  3. Если длина среза больше нуля, тогда среда выполнения Go выделяет новый базовый массив в куче с размером, равным произведению длины среза и размера его типа элемента. Начальное содержимое массива устанавливается равным нулю для типа элемента;
  4. Если емкость среза больше длины, то среда выполнения Go резервирует дополнительную память для базового массива в пределах емкости среза. Это позволяет срезу расти, не требуя перераспределения базового массива.
Когда мы изменяем срез, добавляя к нему элементы, среде выполнения 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
Мы можем инициализировать хэшмапу двумя способами:
  1. С помощью make;
  2. С помощью "пустышки".
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 выполняются в конце функции, даже если она запаниковала.
В листинге вверху мы делаем следующее:
  1. Используем defer для того чтобы выполнить анонимную функцию;
  2. В конце выполнения main вызывается panic, которое прекращает ход выполнения программы;
  3. Затем выполняется анонимная функция, которую мы инициализировали и вызвали вместе с defer;
  4. Внутри данной анонимной функции выполняется recover, который восстанавливает ход выполнения программы и возвращает строку, которая была использована при вызове panic;
  5. Выполняется вывод строки, которая была передана при 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 ]
}
Теперь последовательно:
  1. Ссылка нужна для того чтобы достать адрес для переменной и не более;
  2. Мы не можем доставать значения с помощью ссылки и тем более изменять их. Ссылка нужна для того чтобы
    просто достать адрес памяти;
  3. Мы можем достать значение по переданному адресу и изменить его с помощью указателя, так как мы можем
    поставить перед ним оператор дереференсинга (переадресации) - *pointer
  4. Указатель объявляется с помощью ссылки;
  5. Тип указателя - *<исходный тип данных>;
  6. Мы можем достучаться до значения, которое лежит в памяти с помощью * перед указателем
    В случае вверху это: *pointer_to_arr;
  7. Если мы просто выведем указатель без *, то выведется ссылка, которая содержится в указателе.
Если вы еще не поняли отличия указателей от ссылок, то вот таблица:
ХарактеристикаУказатели (pointers)Ссылки (references)
ОбъявлениеИспользуют знак * перед типом данных, чтобы объявить указательИспользуют знак & перед переменной, чтобы получить ее адрес
Наличие значения по умолчаниюМогут быть nil, что указывает на отсутствие значенияНе могут быть nil, так как они всегда указывают на существующую переменную
РазыменованиеМогут быть разыменованы с помощью знака *, чтобы получить значение, на которое они указываютНе могут быть разыменованы, так как они представляют адрес для переменной
Использование в передаче аргументовИспользуются для передачи значений по ссылкеИспользуются для получения адреса переменной и передачи этого адреса в качестве аргумента функции
Безопасность при работе с памятьюНе гарантируют безопасность при работе с памятью и могут привести к ошибкам во время выполнения программыОбеспечивают безопасность при работе с памятью, так как они не позволяют производить операции над невалидными адресами
Использование для создания структур данныхМогут быть использованы для создания сложных структур данных, таких как связные списки или деревьяИспользуются в Go для создания «алиасов» на переменные, что позволяет иметь несколько имен для одной и той же переменной
Передача указателей
Теперь мы можем написать функцию, которая будет изменять элементы в нашем массиве, мы можем даже сделать это двумя способами:
  1. С помощью объявления указателя:
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
}
  1. С помощью передачи ссылки в массив:
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"
  ** Прошла секунда **

  ...
*/
Рекомендую к прочтению (референсы)