План

  1. Синтаксис и структура: ознакомьтесь с базовыми элементами языка Go, такими как переменные, типы данных, операторы, условные выражения и циклы.
  2. Функции: изучите создание и использование функций в Go, а также передачу параметров по значению и по ссылке.
  3. Срезы (slices) и массивы: узнайте, как создавать, использовать и манипулировать срезами и массивами в Go.
  4. Структуры и методы: ознакомьтесь с определением структур и созданием методов, которые работают с ними.
  5. Указатели: изучите работу с указателями в Go и их применение для изменения значений переменных и передачи данных.
  6. Интерфейсы: узнайте, как создавать интерфейсы и реализовывать их в Go для достижения полиморфизма.
  7. Горутины и каналы: изучите концепцию параллельного и конкурентного программирования с помощью горутин и каналов в Go.
  8. Обработка ошибок: ознакомьтесь с практиками обработки ошибок в Go, используя механизмы возврата ошибок и паники.
  9. Пакеты и импорты: изучите организацию кода в пакеты и импортирование пакетов в Go.
  10. Тестирование: узнайте, как создавать модульные тесты для своего кода с помощью пакета testing в Go.
  11. Работа с файлами и сетью: ознакомьтесь с базовыми операциями работы с файлами и сетью в Go.
  12. Развертывание приложений: изучите процесс сборки и развертывания приложений на языке Go, включая управление зависимостями и компиляцию.

Синтаксис и структура

Настраиваем Goland

Указываем пути до папки с исходными кодами приложений

И до папки с компилятором Go

Первое приложение. main функция

Первым делом в корневом файле всегда обязательно должен присутствовать пакет main и функция main, которые будут хранить основные пакеты приложения и функцию запуска приложения на Go

В myVar записана переменная, а уже через библиотеку форматирования fmt и её метод Print можно вывести значение в консоли

package main
 
import "fmt"
 
func main() {
	myVar := "Hello world"
	fmt.Print(myVar)
}
  • Первой командой устанавливается корневой путь до SDK Go (компилятор)
  • Второй - GOPATH - это переменная среды, используемая в Go для указания корневого каталога рабочего пространства, где хранятся исходные коды и бинарные файлы проектов на Go.
  • Третья команда вызывает компилятор Go через build и ключом -o задаётся путь выходного файла. Путь будет иметь tmp директорию. Последний аргумент - это целевой файл, который нужно скомпилировать.

Так же мы можем запустить нужный файл через данную команду:

go run main.go

Так же мы можем сбилдить нужный нам файл в бинарник нашей системы

go build -o main_app main.go 

Переменные и типы данных

В Go, как и в других языках, присутствуют самые распространённые типы данных:

  • bool: логический тип данных, который может быть true или false.
  • числовые типы данных:
    • int (int8, int16, int32, int64): целое число со знаком.
    • uint (uint8, uint16, uint32, uint64): целое число без знака.
    • float32, float64: числа с плавающей точкой.
    • complex64 / complex128
    • byte = uint8
    • rune = int32
  • string: строки текста.
  • массивы и срезы: коллекции элементов определенного типа.
  • struct: пользовательские типы данных, состоящие из полей разных типов.
  • указатели: хранят адрес памяти другой переменной.
  • interface: набор методов, объединенных под общим интерфейсом.
  • func: фрагменты кода, которые можно вызывать и использовать.
  • maps: коллекции пар ключ-значение.
  • channels: используются для обмена данными между горутинами.

Мы можем указать переменную через три конструкции:

  • var имя тип_данных = значение - сами указываем переменную и тип данных
  • var имя = значение - сами указываем переменную без типа данных
  • имя := значение - автоматическое указание типа данных

Golang почти строготипизированный язык, поэтому если мы не укажем тип, но укажем определённую переменную, то поменять строку на чичсло - не получится

Так же у нас есть явный перевод данных и с помощью того же int можно перевести число одного типа в другое

func main() {
	myVar := "Hello world"
	fmt.Print(myVar)
 
	var a int = 10
	b := 40
 
	var c uint32 = 50
	d := a + int(c)
}

Так же можно указать сразу несколько переменных подобным синтаксисом:

