GoLang

Вольный и расширенный перевод репозитория Go Cheat Sheet на русский язык.


Другие ресурсы

Подборка открытых и бесплатных ресурсов для изучения Go на русском языке:

Бесплатные курсы:

Участники

Если вы нашли ошибку или хотите расширить список заметок, а также знаете другие источники для изучения, сообщите о них, внеся изменения через Pull Requests.

Источники

Большинство примеров кода исходного репозитория взяты из официального тура по Go, который является прекрасным введением для знакомства с языком.

Вы также можете использовать онлайн компилятор на официальном сайте или развернуть собственную песочницу Better Go Playground для запуска и проверки блоков кода.

Описание языка

Пакеты

Получение информации о функции

go doc fmt.Println

package fmt // import "fmt"

func Println(a ...any) (n int, err error)
    Println formats using the default formats for its operands and writes to
    standard output. Spaces are always added between operands and a newline
    is appended. It returns the number of bytes written and any write error
    encountered.

Инициализация проекта

mkdir test
cd test
go mod init test
# Windows
notepad main.go
# Linux
nano main.go

Стандартный ввод и вывод

Вывод в терминал (Print)

Стандартные методы Print и Println (добавляет символ переноса строки в конец вывода) из пакета fmt используются для вывода содержимое на экран.

package main

import "fmt"

func main() {
	// Базовый вывод
	fmt.Println("Hello \n你好, नमस्ते, Привет, ᎣᏏᏲ")
	// Многострочный строковый литерал без экранирования
	hellomsg := `
        "Hello" in Chinese is 你好 ('Ni Hao')\n
        "Hello" in Hindi is नमस्ते ('Namaste')
    `
	fmt.Println(hellomsg)
}

Запуск:

go run main.go

Вывод:

Hello 
你好, नमस्ते, Привет, ᎣᏏᏲ

        "Hello" in Chinese is 你好 ('Ni Hao')\n
        "Hello" in Hindi is नमस्ते ('Namaste')

Ввод с клавиатуры (Scan)

Метод Scan из пакета fmt используются для чтения данных из стандартного ввода. Он считывает значения и записывает их в переменные по указанным адресам (с использованием оператора &).

package main

import "fmt"

func main() {
	var name string
	var age int
	fmt.Print("Введите имя и возраст через пробел: ")
	// Считываем два значения
	_, err := fmt.Scan(&name, &age)
	// Проверяем на ошибку ввода
	if err != nil {
		fmt.Println("Ошибка ввода:", err)
	} else {
		fmt.Printf("Привет, %s! Тебе %d лет.\n", name, age)
	}
}

Табличный вывод (tabwriter)

Метод tabwriter.NewWriter из пакета text инициализирует фильтр, который перехватывает поток текста, ищет в нем знаки табуляции \t и заменяет их на нужное количество пробелов для создания ровных колонок.

package main

import (
	"fmt"
	"os"
	"text/tabwriter"
)

func main() {
    // Инициализация метода NewWriter
    // Параметры: целевой объект для вывода данных, мин. ширина 0, ширина таба 8,
    // кол-во символов разделителей 1, символ разделитель пробел и флаги настройки 0
	w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, ' ', 0)
	fmt.Fprintln(w, "PORT\tSTATUS\tSERVICE")
	fmt.Fprintln(w, "----\t------\t-------")
	fmt.Fprintln(w, "80/tcp\topen\thttp")
	fmt.Fprintln(w, "443/tcp\topen\thttps")
	fmt.Fprintln(w, "22/tcp\tclose\tssh")
	w.Flush()
}

Вывод похож на Format-Table из коллекций в PowerShell:

PORT    STATUS SERVICE
----    ------ -------
80/tcp  open   http
443/tcp open   https
22/tcp  close  ssh

Логирование (log)

Метод slog из пакета log предоставляет структурированный вывод (записывает логи в виде пар ключ-значение), который используется для отображения состояния работы приложения в современной формате.

package main

import (
	"log/slog"
	"os"
)

func main() {
	slog.Info("Сканирование завершено", "subnet", "192.168.1.0", "hosts", 25)
	// Инициализируем JSON обработчик
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	logger.Info("Пользователь вошел в систему", "userName", "root", "ipDddress", "192.168.1.1")
}

// 2009/11/10 23:00:00 INFO Сканирование завершено subnet=192.168.1.0 hosts=25
// {"time":"2009-11-10T23:00:00Z","level":"INFO","msg":"Пользователь вошел в систему","userName":"root","ipDddress":"192.168.1.1"}

Операторы

Арифметика

ОператорОписание
+сложение
-вычитание
*умножение
/деление *
%деление, возвращающие только остаток
&побитовое и
|побитовое или
^побитовое исключающее или * *
&^очистить бит (и нет) * * *
<<сдвиг влево * * * *
>>сдвиг вправо

* Если оба операнда имеют целый тип (int, int8, int32, int64), результат также будет целым числом, при этом остаток отбрасывается. Если хотя бы один из операндов имеет тип с плавающей точкой (float32, float64), результат будет дробным числом.

* * Возвращает 0, если биты двух операндов равны, или 1, если биты двух операндов различны.

* * * Возвращает 0, если соответствующий бит второго операнда равен 1, или бит первого операнда (0 или 1), если соответствующий бит второго операнда равен 0.

* * * * Сдвигает все биты числа влево на указанное количество позиций (аналог умножения числа на 2 в степени количества сдвигов), а новые биты справа заполняются нулями.

Сравнение

ОператорОписание
==равно
!=не равно
<меньше
<=меньше или равно
>больше
>=больше или равно

Логика

ОператорОписание
&&логическое и
||логическое или
!логическое отрецание

Другие

ОператорОписание
&указатель (адрес в памяти на переменную)
*разыменовать указатель
<-оператор отправки / получения

Переменные

Тип указывается после идентификатора (названия переменной):

var foo int                 // объявление без инициализации значения
var foo int = 42            // объявление с инициализацией
var foo, bar int = 42, 1302 // объявить и инициализировать несколько переменных одновременно
var foo = 42                // объявление с инициализацией с пропуском типа данных
foo := 42                   // сокращение при объявление переменной (ключевое слово var опущено, тип данных определяется автоматически, работает только внутри функций)
const constant = "Это константа, которая используется для хранения неизменяемых данных"

// iota можно использовать для увеличения числа, начиная с 0
const (
    _ = iota
    a
    b
    c = 1 << iota
    d
)
    fmt.Println(a, b) // 1 2 (0 - пропускается)
    fmt.Println(c, d) // 8 16 (2^3, 2^4)

Область видимости

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

func main() {
	var v int = 1
	{
		fmt.Println(v) // 1
        // Переопределяем переменную с новым типом данных во вложенном блоке
		var v string = "2"
		fmt.Println(v) // 2
	}
	fmt.Println(v) // 1
}

Функции

// Простая функция
func functionName() {}

// Функция с параметрами (тип идет после идентификаторов)
func functionName(param1 string, param2 int) {}

// Несколько параметров одного типа
func functionName(param1, param2 int) {}

// Объявление типа для возвращаемого значения (идет после скобок параметров или во вторых скобках, если значений несколько)
func functionName() int {
    return 42
}

// Может возвращать несколько значений одновременно
func returnMulti() (int, string) {
    return 42, "foobar"
}
var x, str = returnMulti()

// Возвращаем несколько именованных результатов
func returnMulti2() (n int, s string) {
    n = 42
    s = "foobar"
    // Будут возвращены все значения объявленных переменных "n" и "s"
    return
}
var x, str = returnMulti2()

func main() {
    // Присвоить функцию переменной
    add := func(a, b int) int {
        return a + b
    }
    // Используйте имя переменной для вызова функции
    fmt.Println(add(3, 4))
}

