Конспект по основам Go


Типы данных

Объявление переменных

var num int = 10

var a int  // будет значение по умолчанию. Для int это 0
a = 10  // еще способ
// или просто
var a = 10

А также через :=, который автоматически определяет тип переменной в зависимости от ее значения.

Множественное присваивание (не совсем)

a, b := 10, 20

Константа

const pi = 4

Пакеты

Каждый файл программы в Go должен быть частью пакета. Пакет есть пространство имен.

Пакеты в Go делят программу на модули.
Импортированные пакеты = библиотеки.

package main — особый тип пакета в Go. Он содержит функцию main() — точку входа в программу.

пакет “fmt” предоставляет функции: выводит текст в консоль, читает ввод пользователя и форматирует строки.

пакет математический какой-то—

пакет “sort” для коллекций примитивных типов данных (string, int, float64)

sort.Ints(intSlice)
sort.Strings(stringSlice)

Указатели

Указатели через & — &age

package main

import (
	"fmt"
	"time"
)
func main() {
	var age int
	fmt.Println("Введите ваш возраст:")
	_, err := fmt.Scanln(&age)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(age)
}

  1. Какая функция из пакета fmt используется для форматированного вывода чисел с плавающей точкой?
  2. Какая из предложенных функций нужна для преобразования строки в число?
  3. Какой пакет в Go широко используется для работы с HTTP-запросами и ответами?
  4. Какая команда используется для установки сторонних пакетов в Go?
  5. Какая функция из пакета time используется для форматированного представления времени в строковом виде?

Конструкции

Что-то среднее между питоном и c++ (нет круглых скобок, но надо фигурные. Еще else if).
Оператор && опять же приоритетнее ||.

В switch statement не принято делать табы и нет break. Также можно использовать его без переменной, со своими условиями внутри case i < 0.

Объявление краткого условия if — хороший программистский тон.
if short statement:

if переменная := значение; условие

Хотя использовать конструкцию else в условных выражениях можно, в большинстве случаев желательно избегать её. И использовать return.

Циклы

в Go нет цикла while, но ничто не мешает сделать так:

for i < 10 {
 //
}

Range Loop:

for _, letter := range "Hello, world!" {
    fmt.Println(string(letter))
}

Бесконечный цикл:

for {
	//
}

Функции

Сигнатура func factorial(n int) int.