func main() {
	var (
		name = "Oleg"
		age = 30
	)
 
	fmt.Print(name)
	fmt.Print(age)
}

Или подобным:

func main() {
	var name, age  = "Oleg", 30
 
	fmt.Print(name)
	fmt.Print(age)
}

Работа со строками

Чтобы объединять строки, мы можем использовать просто оператор +

func main() {
    str1 := "Hello"
    str2 := "World"
 
    result := str1 + " " + str2
    fmt.Println(result) // Выводит: Hello World
}

Так же мы можем конструировать строки через использование метода Sprintf, куда первым параметром передаём строку и указатели (%s - строка, %d - число), а дальше переменные с нужными типами данных

func main() {
	var name, age = "Oleg", 30
 
	c := fmt.Sprintf("Hello! I'm %s. And I'm %d years old", name, age)
 
	fmt.Print(c)
}

Через функцию Join мы можем объединить массив в строку

func main() {
    strs := []string{"Hello", "World"}
 
    result := strings.Join(strs, " ")
    fmt.Println(result) // Выводит: Hello World
}

Функция Split позволит разбить строку по определённому разделителю в массив

func main() {
    str := "apple,banana,orange"
 
    arr := strings.Split(str, ",")
    fmt.Println(arr) // Выводит: [apple banana orange]
}
  1. strings.Contains(s, substr):
    • Проверяет, содержит ли строка s подстроку substr.
    • Возвращает булево значение true, если s содержит substr, иначе false.
  2. strings.HasPrefix(s, prefix):
    • Проверяет, начинается ли строка s с префикса prefix.
    • Возвращает булево значение true, если s начинается с prefix, иначе false.
  3. strings.HasSuffix(s, suffix):
    • Проверяет, заканчивается ли строка s суффиксом suffix.
    • Возвращает булево значение true, если s заканчивается на suffix, иначе false.
  4. strings.Replace(s, old, new, n):
    • Заменяет все вхождения подстроки old в строке s на подстроку new.
    • Можно указать необязательный аргумент n, чтобы ограничить количество замен.
    • Возвращает новую строку, полученную после замены.
  5. strings.ToLower(s):
    • Преобразует все символы строки s в нижний регистр.
    • Возвращает новую строку с преобразованными символами.
  6. strings.ToUpper(s):
    • Преобразует все символы строки s в верхний регистр.
    • Возвращает новую строку с преобразованными символами.
  7. strings.TrimSpace(s):
    • Удаляет все начальные и конечные пробелы из строки s.
    • Возвращает новую строку без пробелов в начале и конце.

Циклы

Цикл for

Основной цикл, который существует в Go представлен следующими вариациями:

// Форма 1: С наибольшим контролем
for init; condition; post {
    // Тело цикла
}
 
// Форма 2: Как классический цикл while
for condition {
    // Тело цикла
}
 
// Форма 3: Бесконечный цикл
for {
    // Тело цикла
}

Цикл range

Цикл range используется для итерации по элементам массива, среза (slice), строки, карты (map) или канала (channel). Он возвращает индекс или ключ текущего элемента и его значение

for index, value := range collection {
    // Тело цикла
}

while

Цикл while моделируется обычным оставлением одного кондишенела в цикле перебора

for condition {
    // Тело цикла
}

do-while

В Go также нет прямой поддержки цикла do-while, но можно смоделировать его, используя бесконечный цикл и прерывание с помощью оператора break

for {
    // Тело цикла
 
    if !condition {
        break
    }
}

Ветвления

Представление самого стандартного ветвления:

func main() {
    x := 10
 
    if x > 5 {
        fmt.Println("x больше 5")
    }
}

Представление работы else-if:

func main() {
    age := 20
 
    if age < 18 {
        fmt.Println("Вы несовершеннолетний")
    } else if age >= 18 && age < 60 {
        fmt.Println("Вы взрослый")
    } else {
        fmt.Println("Вы пожилой человек")
    }
}

В Go также есть возможность использовать условные операторы с коротким обозначением переменной (:=). Это позволяет нам объявить и присвоить значение переменной внутри условия if

