- Тестовые вопросы по теме
- Типы данных и синтаксис
- Как устроены строки в Go?
- В чём ключевое отличие слайса (среза) от массива?
- Как вы отсортируете массив структур по алфавиту по полю Name?
- Как работает append в слайсе?
- Задача про слайсы #1
- Задача про слайсы #2
- Какое у слайса zero value? Какие операции над ним возможны?
- Что можешь рассказать про map?
- Как растет map?
- Что там про поиск?
- Есть ли у map такие же методы как у слайса: len, cap?
- Какие типы ключей разрешены для ключа в map?
- Может ли ключом быть структура? Если может, то всегда ли?
- Что будет в map, если не делать make или short assign?
- Race condition. Потокобезопасна ли мапа?
- Что такое интерфейс?
- Как устроен Duck-typing в Go?
- Интерфейсный тип
- Пустой interface{}
- На какой стороне описывать интерфейс — на передающей или принимающей?
- Что такое замыкание?
- Что такое сериализация? Зачем она нужна?
- Что такое type switch?
- Какие битовые операции знаешь?
- Дополнительный блок фигурных скобок в функции
- Что такое захват переменной?
- Как работает defer?
- Как работает init?
- Прерывание for/switch или for/select
- Сколько можно возвращать значений из функции?
- Дженерики — это про что?
- Параметризованные функции
- Параметризованные типы
- Компилятор
- Из каких этапов состоит компиляция?
- Статическая компиляция/линковка — что это, и в чем особенности?
- Какие директивы компилятора знаешь?
- //go:linkname
- //go:nosplit
- //go:norace
- //go:noinline
- //go:noescape
- //go:build
- //go:generate
- //go:embed
- Расскажи о себе?
- Кем был создан язык, какие его особенности?
- Go — императивный или декларативный? А в чем разница?
- Профилирование (pprof)
- Пример использования pprof
- Так как же профилировщик работает в принципе?
- Расскажи о своем самом интересном проекте?
- Часто задаваемые вопросы
- Тестирование
- TDT, Table-driven tests (табличное тестирование)
- Имя пакета с тестами
- Статические анализаторы (линтеры)
- Ошибка в бенчмарке
- Что про функциональное тестирование?
Тестовые вопросы по теме
Правильный ответ — после каждого вопроса выделен
1. Сколько битов потребуется, чтобы размесить в памяти компьютера фразу «Тили-тили тесто!»?
Правильный ответ: b)
2. Максимальное значение энтропии источника, который порождает 16 различных символов равно:
c) нельзя определить
3. Коэффициент сжатия для источника с вероятностями , , , равен:
Правильный ответ: a)
4. Энтропия Шеннона обладает свойством:
Правильный ответ: a)
5. Количество информации, содержащееся в двух статистически зависимых сообщениях, оценивается величиной:
a) энтропии Шеннона
b) условной энтропии
c) относительной энтропии
Правильный ответ: b)
1. Является ли побуквенный код , , для источника префиксным?
c) нельзя определить
Правильный ответ: b)
2. Является ли побуквенный код , , для источника разделимым?
c) нельзя определить
Правильный ответ: b)
3. Является ли побуквенный код , , однозначно декодируемым?
c) нельзя определить
Правильный ответ: a)
4. Выполняется ли неравенство Крафта для кода , , ?
Правильный ответ: a)
тест 5. После кодирования сообщения побуквенным кодом , , получена последовательность 001001100. Исходное сообщение имело вид:
Правильный ответ: a)
1. Средняя длина кодового слова побуквенного кода , , , для источника с равномерным распределением вероятностей равна:
Правильный ответ a)
2. Избыточность побуквенного кода , , , для источника с равномерным распределением вероятностей равна:
Правильный ответ b)
3. Является ли код , , , для источника с равномерным распределением вероятностей оптимальным?
Правильный ответ b)
4. Является ли код , , , для источника с распределением вероятностей ,, , оптимальным?
Правильный ответ a)
5. Средняя длина кодового слова кода , , , для источника с распределением вероятностей ,, , равна:
Правильный ответ a)
1 тест. Для кода Шеннона справедливо соотношение:
Правильный ответ b)
2. Длина кодового слова кода Шеннона для символа определяется из соотношения ( – вероятность появления символа ):
Правильный ответ a)
3. Средняя длина кодового слова кода Фано для источника с равномерным распределением вероятностей равна:
Правильный ответ a)
4. Совпадают ли коды Фано и Хаффмана для источника с равномерным распределением вероятностей?
c) возможны обе ситуации
Правильный ответ с)
5 тест. Совпадают ли средние длины кодов Фано и Хаффмана для источника с равномерным распределением вероятностей?
Правильный ответ a)
a) с известной статистикой
b) с неизвестной статистикой
c) с равномерным распределением вероятностей
Правильный ответ b)
2. Для оценки статистики источника сообщений используется:
a) скользящее окно
b) подвижное окно
c) пластиковое окно
Правильный ответ а)
3. Адаптивный код Хаффмана был предложен:
a) Д. Хаффманом
b) Р. Галлагером
c) К. Шенноном
Правильный ответ b)
4. Адаптивный код «стопка книг» позволяет хорошо сжимать сообщения:
a) с равномерным распределением символов
b) с большим количеством одинаковых символов
c) одинаково хорошо сжимает любые сообщения
Правильный ответ а)
тест№ 5. Необходимо ли при кодировании сообщения кодом «стопка книг» знать вероятностное распределение символов источника?
Правильный ответ а)
a) с известной статистикой
b) с неизвестной статистикой и меняющейся статистикой
c) с равномерным распределением вероятностей
Правильный ответ b)
2. Основными видами словарных методов типа LZ являются:
a) адаптивные коды и оптимальные коды
b) коды со скользящим окном и коды с использованием адаптивного словаря
c) адаптивный код Хаффмана и коды с использованием адаптивного словаря
Правильный ответ b)
3. При словарном кодировании адаптивный словарь используется:
a) для хранения ранее встречавшихся комбинаций символов и их кодов
b) для кодирования сообщения
c) для снижения избыточности кодирования
Правильный ответ а)
Типы данных и синтаксис
К фундаментальным типам данных можно отнести:
- Целочисленные —
int{8,16,32,64}
,int
,uint{8,16,32,64}
,uint
,byte
как синонимuint8
иrune
как синонимint32
. Типыint
иuint
имеют наиболее эффективный размер для определенной платформы (32 или 64 бита), причем различные компиляторы могут предоставлять различный размер для этих типов даже для одной и той же платформы - Числа с плавающей запятой —
float32
(занимает 4 байта/32 бита) иfloat64
(занимает 8 байт/64 бита) - Комплексные числа —
complex64
(вещественная и мнимая части представляют числаfloat32
) иcomplex128
(вещественная и мнимая части представляют числаfloat64
) - Логические aka
bool
- Строки
string
Как устроены строки в Go?
Создание подстрок работает очень эффективно. Поскольку строка предназначена только для чтения, исходная строка и строка, полученная в результате операции среза, могут безопасно совместно использовать один и тот же массив:
var (
str = "hello world"
sub = str[0:5]
usr = "/usr/kot"[5:]
)
print(sub, " ", usr) // hello kot
var str = "привет"
println(str, len(str)) // привет 12
for i, c := range str {
println(i, c, string(c))
}
// 0 1087 п
// 2 1088 р
// 4 1080 и
// 6 1074 в
// 8 1077 е
// 10 1090 т
И мы видим, что для кодирования каждого символа кириллицы используются по 2 байта.
Эффективным способом работы со строками (когда есть необходимость часто выполнять конкатенацию, например) является использование слайса байт или strings.Builder
:
import "strings"
func main() { // происходит только 1 аллокация при вызове `Grow()`
var str strings.Builder
str.Grow(12) // сразу выделяем память
str.WriteString("hello")
str.WriteRune(' ')
str.WriteString("мир")
println(str.String()) // hello мир
}
И ещё одну важную особенность стоит иметь в виду — это подсчет длины строки (например — для какой-нибудь валидации). Если считать по количеству байт, и строка содержит не только ASCII символы — то количество байт и фактическое количество символов будут расходиться:
const str = "hello мир!"
println(len(str), utf8.RuneCountInString(str)) // 13 10
Тут дело в том, что для кодирования символов м
, и
и р
используются 2 байта вместо одного. Поэтому len == 13
, а фактически в строке лишь 10 символов (пакет utf8
, к примеру, нам в помощь).
Что можно почитать: Строка, байт, руна, символ в Golang
В чём ключевое отличие слайса (среза) от массива?
- Срез — всегда указатель на массив, массив — значение
- Срез может менять свой размер и динамически аллоцировать память
В Go не бывает ссылок — но есть указатели. Где говорится про «по ссылке» имеется в виду «по указателю»
const mySize uint8 = 8
type myArray [mySize]byte
var constSized = [...]int{1, 2, 3} // размер сам посчитается исходя из кол-ва эл-ов
Кстати, массивы с элементами одного типа но с разными размерами являются разными типами. Массивы не нужно инициализировать явно; нулевой массив — это готовый к использованию массив, элементы которого являются нулями:
var a [4]int // [0 0 0 0]
a[0] = 1 // [1 0 0 0]
i := a[0] // i == 1
А слайс же это своего рода версия массива но с вариативным размером (структура данных, которая строится поверх массива и предоставляет доступ к элементами базового массива). Слайсы до 64 KB могут быть размещены на стеке. Если посмотреть исходники Go (src/runtime/slice.go), то увидим:
type slice struct {
array unsafe.Pointer // указатель на массив
len int // длина (length)
cap int // вместимость (capacity)
}
Для аллокации слайса можно воспользоваться одной из команд ниже:
var (
a = []int{} // [] len=0 cap=0
b = []int{1, 2} // [1 2] len=2 cap=2
c = []int{5: 123} // [0 0 0 0 0 123] len=6 cap=6
d = make([]int, 5, 10) // [0 0 0 0 0] len=5 cap=10
)
В последнем случае рантайм Go создаст массив из 10 элементов (выделит память и заполнит их нулями) но доступны прямо сейчас нам будут только 5, и установит значения len
в 5
, а cap
в 10
. Cap
означает ёмкость и помогает зарезервировать место в памяти на будущее, чтобы избежать лишних операций выделения памяти при росте слайса (это ключевой параметр для аллокации памяти, влияет на производительность вставки в срез). При добавлении новых элементов в слайс новый массив для него не будет создаваться до тех пор, пока cap
меньше len
.
Слайсы передаются «по ссылке» (фактически будет передана копия структуры slice
со своими len
и cap
, но указатель на массив array
будет тот-же самый). Для защиты слайса от изменений следует передавать его копию:
var (
a = []int{1, 2, 0, 0, 1}
b = make([]int, len(a))
)
copy(b, a)
fmt.Println(a, b) // [1 2 0 0 1] [1 2 0 0 1]
Важной особенностью является то, так как «под капотом» у слайса лежит указатель на массив — при изменении значений слайса они будут изменяться везде, где слайс используется (будь то присвоение в переменную, передача в функцию и т.д.) до момента, пока размер слайса не будет переполнен и не будет выделен новый массив для его значений (т.е. в момент изменения cap
слайса всегда происходит копирование данных массива):
var (
one = []int{1, 2} // [1 2]
two = one // [1 2]
)
two[0] = 123
fmt.Println(one, two) // [123 2] [123 2]
one = append(one, 666)
fmt.Println(one, two) // [123 2 666] [123 2]
Что можно почитать: Как не наступать на грабли в Go, Слайсы в Go: использование и особенности, Принцип работы типа slice в GO
Как вы отсортируете массив структур по алфавиту по полю Name
?
Например, преобразую массив в слайс и воспользуюсь функцией sort.SliceStable
:
package main
import (
"fmt"
"sort"
)
func main() {
var arr = [...]struct{ Name string }{{Name: "b"}, {Name: "c"}, {Name: "a"}}
// ^^^^^^^^^^^^^^^^^^^^^ анонимная структура с нужным нам полем
fmt.Println(arr) // [{b} {c} {a}]
sort.SliceStable(arr[:], func(i, j int) bool { return arr[i].Name < arr[j].Name })
// ^^^ вот тут вся "магия" - из массива сделали слайс
fmt.Println(arr) // [{a} {b} {c}]
}
Вся магия в том, что при создании слайса из массива «под капотом» у слайса начинает лежать исходный массив, и функции из пакета sort
нам становятся доступны над ними. Т.е. изменяя порядок элементов в слайсе функцией sort.SliceStable
мы будем менять их в нашем исходном массиве.
Как работает append
в слайсе?
append()
делает простую операцию — добавляет элементы в слайс и возвращает новый. Но под капотом там делаются довольно сложные манипуляции, чтобы выделять память только при необходимости и делать это эффективно.
Сперва append
сравнивает значения len
и cap
у слайса. Если len
меньше чем cap
, то значение len
увеличивается, а само добавляемое значение помещается в конец слайса. В противном случае происходит выделение памяти под новый массив для элементов слайса, в него копируются значения из старого, и значение помещается уже в новый массив.
Увеличении размера слайса (метод growslice
) происходит по следующему алгоритму — если его размер менее 1024 элементов, то его размер будет увеличиваться вдвое; иначе же слайс увеличивается на ~12.5% от своего текущего размера.
Что важно помнить — если на основании слайса one
выделить подслайс two
, а затем увеличим слайс one
(и его вместимость будет превышена) — то one
и two
будут уже ссылаться на разные участки памяти!
var (
one = make([]int, 4) // [0 0 0 0]
two = one[1:3] // [0 0]
)
one[2] = 11
fmt.Println(one, two) // [0 0 11 0] [0 11]
fmt.Printf("%p %p\n", one, two) // 0xc0000161c0 0xc0000161c8
one = append(one, 1)
fmt.Printf("%p %p\n", one, two) // 0xc00001c1c0 0xc0000161c8
one[2] = 22
fmt.Println(one, two) // [0 0 22 0 1] [0 11]
fmt.Printf("%p %p\n", one, two) // 0xc00001c1c0 0xc0000161c8
Есть еще много примеров добавления, копирования и других способов использования слайсов тут — Slice Tricks.
Что можно почитать: Как не наступать на грабли в Go
Задача про слайсы #1
Вопрос: У нас есть 2 функции — одна делает append()
чего-то в слайс, а другая просто сортирует слайс, используя пакет sort
. Модифицируют ли слайс первая и (или) вторая функции?
Ответ: append()
не модифицирует а возвращает новый слайс, а sort
модифицирует порядок элементов, если он изначально был не отсортирован.
Задача про слайсы #2
Вопрос: Что выведет следующая программа?
package main
import "fmt"
func main() {
a := [5]int{1, 2, 3, 4, 5}
t := a[3:4:4]
fmt.Println(t[0])
}
Появилась эта штука в Go 1.2.
Что можно почитать:
Slicing a slice with slice [a : b : c]
, Full slice expressions
Какое у слайса zero value? Какие операции над ним возможны?
Zero value у слайса всегда nil
, а len
и cap
равны нулю, так как «под ним» нет инициализированного массива:
var a []int
println(a == nil, len(a), cap(a)) // true 0 0
a = append(a, 1)
println(a == nil, len(a), cap(a)) // false 1 1
Как видно из примера выше — несмотря на то, что a == nil
(слайс «не инициализирован»), с этим слайсом возможна операция append
— в этом случае Go самостоятельно создаёт нижележащий массив и всё работает так, как и ожидается. Более того — для полной очистки слайса рекомендуется его присваивать к nil
.
Так же важно помнить, что не делая make
для слайса — не получится сделать пре-аллокацию, что часто очень болезненно для производительности.
Что можешь рассказать про map
?
Карта (map
или hashmap
) — это неупорядоченная коллекция пар вида ключ-значение. Пример:
type myMap map[string]int
Подобно массивам и слайсам, к элементам мапы можно обратиться с помощью скобок:
var m = make(map[string]int) // инициализация
m["one"] = 1 // запись в мапу
fmt.Println(m["one"], m["two"]) // 1 0
Лучше выделить память заранее (передавая вторым аргументом функции
make
), если известно количество элементов — избежим эвакуаций
var m = map[string]int{"one": 1}
v1, ok1 := m["one"] // чтение
v2, ok2 := m["two"]
fmt.Println(v1, ok1) // 1 true
fmt.Println(v2, ok2) // 0 false
for k, v := range m { // итерация всех эл-ов мапы
fmt.Println(k, v)
}
delete(m, "one") // удаление
v1, ok1 = m["one"]
fmt.Println(v1, ok1) // 0 false
Мапы всегда передаются по ссылке (вообще-то Go не бывает ссылок, невозможно создать 2 переменные с 1 адресом, как в С++ например; но зато можно создать 2 переменные, указывающие на один адрес — но это уже указатели). Если же быть точнее, то мапа в Go — это просто указатель на структуру hmap
:
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
Так же структура hmap
содержит в себе следующее:
- Количество элементов
- Количество «ведер» (представлено в виде логарифма для ускорения вычислений)
- Seed для рандомизации хэшей (чтобы было сложнее заddosить — попытаться подобрать ключи так, что будут сплошные коллизии)
- Всякие служебные поля и главное указатель на buckets, где хранятся значения
На картинке схематичное изображение структуры в памяти — есть хэдер hmap, указатель на который и есть map в Go (именно он создается при объявлении с помощью var, но не инициализируется, из-за чего падает программа при попытке вставки). Поле buckets
— хранилище пар ключ-значение, таких «ведер» несколько, в каждом лежит 8 пар. Сначала в «ведре» лежат слоты для дополнительных битов хэшей (e0..e7
названо e
— потому что extra hash bits). Далее лежат ключи и значения как сначала список всех ключей, потом список всех значений.
По хэш функции определяется в какое «ведро» мы кладем значение, внутри каждого «ведра» может лежать до 8 коллизий, в конце каждого «ведра» есть указатель на дополнительное, если вдруг предыдущее переполнилось.
Как растет map
?
В исходном коде можно найти строчку Maximum average load of a bucket that triggers growth is 6.5
. То есть, если в каждом «ведре» в среднем более 6,5 элементов, происходит увеличение массива buckets
. При этом выделяется массив в 2 раза больше, а старые данные копируются в него маленькими порциями каждые вставку или удаление, чтобы не создавать очень крупные задержки. Поэтому все операции будут чуть медленнее в процессе эвакуации данных (при поиске тоже, нам же приходится искать в двух местах). После успешной эвакуации начинают использоваться новые данные.
Из-за эвакуации данных нельзя и взять адрес мапы — представьте, что мы взяли адрес значения, а потом мапа выросла, выделилась новая память, данные эвакуировались, старые удалились, указатель стал неправильным, поэтому такие операции запрещены.
Что там про поиск?
К сожалению, мир не совершенен. Когда имя хешируется, то некоторые данные теряются, так как хеш, как правило, короче исходной строки. Таким образом, в любой реализации хеш таблицы неизбежны коллизии когда по двум ключам получаются одинаковые хеши. Как следствие, поиск может быть дороже чем O(1)
(возможно это связано с кешем процессора и коллизиями коротких хэшей), так что иногда выгоднее использовать бинарный поиск по слайсу данных нежели чем поиск в мапе (пишите бенчмарки).
Что можно почитать: Хэш таблицы в Go. Детали реализации, Кажется, поиск в map дороже чем O(1)
Есть ли у map
такие же методы как у слайса: len
, cap
?
У мапы есть len
но нет cap
. У нас есть только overflow
который указывает «куда-то» когда мапа переполняется, и поэтому у нас не может быть capacity
.
Какие типы ключей разрешены для ключа в map
?
Любым сравнимым (comparable) типом, т.е. булевы, числовые, строковые, указатели, канальные и интерфейсные типы, а также структуры или массивы, содержащие только эти типы. Слайсы, мапы и функции использовать нельзя, так как эти типы не сравнить с помощью оператора ==
или !=
.
Может ли ключом быть структура? Если может, то всегда ли?
Как было сказано выше — структура может быть ключом до тех пор, пока мы в поля структуры не поместим какой-либо слайс, мапу или любой другой non-comparable тип данных (например — функцию).
Что будет в map
, если не делать make
или short assign
?
Будет паника (например — при попытке что-нибудь в неё поместить), так как любые «структурные» типы (а мапа как мы знаем таковой является) должны быть инициализированы для работы с ними.
Race condition. Потокобезопасна ли мапа?
Нет, потокобезопасной является sync.Map
. Для обеспечения безопасности вокруг мапы обычно строится структура вида:
type ProtectedIntMap struct {
mx sync.RWMutex
m map[string]int
}
func (m *ProtectedIntMap) Load(key string) (val int, ok bool) {
m.mx.RLock()
val, ok = m.m[key]
m.mx.RUnlock()
return
}
func (m *ProtectedIntMap) Store(key string, value int) {
m.mx.Lock()
m.m[key] = value
m.mx.Unlock()
}
Что такое интерфейс?
Интерфейсы — это инструменты для определения наборов действий и поведения. Интерфейсы — это в первую очередь контракты. Они позволяют объектам опираться на абстракции, а не фактические реализации других объектов. При этом для компоновки различных поведений можно группировать несколько интерфейсов. В общем смысле — это набор методов, представляющих стандартное поведение для различных типов данных.
Как устроен Duck-typing в Go?
Если это выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, утка и есть.
Если структура содержит в себе все методы, что объявлены в интерфейсе, и их сигнатуры совпадают — она автоматически удовлетворяет интерфейс.
Такой подход позволяет полиморфно (полиморфизм — способность функции обрабатывать данные разных типов) работать с объектами, которые не связаны в иерархии наследования. Достаточно, чтобы все эти объекты поддерживали необходимый набор методов.
Интерфейсный тип
В Go интерфейсный тип выглядит вот так:
type iface struct {
tab *itab
data unsafe.Pointer
}
Где tab
— это указатель на Interface Table или itable
— структуру, которая хранит некоторые метаданные о типе и список методов, используемых для удовлетворения интерфейса, а data
указывает на реальную область памяти, в которой лежат данные изначального объекта (статическим типом).
Компилятор генерирует метаданные для каждого статического типа, в которых, помимо прочего, хранится список методов, реализованных для данного типа. Аналогично генерируются метаданные со списком методов для каждого интерфейса. Теперь, во время исполнения программы, runtime Go может вычислить itable
на лету (late binding) для каждой конкретной пары. Этот itable
кешируется, поэтому просчёт происходит только один раз.
Зная это, становится очевидно, почему Go ловит несоответствия типов на этапе компиляции, но кастинг к интерфейсу — во время исполнения.
Что важно помнить — переменная интерфейсного типа может принимать nil
. Но так как объект интерфейса в Go содержит два поля: tab
и data
— по правилам Go, интерфейс может быть равен nil
только если оба этих поля не определены (faq):
var (
builder *strings.Builder
stringer fmt.Stringer
)
fmt.Println(builder, stringer) // nil nil
fmt.Println(stringer == nil) // true
fmt.Println(builder == nil) // true
stringer = builder
fmt.Println(builder, stringer) // nil nil
fmt.Println(stringer == nil) // false (!!!)
fmt.Println(builder == nil) // true
Пустой interface{}
Ему удовлетворяет вообще любой тип. Пустой интерфейс ничего не означает, никакой абстракции. Поэтому использовать пустые интерфейсы нужно в самых крайних случаях.
Что можно почитать: Краш-курс по интерфейсам в Go, Реализация интерфейсов в Golang, Интерфейсы в Go — как красиво выстрелить себе в ногу
На какой стороне описывать интерфейс — на передающей или принимающей?
Многое зависит от конкретного случая, но по умолчанию описывать интерфейсы следует на принимающей стороне — таким образом, ваш код будет меньше зависеть от какого-то другого кода/пакета/реализации.
Другими словами, если нам в каком-то месте требуется «что-то что умеет себя закрывать», или — умеет метод Close() error
, или (другими словами) удовлетворят интерфейсу:
type something interface {
Close() error
}
То он (интерфейс) должен быть описан на принимающей стороне. Так принимающая сторона не будет ничего знать о том, что именно в неё может «прилететь», но точно знает поведение этого «чего-то». Таким образом реализуется инверсия зависимости, и код становится проще переиспользовать/тестировать.
Что такое замыкание?
Замыкания — это такие функции, которые вы можете создавать в рантайме и им будет доступно текущее окружение, в рамках которого они были созданы.
Функции, у которых есть имя — это именованные функции. Функции, которые могут быть созданы без указания имени — это анонимные функции.
func main() {
var text = "some string"
var ourFunc = func() { // именованное замыкание
println(text)
}
ourFunc() // some string
getFunc()() // another string
}
func getFunc() func() {
return func() { // анонимное
println("another string")
}
}
Замыкания сохраняют состояние. Это означает, что состояние переменных содержится в замыкании в момент декларации. Одна из самых очевидных ловушек — это создание замыканий в цикле:
var funcs = make([]func(), 0, 5)
for i := 0; i < 5; i++ {
funcs = append(funcs, func() { println("counter =", i) })
// исправляется так:
//var value = i
//funcs = append(funcs, func() { println("counter =", value) })
}
for _, f := range funcs {
f()
}
// counter = 5 (так все 5 раз)
Что можно почитать: Замыкания
Что такое сериализация? Зачем она нужна?
Сериализация — это процесс преобразования объекта в поток байтов для сохранения или передачи. Обратной операцией является десериализация (т.е. восстановление объекта/структуры из последовательности байтов). Синонимом можно считать термин «маршалинг» (от англ. marshal
— упорядочивать).
Из минусов сериализации можно выделить нарушение инкапсуляции, т.е. после сериализации «приватные» свойства структур могут быть доступны для изменения.
Типичными примерами сериализации в Go являются преобразование структур в json-объекты. Кроме json существуют различные кодеки типа MessagePack
, CBOR
и т.д.
Что такое type switch
?
Проверка типа переменной, а не её значения. Может быть в виде одного switch
и множеством case
:
package main
func checkType(i interface{}) {
switch i.(type) {
case int:
println("is integer")
case string:
println("is string")
default:
println("has unknown type")
}
}
А может в виде if
-конструкции:
package main
func main() {
var any interface{}
any = "foobar"
if s, ok := any.(string); ok {
println("this is a string:", s)
}
// а так можно проверить наличие функций у структуры
if closable, ok := any.(interface{ Close() }); ok {
closable.Close()
}
}
Какие битовые операции знаешь?
Побитовые операторы проводят операции непосредственно на битах числа.
// Побитовое И/AND (разряд результата равен 1 только тогда, когда оба соответствующих бита операндов равны 1)
println(0b111_000 /* 56 */ & 0b011_110 /* 30 */ == 0b011_000 /* 24 */)
// Побитовое ИЛИ/OR (разряд результата равен 0 только тогда, когда оба соответствующих бита в равны 0)
println(0b111_000 /* 56 */ | 0b011_110 /* 30 */ == 0b111_110 /* 62 */)
// Исключающее ИЛИ/XOR (разряд результата равен 1 только тогда, когда только один бит равен 1)
println(0b111_000 /* 56 */ ^ 0b011_110 /* 30 */ == 0b100_110 /* 38 */)
// Сброс бита AND NOT
println(0b111_001 /* 57 */ &^ 0b011_110 /* 30 */ == 0b100_001 /* 33 */)
// Сдвиг бита влево
println(0b000_001 /* 1 */ << 3 == 0b001_000 /* 8 */)
// Сдвиг бита вправо
println(0b000_111 /* 7 */ >> 1 == 0b000_011 /* 3 */)
Пример использования простой битовой маски:
type Bits uint8
const (
F0 Bits = 1 << iota // 0b00_000_001 == 1
F1 // 0b00_000_010 == 2
F2 // 0b00_000_100 == 4
)
func Set(b, flag Bits) Bits { return b | flag }
func Clear(b, flag Bits) Bits { return b &^ flag }
func Toggle(b, flag Bits) Bits { return b ^ flag }
func Has(b, flag Bits) bool { return b&flag != 0 }
func main() {
var b Bits
b = Set(b, F0)
b = Toggle(b, F2)
for i, flag := range [...]Bits{F0, F1, F2} {
println(i, Has(b, flag))
}
// 0 true
// 1 false
// 2 true
}
Что можно почитать: О битовых операциях, Поразрядные операции
Дополнительный блок фигурных скобок в функции
Его можно использовать, и он означает отдельный скоуп для всех переменных, объявленных в нём (возможен и «захват переменных» объявленных вне скоупа ранее, естественно). Иногда используется для декомпозиции какого-то отдельного куска функции, к примеру.
var i, s1 = 1, "foo"
{
var j, s2 = 2, "bar"
println(i, s1) // 1 foo
println(j, s2) // 2 bar
s1 = "baz"
}
println(i, s1) // 1 baz
//println(j, s2) // ERROR: undefined: j and s2
Так же это может быть связано с AST (Abstract Syntax Tree) — когда оно строится и происходят SSA (Static Single Assignment) оптимизации, к сожалению SSA не работает на всю длину дерева. Как следствие, если у нас слишком длинная функция (примерно дохулион строк) и мы по каким-то причинам не можем её декомпозировать, но можем изолировать какие-то скоупы то, таким образом, мы помогаем SSA произвести оптимизации (если они возможно).
Что такое захват переменной?
Во вложенном скоупе есть возможность обращаться к переменным, объявленных в скоупе выше (но не наоборот). Обращение к переменным из вышестоящего скоупа и есть их захват. Типичной ошибкой является использование значение итератора в цикле:
var out []*int
for i := 0; i < 3; i++ {
out = append(out, &i)
}
println(*out[0], *out[1], *out[2]) // 3 3 3
Испраляется путём создания локальной (для скоупа цикла) переменной с копией знаяения итератора:
var out []*int
for i := 0; i < 3; i++ {
i := i // Copy i into a new variable.
out = append(out, &i)
}
println(*out[0], *out[1], *out[2]) // 0 1 2
Что можно почитать: Using reference to loop iterator variable
Как работает defer
?
Defer
является функцией отложенного вызова. Выполняется всегда (даже в случае паники внутри функции вызываемой) после того, как функция завершила своё выполнение но до того, как управление вернётся вызывающей стороне (более того — внутри defer
возможен захват переменных, и даже возвращаемого результата). Часто используется для освобождения ресурсов/снятия блокировок. Пример использования:
func main() {
println("result =", f())
// f started
// defer
// defer in defer
// result = 25
}
func f() (i int) {
println("f started")
defer func() {
recover()
defer func() { println("defer in defer"); i += 5 }()
println("defer")
i = i * 2
}()
i = 10
panic("panic is here")
}
Когда выполняется ключевое слово defer
, оно помещает следующий за ним оператор в список, который будет вызван до возврата функции.
Как работает init
?
В Go есть предопределенная функция init()
. Она выделяет фрагмент кода, который должен выполняться перед всеми другими частями пакета. Этот код будет выполняться сразу после импорта пакета.
Также функция init()
используется для автоматической регистрации одного пакета в другом (например, так работает подавляющее большинство «драйверов» для различных СУБД, например — go-sql-driver/mysql/driver.go).
Функцию init()
можно использовать неоднократно в рамках даже одного файла, выполняться они будут в этом случае в порядке, как их встречает компилятор.
Хотя использование init()
и является довольно полезным, но часто оно затрудняет чтение/понимание кода, и (почти) всегда можно обойтись без неё, поэтому необходимость её использования — всегда очень большой вопрос.
Прерывание for/switch или for/select
Что произойдёт в следующем примере, если f()
вернёт true
?
for {
switch f() {
case true:
break
case false:
// Do something
}
}
Очевидно, будет вызван break
. Вот только прерван будет switch
, а не цикл for
. Простое решение проблемы – использовать именованный (labeled) цикл и вызывать break
c этой меткой, как в примере ниже:
loop:
for {
switch f() {
case true:
break loop
case false:
// Do something
}
}
Сколько можно возвращать значений из функции?
Теоретически, неограниченное количество значений. Так же хочется отметить, что есть правила «де-факто», которых следует придерживаться:
- Последним значением возвращать ошибку, если её возврат подразумевается
- Первым значением возвращать контекст, если он подразумевается
- Хорошим тоном является не возвращать более четырёх значений
- Если функция что-то проверяет и возвращает значение + булевый результат проверки — то результат проверки возвращать последним (пример —
os.LookupEnv(key string) (string, bool)
) - Если возвращается ошибка, то остальные значения возвращать нулевыми или
nil
Дженерики — это про что?
Дженерики, или обобщения — это средства языка, позволяющего работать с различными типами данных без изменения их описания.
В версии 1.18
появились дженерики (вообще-то они были и ранее, но мы не могли их использовать в своём коде — вспомни функцию make(T type)
), и они позволяют объявлять (описывать) универсальные методы, т.е. в качестве параметров и возвращаемых значений указывать не один тип, а их наборы.
Появились новые ключевые слова:
any
— аналогinterface{}
, можно использовать в любом месте (func do(v any) any
,var v any
,type foo interface { Do() any }
)comparable
— интерфейс, который определяет типы, которые могут быть сравнены с помощью==
и!=
(переменные такого типа создать нельзя —var j comparable
будет вызывать ошибку)
И появилась возможность определять интерфейсы, которые можно будет использовать в параметризованных функциях и типах (переменные такого типа создать нельзя — var j Int
будет вызывать ошибку):
type Int interface {
int | int32 | int64
}
Если добавить знак ~
перед типами то интерфейсу будут соответствовать и производные типы, например myInt из примера ниже:
type Int interface {
~int | ~int32 | ~int64
}
type myInt int
Разработчики golang создали для нас уже готовый набор интерфейсов (пакет constraints
), который очень удобно использовать.
Параметризованные функции
Рассмотрим пример функции, что возвращает максимум из двух переданных значений, причём тип может быть любым:
import "constraints"
func Max[T constraints.Ordered](a T, b T) T {
if a > b {
return a
}
return b
}
Ограничения на используемые типы описываются в квадратных скобочках. В качестве ограничения для типов можно использовать любой интерфейс и особые интерфейсы описанные выше.
Для слайсов и мап был создан набор готовых полезных функций.
Параметризованные типы
import "reflect"
type myMap[K comparable, V any] map[K]V
func main() {
m := myMap[int, string]{5: "foo"}
println(m[5]) // foo
println(reflect.TypeOf(m)) // main.myMap[int,string]
}
Что можно почитать: Зачем нужны дженерики в Go?, Golang пощупаем дженерики
Компилятор
Компиляция — это процесс преобразования вашего (говно)кода в кашу из машинного кода. Первое понятно тебе, второе — машине.
Из каких этапов состоит компиляция?
cmd/compile
содержит основные пакеты Go компилятора. Процесс компиляции может быть логически разделен на четыре фазы:
- Parsing (
cmd/compile/internal/syntax
) — сорец парсится, разбивается на токены, создается синтаксическое дерево - Type-checking and AST (Abstract Syntax Tree) transformations (
cmd/compile/internal/gc
) — дерево переводится в AST, тут же происходит магия по авто-типизации, проверок интерфейсов этапа компиляции, определяется мертвый код и происходит escape-анализ - Generic SSA (Static Single Assignment) (
cmd/compile/internal/gc
,cmd/compile/internal/ssa
) — AST переводится в SSA (промежуточное представление более низкого уровня), что упрощает реализацию оптимизаций; так же применяются множественные оптимизации этого уровня (тут, например, циклыrange
переписываются в обычныеfor
; аcopy
заменяется перемещением памяти), удаляются ненужные проверки наnil
и т.д. - Generating machine code (
cmd/compile/internal/ssa
,cmd/internal/obj
) — универсальные штуки перезаписываются на машинно-зависимые (в зависимости от архитектуры и ОС), после чего над SSA снова выполняются оптимизации, удаляется мертвый код, распределяются регистры, размечается стековый фрейм; после — ассемблер превращает всё это добро в машинный код и записывает объектный файл
Что можно почитать: Введение в компилятор Go
Статическая компиляция/линковка — что это, и в чем особенности?
Линковка (ну или компоновка) последний этап сборки. Статически слинкованный исполняемый файл не зависит от наличия других библиотек в системе во время своей работы.
Однако, это накладывает некоторые ограничения и привносит особенности, которые необходимо помнить:
C
-код будет недоступен, совсем (часть модулей из stdlib Go от него зависят, к слову, но не критичных)- Не будет использоваться системный DNS-резольвер
- Не будет работать проверка
x.509
сертификатов, которая должна работать на MacOS X
И ещё, если итоговый бинарный файл планируется использовать в docker scratch
, то так же следует иметь в виду:
- Для осуществления HTTP запросов по протоколу HTTPS вашим приложением, в образ нужно будет поместить корневые SSL/TLS сертификаты
/etc/ssl/certs
- Файл временной зоны (
/etc/timezone
) тоже будет необходим, чтоб корректно работать с датой/временем
Что можно почитать: Docker scratch & CGO_ENABLED=0, Кросс-компиляция в Go, Go dns
Какие директивы компилятора знаешь?
Компилятор Go понимает некоторые директивы (пишутся они в виде комментариев, как правило //go:directive
), которые влияют на процесс компиляции (оптимизации, проверок, и т.д.) но не являются частью языка. Вот некоторые из них:
//go:linkname
Указывает компилятору реальное местонахождение функции или переменной. Можно использовать для вызова приватных функций из других пакетов. Требует импортирования пакета unsafe
(import _ "unsafe"
). Формат следующий:
//go:linkname localname [importpath.name]
import (
_ "strings" // for explodeString
_ "unsafe" // for go:linkname
)
//go:linkname foo main.bar
func foo() string
func bar() string { return "bar" }
//go:linkname explodeString strings.explode
func explodeString(s string, n int) []string
func main() {
println(foo()) // bar
println(explodeString("foo", -1)) // [3/3]0xc0000a00f0
}
//go:nosplit
Указывается при объявлении функции, и указывает на то, что вызов функции должен пропускать все обычные проверки на переполнение стека.
//go:norace
Так же указывается при объявлении функции и «выключает» детектор гонки (race detector) для неё.
//go:noinline
Отключает оптимизацию «инлайнига» для функции. Обычно используется отладки компилятора, escape-аналитики или бенчаркинга.
//go:noescape
Тоже «функциональная» директива, смысл которой сводится к тому, что «я доверяю этой функции, и ни один указатель, переданных в качестве аргументов (или возвращенных) этой функции не должен быть помещен в кучу (heap)».
//go:build
Эта директива обеспечивает условную сборку. То есть мы можем «размечать» тегами файлы, и таким образом компилировать только определенные их «наборы» (тегов может быть несколько, а так же можно использовать !
для указания «не»). Часто используется для кодогенерации, указывая какой-то специфичный тег (например ignore
— //go:build ignore
) чтоб файл никогда не учавствовал с борке итогового приложения.
В качестве примера создадим 2 файла в одной директории:
// file: main.go
//go:build one
package main
func main() { println("one!") }
// file: main2.go
//go:build two
package main
func main() { println("two!") }
И соберем с разными значениями -tags
для go build
или go run
(обрати внимение — какой именно файл собирать не указывается, только тег):
$ go run -tags one .
one!
$ go run -tags two .
two!
//go:generate
Позволяет указать какие внешние команды должны вызваться при запуске go generate
. Таким образом, мы можем использовать кодогенерацию, к примеру, или выполнять какие-то операции что дожны предшевствовать сборке (например — //go:generate go run gen.go
где gen.go
это файл, что содержит //go:build ignore
т.е. исключён из компиляции, но при этом генерирует для нас какие-то полезные данные и/или целые .go
файлы):
package main
//go:generate echo "my build process"
func main() {
println("hello world")
}
$ go generate
my build process
//go:embed
package main
import _ "embed"
//go:embed test.txt
var hello string
func main() {
println(hello)
}
$ echo "hello world" > test.txt
$ go run .
hello world
Что можно почитать: pkg.go.dev/cmd/compile, Go Compiler Directives, Генерация кода в Go, pkg.go.dev/embed
Расскажи о себе?
Чаще всего этот вопрос идёт первым и даёт возможность интервьюверу задать вопросы связанные с твоим резюме, познакомиться с тобой, попытаться понять твой характер для построения последующих вопросов. Следует иметь в виду, что интервьюверу не всегда удается подготовиться к интервью, или он банально не имеет перед глазами твоего резюме. Тут есть смысл ещё раз представиться (часто в мессенджерах используются никнеймы, а твоё реальное имя он мог забыть), назвать свой возраст, образование, рассказать о предыдущих местах работы и должностях, сколько лет в индустрии, какие ЯП и технологии использовал — только «по верхам», для того чтоб твой собеседник просто понял с кем он «имеет дело».
Кем был создан язык, какие его особенности?
Go (часто также golang) — компилируемый многопоточный язык программирования, разработанный внутри компании Google. Разработка началась в 2007 году, его непосредственным проектированием занимались Роберт Гризмер, Роб Пайк и Кен Томпсон. Официально язык был представлен в ноябре 2009 года.
В качестве ключевых особенностей можно выделить:
- Простая грамматика (минимум ключевых слов — язык создавался по принципу «что ещё можно выкинуть» вместо «что бы ещё в него добавить»)
- Строгая типизация и отказ от иерархии типов (но с сохранением объектно-ориентированных возможностей)
- Сборка мусора (GC)
- Простые и эффективные средства для распараллеливания вычислений
- Чёткое разделение интерфейса и реализации
- Наличие системы пакетов и возможность импортирования внешних зависимостей (пакетов)
- Богатый тулинг «из коробки» (бенчмарки, тесты, генерация кода и документации), быстрая компиляция
Для того, чтоб вспомнить историю создания Go и о его особенностях можно посмотреть:
Go — императивный или декларативный? А в чем разница?
Go является императивным языком.
Императивное программирование — это описание того, как ты делаешь что-то (т.е. конкретно описываем необходимые действия для достижения определенного результата), а декларативное — того, что ты делаешь (например, декларативным ЯП является SQL
— мы описываем что мы хотим получить от СУБД, но не описываем как именно она должна это сделать).
Профилирование (pprof)
Для профилирования «родными» средствами в поставке с Go имеется пакет pprof
и одноименная консольная утилита go tool pprof
. Причинами необходимости в профилировании могут стать:
- Длительная работа различных частей программы
- Высокое потребление памяти
- Высокое потребление ресурсов процессора
Профилировщик является семплирующим — с какой-то периодичностью мы прерываем работу программы, берем стек-трейс, записываем его куда-то, а в конце, на основе того, как часто в стек-трейсах встречаются разные функции, мы понимаем, какие из них использовали больше ресурсов процессора, а какие меньше. Работа с ним состоит из двух этапов — сбор статистики по работе сервиса, и её визуализация + анализ. Собирать статистику можно добавив вызовы пакета pprof
, либо запустив HTTP сервер.
Пример использования pprof
Рассмотрим простой случай, когда у нас есть функция, которая выполняется по какой-то причине очень долго. Обрамим вызовы потенциально-тяжелого кода в startPprof
и stopPprof
:
package main
import (
"os"
"runtime/pprof"
"time"
)
func startPprof() *os.File { // вспомогательная функция начала профилирования
f, err := os.Create("profile.pprof")
if err != nil {
panic(err)
}
if err = pprof.StartCPUProfile(f); err != nil {
panic(err)
}
return f
}
func stopPprof(f *os.File) { // вспомогательная функция завершения профилирования
pprof.StopCPUProfile()
if err := f.Close(); err != nil {
panic(err)
}
}
func main() { // наша основания функция
var (
slice = make([]int, 0)
m = make(map[int]int)
)
pprofFile := startPprof() // начинаем профилирование
// тут начинается какая-то "тяжелая" работа
for i := 0; i < 10_000_000; i++ {
slice = append(slice, i*i)
}
for i := 0; i < 10_000_000; i++ {
m[i] = i * i
}
<-time.After(time.Second)
// а тут она завершается
stopPprof(pprofFile) // завершаем профилирование
}
После компиляции и запуска приложения (go build -o ./main . && ./main
) в текущей директории появится файл с именем profile.pprof
, содержащий профиль работы. «Конвертируем» его в читаемое представление в виде svg
изображения с помощью go tool pprof -svg ./profile.pprof
(на Linux для этого понадобится установленный пакет graphviz
) и открываем его (имя файла будет в виде profile001.svg
):
Посмотрим на получившийся граф вызовов. Изучая такой граф, в первую очередь нужно обращать внимание на толщину ребер (стрелочек) и на размер узлов графа (квадратиков). На ребрах подписано время — сколько времени данный узел или любой из ниже лежащих узлов находился в стек-трейсе во время профилирования.
В нашем профиле можем заметить, что runtime evacuate_fast64
занимает очень много времени. Связано это с тем, что из мапы данным приходиться эвакуироваться, так как размер мапы очень сильно растёт. Исправляем это (а заодно и слайс) всего в двух строчках:
var (
slice = make([]int, 0, 10_000_000) // заставляем аллоцировать память в слайсе
m = make(map[int]int, 10_000_000) // и в мапе заранее
)
Повторяем все сделанные ранее операции снова, и видим уже совсем другую картину:
Теперь картина значительно лучше, и следующее место оптимизации (потенциально) это пересмотреть работу с данными, а именно — нужна ли нам работа с мапой в принципе (может заменить её каким-то слайсом), и если нет — то как можно улучшить (оптимизировать) запись в неё.
Так как же профилировщик работает в принципе?
Go runtime просит ОС посылать сигнал (man setitimer
) с определенной периодичностью и назначает на этот сигнал обработчик. Обработчик берет стек-трейс всех горутин, какую-то дополнительную информацию, записывает ее в буфер и выходит.
Каковы же недостатки данного подхода?
- Каждый сигнал — это изменение контекста, вещь довольно затратная в наше время. В Go сейчас получается получить порядка 100 в секунду. Иногда этого мало
- Для нестандартных сборок, например, с использованием
-buildmode=c-archive
или-buildmode=c-shared
, профайлер работать по умолчанию не будет. Это связано с тем, что сигналSIGPROF
(который посылает ОС) придет в основной поток программы, который не контролируется Go - Процесс
user space
, которым является программа на Go, не может получить ядерный стек-трейс. Неоптимальности и проблемы иногда кроются и в ядре
Основное преимущество, конечно, в том, что Go runtime обладает полной информацией о своем внутреннем устройстве. Внешние средства, например, по умолчанию ничего не знают о горутинах. Для них существуют только процессы и треды.
Что можно почитать: Профилирование и оптимизация программ на Go
Расскажи о своем самом интересном проекте?
К этому вопросу есть смысл подготовиться заранее и не спустя рукава. Дело в том, что это тот момент, когда тебе надо подобно павлину распустить хвост и создать правильное первое впечатление о себе, так как этот вопрос тоже очень часто идёт впереди всех остальных. Возьми и выпиши для себя где-нибудь на листочке основные тезисы о том, что это был за проект/сервис/задача, уделяя основное внимание тому какой профит это принесло для компании/команды в целом. Например:
- Я со своей командой гоферов из N человек в течении трех месяцев создали аналог сервиса у которого компания покупала данные за $4000 в месяц, а после перехода на наш сервис — расходы сократились до $1500 в месяц и значительно повысилось их качество и uptime;
- Внедренные мной практики в CI/CD пайплайны позволили сократить время на ревью изменений в проектах на 25..40%, а зная сколько стоит время работы разработчиков — вы сами всё понимаете;
- Разработанный мной сервис состоял из такого-то набора микросервисов, такие-то службы и протоколы использовал, были такие-то ключевые проблемы которые мы так-то зарешали; основной ценностью было то-то.
Часто задаваемые вопросы
Можно ли тестироваться из дома?
Из дома можно пройти обучающее тестирование. Аттестующее тестирование проводится только в компьютерных классах университета.
На что влияют результаты тестирования?
Результаты аттестующего тестирования могут быть использованы для текущего контроля успеваемости, допуска к лабораторным работам, формирования экзаменационной оценки. При обучении с использованием балльно-рейтинговой системы оценивания результаты тестирования непосредственно влияют на рейтинг.
Что такое задолженность, и на что она влияет?
В системе ДО каждый несданный вовремя тест является задолженностью. От количества задолженностей зависит выбор меры воздействия, которую применяет деканат.
Почему после сдачи ряда тестов количество задолженностей, показываемых системой формирования расписания, не изменилось?
Количество задолженностей в системе формирования расписания определяется по состоянию на начало суток.
Где можно посмотреть список тестов, которые я должен пройти в текущем семестре?
Список тестов, которые необходимо пройти в текущем семестре, и информацию о крайних сроках их сдачи можно посмотреть на странице «График аттестаций». Если номер вашей группы на странице «График аттестаций» не является ссылкой, то в ближайшее время Вам тестироваться не надо. Если тесты для Вас будут размещены, то срок сдачи будет определен не ранее, чем через три недели после размещения. Поэтому регулярно проверяете информацию на странице «Аттестации».
Вам необходимо зайти в настройки учетной записи в информационной системе университета https://isu.ifmo.ru/, указать “Да” в настройке пароля “Использовать пароль ИСУ в ЦДО?” и обязательно нажать на кнопку «Сохранить пароль и/или настройку ЦДО».
Какими материалами можно пользоваться во время аттестации?
Возможность использования вспомогательных материалов определяется преподавателями и сообщается в ЦДО при подаче заявки на тестирование.
Сколько попыток предоставляется на прохождение одного теста?
Максимальное число попыток равно трем, но студенту разрешается использовать каждую следующую из трех попыток только в том случае, если все предыдущие попытки были неуспешными.
Что делать, если попытки все исчерпаны?
Если попытки все исчерпаны, то аттестации по соответствующим темам проходят непосредственно у преподавателей. Результаты аттестации преподаватель должен внести в базу данных системы ДО.
Где можно посмотреть результаты своего тестирования?
Информацию о результатах своего тестирования вы можете получить в «Электронном журнале», в таблице «Виды электронного контроля».
Что означает пункт «Пороговое значение» в «Электронном журнале»?
Пороговым называется значение, достижение или превышение которого свидетельствует об успешном выполнении соответствующего вида учебной работы по дисциплине.
Для чего нужен столбец «Минимальное значение»?
Параметры «Минимальное значение» и «Максимальное значение» служат для предотвращения ошибок, связанных с ручным вводом данных, а также указывают диапазон изменения соответствующего количества баллов.
В каком случае закрывается доступ к аттестующему тестированию?
Доступ к аттестующему тестированию закрывается если у студента есть положительная оценка по дисциплине за данный семестр, если студент переведен преподавателем в состояние «Оценивание» или закончился срок обучения, включая период ликвидации академической задолженности.
Тестирование
Для unit-тестирования (aka модульного) используется команда вида go test
, которая запускает все функции, что начинаются с префикса Test
в файлах, что имеют в своем имени постфикс _test.go
— всё довольно просто.
Важно писать сам код так, чтоб его можно было протестировать (например — не забывать про инвертирование зависимостей и использовать интерфейсы там, где они уместны).
TDT, Table-driven tests (табличное тестирование)
Являются более предпочтительным вариантом для тестирования множества однотипных кейсов перед описанием «один кейс — один тест», так как позволяют отделить часть входных данных и ожидаемых данных от всех этапов инициализации и tear-down (не знаю как это будет по-русски). Например, тестируемая функция и её тест могут выглядеть так:
package main
func Sum(a, b int) int { return a + b }
package main
import "testing"
func TestSum(t *testing.T) {
for name, tt := range map[string]struct { // ключ мапы - имя теста
giveOne, giveSecond int
wantResult int
}{
"1 + 1 = 2": {
giveOne: 1, giveSecond: 1, wantResult: 2,
},
"140 + 6 = 146": {
giveOne: 140, giveSecond: 6, wantResult: 146,
},
} {
t.Run(name, func(t *testing.T) {
// setup here
if res := Sum(tt.giveOne, tt.giveSecond); res != tt.wantResult {
t.Errorf("Unexpected result. Want %d, got %d", tt.wantResult, res)
}
// teardown here
})
}
}
Имя пакета с тестами
Если имя пакета в файле с тестами (foo_test.go
) указывать с постфиксом _test
(например — имя пакета, для которого пишутся тесты foo
, а имя пакета указанное в самом файле с тестами для него — foo_test
), то в тестах не будет доступа в не-экспортируемым свойствам, структурам и функциям, таким образом тестирование пакета будет происходить «как извне», и не будет соблазна пытаться использовать что-то приватное, что в пакете содержится. По идее, в одной директории не может находиться 2 и более файлов, имена пакетов в которых отличаются, но *_test
является исключением из этого правила.
Более того, этот подход стимулирует тестировать API, а не внутренние механизмы, т.е. относиться к функциональности как к «черному ящику», что очень правильно.
Статические анализаторы (линтеры)
Уже давно на все случаи жизни существует golangci-lint, который является универсальным решением, объединяющим множество линтеров в «одном флаконе». Удобен как для запуска локально, так и на CI.
Ошибка в бенчмарке
Про бенчмарки — иногда встречается кейс с написанием бенчмарка который внутри своего цикла выполняет тестируемую функцию, а результат этого действия никуда не присваивается и не передаётся:
func BenchmarkWrong(b *testing.B) {
for i := 0; i < b.N; i++ {
ourFunc()
}
}
Компилятор может принять это во внимание, и будет выполнять её содержимое как inline-последовательность инструкций. После чего, компилятор определит, что вызовы тестируемой функции не имеет никаких побочных эффектов (side-effects), т.е. никак не влияет на среду исполнения. После чего вызов тестируемой функции будет просто удалён. Один из вариантов избежать сценария выше – присваивать результат выполнения функции переменной уровня пакета. Примерно так:
var result uint64
func BenchmarkCorrect(b *testing.B) {
var r uint64
for i := 0; i < b.N; i++ {
r = ourFunc()
}
result = r
}
Теперь компилятор не будет знать, есть ли у функции side-effect и бенчмарк будет точен.
Что про функциональное тестирование?
Тут всё зависит от того, что мы собираемся тестировать, и тянет на отдельную тему для разговора. Для HTTP посоветовать можно postman и его CLI-версию newman. Ещё как вариант «быстро и просто» — это hurl.
Для за-mock-ивания стороннего HTTP API — jmartin82/mmock или lamoda/gonkey.