Замыкания

// Дочерние функции могут получить доступ к переменным, объявленным в родительской функции
func scope() func() int {
	outer_var := 2
	foo := func() int { return outer_var }
	return foo
}

func main() {
    // Функция возвращяет в результате дочернюю функцию
	test := scope()
    // Вызываем дочернюю функцию для получения ее результата
	fmt.Println(test())
}

func outer() (func() int, int) {
	outer_var := 2
	inner := func() int {
		outer_var += 99 // изменена переменная, взятая из внешней области
		return outer_var
	}
	inner()
	return inner, outer_var // вернуть результат дочерней функции (200) и переменной (101)
}

func main() {
	int1, int2 := outer()
	fmt.Println(int1(), int2)
}

Вариативные функции

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

package main

import "fmt"

// Функция принимаем любое количество аргументов с типом данных int и возвращяет 2 значения
func adder(args ...int) (int, int) {
	sum := 0
	// Перебирает все переданные аргументы в цикле
	for _, a := range args {
		sum += a
	}
	// Возвращяем 2 значения: количество аргументов и их сумму
	return len(args), sum
}

// Функция принимает любое количество аргументов с любым типом данных, используя пустой интерфейс interface{}
func other(args ...any) {
	for i := range args {
		fmt.Print(args[i], " ")
	}
}

func main() {
	fmt.Println(adder(2, 2))      // 2 4
	nums := []int{10, 20, 30}     // создаем срез для передачи в функцию
	fmt.Println(adder(nums...))   // 3 60
	other(1, 1.2, "string", true) // 1 1.2 string true
}

Типы данных

bool // логический тип (принимает true или false)

string // строка (текст)

int  int8  int16  int32  int64 // знаковые целые числа (signed integer), могут быть как положительными, так и отрицательными
uint uint8 uint16 uint32 uint64 uintptr // беззнаковые целые числа (unsigned integer), могут быть только положительными или равными нулю

byte // псевдоним для uint8 (диапазон значений от 0 до 255)

rune // псевдоним для типа int32, представляет собой кодовую точку (Unicode code point) - это число, соответствующее символу в стандарте Unicode
// В отличие от char в некоторых языках (например, C/C++), который обычно занимает 1 байт и хранит символы ASCII, rune занимает 4 байта
// и может хранить любой символ Unicode, включая буквы разных алфавитов, эмодзи и спецсимволы

float32 float64 // число с плавающей точкой одинарной и двойной точности

complex64 complex128 // комплексное число (1 + 2i или 3.14 + 4.2i), имеющие реальную и мнимую часть

interfae{} // универсальный тип, который может позволяет работать с переменными неизвестного или изменяющегося типа
Тип данныхОписаниеДиапазон значений
uint8Беззнаковые 8-битные целые числаот 0 до 255
uint16Беззнаковые 16-битные целые числаот 0 до 65535
uint32Беззнаковые 32-битные целые числаот 0 до 4294967295
uint64Беззнаковые 64-битные целые числаот 0 до 18446744073709551615
int8Знаковые 8-битные целые числаот -128 до 127
int16Знаковые 16-битные целые числаот -32768 до 32767
int32Знаковые 32-битные целые числаот -2147483648 до 2147483647
int64Знаковые 64-битные целые числаот -9223372036854775808 до 9223372036854775807

Все предварительно объявленные идентификаторы Go определены в пакете builtin.

Преобразование типов

var i int = 42
var f float64 = float64(i)  // преобразуем тип данных int в float64
var u uint = uint(f)        // преобразуем тип данных float64 в unit

// Альтернативный синтаксис
i := 42
f := float64(i)
u := uint(f)

Форматированние вывода

Printf

Форматирование вывода - это процесс преобразования данных из внутреннего представления (переменных, структур, чисел) в определенный текстовый формат для отображения на экран или записи в файл.

ФорматОписание
%Tвывод типа данных содержимого переменной
%pвывод значения указателя (адрес в памяти, в шестнадцатеричном виде)
%vуниверсальный спецификатор, для типа boolean (аналогичен %t), целочисленных типов (%d), чисел с плавающей точкой - %g, строк - %s
%#vвывод значения (структуру, срез, карту или другой тип) в виде Go-литерала (как его можно было бы записать в исходном коде Go) для отладки
%tвывод значений типа boolean (true или false)
%sвывод строки (string)
%fвывод чисел с плавающей точкой (float32 или float64)
%5fширина значения (если значение меньше ширины, то остаток заполняется пробелами)
%.2fточность остатока (выводит 2 цифры в дробной части после точки)
%dвывод целых чисел в десятичной системе
%oвывод целых чисел в восьмеричной системе
%bвывод целых чисел в двоичной системе
%cвывод символов, представленных числовым кодом (тип данных rune или byte) в формате их числовых/буквенных значений
%qвывод символов в одинарных кавычках (тип данных rune или byte в формате кодового значения Unicode)
%xвывод целых чисел в шестнадцатеричной системе, буквенные символы числа имеют нижний регистр a-f
%Xвывод целых чисел в шестнадцатеричной системе, буквенные символы числа имеют верхний регистр A-F
%Uвывод символов в формате кодов Unicode, например, U+1234
%eвывод чисел с плавающей точкой в экспоненциальном представлении, например, -1.234456e+78
%Eтоже самое что %e но в верхнем регистре, например, -1.234456E+78
package main

import (
	"fmt"
)

func main() {
	var a byte = 'A'
	var b rune = '1'
	var c rune = 49
	var d string = "123"
	var e string = "123"
	var f int = 123
	var g bool = true
	var h float32 = 1.23
	fmt.Printf(
		"%q (%T) \n %c (%T) \n %q (%T) \n %q (%T) \n %s (%T) \n %d (%T) \n %t (%T) \n %.1f (%T) \n",
		a, a, b, b, c, c, d, d, e, e, f, f, g, g, h, h,
	)
}

// 'A' (uint8) 
//  1 (int32) 
//  '1' (int32) 
//  "123" (string) 
//  123 (string) 
//  123 (int) 
//  true (bool) 
//  1.2 (float32)

Sprintf

Метод Sprintf может использоваться для преобразования целого числа в строку или округления целих дробных с помощью форматирования.

package main

import (
	"fmt"
	"strconv"
)

func main() {
	in := 42
	// Конвертируем число (тип данных int) в строку (тип данных string)
	str := fmt.Sprintf("%d", in)
	fmt.Printf("Значение: %s (тип данных: %T)\n", str, str)

	// Конвертируем строку обратно в число с помощью метода Atoi из пакета strconv
	num, err := strconv.Atoi(str)
	if err != nil {
		fmt.Println("Ошибка конвертации:", err)
		return
	}
	fmt.Printf("Значение: %d (тип данных: %T)\n", num, num)

	// Округлить дробную часть до двух чисел после запятой
	var z float64 = 100.123456789
	result := fmt.Sprintf("%.2f", z)
	fmt.Printf("%q", result)
}

// Значение: 42 (тип данных: string)
// Значение: 42 (тип данных: int)
// "100.12"

Структуры управления

Условия (if else)

func main() {
    // Базовый
    if x > 10 {
    	return x
    } else if x == 10 {
    	return 10
    } else {
    	return -x
    }

    // Возможно поставить одно утверждение перед условием
    if a := b + c; a < 42 {
    	return a
    } else {
    	return a - 42
    }

    // Утверждение (проверка) типа внутри условия
    var val interface{} = "foo"
    // Проверяется, содержит ли переменная val значение типа string
    if str, ok := val.(string); ok {
        // Если тип не совпадает, значение не вернется.
        // При этом panic не вызывается, т.к. используется безопасное утверждение типа (ok)
    	fmt.Println(str)
    }
}