func main() {
    if x := 5; x > 0 {
        fmt.Println("x положительное число")
    } else if x < 0 {
        fmt.Println("x отрицательное число")
    } else {
        fmt.Println("x равно нулю")
    }
}

Функции

Создание функций

Самая стандартная функция начинается с конструкции func. Далее следует имя функции, её сигнатура, в которой указываются переменные и их тип. После сигнатуры указывается тип возвращаемого значения (если есть).

В теле функции выполняется повторяемый код. Если мы хотим что-то вернуть, то указываем return

func sayHello() {
    fmt.Println("Привет!")
}
 
func add(a, b int) int {
    return a + b
}

Так же функция может вернуть сразу несколько параметров

func values() (int, int) {
	x := 3
	y := 4
	
	return x, y
}
 
horizontal, vertical := valuews()

Вызов функций

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

sayHello() // Выведет "Привет!"
 
result := add(3, 5) // Присваивает переменной result значение 8

Передача параметров по значению

По умолчанию в Go параметры функции передаются по значению, то есть создается копия значения аргумента и эта копия используется внутри функции

func increment(x int) {
    x = x + 1
    fmt.Println("Внутри функции:", x)
}
 
num := 5
increment(num)
fmt.Println("После вызова функции:", num)
 
// Внутри функции: 6 - значение изменено
// После вызова функции: 5 - значение не изменено (не мутировано внутри функции)

Передача параметров по ссылке

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

func incrementByReference(x *int) {
    *x = *x + 1
    fmt.Println("Внутри функции:", *x)
}
 
num := 5
incrementByReference(&num)
fmt.Println("После вызова функции:", num)
 
// Внутри функции: 6
// После вызова функции: 6

Отложенное выполнение

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

Вот некоторые особенности defer:

  1. Порядок выполнения: Если в функции используются несколько инструкций defer, они будут выполнены в порядке обратном их объявлению.
  2. Захват аргументов: Аргументы функции, передаваемые в инструкцию defer, вычисляются немедленно при объявлении defer.
  3. Аргументы функции считываются заранее: Если аргументы функции, передаваемые в defer, подвержены изменениям до момента выполнения отложенной функции, то отложенная функция все равно получит их исходные значения, а не значения на момент вызова.
  4. Работа с файлами и ресурсами: defer часто используется для освобождения ресурсов, таких как закрытие файла или соединения с базой данных, чтобы гарантировать, что эти ресурсы будут корректно освобождены независимо от пути выполнения функции.
func readFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()
 
    // Чтение файла и выполнение других операций
 
    return nil
}

В этом примере отложенный вызов file.Close() гарантирует, что файл будет закрыт независимо от того, как завершится функция readFile(). Это помогает избежать утечек ресурсов и обеспечивает более безопасное и удобное управление ресурсами.

Срезы, массивы и map

Массивы

Объявление и использование массивов. Массивы хранят в себе только тот тип данных, который мы указали

func main() {
	var arr [5]int // инициализация массива
	arr[1] = 5     // задание значения
 
	arr2 := [5]int{}
	arrWithValues := [5]int{1, 2, 3, 4}
}

Слайсы

Слайс - это динамический массив, который не имеет определённого количества элементов

package main
 
import "fmt"
 
func main() {
	week := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8}
	weekend := week[6:8]
	fmt.Println(len(week))    // len покажет длину массива - 8
	fmt.Println(weekend)      // [6 7]
	fmt.Println(week[6:])     // [6 7 8]
	fmt.Println(cap(weekend)) // cap покажет, сколько памяти может занимать слайс
 
	animals := []string{}             // это пример создания слайса - не указываем размер массива
	animals = append(animals, "lion") // тут мы добавляем значение в слайс
 
	cars := make([]string, 5, 10) // так же мы имеем такую возможность создать значение любого типа данных
}

Слайс имеет определённые особенности при его использовании

package main
 
import "fmt"
 
func main() {
	a := []int{1}
	b := a[0:1] // тут мы связали слайсы, так как они сейчас начали смотреть на один массив
	b[0] = 0    // задали нолик в стоковом массиве
 
	fmt.Println(a) // [0] - вышел нолик
	a[0] = 1       // поменяли стоковый массив на 1
	fmt.Println(b) // [1] - вышла единичка
 
	a = append(a, 2)  // данная функция вместе с добавлением значения разделит слайсы
	fmt.Println(a, b) // [1 2] [1] - тут уже слайсы стали смотреть на разные массивы
}

