Типы данных
- 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) // создаём слайс целых чисел длиной
-
литерал или
var
var 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)).
Создание мапы:
-
функция
make
var 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
— сохранить в файл по умолчанию
enter
x2 — оставить файл без пароля
Ключи 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) ?