Переключатели (switch)

После выполнения условия при использование переключателей, прерывания обрабатываются автоматически.

package main

import (
    "fmt"
    "os"
)

func main() {
    var operatingSystem string = runtime.GOOS
    // Используем оператор (ключевое слово) switch
    switch operatingSystem {
    case "darwin":
        fmt.Println("Используется macOS")
    case "linux":
        fmt.Println("Используется Linux")
    // Условие по умолчанию (аналог else в if)
    default:
        fmt.Println("Используется Windows, OpenBSD, FreeBSD или другая")
    }
}

// Как в случае с "for" и "if", возможно иметь оператор присваивания перед значением switch
switch os := runtime.GOOS; os {
    case "darwin": ...
}

// Возможно использовать сравнения
number := 42
switch {
    case number < 42:
        fmt.Println("Переданное значение:", number, "меньше 42 в условие")
    case number == 42:
        fmt.Println("Переданное значение:", number, "равно 42 в условие")
    case number > 42:
        fmt.Println("Переданное значение:", number, "больше 42 в условие")
}

// Все случаи могут быть представлены в виде списков, разделенных запятыми
var char byte = '?'
switch char {
    case ' ', '?', '&', '=', '#', '+', '%':
        fmt.Println("Переданное значение присутствует в списке")
}

Переключение типа

Переключение типа похоже на обычный оператор switch, но в условиях указывается типы (а не значения), которые сравниваются с типом значения, содержащегося в данном значении интерфейса.

package main

import "fmt"

func do(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Число %v равно %v по типу данных\n", v, v*2)
    case string:
        fmt.Printf("Значение %q равно %v bytes\n", v, len(v))
    default:
        fmt.Printf("Тип %T неизвестен\n", v)
    }
}

func main() {
    do(21)
    do("hello")
    do(true)
}

// Число 21 равно 42 по типу данных
// Значение "hello" равно 5 bytes
// Тип bool неизвестен

Сквозной переход (fallthrough)

Fallthrough - это поведение оператора switch, когда после выполнения кода для одного case выполнение не прерывается, а продолжается в следующий case, даже если он не совпадает, пока не встретит break или не закончится блок.

package main

import (
	"fmt"
)

func main() {
	num := 10
	switch num {
	case 10:
		fmt.Println("Число равно 10")
        // Переходим к следующему блоку case без проверки условия
		fallthrough
	case 20:
		fmt.Println("Этот блок тоже выполнится")
	case 30:
		fmt.Println("А этот — уже нет")
	}
}

// Число равно 10
// Этот блок тоже выполнится

Циклы (for)

В Go используются только универсальные циклы for, другие операторы (например, while или until) отсутствуют.

// Используется 9 интераций с 1 по 9 (до 10)
// for [инициализация счетчика]; [условие проверки счетчика для продолжение (если true) или остановки цикла]; [изменение счетчика]{
for i := 1; i < 10; i++ {
}
// Цикл (loop) - while
for ; i < 10;  {
}
// Если есть только условие, точки с запятой опускаются
for i < 10  {
}
// Если опустить условие, равноценно использованию бесконечного цикла while (true)
var i = 1
for {
    i++
    // Для остановки цикла используется оператор break (при достижение соблюдения условия)
    if i >= 10 {
        break
    }
}
    
// Использование пропуска и прерывания в цикле
// Метка here (произвольное имя) позволяет указать целевой цикл, на который будут ссылаться операторы continue и break
here:
    // Используем 2 интерации во внешнем цикле
    for i := 0; i < 2; i++ {
        // Используем 2 интерации во внутреннем цикле 
        for j := i + 1; j < 3; j++ {
            if i == 0 {
                // Пропустить интерацию внешнего цикла по названию его метки
                continue here
            }
            fmt.Println(j)
            if j == 2 {
                // Завершить внутренний цикл
                break
            }
        }
    }

// 1-я интерация: внешний цикл с значением i=0 в внутреннем цикле пропускает интерацию внешнего цикла, т.к. срабатывает условие i==0
// 2-я интерация: внешний цикл с значением i=1 в внутреннем цикле пропускает условие i==0
// Переменная j получает значение 2, которое печатается и завершает внутренний (текущий) цикл во втором условие
// Программа завершается, т.к. интерации внешнего цикла закончились

there:
    for i := 0; i < 2; i++ {
        for j := i + 1; j < 3; j++ {
            if j == 1 {
                // Пропускаем интерацию внутреннего цикла
                continue
            }
            fmt.Println(j)
            if j == 2 {
                // Завершаем выполнение внешнего цикла
                break there
            }
        }
    }

// 1-я интерация внешнего цикла начинается с i=0, в внутреннем цикле j=1 пропускает первую интерацию
// 2-я интерация внутреннего цикла пропускает первое условие, печатает на экран текущее значение j=2 и во втором условие завершает внешний цикл

Примеры циклов

package main

import "fmt"

// Функция, возвращающая название месяца через условную конструкцию switch
func getMonthName(month int) string {
    switch month {
    case 1:
        return "January"
    case 2:
        return "February"
    case 3:
        return "March"
    case 4:
        return "April"
    case 5:
        return "May"
    case 6:
        return "June"
    case 7:
        return "July"
    case 8:
        return "August"
    case 9:
        return "September"
    case 10:
        return "October"
    case 11:
        return "November"
    case 12:
        return "December"
    default:
        return "Invalid month (range: 1-12)"
    }
}