Map

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

Ключи и значения имеют одинаковый тип данных

package main
 
import "fmt"
 
func main() {
	workdays := map[string]int{
		"monday":    1,
		"tuesday":   2,
		"wednesday": 3,
		"thursday":  4,
		"friday":    5,
	}
 
	workdays["saturday"] = 6
	workdays["sunday"] = 7
 
	day, ok := workdays["saturday"] // вторым аргументом присваивается значение существования ключа - true
 
	if ok { // если ключ существует
		fmt.Println(day)
	}
 
	delete(workdays, "sunday") // удаление ключа
 
	fmt.Println(workdays["tuesday"]) // 2
}

Структуры и методы

структуры

Структура - это набор различных типов данных, которые описывают единый объект

Имя поля с заглавной - экспортируется, со строчной - не экспортируется и не может быть отображено в другом пакете вне данного

type Human struct {  
    Name   string  
    age    int  
    Width  int  
    Height int  
}

Далее в любую другую переменную мы можем записать экземпляр структуры Human

package main
 
import "fmt"
 
type Human struct {
	Name   string
	age    int
	Width  int
	Height int
}
 
func main() {
	person := Human{
		age:    20,
		Name:   "Valery",
		Height: 178,
		Width:  109,
	}
 
	fmt.Println(person)
}

Методы

Базовая структура

type Person struct {
    Name string
    Age  int
}

Метод - это функция, которая привязана к определённой структуре. То есть для вызова метода, нужно создать соответствующую структуру

В приведенном примере мы создаем два метода на структуре Person. Нужно обратить внимание, что имя структуры указывается перед именем метода в скобках - это ресивер (приёмник) - он обозначает привязанность метода к структуре.

Первый метод называется GetName() и возвращает значение поля Name структуры Person. Второй метод называется SetAge(age int) и устанавливает поле Age структуры Person в указанное значение age.

Во втором случае мы использовали указатель, так как нам нужно получить доступ (ссылку) к объекту структуры и изменить его значение. В первом случае мы не использовали указатель, так как нам нужно только отобразить имя

func (p Person) GetName() string {
    return p.Name
}
 
func (p *Person) SetAge(age int) {
    p.Age = age
}

Чтобы использовать структуры и их методы, мы можем создать экземпляр структуры и вызвать методы на этом экземпляре

person := Person{Name: "Alice", Age: 25}
 
name := person.GetName()   // Вызываем метод GetName()
fmt.Println(name)         // Выводит: Alice
 
person.SetAge(30)         // Вызываем метод SetAge()
fmt.Println(person.Age)  // Выводит: 30

Конструктор

Иногда, когда нам нужно будет создавать объект по структуре, мы можем сократить запись и реализовать конструктор, который может задавать определённые поля по умолчанию

type User struct {
	name, email string
	isConfirmed bool
}
 
func NewUser(name, email string) User {
	return User{
		email:       email,
		name:        name,
		isConfirmed: false,
	}
}
 
func main() {
	user := NewUser("Oleg", "oleg@yandex.ru")
 
	fmt.Println(user) // {Oleg oleg@yandex.ru false}
}

Встраивание структуры

Структуры так же можно вкладывать друг друга, разбивая их данные

type Person struct {
	name       string
	Parameters Parameters
}
 
type Parameters struct {
	height int
	age    uint8
}

Так же есть и второй способ указания встроенной структуры, при котором поля уже будут принадлежать не отдельно структуре Parameters, а именно Person

type Person struct {
	name       string
	Parameters
}
 
type Parameters struct {
	height int
	age    uint8
}

Занесение структуры

param := Parameters{height: 178, age: 20}  
person := Person{name: "Oleg", Parameters: param}

Указатели

Определение указателя

В Go указатель - это переменная, которая содержит адрес в памяти другой переменной. Через оператор * мы указываем на ту переменную, которая будет являться указателем. Оператор & используется для получения адреса переменной.

