GoLang

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


Участники

Если вы нашли ошибку или хотите расширить список шпаргалок, а также знаете другие источники для изучения, сообщите о них, внеся изменения через 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)

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)

package main

import "fmt"

func main() {
    var name string
    var age int
    fmt.Print("Введите имя и возраст: ")
    fmt.Scan(&name, &age)
    fmt.Printf("Имя: %s, возраст: %d\n", name, age)
}

Операторы

Арифметика

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

* Если оба операнда имеют целый тип (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)

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

Условия if

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 неизвестен

Циклы

В 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:", m["key"])
	} 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)
	}
}

Структуры

Вместо классов (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}

Интерфейсы

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

// Создаем пустой интерфейс (принимает произвольное количество значений любого типа)
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 используется для ожидания завершения всех запущенных горутин.

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)
}

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

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

// Создаем небуферизованный канал типа "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("Все горутины завершили свою работу")
}

Форматированный вывод (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 %c %q %q \n %s %d %t %.1f \n",
		a, b, c, d, e, f, g, h,
	)

	var z float64 = 100.123456789
    // Получаем результат форматирования с помощью метода Sprintf()
	result := fmt.Sprintf("%.2f", z)
	fmt.Printf("%q", result)
}

// 'A' 1 '1' "123" 
//  123 123 true 1.2 
// "100.12"

Строки (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

		// Вывести первый найденный порядковый индекс подстроки в строке
		strings.Index("test", "t"),
		// 0
		// При отсутствии вхождения
		strings.Index("test", "r"),
		// -1

		// Повторяет строку 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
	)
}

Регулярные выражения (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

Основные функции пакета regexp:

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)
}
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
}
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]
}
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])
    }
}

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

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
}

Встроенные пакеты

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

Программы 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

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

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

Проверка доступности всех хостов в указанной подсети (асинхронный 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