// Второй вариант функции через классическое условие по индеку массива
func getMonthName2(month int) string {
    months := []string{"", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    if month >= 1 && month <= 12 {
        return months[month]
    }
    return "Invalid month"
}

func main() {
    // Классический цикл из 13-ти итераций
    for i := 1; i <= 13; i++ {
        fmt.Printf("Month %d: %s\n", i, getMonthName(i))
    }

    // Увеличение индекса итерации в теле цикла
    j := 1
    for j <= 13 {
        fmt.Printf("Month %d: %s\n", j, getMonthName(j))
        j++
    }

    // Бесконечный цикл
    months := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
    k := 0
    for {
        // Пропускаем итерацию, если 6-й месяц (5-й индекс)
        if k == 5 {
            k++ // переход к следующей итерации
            continue
        }
        // Выходим из цикла, если индекс больше или равен длине массива
        if k >= len(months) {
            break
        }
        fmt.Printf("Month %d: %s\n", months[k], getMonthName(months[k]))
        k++
    }

    // Конструкция range используется для перебора всех элементов в коллекциях (срезах, картах и каналах)
    // for index, element := range array {}
    // range возвращает копию элемента, а не сам элемент массива, чтобы изменить содержимое массива, необходимо обращаться к элементам по индексу
    for _, month := range months {
        fmt.Printf("Month %d: %s\n", month, getMonthName(month))
    }

    // Индекс может использоваться для карты (map) как ключ
    m := map[string]int{"a": 1, "b": 2, "c": 3}
    for index, value := range m {
        fmt.Println("Key:", index, "Value:", value)
    }

    // Перебор строки по символам
    s := "string"
    for index, char := range s {
        fmt.Println("Index:", index, "Char:", string(char))
    }

    // Перебор канала
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3
    close(ch)
    for val := range ch {
        fmt.Println(val)
    }
}

Типы последовательностей

Типы последовательностей представляют собой структуры данных, хранящие упорядоченные наборы значений.

Массивы (array)

Массив (статические срезы) - фиксированная по размеру последовательность элементов (arr[10]).

var a [10]int // объявить массив int длиной 10 (длина массива является частью типа)
a[3] = 42     // присвоить значение элементу, по его порядковому номеру
i := a[3]     // прочитать элементы

// Возможные варианты объявление с инициализацией значений
var a = [2]int{1, 2}
// Массив из двух элементов: [1 2]
a := [2]int{1, 2}
// Многоточие используется компилятором для вычисления длины массива
a := [...]int{1, 2}

Срезы (slice)

Срез (динамические массивы) - это последовательность элементов одного типа с динамической структурой, которая может быть получена из массивов или других срезов с помощью операции среза (arr[start:end]). Срезы могут иметь явное указание длины и емкости, которые можно задать с помощью встроенный функции make, например, чтобы инициализировать элементы среза нулевыми значениями или заранее выделить нужное количество памяти (количество элементов в срезе не ограничивается, но потребует выделения новой памяти).

package main

import "fmt"

func main() {
	var a []int               // объявить срез
	var b = []int{1, 2, 3, 4} // объявить и инициализировать срез
	c := []int{7, 8, 9, 10}   // [7 8 9 10]
	fmt.Println(a)
	fmt.Println(b)
	fmt.Println(c)
	chars := []string{"a", "b", "c"} // срез из букв [a b c]
	fmt.Println(chars)
	chars = []string{0: "a", 2: "b", 1: "c"} // задать порядок индексов при инициализии среза [a c b]
	fmt.Println(chars)

	fmt.Println(b[1:4]) // срез c индекса 1 по 3 (до 4)
	fmt.Println(b[:3])  // срез c индекса 0 по 2
	fmt.Println(b[1:])  // срез с индекса 1 по 3 (длинна среза/массива - len(a))
	b = append(b, 5, 6) // добавление элементов к срезу a с помощью функции append
	fmt.Println(b)      // [1 2 3 4 5 6]

	c = append(b, c...) // объединение срезов b и c
	fmt.Println(c)      // [1 2 3 4 5 6 7 8 9 10]

	a = append(c[:1], c[9:10]...) // удаление элементов из среза
	fmt.Println(a)                // [1 10]

	e := make([]byte, 5, 10) // первый аргумент длина (будет содержить 5 элементов с значением 0, по умолчанию), второй емкость
	fmt.Println(len(e))      // 5
	fmt.Println(cap(e))      // 10
	e = make([]byte, 5)      // Если не указывать емкость, то она будет равна длинне среза
	fmt.Println(cap(e))      // 5

	x := []int{1, 2, 3}                        // Исходный срез (откуда копировать)
	y := make([]int, 3)                        // Новый срез с заданной длинной
	n := copy(y, x)                            // Возвращяет число скопированных объектов
	fmt.Printf("a = %v\n", x)                  // a = [1 2 3]
	fmt.Printf("b = %v\n", y)                  // b = [1 2 3]
	fmt.Printf("Скопировано %d элемента\n", n) // Скопировано 3 элемента
}

Операции с срезами

Указатель — ссылается на первый элемент массива, доступный через срез (может не совпадать с началом самого массива).

Длина (length) — количество элементов в срезе.

Емкость (capacity) — общее количество элементов от начала среза до конца базового массива, на котором основан срез.

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

package main

import (
	"fmt"
)

func check(baseArray [10]int, baseSlice []int) {
	fmt.Printf("Базовый массив: %v\n", baseArray)
	fmt.Printf(
		"Элементы среза: %v\nДлинна среза: %d \nEмкость среза: %d\n",
		baseSlice,
		len(baseSlice),
		cap(baseSlice),
	)
}

func main() {
	baseArray := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	// Создаем срез из массива
	baseSlice := baseArray[5:8]
	check(baseArray, baseSlice)
	// Элементы среза: [5 6 7]
	// Длинна среза: 3 (6, 7 и 8 индексы базового массива)
	// Eмкость среза:  5 (с 6-го индекса по 10-й)
	// Фиксируем адрес элемента массива, на который ссылается срез
	pointer := fmt.Sprintf("%p", baseSlice)
	// Добавляем новы элемент в срез (присваиваем значение 10 для следующего порядкового индекса 4 в срезе)
	baseSlice = append(baseSlice, 10)
	check(baseArray, baseSlice)
	// Базовый массив: [0 1 2 3 4 5 6 7 10 9] (изменился 9-й индекс в базовом массиве)
	// Элементы среза: [5 6 7 10]
	// Длинна среза: 4
	// Eмкость среза: 5
	// Проверяем, что адрес среза не измелися
    fmt.Println(pointer == fmt.Sprintf("%p", baseSlice)) // true
    // Добавляем элементы свыше емкости среза
	baseSlice = append(baseSlice, 11, 12)
	check(baseArray, baseSlice)
	// Базовый массив не изменился, вместо этого создался новы срез основанный на массиве большего объема
	// Элементы среза: [5 6 7 10 11 12]
	// Длинна среза: 6
	// Eмкость среза: 10
	fmt.Println(pointer == fmt.Sprintf("%p", baseSlice)) // false
}

Диапазоны (range)

Диапазон используется для перебора индексов и элементов массива в цикле.

// Цикл по массиву/срезу
for i, e := range a {
    // "i" — индекс, "e" — элемент
}

// Если нужен только элемент "e"
for _, e := range a {
    // Индекс опускается
}

// Если нужен только индекс
for i := range a {
}

Карта (map)

Маппинг используется для хранения и сопоставления данных.

package main

import "fmt"

type Custom struct {
	s string
	i int
}

func main() {
	m := make(map[string]int) // объявить карту (словарь) где ключ имеет тип данных string а значение int
	m["key"] = 42             // инициализировать карту одним "ключ-значение"
	fmt.Println(m["key"])     // вывести содержимое значения (value) по его уникальному названию клчюча

	delete(m, "key")      // удалить элемент из карты
	elem, ok := m["key"]  // проверка, если ключ присутствует, то получить его значение
	fmt.Println(ok, elem) // false 0
	if ok {
		fmt.Println("Value:", elem)
	} else {
		fmt.Println("Key not found")
	}

	// Создаем карту с значениями массива данных
	var m2 = map[string][]int{
		"Microsoft": {100, 200, 300},
		"Google":    {400, 500},
	}

	// Перебрать содержимое карты (словаря) в цикле
	for key, value := range m2 {
		fmt.Print(key, ": ")
		// Перебрать содержиме массива (среза)
		for _, v := range value {
			fmt.Print(v, "; ")
		}
		fmt.Println()
	}

	// Используем предопределенную структуру для значений
	var m3 = map[int]Custom{
		1: {"line: ", 1},
		2: {"line: ", 2},
	}

	for key, value := range m3 {
		fmt.Print("Key: ", key, "; ")
		fmt.Println("Value:", value)
	}

	// Используем динамическую структуру данных для значений
	obj := make(map[string]interface{})

	// Заполняем значения разными типами данных
	obj["int"] = 1
	obj["string"] = "line"
	obj["float"] = 1.23
	obj["bool"] = true
	obj["slice"] = []int{1, 2, 3}
	obj["map"] = map[string]string{"key": "value"}

	// Выводим содержимое карты
	for k, v := range obj {
		fmt.Printf("%s: %v\n", k, v)
	}
}

Сортировка (sort)

Map реализована как хэш-таблица, и при переборе в цикле порядок элементов будет рандомным, даже если структура не меняется.

Для опредиления порядка, необходимо сначала извлечь ключи из map в срез (slice), отсортировать этот срез с помощью функции sort и затем итерироваться по отсортированным ключам.

package main

import (
	"fmt"
	"sort"
)

func main() {
	m := map[string]int{
		"banana": 2,
		"apple":  1,
		"cherry": 3,
	}

	for k, v := range m {
		fmt.Println(k, v)
	}

	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}

	sort.Strings(keys)

	for _, k := range keys {
		fmt.Println(k, m[k])
	}
}

