Типы данных
- bool — true/false, false по умолчанию
- int
- float, float32/float64. По умлочанию 64, но для оптимизации подойдет 32
- string — по умолчанию "". Только двойные кавычки?
- nil — нулевой указатель
Объявление переменных
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)
}
- Какая функция из пакета fmt используется для форматированного вывода чисел с плавающей точкой?
- Какая из предложенных функций нужна для преобразования строки в число?
- Какой пакет в Go широко используется для работы с HTTP-запросами и ответами?
- Какая команда используется для установки сторонних пакетов в Go?
- Какая функция из пакета 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 // ёмкость
}
Когда мы меняем элемент слайса, мы меняем элемент исходного массива
Создание слайса:
-
функция
make(тип, длина[, емкость])a := make([]int, 2) // создаём слайс целых чисел длиной -
литерал или
varvar a []int // объявляем переменную типа «слайс целых чисел» fmt.Println(a == nil) // выведет true b := []int{0, 1, 1, 2} // присваиваем переменной b новый слайс fmt.Println(b) // [0 1 1 2] -
срез уже существующего слайса или массива
b := a[:4]
Длина и ёмкость слайса
Узнать можно через встроенные функции 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
- strings.ToLower(name)
- strings.Join(words, ” ”)
- strings.Replace(text, “Golang”, “GO”, -1)
Хеш-таблицы. Мапы
Реализация хэш-таблицы в Go называется map.
Мапы (отображения) в Go представляют собой коллекцию пар «ключ-значение», где у ключей и значений могут быть (почти) любые типы данных. Тип данных ключей должен поддерживать операции сравнения: ==, !=, <, <=, >, >= (то есть они должны быть сравнимыми (comparable)).
Создание мапы:
-
функция
makevar m map[string]int m = make(map[string]int) -
с помощью литерала
m := map[keyType]valueType{ key1: value1, key2: value2, // ... }
Получаем такой функционал
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)
Пустые интерфейсы
-
функция сможет принимать аргументы любого типа
func SomeFunction(arg interfacr{}) { // ... } -
создавать универсальные типы данных, которые могут хранить значения разных типов. Например, можно создать слайс
[]interface{}
Неясно, что в них уже хранится и что может быть туда передано. Вместо них лучше использовать типизированные интерфейсы.
Вместо 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
Создавать ошибки можно:
-
return 0, fmt.Errorf("division by zero") -
return 0, errors.New("division by zero") -
Удобнее выносить ошибки как константы
var ( ErrDivisionByZero = errors.New("division by zero") )
Встроенные функции как-то так реализованы, что позволяют получать 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
Работа со временем
Форматы времени
- Unix-время или Epoch time — 1694298140 сек. По умолчанию
- ISO 8601 — 2019-09-07T-15:50+00
- RFC 2822 — Tue, 20 Jan 1970 21:45:56 GMT+07:00
Тип 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)
}
}
Примеры
-
Определение продолжительности выполнения
elapsed := time.Since(startTime) -
Планирование выполнения задачи с использованием таймера
func main() { // Устанавливаем интервал времени для таймера (в данном случае, 5 секунд) interval := 5 * time.Second // Создаём новый таймер с указанным интервалом timer := time.NewTimer(interval) fmt.Println("Задача будет выполнена через", interval) // Ожидаем события от таймера (пока не прошло 5 секунд) <-timer.C fmt.Println("Задача выполнена!") } -
Работа с часовыми поясами и форматирование времени
func main() { // Устанавливаем часовой пояс UTC loc, err := time.LoadLocation("UTC") if err != nil { fmt.Println("Ошибка при загрузке часового пояса:", err) return } // Получаем текущее время в указанном часовом поясе currentTime := time.Now().In(loc) // Форматируем время в строку с заданным форматом formattedTime := currentTime.Format("2006-01-02 15:04:05") fmt.Println("Текущее время (UTC):", formattedTime) }
Тестирование в Go
В GO есть стандартный тестовый фреймворк. Он позволяет запускать тесты параллельно, группировать их в подгруппы, пропускать тесты, если они не могут быть запущены, и т. д.
Соглашение:
- пакет
testing - Тестовые функции должны начинаться с
Test - сигнатура принимает аргумент типа
*testing.T - Файлы с тестами иметь окончание
_test - Для подтестов используется
t.Run
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 состояния:
- untracked
- modified
- staged — подготовленный
- commited
Коммит — это основной объект в управлении контроля версий. Он объединяет все изменения в различных файлах за время этого коммита. Коммиты связаны между собой и следуют друг за другом.
У каждого коммита есть метаданные:
- id
- имя автора
- дата создания
- комментарий
Ветка — это указатель на коммит. Но из-за того, что коммиты связаны между собой, ветка указывает на целую цепочку коммитов — на сам коммит и на все коммиты до него.
Основные команды
- Отслеживать все файлы
git add . - Проверить это можно
git status - Сделать коммит (после флага
-mпишется сообщениеgit commit -m "Initial commit" - Посмотреть историю коммитов
git log - Выйти из просмотра
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
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) определяет, сколько элементов может содержать в себе исходный массив.
- [] в задаче везде реаллокация происходит? Или дело в arr = remove(arr, i) ?