var x int = 10
var ptr *int = &x

Разыменование указателя

В Go оператор * используется для разыменования указателя и доступа к значению, на которое он указывает.

fmt.Println(*ptr) // Выводит значение переменной, на которую указывает ptr (10)

Изменение значения переменной через указатель

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

*ptr = 20 // Изменяет значение переменной x через указатель ptr
fmt.Println(x) // Выводит измененное значение (20)

Передача указателя в функции

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

func changeValue(ptr *int) {
    *ptr = 30 // Изменяет значение переменной через указатель
}
 
var y int = 10
changeValue(&y) // Передача указателя на переменную y в функцию
fmt.Println(y) // Выводит измененное значение (30)

Передача указателя в функции

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

type Person struct {
    Name string
    Age  int
}
 
func changeName(p *Person, newName string) {
    p.Name = newName
}
 
var person1 = Person{"John", 25}
changeName(&person1, "Mike")
fmt.Println(person1.Name) // Выводит измененное имя ("Mike")

Интерфейсы

Создание интерфейса

Для создания интерфейса в Go необходимо объявить новый тип с набором методов, которые требуется реализовать. Например, предположим, что мы хотим создать интерфейс “Shape” для различных геометрических фигур:

type Shape interface {
    Area() float64
    Perimeter() float64
}

Использование интерфейса

Затем можно создать структуры, которые реализуют этот интерфейс. Например, вот реализация интерфейса “Shape” для круга или “Square” для квадрата:

type Circle struct {
    radius float64
}
 
func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}
 
func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.radius
}
type Square struct {
    sideLength float64
}
 
func (s Square) Area() float64 {
    return s.sideLength * s.sideLength
}
 
func (s Square) Perimeter() float64 {
    return 4 * s.sideLength
}

Обращение к интерфейсу

И теперь мы можем сделать обобщённую функцию, которая будет принимать в себя значения по интерфейсу

func PrintShapeDetails(s Shape) {
    fmt.Println("Area:", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
}
 
func main() {
    circle := Circle{radius: 5}
    square := Square{sideLength: 10}
 
    PrintShapeDetails(circle)
    PrintShapeDetails(square)
}

Итог

Таким образом, интерфейс - это структура, используемая для определения набора методов, которые тип должен реализовывать.

Чтобы воспользоваться интерфейсом, мы должны для структуры указать её методы, которые должны соответствовать интерфейсу по имени и типу возвращаемых данных

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

package main
 
import (
	"fmt"
	"math"
)
 
type Shape interface {
	Area() float64
	Perimeter() float64
}
 
type Square struct {
	sideLength float64
}
 
type Circle struct {
	radius float64
}
 
func (c Circle) Area() float64 {
	return math.Pi * c.radius * c.radius
}
 
func (c Circle) Perimeter() float64 {
	return 2 * math.Pi * c.radius
}
 
func (s Square) Area() float64 {
	return s.sideLength * s.sideLength
}
 
func (s Square) Perimeter() float64 {
	return 4 * s.sideLength
}
 
func printShapeDetails(s Shape) {
	fmt.Println("Area:", s.Area())
	fmt.Println("Perimeter:", s.Perimeter())
}
 
func main() {
	circle := Circle{radius: 10}
	square := Square{sideLength: 10}
	
	printShapeDetails(circle)
	printShapeDetails(square)
}

Горутины и каналы

Горутины

Горутины (goroutines) в Go - это легковесные потоки выполнения, которые работают независимо друг от друга и могут быть запущены параллельно. Они позволяют выполнять асинхронные операции без явного создания отдельных потоков или процессов. Горутины разделяют общее пространство адресов и управляются сборщиком мусора, что делает их эффективными и недорогими в использовании.

Горутина - это функция, которая может быть запущена в отдельном потоке выполнения

Для создания горутины нужно просто передать функцию с ключевым словом go

func main() {
    go myFunc() // Создание горутины
    // ...
}
 
func myFunc() {
    // Код функции
}

Канал

Каналы (channels) служат для обмена данными между горутинами и обеспечивают синхронизацию и координацию выполнения параллельных процессов. Каналы можно представить как “трубы”, через которые горутины передают информацию. Они обеспечивают безопасный доступ к данным при параллельном выполнении, предотвращая состояние гонки (race condition) и другие проблемы синхронизации.

Каналы используются для обмена данными между горутинами

Каналы могут быть созданы с помощью функции make

ch := make(chan int) // Создание канала типа int

Так же можно создать канал через стандартную запись

var intChannel chan int // если мы хотим принимать любые типы данных, то здесь нужно указать interface
intChannel <- 1
number := <- intChannel 

Затем вы можете отправлять и получать значения через канал, используя операторы <-:

ch <- value // Отправка значения в канал
result := <-ch // Получение значения из канала

Каналы по умолчанию блокируют выполнение горутины при чтении или записи, пока другая горутина не будет готова для обмена. Это предотвращает состояние гонки (race condition) и делает программу безопасной для параллельного выполнения.

Кроме того, Go предлагает различные средства синхронизации, такие как мьютексы (mutexes) и условные переменные (condition variables), чтобы обеспечить правильную синхронизацию доступа к общим ресурсам и согласование работы горутин.

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

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

ch := make(chan int)

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

ch := make(chan int, 5)

Использование горутин и каналов

В данном примере мы создаём канал resultChan, который принимает в себя все просуммированные квадраты. Затем из этого канала мы достаём полученные значения функции.

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    resultChan := make(chan int)
 
    for _, num := range numbers {
        go square(num, resultChan)
    }
 
    total := 0
    for i := 0; i < len(numbers); i++ {
        total += <-resultChan
    }
 
    fmt.Println("Сумма квадратов чисел:", total)
}
 