Структуры (struct)

Вместо классов (class) в Go используются структуры (struct), которые являются новым типом данных комбинированных значений, а также могут содержать методы. Поля структуры всегда инициализируются нулевыми значениями при ее объявлении.

package main

import (
	"fmt"
)

// Определение структуры Vertex
type Vertex struct {
	X, Y float64
}

// Создаем метод для переданной структуры, который может оперировать ее элементами
func (v Vertex) sum() float64 {
	v.X = v.X + v.Y
	return v.X
}

// Мутирующий метод, изменяющий поля переданной структуры
// Метод получает указатель на структуру (тип *Vertex), а не копию структуры (тип Vertex)
func (v *Vertex) add(n float64) {
	v.X += n
	v.Y += n
}

func main() {
	// Создание и инициализация структуры Vertex без указания ключей
	vertex := Vertex{1, 2}
	// Создание среза структур Vertex с несколькими элементами
	vSlice := []Vertex{{1, 2}, {5, 2}, {5, 5}}
	// Инициализация структуры с указанием ключей полей
	vertex2 := Vertex{X: 1, Y: 2}
	// Доступ и изменение значения поля структуры
	vertex2.X = 4

	fmt.Println(vSlice)
	// [{1 2} {5 2} {5 5}]
	fmt.Println(vSlice[0])
	// {1 2}
	fmt.Println(vSlice[0].X)
	// 1
	fmt.Println(vSlice[0].Y)
	// 2
	fmt.Println(vertex2)
	// {4 2}

	fmt.Println(vertex)
	// {1 2}
	fmt.Println(vertex.sum())
	// 3
	fmt.Println(vertex.X)
	// 1
	vertex.add(3)
	fmt.Println(vertex)
	// {4 5}
	fmt.Println(vertex.sum())
	// 9
}

Пример использования структуры и перебор элементов в цикле.

package main

import "fmt"

type test struct {
	s string
	i []int
	m map[string]int
}

func main() {
	var arr []test
	arr = append(arr, test{"str1", []int{1, 2, 3}, map[string]int{"year": 2025, "mount": 6, "day": 1}})
	arr = append(arr, test{"str2", []int{4, 5, 6}, map[string]int{"year": 2035}})
	for _, e := range arr {
		fmt.Println(e.s)
		for _, v := range e.i {
			fmt.Println(v)
		}
		for key, val := range e.m {
			fmt.Printf("%s: %d\n", key, val)
		}
	}
}

Анонимные структуры

В отличии от map[string]interface{}, анонимные структуры имеют строгую типизацию, что уменьшает ошибки и повышает производительность, но его нельзя использовать в разных местах без дублирования объявления.

package main

import "fmt"

func main() {
	point := struct {
		X, Y int
		S    string
	}{1, 2, "test"}
	fmt.Println(point)
}

Указатели

Аргументы в функциях и методах всегда копируются, указатели позволяют работать напрямую с содержимым переданных переменных и структурами данных, без копирования их содержимого (изменяя оригинальную переменную).

Оператор & используется для взятия адреса из памяти, а не значения самой переменной.

Функция new() выделяет память для указанного типа и возвращает переменной указатель на него, в отличие от оператора &, который возвращает указатель на существующую переменную.

package main

import (
	"fmt"
)

func test(param *int) {
	*param = 1
}

func main() {
	a := 100        // объявляем и инициализируем переменную
	var b *int = &a // используем "b" как указатель (или просто b := &a) на значение переменной "a"
	fmt.Println(b)  // 0xc000104040
	*b++            // изменяем значение переменной "a", ссылаясь по указателю "*"
	fmt.Println(a)  // 101
	c := &b         // создать указатель на указатель
	**c++           // изменить значение
	fmt.Println(a)  // 102

	d := new(int)   // выделяем память для переменной типа int
	test(d)         // передаем указатель в функцию
	fmt.Println(*d) // 1
}
p := Vertex{1, 2}  // "p" - это структура Vertex
q := &p            // "q" указывает на структуру Vertex
r := &Vertex{1, 2} // "r" также указывает на структуру Vertex

// Объявление переменной с указателем на структуру *Vertex
var s *Vertex = new(Vertex) // функция "new" создает указатель на новый экземпляр структуры с пустыми значениями &{0 0}

Интерфейсы (interface)

Интерфейс - это набор методов (требований), которые должен иметь тип, чтобы соответствовать этому интерфейсу.

// Создаем пустой интерфейс (принимает произвольное количество значений любого типа)
s := []interface{}{"a", 2, "c", 4, "e"}
s = []any{"a", 2, "c", 4, "e"} // []any{} это alias для []interface{}{}
fmt.Println(s)                 // [a 2 c 4 e]
fmt.Println(s...)              // распаковываем: a 2 c 4 e


// Объявление интерфейса с одинм методом Awesomize(), который возвращает строку
type Awesomizer interface {
    Awesomize() string
}

// Обычная структура, которая может реализовывать методы
type Foo struct {}

// Добавление (реализация) метода Awesomize() в структуре Foo
// Тип автоматически соответствует интерфейсу, если он реализует все его методы
func (foo Foo) Awesomize() string {
    return "Awesome!"
}

Встраивание

В Go нет подклассов, вместо этого используется встраивание интерфейса и структуры, которое добавляет методы встроенной структуры к внешней.

// В структуру Server встраиваются все методы, которые есть у метода Logger из структуры log
type Server struct {
    Host string
    Port int
    *log.Logger
}

// Структура Server инициализируется с помощью указателя на log.Logger
server := &Server{"localhost", 80, log.New(...)}

// Когда вызывается server.Log(...), Go автоматически перенаправляет вызов к server.Logger.Log(...).
server.Log(...)

// Поле встроенного типа доступно через его имя, по этому переменной можно присвоить ссылку на server.Logger
var logger *log.Logger = server.Logger

Обработка ошибок

Обработка исключений отсутствует. Вместо этого функции, которые могут выдать ошибку, просто объявляют дополнительное возвращаемое значение типа error (чаще всего вторым возвращаемым параметром).

Встроенный тип интерфейса error — это общепринятый интерфейс для представления состояния ошибки, при этом нулевое значение не представляет ошибки.

type error interface {
    Error() string
}

Пример:

package main

import (
    "errors"
    "fmt"
    "math"
)

// Определение функции sqrt должно быть вне main
func sqrt(x float64) (float64, error) {
    if x < 0 {
        // Создаем объект типа error с текстовым описанием ошибки
        return 0, errors.New("ошибка: отрицательное значение")
    }
    return math.Sqrt(x), nil
}

func main() {
    val, err := sqrt(-1)
    if err != nil {
        // Обработка ошибки
        fmt.Println(err) // отрицательное значение
        return
    }
    // Если все хорошо (переданное значение не отрицательное), вывести содержимое "val"
    fmt.Println(val)
}

Параллелизм

Горутины

Горутины — это легковесные потоки (управляемые Go, а не потоками ОС).

go f(a, b) запускает новую горутину, которая запускает f (при условии, что f — это функция).

// Просто функция (которая позже может быть запущена в горутине)
func doStuff(s string) {
    fmt.Println(s)
}

func main() {
    // Запуск существующий функции в горутине по ее имени
    go doStuff("foobar")

    // Использование анонимной внутренней функции в горутине
    go func (x int) {
        fmt.Println(x)
    } (42) // Параметр анонимной функции
}

Синхронизация

Пакет sync используется для ожидания завершения всех запущенных горутин.

Ключевое слово defer (откладывать) используется для планирования выполнения указанной внешней функции непосредственно на момент выхода из текущей (вызывающей) функции (альтернатива try...finally).