func findDiscriminant(a, b, c float64)`

Также функция может возвращать несколько значений. Частый пример множественного возврата — это возврат ошибок.

return 0, fmt.Errorf("wrong divider with value %v", b)

Писать функцию с более чем 4 возвращаемыми значениями не стоит.
При использовании рекурсии можно сохранять промежуточные результаты, чтобы ускорить программу. Эта распространённая техника оптимизации называется мемоизацией

Массивы

Массив в Go — это набор элементов одного типа фиксированного размера.

var numbers [5]int
var fruits [3]string = [3]string{"apple", "banana", "orange"}

// автоматически определить по количеству инициализирующих значений
var fruits = [...]string{"apple", "banana", "orange"}

a := [5 или ...]int{0,1,2,3,4}

Длина массива len(arr)

Слайсы

Слайсы в Go надстраиваются над массивом. Они позволяют легко добавлять, удалять и изменять элементы в любом месте массива, обращаться к группе элементов по индексам или диапазонам. На практике слаийсы встречаются гораздо чаще массивов.

Слайс изнутри

type slice struct {
	array unsafe.Pointer // указатель на элемент массива, с которого начинается слайс
	len   int            // длина
	cap   int            // ёмкость
}

Когда мы меняем элемент слайса, мы меняем элемент исходного массива

Создание слайса:

Длина и ёмкость слайса

Узнать можно через встроенные функции len и cap
У слайса та же емкость, что и у массива, на который он ссылается (точнее, они оба ссылаются).

Функция append позволяет “увеличить” слайс, точнее вернуть слайс с новой длиной.

a = append(a, elem)

Ёмкость слайса (capacity) определяет, сколько элементов может содержать в себе исходный массив. Если ёмкость переполнится, Go найдёт новое место в памяти компьютера и скопирует туда все элементы старого массива — реаллоцирует их.

Так может быть создан совсем другой массив.

Сами слайсы в Go в отличие от Python не самостоятельные (до момента реаллокации).

Руны

Это синтаксис среза строки

firstletter := name[0]

И тот выведет номер символа в таблице Unicode

Тип данных byte в Go — беззнаковый 8-битный целочисленный тип, может принимать значения от 0 до 255.

Строки в Go можно создавать из массивов байт, что позволяет работать с их содержимым как с текстовыми данными: читать их, записывать в файл или передавать по сети.

Для преобразования строки в массив байт и наоборот предусмотрены функции []byte и string.
При этом преобразование строк в байты не всегда приводит к нужному ответу, т.к. byte ограничен 255

Руны — символы из набора Unicode в Go. Каждая руна представляет собой один символ и может быть представлена числовым значением типа rune.

fmt.Printf("%c\n", r) // напечатать значение руны как символ
fmt.Println(string(f)) // или так

firstLetter := []rune(name) // конвертируем строку в слайс рун

Методы пакета strings

Хеш-таблицы. Мапы

Реализация хэш-таблицы в Go называется map.

Мапы (отображения) в Go представляют собой коллекцию пар «ключ-значение», где у ключей и значений могут быть (почти) любые типы данных. Тип данных ключей должен поддерживать операции сравнения: ==, !=, <, <=, >, >= (то есть они должны быть сравнимыми (comparable)).

Создание мапы:

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

res, ok := m[key] // получим значение, а в переменную ok — булевый ответ, существует ли такой ключ в массиве.

Пример программы

func containsDuplicate(nums []int) bool {
    set := make(map[int]bool)
    for _, num := range nums {
        if _, ok := set[num]; ok // если такой ключ существует, переходим к return 
        {
            return true
        }
        set[num] = true
    }
    return false
}

Еще примеры:

name, found := students[id] // found будет false если такого КЛЮЧА нет

Для мемоизации в fib

if val, ok := memo[n]; ok {
	return val
}

а чем мапы отличаются от слайса, если обход такой же?:

for _, val := range contacts {
	res[val]++
}

Мемоизация вместо рекурсии

var memo = map[int]int{}

func fib(n int) int {
    if n < 2 {
        return n
    }
    if val, ok := memo[n]; ok {
        return val
    }
    memo[n] = fib(n-1) + fib(n-2)
    return memo[n]
}

Структуры

Объявление

type Student struct {
    name string
    age  int
}

или

func main() {
    student1 := Student{}
}

Инициализация

student1 := Student{name: "vasya", age: 15}

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

Чаще всего названием берут ссылку на репозиторий в Github или Gitlab.

С помощью команды go mod init мы создаём новый модуль для нашего проекта и можем управлять зависимостями в нём.

Методы структур

Перед именем функции указывается имя структуры. ресивер

func (s Student) printData() {
    fmt.Printf("Name: %s, Age: %d\n", s.Name, s.Age)
}

поддерживаются ли конструкции в одну строчку

есть ли template (для методов, структур)


Интерфейсы

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

Интерфейсы в Go — это типы, которые определяют поведение объектов. Интерфейсы позволяют абстрагироваться от конкретной реализации объекта и работать с ним через определённый набор методов.

type MyInterface interface {
    Method1(arg1 type1, arg2 type2) returnType1
    Method2(arg1 type1) returnType2
    ...
}

var obj MyInterface = MyStruct{} // объект типа MyStruct через интерфейс MyInterface

Структуры могут реализовывать интерфейсы, если реализуют все методы, определённые в интерфейсе.

Передача интерфейсов

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

type Shape interface {
    Area() float64
}

func CalculateArea(s Shape) float64 {
    return s.Area()
}

rect := Rectangle{Width: 10, Height: 5}
area := CalculateArea(rect)

Пустые интерфейсы

Неясно, что в них уже хранится и что может быть туда передано. Вместо них лучше использовать типизированные интерфейсы.

Вместо interface{} вам может встретиться тип any. Но это уже про дженерики.

Приведение типов

Оператор type assertion

value, ok := x.(тип) // тип, true или тип, false

Пример:

var x interface{} = "hello"

s, ok := x.(string)
if ok {
    fmt.Println(s)
} else {
    fmt.Println("not a string")
}

Пример создания журнала ошибок (?) :

package main

import "fmt"

type Logger interface {
	Log()
}

type LogLevel string

const Error LogLevel = "Error"
const Info LogLevel = "Info"

type Log struct {
	Level LogLevel
}

func (s Log) Log(err string) {
	if s.Level == Error {
		fmt.Printf("ERROR: %s", err)
	} else {
		fmt.Printf("INFO: %s", err)
	}
}

Возврат ошибок

Ошибки в Go представлены интерфейсом error, который определяет метод Error() string. Любой тип, который реализует этот метод, может быть использован как ошибка в Go. Принято, чтобы ошибки всегда писались со строчной буквы.

Ошибки обрабатывают if err != nil

Создавать ошибки можно:


Встроенные функции как-то так реализованы, что позволяют получать 2й вывод (обычно про ошибку) или только 1


Отложенный вызов (defer)

несколько defer исполняются в обратном порядке

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

func createFile(filename string, data []byte) error {
    f, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer f.Close() // вот тут

    _, err = f.Write(data)
    if err != nil {
        return err
    }

    return nil
}

Ошибка panic

Паника в Go — это непредвиденное и критическое завершение программы, которое происходит из-за фатальной ошибки.

Вызов паники panic("Аааааааа")

Обработка паники с помощью recover().
Должна быть внутри функции, вызываемой с помощью defer. Если паники не было, функция recover() возвращает nil

defer func() {
	if r := recover(); r != nil {
		fmt.Println("Recovered from panic:", r)
	}
}()

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

Типы и кастинг

Неявное преобразование
Если мы объявляем переменную типа int, а затем присваиваем ей значение типа float64, Go автоматически преобразует значение float64 в int без явного указания.

Так это же явное преобразование
a = int(b) // неявное преобразование типа или casting

Явное преобразование
s := i.(string) // type assertion (декларация типа)
Если за интерфейсом лежит другой тип, включится паника, т. е. runtime error. Чтобы паники не было, используют такой синтаксис s,ok := i.(string).

Можно создавать собственные типы:

type Money float64

Работа со временем

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

Тип Go time.Time — это момент времени с точностью до наносекунд.

Пакет time

Он позволяет работать с различными форматами времени, в том числе ISO 8601 и RFC 2822. Доступ к базе данных часовых поясов IANA даёт пакет time/zoneinfo.

Разница между двумя моментами времени:

t1 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Now()
diff := t2.Sub(t1)

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

Метод Format типа time.Time

Как пример форматирования метод использует дату January 2, 15:04:05, 2006.
01/02 03:04:05PM ’06 −0700.

1: месяц (January (Январь), Jan, 01, etc)
2: день
3: час
4: минуты
5: секунды
6: год (2006)
7: часовой пояс (GMT-7 is MST)

now := time.Now()

fmt.Println("1. Время в формате RFC3339:", now.Format(time.RFC3339))
fmt.Println("2. Полная дата и время:", now.Format("2006-01-02 15:04:05"))
fmt.Println("3. Краткая дата:", now.Format("2006-01-02"))
fmt.Println("4. Время в 24-часовом формате:", now.Format("15:04:05"))
fmt.Println("5. Время в 12-часовом формате с AM/PM:", now.Format("03:04 PM"))
fmt.Println("6. День недели:", now.Format("Monday"))
fmt.Println("7. Сокращённый месяц:", now.Format("Jan"))

time.Sleep

Приостанавливает текущую горутину на указанный промежуток времени (устанавливается аргументом типа time.Duration)

time.Sleep(5 * time.Second)

Таймеры

Функция NewTimer: ей в качестве аргумента передаётся промежуток времени в виде типа time.Duration.


непонятно что делает этот NewTimer
_ := time.NewTimer(5 * time.Second) // Создаётся таймер на 5 секунд


Тики

В современных компьютерах тики (короткие промежутки времени) помогают синхронизировать работу компонентов системы и настроить работу системных таймеров.

Тики приостанавливают действия, пока не истечёт заданный интервал времени.

func main() {
    ticker := time.Tick(1 * time.Second)
    for t := range ticker {
        fmt.Println("Tick at", t)
    }
}

Примеры

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

В GO есть стандартный тестовый фреймворк. Он позволяет запускать тесты параллельно, группировать их в подгруппы, пропускать тесты, если они не могут быть запущены, и т. д.

Соглашение:

func TestConcurrentFunctionality(t *testing.T) {
    t.Run("Increment", testIncrement)
    t.Run("Decrement", testDecrement)
}

func testIncrement(t *testing.T) {
// тело теста инкрементации...
}

func testDecrement(t *testing.T) {
// тело теста декрементации...
}

Команда go test
Флаг -v позволяет выводить подробную информацию о тестах

Принудительно уронить тест t.Fail()

Кеширование результатов тестов
Команда go clean -cache

Флаг -count=N позволяет запускать тесы несколько раз
Это нужно, когда тест падает не в 100% запусков — например, если он зависит от базы данных или других внешних факторов. Если тест проходит успешно не в 100% запусков и при отсутствии внешних факторов, — значит, тест написан неверно.

Встроенный оператор range возвращает элементы в случайном порядке. При каждом запуске теста порядок элементов в мапе names будет разным, поэтому тест упадёт.

Для запуска отдельных тестов используется -run=Имя
При этом запустяться все тесты, начинающиеся с этого имени. Чтобы они не запускались, можно использовать ^Имя, т.к. она опция принмает регулярное выражение
Но лучше просто давать уникальные названия тестам

go test -v ./... запускает тесты и в дочерних директориях

Table driven testing — это принцип тестирования, который гласит: тест должен состоять из множества случаев.

func TestSum(t *testing.T) {
        // набор тестов
        cases := []struct {
            // имя теста
            name string
            // значения на вход тестируемой функции
            values []int
            // желаемый результат
            want int
        }{
            // тестовые данные № 1
            {
                name: "positive values",
                values: []int{1, 2, 3},
                want: 6,
            },
            // тестовые данные № 2
            {
                name: "mixed values",
                values: []int{-1, 2, -3},
                want: -2,
            },
        }
        // перебор всех тестов
        for _, tc := range cases {
            tc := tc
            // запуск отдельного теста
            t.Run(tc.name, func(t *testing.T) {
                // тестируем функцию Sum
                got := Sum(tc.values...)
                // проверим полученное значение
                if got != tc.want {
                        t.Errorf("Sum(%v) = %v; want %v", tc.values, got, tc.want)
                }
            })
        }
}

Тестовое покрытие (test coverage) — количественная мера, которая показывает, какая часть кода пакета выполняется при запуске тестов пакета.
go test -cover

type Test struct {
	in  int
	out string
}

var tests = []Test{
	{-1, "negative"},
	{5, "short"},
}

func TestLength(t *testing.T) {
	for i, test := range tests {
		size := Length(test.in)
		if size != test.out {
			t.Errorf("#%d: Size(%d)=%s; want %s", i, test.in, size, test.out)
		}
	}
}

Методы Fail(), Fatalf, Errorf чем отличаются?

Какие были тесты в задачах курса?

Не умею пользоватсья ключами типа %d



Git

git init

.git — это папка, которая хранит всю информацию о Git-репозитории.

Сущности Git

git status

4 состояния:

  1. untracked
  2. modified
  3. staged — подготовленный
  4. commited

Коммит — это основной объект в управлении контроля версий. Он объединяет все изменения в различных файлах за время этого коммита. Коммиты связаны между собой и следуют друг за другом.

У каждого коммита есть метаданные:

Ветка — это указатель на коммит. Но из-за того, что коммиты связаны между собой, ветка указывает на целую цепочку коммитов — на сам коммит и на все коммиты до него.

Основные команды

  1. Отслеживать все файлы git add .
  2. Проверить это можно git status
  3. Сделать коммит (после флага -m пишется сообщение git commit -m "Initial commit"
  4. Посмотреть историю коммитов git log
  5. Выйти из просмотра q

SSH GitHub

После авторизации вам нужно перейти в настройки аккаунта (кликнуть на значок своего аватара в правом верхнему углу экрана и выбрать пункт “Settings”). А в открывшемся окне найти пункт “SSH and GPG keys” и кликнуть зелёную кнопку “New SSH key”.

Если у вас уже есть SSH-ключ, просто вставьте его в поле “Key” и введите произвольное название в поле “Title”. А если нет, и вы не знаете, как его получить, выполните действия, описанные ниже.

ssh-keygen -t ed25519 -C "my_email@example.com"
Enter — сохранить в файл по умолчанию
enterx2 — оставить файл без пароля

Ключи id_ed25519.pub — открытый, id_ed25519 — закрытый

Сгенерированный публичный ключ нужно ввести в поле на GitHub, описанное выше (а приватный просто не удалять с вашего ПК).

git remote add origin git@github.com:YandexLyceum/yandex_lyceum_go.git

Все ваши изменения, которые вы сделали локально, можно отправить в удалённый репозиторий git push

Go Delve

Go Delve — это мощный инструмент для отладки программ на Go. Он предоставляет расширенные возможности для контроля выполнения программы, анализа переменных и стека вызовов.

Установка go get github.com/go-delve/delve/cmd/dlv

Запуск программы в режиме отладки dlv debug имя_файла.go

Полезные команды break, continue, next, step, print, stack, goroutines, quit

Запуск из среды разработки

VSCode красные точки и вкладка Run&Debug (Ctrl+Shift+D), кнопки управления на появившейся панели отладки


Как сделать массив смволом/рун из строки?

Почему []rune(str) — принимает в круглых скобках

В Go слайсы передаются не по указателю, а по значению (точно?) (как это может быть быстро?). Как тогда встроенные функции, типа sort.Strings(arr) меняют исходный массив?

В Go не поддерживаются вложенные функции?

Нельзя писать в глобальной области a := 1, но можно через var — wtf


строки организованы как срезы

один символ строки это целочисленный тип byte (0-255) по Unicode (не ASCII)

Строки в Go можно создавать из массивов байт, что позволяет работать с их содержимым как с текстовыми данными: читать их, записывать в файл или передавать по сети.

range перебирает строку по целочисленному типу rune Руна это как бы расширение byte.

byte - uint8 rune - int32

Правда, прямая работа с рунами в строках может быть сложной, если вы используете символы не из ASCII или мультибайтовые кодировки. ??


Длина слайса не может выходить за рамки исходного массива.

Длина слайса — его неизменяемое свойство, но функция append всё же может её «увеличить», то есть вернуть новый слайс с новой длиной.

Ёмкость слайса (capacity) определяет, сколько элементов может содержать в себе исходный массив.