func square(num int, resultChan chan<- int) {
    square := num * num
    resultChan <- square
}

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

Ошибки

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

Проверка ошибок (Error Checking): Для проверки ошибок используется условное выражение, обычно с помощью if или switch. Пример:

result, err := SomeFunction()
if err != nil {
    // Обработка ошибки
} else {
    // Обработка успешного результата
}

Возврат ошибок (Returning Errors): Функции в Go могут возвращать ошибку вместе с другими значениями. Это позволяет вызывающему коду обрабатывать ошибку непосредственно. Пример:

func SomeFunction() (int, error) {
    // ...
    if someCondition {
        return 0, errors.New("some error")
    }
    // ...
    return result, nil
}

Ошибки как значения (Errors as Values): Важно помнить, что в Go ошибки являются значениями, а не исключениями. Они должны быть явно обработаны кодом.

Паники

Паника (Panic): Паника - это серьезная ошибка, которая приводит к немедленной остановке выполнения программы. Когда возникает паника, выполнение обычно останавливается, и выводится стек вызовов (stack trace). Паника может возникать, например, при доступе к nil-указателю или при делении на ноль.

Восстановление (Recover): Чтобы предотвратить полную остановку программы при возникновении паники, можно использовать механизм восстановления (recover). Recover может быть использован только в отложенных функциях (deferred functions). Он возвращает значение типа interface{} и используется для перехвата паники и возврата управления программе.

func main() {
    defer func() {
        if r := recover(); r != nil {
            // Обработка паники
        }
    }()
    // ...
    panic("something went wrong")
    // ...
}

Упрощенное восстановление (Simplified Recovery): В Go 1.13 и более поздних версиях было добавлено упрощенное восстановление, которое позволяет перехватывать и обрабатывать панику в одной инструкции.

func main() {
    // ...
    if r := recover(); r != nil {
        // Обработка паники
    }
    // ...
}

Пакеты и импорты

Тестирование

Работа с файлами и сетью

Развертывание приложений

Прочее

Веб-сервер на Golang

Самый простой веб-сервер:

package main
 
import (
	"log"
	"net/http"
)
 
func Handler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello world!\n"))
}
 
func main() {
	http.HandleFunc("/", Handler)
 
	log.Println("Start server on PORT:8081")
	log.Fatal(http.ListenAndServe(":8081", nil))
}

Сборка и запуск веб-сервера под Windows и Linux

Чтобы собрать приложение под другую ОС, нужно указать в переменных окружения GOOS и GOARCH