package main

import (
    "fmt"
    "sync"
)

func doStuff(s string, wg *sync.WaitGroup) {
    fmt.Println(s)
    defer wg.Done()
}

func main() {
    // Объект для отслеживания завершение групп горутин
    var wg sync.WaitGroup

    // Задаем счетчик для запуска 2-х горутин
    wg.Add(2)

    // Запуск функции в горутине
    go doStuff("foobar", &wg)

    // Запуск анонимной функции в горутине
    go func(x int, wg *sync.WaitGroup) {
        fmt.Println(x)
        // Уменьшить счетчик WaitGroup, когда горутина завершится
        defer wg.Done()
    }(42, &wg)

    // Ожидание завершения всех горутин
    wg.Wait()
}

Таймер

Таймеры из пакета time используются для задержки (паузы) на указанное время:

package main

import (
    "fmt"
    "time"
)

func goRun() {
    // Симуляция работы
    time.Sleep(2 * time.Second)
    fmt.Println("Выполнение горутины завершено")
}

func main() {
    // Запуск горутины
    go goRun()
     // Основная функция продолжает работать параллельно
    fmt.Println("Запуск выполнения основной функции и ожидание завершения горутины")
    // Ждем завершения выполнения горутины
    time.Sleep(3 * time.Second)
}

Mutex

Mutex позволяет избежать конги данных (конфликт на запись).

package main

import (
	"fmt"
	"sync"
	"time"
)

// Структура с потокобезопасным буфером
type SafeBuffer struct {
	mu   sync.RWMutex
	data []byte
}

// Метод для добавления данных в конец буфера
func (sb *SafeBuffer) Write(p []byte) {
	sb.mu.Lock()
	defer sb.mu.Unlock()
	sb.data = append(sb.data, p...)
}

// Метод, который возвращает копию текущего содержимого буфера
func (sb *SafeBuffer) ReadAll() []byte {
	sb.mu.RLock()
	defer sb.mu.RUnlock()
	// Создаем копию
	res := make([]byte, len(sb.data))
	copy(res, sb.data)
	return res
}

func main() {
	sb := &SafeBuffer{}
	var wg sync.WaitGroup
	// Запускаем 5 горутин для записи данных
	for i := 1; i <= 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			msg := []byte(fmt.Sprintf("[Go %d]", id))
			sb.Write(msg)
			// fmt.Printf("Горутина %d записала данные\n", id)
		}(i)
	}
	// Ждем завершения всех пишущих горутин
	wg.Wait()
	// Читаем результат
	finalData := sb.ReadAll()
	fmt.Printf("Финальный результат: %s\n", string(finalData))
}

Небуферизованный канал

Небуферизованный канал блокирует операцию записи, пока не будет выполнено чтение, и наоборот.

// Создаем небуферизованный канал типа "int"
ch := make(chan int)
// Отправляем значение 42 в канал "ch"
// Операция блокирует текущую горутину, пока другая горутина не прочитает его значение
ch <- 42
// Получаем значение из канала "ch"
// Это также блокирует выполнение, пока не будет доступно значение для чтения в канале
v := <-ch

Буферизованный канал

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

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

package main

import "fmt"

func main() {
    // Создаем буферизованный канал с размером буфера 100
    ch := make(chan int, 100)

    // Отправляем некоторое количество значений в канал
    for i := 0; i < 10; i++ {
        ch <- i
    }

    // Закрываем канал, чтобы цикл мог завершиться
    close(ch)

    // Читать из канала, пока он не будет закрыт
    for i := range ch {
        fmt.Println(i)
    }

    // Прочитать данные из канала и проверить, закрыт ли он
    v, ok := <-ch
    if !ok {
        fmt.Println("Канал закрыт, данные не доступны")
    } else {
        fmt.Println("Прочитано из канала:", v)
    }
}

Вывод: 0 1 2 3 4 5 6 7 8 9 Канал закрыт, данные не доступны

Селекторы

Оператор select работает как многоканальный оператор switch. Выбор блоков в операциях с несколькими каналами, если один из них разблокируется, выполняется соответствующие условие. Он блокируется до тех пор, пока одно из выражений case не будет готов к выполнению, при этом остальные игнорируются.

package main

import (
	"fmt"
	"time"
)

func doStuff(channelOut, channelIn chan int) {
    select {
    case channelOut <- 42:
        fmt.Println("Отправить значение 42 в channelOut")
    case x := <-channelIn:
        fmt.Println("Прочитать из channelIn:", x)
    case <-time.After(time.Second * 1):
        fmt.Println("Задержка в одну секунду")
    }
}

func main() {
    // Создание двух каналов (один для записи, другой для чтения)
    channelOut := make(chan int)
    channelIn := make(chan int)

    // Запуск горутины для записи в канал "channelOut"
    go func() {
        time.Sleep(500 * time.Millisecond) // Пауза перед отправкой
        channelOut <- 42                   // Отправить значение 42
        fmt.Println("Значение 42 отправлено в channelOut")
    }()

    // Запуск горутины для чтения из канала "channelIn"
    go func() {
        time.Sleep(200 * time.Millisecond) // Пауза перед отправкой
        channelIn <- 99                    // Отправить значение 99
        fmt.Println("Значение 99 отправлено в channelIn")
    }()

    // Запуск функции doStuff с двумя каналами
    doStuff(channelOut, channelIn)
}

Аксиомы канала

Отправка в пустой канал блокируется навсегда и вызывает фатальную ошибку:

var c chan string
c <- "Hello, World!"

Чтение из нулевого канала блокируется навсегда:

var c chan string
fmt.Println(<-c)

Отправка в закрытый канал вызывает панику:

var c = make(chan string, 1)
c <- "Hello, World!"
close(c)
c <- "Hello, Panic!"

Прием из закрытого канала немедленно возвращает нулевое значение:

var c = make(chan int, 2)
c <- 1
c <- 2
close(c)
for i := 0; i < 3; i++ {
    fmt.Printf("%d ", <-c)
}
// 1 2 0

Примеры каналов и горутин

package main

import (
    "fmt"
    "time"
    "sync"
)

func goRun(ch chan string) {
    time.Sleep(2 * time.Second)
    // Возвращяем сообщене о выполнение в канал
    ch <- "Первая горутина завершена за 2 секунды"
}

func goRunThree(ch chan string) {
    time.Sleep(3 * time.Second)
    ch <- "Вторая горутина завершена за 3 секунды"
}

func printMessage(msg string, wg *sync.WaitGroup) {
    // Уменьшает счетчик в WaitGroup, когда горутина завершена
    defer wg.Done()
    fmt.Println(msg)
}

func main() {
    // Создаем канал
    ch := make(chan string)
    // Запускаем горутину
    go goRun(ch)
    fmt.Println("Ожидаем завершения горутины в канале")
    // Блокируем main, пока не получим сообщение от горутины
    result := <-ch
    // После получения вывода, программа продолжает выполнение
    fmt.Println(result)

    // Создаем два канала и запускаем две горутины
    ch1 := make(chan string)
    ch2 := make(chan string)
    go goRun(ch1)
    go goRunThree(ch2)
    fmt.Println("Ожидаем завершения первой выполненной горутины")
    // Используем select для ожидания данных с двух каналов и выбора первого завершенного канала
    select {
    case msg1 := <-ch1:
        fmt.Println("Ответ:", msg1)
    case msg2 := <-ch2:
        fmt.Println("Ответ:", msg2)
    }
    
    // Создаем группу ожидания для синхронизации выполнения нескольких горутин
    var wg sync.WaitGroup
    fmt.Println("Ожидаем выполнения всех запущенных горутин")
    // Указать количество горутин, за которыми нужно следить
    wg.Add(2)
    go printMessage("Результат первой горутины", &wg)
    go printMessage("Результат второй горутины", &wg)
    // Ожидаем завершения всех горутин
    wg.Wait()
    fmt.Println("Все горутины завершили свою работу")
}

Математические вычисления (math)

package main

import (
	"fmt"
	"math"
)

func customCeil(numerator int, denominator int) int {
    result := numerator / denominator
    if numerator%denominator != 0 {
        result++
    }
    return result
}

func main() {
    fmt.Println("Возвращает наименьшее значение из двух чисел 9 и 10:", math.Min(9, 10)) // 9
    fmt.Println("Возвращает наибольшее значение из двух чисел 9 и 10:", math.Max(9, 10)) // 10
    fmt.Println("Округляет число в меньшую сторону 10 / 3:", math.Floor(10/3)) // 3
    fmt.Println("Округляет число в большую сторону 10 / 3:", math.Ceil(10.0/3))
    fmt.Println("Округляет число в большую сторону 10 / 3:", customCeil(10, 3)) // 4
    fmt.Println("Отбрасывает дробную часть числа (не округляет) 4,9:", math.Trunc(4.9)) // 4
    fmt.Println("Округляет число до ближайшего целого в большую сторону от 4,5:", math.Round(4.5)) // 5
    fmt.Println("Округляет число до ближайшего целого в меньшую сторону от 4,5:", math.Round(4.45)) // 4
    fmt.Println("Возвращает абсолютное значение числа -7:", math.Abs(-7)) // 7
    fmt.Println("Возводит число 2 в степень 3:", math.Pow(2, 3)) // 8
    fmt.Println("Вычисляет квадратный корень числа 16:", math.Sqrt(16)) // 4
}

Обработка текста (strings)

package main

import "fmt"

func main() {
	// Строка состоит из 10 символов, но её длина в байтах будет 19,
	// так как кириллические символы занимают 2 байта (UTF-8) в отличии от латинских символов, а пробел - 1 байт.
	var s string = "Это строка"

	fmt.Printf("Длина строки: %d байт\n", len(s)) // Длина строки: 19 байт

	// Получим подстроку строки по индексу и выводим его значение в исходном (%v) или строковом виде (%s)
	fmt.Printf("Напечатаем только второе слово в кавычках: \"%s\"\n", s[7:]) // Напечатаем только второе слово в кавычках: "строка"

	// При изменение строки возникнет ошибка компиляции, так как строки неизменяемы
	// s[3] = 12

	// Изменим строку, создав новую строку из частей
	word := "новая "
	newS := fmt.Sprintf("%v%v%v", s[:7], word, s[7:])
	fmt.Printf("%v\n", newS) // Это новая строка

	// Прогнать строку в цикле
	for _, b := range s {
		// Пропускаем пробел по символу кодировки Unicode
		if b == 32 {
			continue
		}
		// Выводим тип данных "rune" (содержащий символ Unicode) в человекочитаемом виде с помощью спецификатора %c
		fmt.Printf("%c ", b)
	}
    // Э т о с т р о к а
}

Методы из пакета strings для работы со строками:

package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(
		// Возвращает строку c нижним регистром
		strings.ToLower("TEST"),
		// test

		// Возвращает строку c верхним регистром
		strings.ToUpper("test"),
		// TEST

		// Проверить, содержится ли подстрока в строке
		strings.Contains("test", "es"),
		// true

		// Вывести количество подстрок в строке
		strings.Count("test", "t"),
		// 2

		// Проверить, что строка начинается с префикса
		strings.HasPrefix("test", "te"),
		// true

		// Проверить, что строка заканчивается суффиксом
		strings.HasSuffix("test", "st"),
		// true

		// Повторяет строку n раз подряд
		strings.Repeat("a", 5),
		// aaaaa

		// Разбивает строку на массив из строк (тип данных []string) по разделителю
		strings.Split("hello-world", "-"),
		// [hello world]

		// Объединяет массив из строк с использованием разделителя
		strings.Join([]string{"hello", "world"}, "-"),
		// hello-world

		// Замена, где последний аргумент позволяет указать количество замен ("-1" - заменить все)
		strings.Replace("true true true", "tru", "fals", 2),
		// false false true
	)

	// Вывести первый найденный порядковый индекс подстроки в строке или -1 при отсутствии значений
	line := "this is test"
	spaceIndex := strings.Index(line, " ")
	// Вернуть строку после найденного индекса
	if spaceIndex != -1 {
		fmt.Println(line[spaceIndex+1:])
	}
    // is test
}

Регулярные выражения (regexp)

Основные элементы синтаксиса регулярных выражений:

СимволОписание
.любой символ, кроме символа новой строки
*0 или более повторений
+1 или более повторений
{n}точно n повторений (например, a{3}, соответствует: "aaa")
{n,}минимум n повторений (например, a{2,}, соответствует: "aa", "aaa" и т.д.)
{n,m}от n до m повторений (например, a{2,4}, соответствует: "aa", "aaa", "aaaa")
?0 или 1 повторений
^начало строки
$конец строки
[]группа символов (например, [a-z])
\sлюбой пробельный символ (пробел, табуляция, новая строка и другие пробельные символы)
\dцифра (эквивалентно [0-9])
\Dлюбой символ, не являющийся цифрой (эквивалентно [^0-9])
\wбуквенно-цифровой символ (буквы, цифры и подчеркивание, эквивалентно [a-zA-Z0-9_])
\Wне буквенно-цифровой символ (эквивалентно [^a-zA-Z0-9_])
\bграница слова (например, \bword\b соответствует "word", и не подхоит "wordy")
(?i)делает выражение нечувствительным к регистру
\экранирование специальных символов
()группа захвата
|логическое ИЛИ (например, `a

MatchString

regexp.MatchString — проверяет, соответствует ли строка регулярному выражению.

package main

import (
	"fmt"
	"regexp"
)

func main() {
    pattern := `^[a-z]+$`
	str := "string"
    matched, err := regexp.MatchString(pattern, str)
	if err != nil {
		fmt.Println("Ошибка в регулярном выражении:", err)
		return
	}
	fmt.Printf("Строка '%s' соответствует регулярному выражению '%s' (результат: %v)", str, pattern, matched)
}

Compile

regexp.Compile — компилирует регулярное выражение и возвращает объект типа *regexp.Regexp, если выражение корректное, или возвращается ошибка.

package main

import (
    "fmt"
    "regexp"
)

func main() {
    // Компилируем регулярное выражение
    r, err := regexp.Compile(`\d+`)
    if err != nil {
        fmt.Println("Ошибка компиляции регулярного выражения:", err)
        return
    }
    // Применяем регулярное выражение к строке
    fmt.Println(r.FindString("123 abc 456")) // 123
}

FindAllString

regexp.FindAllString — находит все подстроки в строке, которые соответствуют регулярному выражению, и возвращает их в виде среза строк.

package main

import (
    "fmt"
    "regexp"
)

func main() {
    r, err := regexp.Compile(`\d+`)
    if err != nil {
        fmt.Println("Ошибка компиляции регулярного выражения:", err)
        return
    }
    matches := r.FindAllString("123abc456", -1)
    fmt.Println(matches) // [123 456]
}

ReplaceAllString

regexp.ReplaceAllString — заменяет все соответствующие части строки.

package main

import (
    "fmt"
    "regexp"
)

func main() {
    pattern := `\d+`
    str := "Диапазон от 1 до 10"
    // Заменяем все цифры на "X"
    r, err := regexp.Compile(pattern)
    if err != nil {
        fmt.Println("Ошибка компиляции регулярного выражения:", err)
        return
    }
    result := r.ReplaceAllString(str, "X")
    fmt.Println(result)
}

Группы захвата

package main

import (
    "fmt"
    "regexp"
)

func main() {
    // Регулярное выражение с группой захвата для даты в формате "dd.mm.yyyy"
    pattern := `(\d{2}).(\d{2}).(\d{4})`
    r, err := regexp.Compile(pattern)
    if err != nil {
        fmt.Println("Ошибка компиляции регулярного выражения:", err)
        return
    }
    // Поиск и извлечение данных
    result := r.FindStringSubmatch("01.12.2024")
    if len(result) > 0 {
        fmt.Println("День:", result[1])
        fmt.Println("Месяц:", result[2])
        fmt.Println("Год:", result[3])
    }
}

Извлечение логина и домена из почтовых адресов:

package main

import (
    "fmt"
    "regexp"
)

func main() {
    pattern := `([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})`
    str := "contact@example.com, support@example.net"
    r, err := regexp.Compile(pattern)
    if err != nil {
        fmt.Println("Ошибка компиляции регулярного выражения:", err)
        return
    }
    matches := r.FindAllStringSubmatch(str, -1)
    for _, match := range matches {
        fmt.Printf("Логин: %s, Домен: %s\n", match[1], match[2])
    }
}

REST API

HTTP сервер

Реализация простого API сервера на базе встроенной библиотеки net/http:

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

// Обработчик API
func apiHandler(w http.ResponseWriter, r *http.Request) {
	// Устанавливаем заголовок для ответа (Content-Type: application/json)
	w.Header().Set("Content-Type", "application/json")

	switch r.Method {
	case "GET":
		// Получение параметра "name" из URL
		name := r.URL.Query().Get("name")
		if name == "" {
			name = "Guest" // Значение по умолчанию, если параметр отсутствует
		}
		// Формируем JSON-ответ
		json.NewEncoder(w).Encode(map[string]string{
			"message": fmt.Sprintf("Hi %s", name),
		})

	case "POST":
		// Парсим JSON из тела запроса
		var data map[string]interface{}
		if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
			http.Error(w, "invalid JSON", http.StatusBadRequest)
			return
		}

		// Формируем JSON-ответ
		json.NewEncoder(w).Encode(map[string]interface{}{
			"received": data,
			"status":   "OK",
		})

	default:
		// Обработка неподдерживаемых методов
		http.Error(w, "Метод не поддерживается", http.StatusMethodNotAllowed)
	}
}

func main() {
	// Регистрируем обработчик для пути /api
	http.HandleFunc("/api", apiHandler)

	// Запуск сервера
	fmt.Println("Сервер запущен на http://localhost:8080")
	http.ListenAndServe(":8080", nil)
}

Делаем запрос к API через curl:

curl -s "http://localhost:8080/api" | jq .message # "Hi Guest"
curl -s "http://localhost:8080/api?name=Alex" | jq .message # "Hi Alex"
curl -s -X POST -d '{"key":"value"}' -H "Content-Type: application/json" http://localhost:8080/api | jq .received.key # "value"
curl -s -X POST "http://localhost:8080/api" # invalid JSON

HTTP клиент

Делаем запрос к API в Go:

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

func main() {
	// URL для отправки POST-запроса
	url := "http://localhost:8080/api"

	// Тело запроса в формате JSON
	requestBody := map[string]string{"key": "value"}
	jsonData, err := json.Marshal(requestBody)
	if err != nil {
		fmt.Println("Ошибка при создании тела запроса в формате JSON:", err)
		return
	}

	// Создаем запрос
	resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		fmt.Println("Ошибка при отправке запроса:", err)
		return
	}
	defer resp.Body.Close()

	// Читаем тело ответа
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("Ошибка при чтении ответа:", err)
		return
	}

	// Разбираем ответ в формате JSON
	var response map[string]interface{}
	if err := json.Unmarshal(body, &response); err != nil {
		fmt.Println("Ошибка при парсинге JSON:", err)
		return
	}

	// Выводим значение "key" из ответа
	if received, ok := response["received"].(map[string]interface{}); ok {
		if value, exists := received["key"]; exists {
			fmt.Println(value) // "value"
		} else {
			fmt.Println("Ключ 'key' не найден в ответе")
		}
	} else {
		fmt.Println("Ответ не содержит ожидаемую структуру received")
	}
}

HTTP запрос к API для получения последней версии релиза указаного репозитория в GitHub:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
)

// Формируем структуру ответа от API
type GitHubRelease struct {
	TagName string `json:"tag_name"`
}

func main() {
	// Формируем URL для получения информации
    repos := "Lifailon/lazyjournal"
	url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repos)
	// Выполнение GET-запроса
	resp, err := http.Get(url)
	if err != nil {
		log.Fatal("Ошибка при выполнении запроса:", err)
	}
	defer resp.Body.Close()
	// Проверка на успешный ответ
	if resp.StatusCode != http.StatusOK {
		log.Fatalf("Ошибка HTTP: %s", resp.Status)
	}
	// Декодирование JSON-ответа в заданную структуру
	var release GitHubRelease
	err = json.NewDecoder(resp.Body).Decode(&release)
	if err != nil {
		log.Fatal("Ошибка при декодировании JSON:", err)
	}
	// Вывод последней версии
	fmt.Println("Latest version:", release.TagName)
}

go run main.go

Вызов системных команд (exec)

Проверка доступности всех хостов в указанной подсети (асинхронный ICMP опрос):

package main

import (
	"fmt"
	"os"
	"os/exec"
	"strings"
	"sync"
)

func pingHost(ip string, wg *sync.WaitGroup) {
	defer wg.Done()
	// Запускаем команду ping
	cmd := exec.Command("ping", "-n", "1", ip)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return
	}
	// Обрабатываем вывод команды
	if strings.Contains(string(output), "TTL=") {
		fmt.Printf("%s - доступен\n", ip)
	}
}

func main() {
	if len(os.Args) < 2 {
		fmt.Println("Использование: go run main.go <подсеть>")
		return
	}
	// Извлекаем аргумент
	subnet := os.Args[1]
	// Убираем последний октет
	ipBase := subnet[:len(subnet)-1]
	var wg sync.WaitGroup
	for i := 1; i <= 254; i++ {
		ip := fmt.Sprintf("%s%d", ipBase, i)
		wg.Add(1)
		// Запускаем асинхронный пинг
		go pingHost(ip, &wg)
	}
	// Ждем завершения всех горутин
	wg.Wait()
}

go run main.go 192.168.3.0

Встраивание файлов (embed)

Программы Go могут встраивать статические файлы с помощью пакета embed и директиву go:embed path/filename:

package main

import (
    "embed"
    "fmt"
    "io"
    "log"
    "net/http"
)

//go:embed static/*
var content embed.FS

func main() {
    http.Handle("/", http.FileServer(http.FS(content)))
    go func() {
        log.Fatal(http.ListenAndServe(":8080", nil))
    }()

    // Чтение содержимого файлов из файловой системы
    entries, err := content.ReadDir("static")
    if err != nil {
        log.Fatal(err)
    }

    for _, e := range entries {
        resp, err := http.Get("http://localhost:8080/static/" + e.Name())
        if err != nil {
            log.Fatal(err)
        }
        body, err := io.ReadAll(resp.Body)
        if err != nil {
            log.Fatal(err)
        }
        if err := resp.Body.Close(); err != nil {
            log.Fatal(err)
        }
        fmt.Printf("%q: %s", e.Name(), body)
    }

    // Блокировка программы, чтобы сервер продолжал работать для доступа к статическим файлам через Web-интерфейс
    select {}
}

// Имитация реальных файлов с их содержимым для запуска в Playground
-- static/a.txt --
hello a
-- static/b.txt --
hello b