Golang Questions

*Часть I. Базовые

*Часть II. Что там с конкурентностью?

Junior

Software engineering

Какие бывают области памяти программы? В чем их особенности и отличия?
  • stack - ограниченный размер, диамически растет для goroutines
  • heap - требует работу GC
Расскажите, что вы знаете о HTTP-протоколе, какие составные части запроса? Какие статус-коды знаете, какие группы можно выделить?

HTTP 1:

  • Request Line - method, URL, protocol version
  • Headers (обязательно только host)
  • Request Body (не обязатель)

HTTP 2:

  • binary
  • multiplexed (streams)
  • cancel stream
  • server push

Statuses:

  • 1xx Informational responses
  • 2xx Success
  • 3xx Redirection
  • 4xx Client errors
  • 5xx Server errors
Назовите и опишите любой паттерн программирования (по вашему выбору). Когда его целесообразно использовать и почему?

go-patterns

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

В качестве примера можно привести интерфейс автомобиля. Современные автомобили имеют унифицированный интерфейс для водителя, под которым скрывается сложная подсистема. Благодаря применению навороченной электроники, делающей большую часть работы за водителя, тот может с лёгкостью управлять автомобилем, не задумываясь, как там все работает.

Что такое процессы и потоки операционных систем? Опишите их взаимосвязь в контексте выполнения программы?
  • Both processes and threads are independent sequences of execution. The typical difference is that threads (of the same process) run in a shared memory space, while processes run in separate memory spaces.
  • A thread is a lightweight process. Each process has a separate stack, text, data and heap. Threads have their own stack, but share text, data and heap with the process. Text is the actual program itself, data is the input to the program and heap is the memory which stores files, locks, sockets.
Какие знаете алгоритмы сортировки?
  • Сортировка пузырьком
  • Сортировка вставками
  • Quick sort
  • Merge sort
Что такое SOLID?
  • S (single responsibility principle, принцип единственной ответственности) — определенный класс/модуль должен решать только определенную задачу, максимально узко но максимально хорошо (своеобразные UNIX-way). Если для выполнения своей задачи ему требуются какие-то другие ресурсы — они в него должны быть инкапсулированы (это отсылка к принципу инверсии зависимостей)
  • O (open-closed principle, принцип открытости/закрытости) — классы/модули должны быть открыты для расширения, но закрыты для модификации. Должна быть возможность расширить поведение, наделить новым функционалом, но при этом исходный код/логика модуля должна быть неизменной
  • L (Liskov substitution principle, принцип подстановки Лисков) — поведение наследующих классов не должно противоречить поведению, заданному базовым классом, то есть поведение наследующих классов должно быть ожидаемым для кода
  • I (interface segregation principle, принцип разделения интерфейса) — много тонких интерфейсов лучше, чем один толстый
  • D (dependency inversion principle, принцип инверсии зависимостей) — “завязываться” на абстракциях (интерфейсах), а не конкретных реализациях. Так же (это уже про IoC, но всё же) можно рассказать что если какому-то классу для своей работы требуется функциональность другого — то есть смысл “запрашивать” её в конструкторе нашего класса используя интерфейс, под который подходит наша зависимость. Таким образом целевая реализация опирается только на интерфейсы (не зависит от реализаций) и соответствует принципу под буквой S

Go

Что такое горутины и зачем они?

Горутины — это легковесные потоки, которые реализуют конкурентное программирование в Go. Их называют легковесными потоками, потому что они управляются рантаймом языка, а не операционной системой. Стоимость переключения контекста и расход памяти намного ниже, чем у потоков ОС.

В исходном коде (src/pkg/runtime/proc.c) приняты такие термины: G (Goroutine) — Горутина M (Machine) — Машина

Каждая Машина работает в отдельном потоке и способна выполнять только одну Горутину в момент времени. Планировщик операционной системы, в которой работает программа, переключает Машины. Число работающих Машин ограничено переменной среды GOMAXPROCS или функцией runtime.GOMAXPROCS(n int). По умолчанию оно равно 1. Обычно имеет смысл сделать его равным числу ядер.

Планировщик Go

Цель планировщика (scheduler) в том, чтобы распределять готовые к выполнению горутины (G) по свободным машинам (M).

Что делает функция init()? Приведите примеры, где ее следует использовать. Приведите примеры, когда следует избегать.

The predefined init() function sets off a piece of code to run before any other part of your package. This code will execute as soon as the package is imported, and can be used when you need your application to initialize in a specific state, such as when you have a specific configuration or set of resources with which your application needs to start. It is also used when importing a side effect, a technique used to set the state of a program by importing a specific package. This is often used to register one package with another to make sure that the program is considering the correct code for the task.

Although init() is a useful tool, it can sometimes make code difficult to read, since a hard-to-find init() instance will greatly affect the order in which the code is run.

Какие типы данных используются в Go?
  • Целочисленные — 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
Как отлавливать паники?

recover()

Как получить настоящее время?

time.Now()

Что такое iota?

Ключевое слово iota представляет последовательные целочисленные константы 0, 1, 2, … Оно сбрасывается в 0 всякий раз, когда слово const появляется в исходном коде, и увеличивается после каждой спецификации const

Какая разница между слайсом и массивом?
  • Срез — всегда указатель на массив, массив — значение
  • Срез может менять свой размер и динамически аллоцировать память

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

Кстати, массивы с элементами одного типа но с разными размерами являются разными типами. Массивы не нужно инициализировать явно; нулевой массив — это готовый к использованию массив, элементы которого являются нулями:

Так же следует помнить что в Go массивы передаются по значению, т.е. передавая массив в какую-либо функцию она получает копию массива (для передачи его указателя нужно явно это указывать, т.е. foo(&a)).

А слайс же это своего рода версия массива но с вариативным размером (структура данных, которая строится поверх массива и предоставляет доступ к элементами базового массива). Слайсы до 64 KB могут быть размещены на стеке. Если посмотреть исходники Go (src/runtime/slice.go), то увидим:

type slice struct {
    array unsafe.Pointer // указатель на массив
    len   int            // длина (length)
    cap   int            // вместимость (capacity)
}

Слайсы передаются “по ссылке” (фактически будет передана копия структуры slice со своими len и cap, но указатель на массив array будет тот-же самый). Для защиты слайса от изменений следует передавать его копию:

Важной особенностью является то, так как “под капотом” у слайса лежит указатель на массив — при изменении значений слайса они будут изменяться везде, где слайс используется (будь то присвоение в переменную, передача в функцию и т.д.) до момента, пока размер слайса не будет переполнен и не будет выделен новый массив для его значений (т.е. в момент изменения cap слайса всегда происходит копирование данных массива).

Из каких частей состоит переменная типа slice?

А слайс же это своего рода версия массива но с вариативным размером (структура данных, которая строится поверх массива и предоставляет доступ к элементами базового массива). Слайсы до 64 KB могут быть размещены на стеке. Если посмотреть исходники Go (src/runtime/slice.go), то увидим:

type slice struct {
    array unsafe.Pointer // указатель на массив
    len   int            // длина (length)
    cap   int            // вместимость (capacity)
}

Слайсы передаются “по ссылке” (фактически будет передана копия структуры slice со своими len и cap, но указатель на массив array будет тот-же самый). Для защиты слайса от изменений следует передавать его копию:

Как работает append?

append() делает простую операцию — добавляет элементы в слайс и возвращает новый. Но под капотом там делаются довольно сложные манипуляции, чтобы выделять память только при необходимости и делать это эффективно.

Сперва append сравнивает значения len и cap у слайса. Если len меньше чем cap, то значение len увеличивается, а само добавляемое значение помещается в конец слайса. В противном случае происходит выделение памяти под новый массив для элементов слайса, в него копируются значения из старого, и значение помещается уже в новый массив.

Увеличении размера слайса (метод growslice) происходит по следующему алгоритму — если его размер менее 1024 элементов, то его размер будет увеличиваться вдвое; иначе же слайс увеличивается на ~12.5% от своего текущего размера.

Что важно помнить — если на основании слайса one выделить подслайс two, а затем увеличим слайс one (и его вместимость будет превышена) — то one и two будут уже ссылаться на разные участки памяти!

Какое у слайса zero value? Какие операции над ним возможны?

Zero value у слайса всегда nil, а len и cap равны нулю, так как “под ним” нет инициализированного массива:

Как видно из примера выше — несмотря на то, что a == nil (слайс “не инициализирован”), с этим слайсом возможна операция append — в этом случае Go самостоятельно создаёт нижележащий массив и всё работает так, как и ожидается. Более того — для полной очистки слайса рекомендуется его присваивать к nil.

Так же важно помнить, что не делая make для слайса — не получится сделать пре-аллокацию, что часто очень болезненно для производительности.

Что такое пакеты?

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

Код пакета располагается в одном или нескольких файлах с расширением go. Для определения пакета применяется ключевое слово package.

Есть два типа пакетов: исполняемые (executable) и библиотеки (reusable). Для создания исполняемых файлов пакет должен иметь имя main. Все остальные пакеты не являются исполняемыми. При этом пакет main должен содержать функцию main, которая является входной точкой в приложение.

Что такое интерфейсы и как они работают?

Интерфейсы — это инструменты для определения наборов действий и поведения. Интерфейсы — это в первую очередь контракты. Они позволяют объектам опираться на абстракции, а не фактические реализации других объектов. При этом для компоновки различных поведений можно группировать несколько интерфейсов. В общем смысле — это набор методов, представляющих стандартное поведение для различных типов данных.

В 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 только если оба этих поля не определены

Что такое тип данных string?

В Go строка в действительности является слайсом (срезом) байт, доступным только для чтения. Строка содержит произвольные байты, и у неё нет ёмкости (cap). При преобразовании слайса байт в строку (str := string(slice)) или обратно (slice := []byte(str)) — происходит копирование массива (со всеми следствиями).

Создание подстрок работает очень эффективно. Поскольку строка предназначена только для чтения, исходная строка и строка, полученная в результате операции среза, могут безопасно совместно использовать один и тот же массив:

Go использует тип rune (алиас int32) для представления Unicode. Конструкция for … range итерирует строку посимвольно (а не побайтово, как можно было бы предположить):

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

Эффективным способом работы со строками (когда есть необходимость часто выполнять конкатенацию, например) является использование слайса байт или strings.Builder:

И ещё одну важную особенность стоит иметь в виду — это подсчет длины строки (например — для какой-нибудь валидации). Если считать по количеству байт, и строка содержит не только ASCII символы — то количество байт и фактическое количество символов будут расходиться:

Тут дело в том, что для кодирования символов м, и и р используются 2 байта вместо одного. Поэтому len == 13, а фактически в строке лишь 10 символов (пакет utf8, к примеру, нам в помощь).

Чем отличаются кавычки в Go (двойные, одинарные, обратные): "", '', ``?
  • Двойные "" - обычные строки, можно использоват \n как перенос строки
  • Одинарные ’’ - для рун, или байт (если указан тип). Можно только один символ.
  • Обратные `` - raw string, только буквальный текст

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

Что такое rune?

Rune literals are just 32-bit integer values (however they’re untyped constants, so their type can change). They represent unicode codepoints. For example, the rune literal ‘a’ is actually the number 97.

Может ли переменная типа string принимать nil-значение?

Нет

Можно ли возвратить из функции несколько значений?

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

  • Последним значением возвращать ошибку, если её возврат подразумевается
  • Первым значением возвращать контекст, если он подразумевается
  • Хорошим тоном является не возвращать более четырёх значений
  • Если функция что-то проверяет и возвращает значение + булевый результат проверки — то результат проверки возвращать последним (пример — os.LookupEnv(key string) (string, bool))
  • Если возвращается ошибка, то остальные значения возвращать нулевыми или nil
Как записать в файл?
func main() {
    data := []byte("Hello Bold!")
    file, err := os.Create("hello.bin")
    if err != nil{
        fmt.Println("Unable to create file:", err) 
        os.Exit(1) 
    }
    defer file.Close() 
    file.Write(data)
     
    fmt.Println("Done.")
}
Что такое структура?

Структуры представляют тип данных, определяемый разработчиком и служащий для представления каких-либо объектов. Структуры содержат набор полей, которые представляют различные атрибуты объекта. Для определения структуры применяются ключевые слова type и struct:

type person struct{
    name string
    age int
}
Что будет, если вызвать log.Fatal?

Выход из приложения

​​Объясните разницу между конкурентностью и параллельностью?
  • Конкурентность — это выполнение задач за определённое время (например, есть 5 процессов и все они в сумме выполняются в течение 60 минут по очереди). Важная деталь заключается в том, что задачи необязательно выполняются одновременно, поэтому их можно разделить на более мелкие и чередующиеся.

  • Параллелизм — это выполнение задач в одно и то же время (например, есть 5 задач, каждая из них выполняется в течение 60 минут). Само название подразумевает, что они выполняются параллельно.

Виписать из лекции

Как работает defer?

Defer является функцией отложенного вызова. Выполняется всегда (даже в случае паники внутри функции вызываемой) после того, как функция завершила своё выполнение но до того, как управление вернётся вызывающей стороне (более того — внутри defer возможен захват переменных, и даже возвращаемого результата). Часто используется для освобождения ресурсов/снятия блокировок.

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

Что такое канал? Какие типы каналов вы знаете? Зачем они нужны?

Если которотко, то синхронные (небуферизированным) и асинхронные (буферизированные), оба работают по принципу FIFO (first in, first out) очереди.

Канал — это объект связи, с помощью которого (чаще всего) горутины обмениваются данными. Потокобезопасен, передаётся “по указателю”. Технически это можно представить как конвейер (или трубу), откуда можно считывать и помещать данные. Для создания канала предоставляет ключевое слово chan — создание не буферизированного канала c := make(chan int), для чтения из канала — data := <-c, для записи — c <- 123, и закрытие close(c).

Запись данных в закрытый канал вызовет панику.

Чтение или запись данных в небуферизированный канал блокирует горутину и контроль передается свободной горутине. Через закрытый канал невозможно будет передать или принять данные (проверить открытость канала можно используя val, isOpened := <- channel, где isOpened == true в том случае, если канал открыт; в противном случае вернётся false и нулевое значение val исходя из типа данных для канала; isOpened == false если канал закрыт и отсутствуют данные для чтения из него).

Буферизированный канал создается указанием второго аргумента для make — c := make(chan int, 5), в этом случае горутина не блокируется до тех пор, пока буфер не будет заполнен. Подобно слайсам, буферизированный канал имеет длину (len, количество сообщений в очереди, не считанных) и емкость (cap, размер самого буфера канала):

c := make(chan string, 5)

c <- "foo"
c <- "bar"
close(c)

println(len(c), cap(c)) // 2 5

for {
    val, ok := <-c // обрати внимание - читаем из уже закрытого канала

    if !ok {
        break
    }

    println(val)
}
// "foo"
// "bar"

При этом ok == true до того момента, пока в канале есть сообщения (вне зависимости от того, открыт он или закрыт), в противном случае ok == false а val будет нулевым значением в зависимости от типа данных канала. При попытке записи в закрытый канал будет паника (авторы языка так сделали “ибо нефиг — канал закрыт значит закрыт”).

Используя буферизованный канал и цикл for val := range c { … } мы можем читать с закрытых каналов (поскольку у закрытых каналов данные все еще живут в буфере).

Кроме того, существует синтаксический сахар однонаправленных каналов (улучшает безопасность типов в программe, что, как следствие, порождает меньше ошибок):

c := make(<-chan int) — только для чтения c := make(chan<- int) — только для записи

Так же можно в сигнатуре принимаемой функции указать однонаправленность канала (func write(c chan<- string) { … }) — в этом случае функция не сможет из него читать, а сможет только писать или закрыть его.

Читать “одновременно” из нескольких каналов возможно с помощью select (оператор select является блокируемым, за исключением использования default):

c1, c2 := make(chan string), make(chan string)
defer func() { close(c1); close(c2) }() // не забываем прибраться

go func(c chan<- string) { <-time.After(time.Second); c <- "foo" }(c1)
go func(c chan<- string) { <-time.After(time.Second); c <- "bar" }(c2)

for i := 1; ; i++ {
    select { // блокируемся, пока в один из каналов не попадёт сообщение
    case val := <-c1:
        println("channel 1", val)

    case val := <-c2:
        println("channel 2", val)
    }

    if i >= 2 { // через 2 итерации выходим (иначе будет deadlock)
        break
    }
}
// channel 1 foo
// channel 2 bar
// Total execution time: 1.00s

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

Если ни один из каналов недоступен для взаимодействия, и секция default отсутствует, то текущая горутина переходит в состояние waiting до тех пор, пока какой-то из каналов не станет доступен.

Если в select указан default, то он будет выбран в том случае, если все каналы не имеют сообщений (таким образом select становится не блокируемым).

Под капотом (src/runtime/chan.go) канал представлен структурой:

type hchan struct {
    qcount   uint           // количество элементов в буфере
    dataqsiz uint           // размерность буфера
    buf      unsafe.Pointer // указатель на буфер для элементов канала
    elemsize uint16         // размер одного элемента в канале
    closed   uint32         // флаг, указывающий, закрыт канал или нет
    elemtype *_type         // содержит указатель на тип данных в канале
    sendx    uint           // индекс (смещение) в буфере по которому должна производиться запись
    recvx    uint           // индекс (смещение) в буфере по которому должно производиться чтение
    recvq    waitq          // указатель на связанный список горутин, ожидающих чтения из канала
    sendq    waitq          // указатель на связанный список горутин, ожидающих запись в канал
    lock     mutex          // мьютекс для безопасного доступа к каналу
}

В общем случае, горутина захватывает мьютекс, когда совершает какое-либо действие с каналом, кроме случаев lock-free проверок при неблокирующих вызовах.

Go не выделяет буфер для синхронных (небуферизированных) каналов, поэтому указатель на буфер равен nil и dataqsiz равен нулю. При чтении из канала горутина произведёт некоторые проверки, такие как: закрыт ли канал, буферизирован он или нет, содержит ли гоуртины в send-очереди. Если ожидающих отправки горутин нет — горутина добавит сама себя в recvq и заблокируется. При записи другой горутиной все проверки повторяются снова, и когда она проверяет recvq очередь, она находит ожидающую чтение горутину, удаляет её из очереди, записывает данные в её стек и снимает блокировку. Это единственное место во всём рантайме Go, когда одна горутина пишет напрямую в стек другой горутины.

При создании асинхронного (буферизированного) канала make(chan bool, 1) Go выделяет буфер и устанавливает значение dataqsiz в единицу. Чтобы горутине отправить отправить значение в канал, сперва производятся несколько проверок: пуста ли очередь recvq, пуст ли буфер, достаточно ли места в буфере. Если всё ок, то она просто записывает элемент в буфер, увеличивает значение qcount и продолжает исполнение далее. Когда буфер полон, буферизированный канал будет вести себя точно так же, как синхронный (небуферизированный), тоесть горутина добавит себя в очередь ожидания и заблокируется.

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

При закрытии канала Go проходит по всем ожидающим на чтение или запись горутинам и разблокирует их. Все получатели получают дефолтные значение переменных того типа данных канала, а все отправители паникуют.

Что можно делать с закрытым каналом??

Из закрытого канала можно читать с помощью for val := range c { … } — вычитает все сообщения что в нём есть, или с помощью:

for {
    if val, ok := <-c; ok {
        println(val)
    } else {
        break
    }
}
Что будет, если читать по закрытому каналу? Что будет, если писать в закрытый канал?
  • Чтение из пустого канала вернет пустое значение типа

  • Запись данных в закрытый канал вызовет панику.

  • A send to a nil channel blocks forever

  • A receive from a nil channel blocks forever

  • A send to a closed channel panics

  • A receive from a closed channel returns the zero value immediately

Какие стандартные env-переменные в Go?

GOMAXPROCS GOROOT

Как сделать type assertion?

A type assertion provides access to an interface value’s underlying concrete value.

t := i.(T)

This statement asserts that the interface value i holds the concrete type T and assigns the underlying T value to the variable t.

If i does not hold a T, the statement will trigger a panic.

To test whether an interface value holds a specific type, a type assertion can return two values: the underlying value and a boolean value that reports whether the assertion succeeded.

t, ok := i.(T)
If i holds a T, then t will be the underlying value and ok will be true.
Что такое type switch?

Проверка типа переменной, а не её значения. Может быть в виде одного switch и множеством case:

func checkType(i interface{}) {
  switch i.(type) {
  case int:
    println("is integer")

  case string:
    println("is string")

  default:
    println("has unknown type")
  }
}

А может в виде if-конструкции:

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()
  }
}
В каком порядке выполняются кейсы в select?

select блокируется до тех пор, пока один из его блоков case не будет готов к запуску, а затем выполняет этот блок. Если сразу несколько блоков могут быть запущены, то выбирается произвольный.

Блок default в select запускается, если никакой другой блок не готов.

Используйте default, чтобы посылать и получать данные без блокировок:

(посмотреть еще раз лекцию)

Практические задачи

Middle

Software engineering

Назовите некоторые из принципов 12-factor-app. Зачем используют graceful shutdown?

https://12factor.net/

Что такое dependency injection? А dependency inversion? ?

Dependency Injection is an implementation technique for populating instance variables of a class. Dependency Inversion is a general design guideline which recommends that classes should only have direct relationships with high-level abstractions.

Как работает TLS handshake?
  1. The ‘client hello’ message: The client initiates the handshake by sending a “hello” message to the server. The message will include which TLS version the client supports, the cipher suites supported, and a string of random bytes known as the “client random.”

  2. The ‘server hello’ message: In reply to the client hello message, the server sends a message containing the server’s SSL certificate, the server’s chosen cipher suite, and the “server random,” another random string of bytes that’s generated by the server.

  3. Authentication: The client verifies the server’s SSL certificate with the certificate authority that issued it. This confirms that the server is who it says it is, and that the client is interacting with the actual owner of the domain.

  4. The premaster secret: The client sends one more random string of bytes, the “premaster secret.” The premaster secret is encrypted with the public key and can only be decrypted with the private key by the server. (The client gets the public key from the server’s SSL certificate.)

  5. Private key used: The server decrypts the premaster secret.

  6. Session keys created: Both client and server generate session keys from the client random, the server random, and the premaster secret. They should arrive at the same results.

  7. Client is ready: The client sends a “finished” message that is encrypted with a session key.

  8. Server is ready: The server sends a “finished” message encrypted with a session key.

  9. Secure symmetric encryption achieved: The handshake is completed, and communication continues using the session keys.

Что такое Clean Architecture? Приведите пример

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

Минусы чистой архитектуры На мой взгляд, основной минус чистой архитектуры в том, что тебе нужно писать больше кода. Допустим, ты хочешь настроить получение контактов из базы данных. Ты можешь написать метод, который обратится к библиотеке, сделать там небольшой select и задать ID-шник. Затем отправить данные на front, и это будет что-то около 200 строк. В чистой архитектуре тебе сначала нужно сделать папку со слоем delivery, который будет принимать данные от front. Затем описать данные, которые должен прислать front, и вызвать слой use case. Use case проведёт валидацию, вызовет метод обращения к базе, и только после этого ты сможешь получить данные. Ещё один минус — высокий порог входа. Продумать взаимодействие всех модулей системы довольно сложно, а для новичков это и вовсе непосильная задача.

Что такое heap и stack?

Стек (stack) — это область оперативной памяти, которая создаётся для каждого потока. Он работает в порядке LIFO (Last In, First Out), то есть последний добавленный в стек кусок памяти будет первым в очереди на вывод из стека. Каждый раз, когда функция объявляет новую переменную, она добавляется в стек, а когда эта переменная пропадает из области видимости (например, когда функция заканчивается), она автоматически удаляется из стека. Когда стековая переменная освобождается, эта область памяти становится доступной для других стековых переменных.

Стек быстрый, так как часто привязан к кэшу процессора. Размер стека ограничен, и задаётся при создании потока.

Куча (heap) — это хранилище памяти, также расположенное в ОЗУ, которое допускает динамическое выделение памяти и не работает по принципу стека: это просто склад для ваших переменных. Когда вы выделяете в куче участок памяти для хранения переменной, к ней можно обратиться не только в потоке, но и во всем приложении. Именно так определяются глобальные переменные. По завершении приложения все выделенные участки памяти освобождаются. Размер кучи задаётся при запуске приложения, но, в отличие от стека, он ограничен лишь физически, и это позволяет создавать динамические переменные.

В сравнении со стеком, куча работает медленнее, поскольку переменные разбросаны по памяти, а не сидят на верхушке стека. То что попадает в кучу, живёт там пока не придёт GC.

Но почему стек так быстр? Основных причин две:

Стеку не нужно иметь сборщик мусора (garbage collector). Как мы уже упоминали, переменные просто создаются и затем вытесняются, когда функция завершается. Не нужно запускать сложный процесс освобождения памяти от неиспользуемых переменных и т.п. Стек принадлежит одной горутине, переменные не нужно синхронизировать в сравнении с теми, что находятся в куче. Что также повышает производительность

Расскажите о Data structures: stack, queue, linked list, trie, balanced tree

Dependency Injection is an implementation technique for populating instance variables of a class. Dependency Inversion is a general design guideline which recommends that classes should only have direct relationships with high-level abstractions.

Go

Какими библиотеками Go вы использовали для доступа к RDBMS? Какие у них положительные и отрицательные стороны?
  • GORM
  • SQLC
  • SQLX
  • Beego
  • GORP
  • Go-firestorm
  • SQLBoiler

Посмотреть лекцию, дописать необходимое.

Для чего используют Context? Какие варианты отмены контекстов?

Context — это Golang пакет включающий структуру Context и вспомогательные функции. Основная идея этого пакета — контролировать время выполнения сетевых запросов.

Пакет context в Go особенно полезен при взаимодействиях с API и медленными процессами, особенно в production-grade системах. С его помощью можно уведомить горутины о необходимости завершить свою работу, “пошарить” какие-то данные (например, в middleware), или легко организовать работу с таймаутом.

context.WithCancel() - Эта функция создает новый контекст из переданного ей родительского, возвращая первым аргументом новый контекст, а вторым — функцию “отмены контекста” (при её вызове родительский контект “отменен” не будет). Важно — вызывать функцию отмены контекста должна только та функция, которая его создает. При вызове функции отмены сам контекст и все контексты, созданные на основе него получат в ctx.Done() пустую структуру и в ctx.Err() ошибку context.Canceled.

ctx, cancel := context.WithCancel(context.Background())
fmt.Println(ctx.Err()) // nil

cancel()

fmt.Println(<-ctx.Done())      // {}
fmt.Println(ctx.Err().Error()) // context canceled

context.WithDeadline() - Так же создает контекст от родительского, который отменится самостоятельно при наступлении переданной временной отметке, или при вызове функции отмены. Отмена/таймаут затрагивает только сам контекст и его “наследников”. ctx.Err() возвращает ошибку context.DeadlineExceeded. Полезно для реализации таймаутов:

ctx, cancel := context.WithDeadline(
    context.Background(),
    time.Now().Add(time.Millisecond*100),
)
defer cancel()
fmt.Println(ctx.Err()) // nil

<-time.After(time.Microsecond * 110)

fmt.Println(<-ctx.Done())      // {}
fmt.Println(ctx.Err().Error()) // context deadline exceeded

context.WithTimeout() - Работает аналогично context.WithDeadline() за исключением того, что принимает в качестве значения таймаута длительность (например — time.Second):

ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)

context.WithValue() - Позволяет “пошарить” данные через всё контекстное дерево “ниже”. Часто используют чтоб передать таким образом, например, логгер или HTTP запрос в цепочке middleware (но в 9 из 10 случаев так делать не надо, это можно считать антипаттерном). Лучше всего использовать функции для помещения/извлечения данных из контекста (так как “в нём” они храняться как interface{}):

Какими способами можно исключить (скрыть) поля структуры при JSON-сериализации?

Использовать omitempty, если значение пустое.

type User struct {
    Name string `json:"name,omtiempty"`
    Age  int    `json:"age,omitempty"`
}

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

type UserResponse struct {
    Name string `json:"name"`
    age  int                  // ну или Age int `json:"-"`
}

Использовать свой метод MarshalJSON

Назовите примитивы пакета sync стандартной библиотеки. Каково назначение и примеры применения sync.WaitGroup? /b>

Как устроен мьютекс?

Mutex означает MUTual EXclusion (взаимное исключение), и обеспечивает безопасный доступ к общим ресурсам.

Под капотом мьютекса используются функции из пакета atomic (atomic.CompareAndSwapInt32 и atomic.AddInt32), так что можно считать мьютекс надстройкой над atomic. Мьютекс медленнее чем atomic, потому что он блокирует другие горутины на всё время действия блокировки. А в свою очередь atomic быстрее потому как использует атомарные инструкции процессора.

В момент, когда нужно обеспечить защиту доступа — вызываем метод Lock(), а по завершению операции изменения/чтения данных — метод Unlock().

В чем отличие sync.Mutex от sync.RWMutex?

Помимо Lock() и Unlock() (у sync.Mutex), у sync.RWMutex есть отдельные аналогичные методы только для чтения — RLock() и RUnlock(). Если участок в памяти нуждается только в чтении — он использует RLock(), который не заблокирует другие операции чтения, но заблокирует операцию записи и наоборот.

По большому счёту, RWMutex это комбинация из двух мьютексов.

Что такое synс.Map?

Коротко — предоставляет атомарный доступ к элементам map.

Go, как известно, является языком созданным для написания concurrent программ — программ, который эффективно работают на мультипроцессорных системах. Но тип map не безопасен для параллельного доступа. То есть для чтения, конечно, безопасен — 1000 горутин могут читать из map без опасений, но вот параллельно в неё ещё и писать — уже нет.

Для обеспечения потоко-безопасного доступа к map можно использовать sync.RWMutex, но он имеет проблему производительности при работе на большом количестве ядер процессора (в RWMutex при блокировке на чтение каждая горутина должна обновить поле readerCount — простой счётчик, с помощью atomic.AddInt32(), что проиводит к сбросу кэша для этого адреса памяти для всех ядер, и каждое ядро становится в очередь и ждёт этот сброс и вычитывание из кэша — эта проблема называется cache contention).

sync.Map решает совершенно конкретную проблему cache contention в стандартной библиотеке для таких случаев, когда ключи в map стабильны (не обновляются часто) и происходит намного больше чтений, чем записей.

Пример работы с sync.Map:

var m sync.Map

m.Store(“one”, 1) // запись one, ok := m.Load(“one”) // чтение

fmt.Println(one, ok) // 1 true

m.Range(func(k, v interface{}) bool { // итерация эл-ов мапы fmt.Println(k, v) // one 1

return true

})

m.Delete(“one”) // удаление

Как было сказано выше — для синхронизации можно использовать мьютексы. Кроме того из стандартной библиотеки нам доступны:

sync.WaitGroup

Используется для координации в случае, когда программе приходится ждать окончания работы нескольких горутин (эта конструкция похожа на CountDownLatch в Java). Отличный способ дождаться завершения набора одновременных операций. Принцип работы следующий:

var wg sync.WaitGroup

wg.Add(1) // увеличиваем счётчик на 1 go func() { fmt.Println(“task 1”) <-time.After(time.Second) fmt.Println(“task 1 done”)

wg.Done() // уменьшаем счётчик на 1

}()

wg.Add(1) // увеличиваем счётчик на 1 go func() { fmt.Println(“task 2”) <-time.After(time.Second) fmt.Println(“task 2 done”)

wg.Done() // уменьшаем счётчик на 1

}()

wg.Wait() // блокируемся, пока счётчик не будет == 0 // task 2 // task 1 // task 2 done // task 1 done // Total time: 1.00s

sync.Cond

Условная переменная (CONDition variable) полезна, например, если мы хотим разблокировать сразу несколько горутин (Broadcast), что не получится сделать с помощью канала. Метод Signal отправляет сообщение самой долго-ожидающей горутине. Пример использования:

var ( c = sync.NewCond(&sync.Mutex{}) wg sync.WaitGroup // нужна только для примера

free = true

)

wg.Add(1) go func() { defer wg.Done() c.L.Lock()

for !free { // проверяем, что ресурс свободен
    c.Wait()
}
fmt.Println("work")

c.L.Unlock()

}()

free = false // забрали ресурс, чтобы выполнить с ним работу <-time.After(1 * time.Second) // эмуляция работы free = true // освободили ресурс c.Signal() // оповестили горутину

wg.Wait()

sync.Once

Позволяет определить задачу для однократного выполнения за всё время работы программы. Содержит одну-единственную функцию Do, позволяющую передавать другую функцию для однократного применения.

var once sync.Once

for i := 0; i < 10; i++ { once.Do(func() { fmt.Println(“Hell yeah!”) }) }

// Hell yeah! (выводится 1 раз вместо 10)

sync.Pool

Используется для уменьшения давления на GC путём повторного использования выделенной памяти (потоко-безопасно). Пул необязательно освободит данные при первом пробуждении GC, но он может освободить их в любой момент. У пула нет возможности определить и установить размер и нет необходимости заботиться о его переполнении.

Какая разница между Mutex и RWMutex?

Помимо Lock() и Unlock() (у sync.Mutex), у sync.RWMutex есть отдельные аналогичные методы только для чтения — RLock() и RUnlock(). Если участок в памяти нуждается только в чтении — он использует RLock(), который не заблокирует другие операции чтения, но заблокирует операцию записи и наоборот.

По большому счёту, RWMutex это комбинация из двух мьютексов.

Какие способы остановить N горутин, запущенных одновременно (например, worker pool)?

context, другой канал

Что такое замыкание функций?

Замыкания — это такие функции, которые вы можете создавать в рантайме и им будет доступно текущее окружение, в рамках которого они были созданы.

Функции, у которых есть имя — это именованные функции. Функции, которые могут быть созданы без указания имени — это анонимные функции.

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 раз)
Объясните разницу между switch и select?
  • A select is only used with channels

  • A switch is used with concrete types

  • A select will choose multiple valid options at random, while aswitch will go in sequence (and would require a fallthrough to match multiple.)

Как устроен тип 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), если известно количество элементов — избежим эвакуаций В случае с m[“two”] вернулся 0 так как это является нулевым значением для типа int. Для проверки существования ключа используем конструкцию вида (доступ к элементу карты может вернуть два значения вместо одного) называемую “multiple assignment”:

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 раза больше, а старые данные копируются в него маленькими порциями каждые вставку или удаление, чтобы не создавать очень крупные задержки. Поэтому все операции будут чуть медленнее в процессе эвакуации данных (при поиске тоже, нам же приходится искать в двух местах). После успешной эвакуации начинают использоваться новые данные.

Из-за эвакуации данных нельзя и взять адрес мапы — представьте, что мы взяли адрес значения, а потом мапа выросла, выделилась новая память, данные эвакуировались, старые удалились, указатель стал неправильным, поэтому такие операции запрещены.

Что там про поиск?

Поиск, если разобраться, устроен не так уж и сложно: проходимся по цепочкам “ведер”, переходя в следующее, если в этом не нашли. Поиск в “ведре” начинается с быстрого сравнения дополнительного хэша, для которого используется всего 8 бит (вот для чего эти e0…e7 в начале каждого — это “мини” хэш пары для быстрого сравнения). Если не совпало, идем дальше, если совпало, то проверяем тщательнее — определяем где лежит в памяти ключ, подозреваемый как искомый, сравниваем равен ли он тому, что запросили. Если равен, определяем положение значения в памяти и возвращаем.

К сожалению, мир не совершенен. Когда имя хешируется, то некоторые данные теряются, так как хеш, как правило, короче исходной строки. Таким образом, в любой реализации хеш таблицы неизбежны коллизии когда по двум ключам получаются одинаковые хеши. Как следствие, поиск может быть дороже чем O(1) (возможно это связано с кешем процессора и коллизиями коротких хэшей), так что иногда выгоднее использовать бинарный поиск по слайсу данных нежели чем поиск в мапе (пишите бенчмарки).

Есть ли у map такие же методы как у слайса: len, cap?

У мапы есть len но нет cap. У нас есть только overflow который указывает “куда-то” когда мапа переполняется, и поэтому у нас не может быть capacity.

Какие типы ключей разрешены для ключа в map?

Любым сравнимым (comparable) типом, т.е. булевы, числовые, строковые, указатели, канальные и интерфейсные типы, а также структуры или массивы, содержащие только эти типы. Слайсы, мапы и функции использовать нельзя, так как эти типы не сравнить с помощью оператора == или !=.

Может ли ключом быть структура? Если может, то всегда ли?

Как было сказано выше — структура может быть ключом до тех пор, пока мы в поля структуры не поместим какой-либо слайс, мапу или любой другой non-comparable тип данных (например — функцию).

Что будет в map, если не делать make или short assign?

Будет паника (например — при попытке что-нибудь в неё поместить), так как любые “структурные” типы (а мапа как мы знаем таковой является) должны быть инициализированы для работы с ними.

Каков порядок перебора map?

произвольный

Что такое сериализация? Где она используется?

Сериализация — это процесс преобразования объекта в поток байтов для сохранения или передачи. Обратной операцией является десериализация (т.е. восстановление объекта/структуры из последовательности байтов). Синонимом можно считать термин “маршалинг” (от англ. marshal — упорядочивать).

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

Типичными примерами сериализации в Go являются преобразование структур в json-объекты. Кроме json существуют различные кодеки типа MessagePack, CBOR и т.д.

Можно ли использовать nil для инициализации переменной?

Нельзя использовать nil для инициализации переменной без явного указания типа Идентификатор nil можно использовать как «нулевое значение» (zero value) для интерфейсов, функций, указателей, хеш-таблиц (map), слайсов (slices) и каналов.

Можно ли задать емкость map? Можно ли получить емкость map?

У мапы есть len но нет cap. У нас есть только overflow который указывает “куда-то” когда мапа переполняется, и поэтому у нас не может быть capacity.

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

utf8.RuneCountInString(str)

Как указать главной горютине ожидать завершения работы всех рабочих горютин?

sync.WaitGroup{}

Чем отличается goroutine от OS thread?

Gorutine

  • Управляются рантаймом языка
  • Более высокоуровневая абстракция, поэтому не зависит от системы
  • Более легковесны
  • Асинхронно вытесняющий планировщик
  • Имеет стэк, который может расти

Thread

  • Управляются процессорным ядром
  • Зависит от системы
  • Требуют большего количества ресурсов
  • Вытесняющий планировщик
  • Фиксированный стэк
Как указать главной горютине ожидать завершения работы всех рабочих горютин?

sync.WaitGroup{}

Всегда ли будет быстрее передача Pointer в качестве аргумента функции?

посмотреть онлайн, вроде там было

Что такое вариативная переменная функции? Как работать с этой переменной?

Variadic functions can be called with any number of trailing arguments. For example, fmt.Println is a common variadic function. Like an array

Как работать с пакетом internal?

модули, еще раз посмотреть лекцию

ККак работает импорт через точку и почему это плохая практика?

Импорт с точкой добавляет все экспортируемые поля пакета в текущий скоуп (точнее говоря область видимости файла). И теперь Вы можете работать с полями импортированного пакет так, как будто они у вас в пакете.

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

Поэтому подумайте лишний раз перед тем как использовать такой импорт — можно получить ошибку совершенно неожиданно.

Как работает импорт через подчеркивание?

Как видно по коду, мы импортируем два пакета: a и c. При этом перед пакетом c стоит _ и в самом коде пакет никак не используется. Такой прием используется для того, чтобы выполнить init() из пакета.

Импорт с точкой добавляет все экспортируемые поля пакета в текущий скоуп (точнее говоря область видимости файла). И теперь Вы можете работать с полями импортированного пакет так, как будто они у вас в пакете.

Что такое 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, оно помещает следующий за ним оператор в список, который будет вызван до возврата функции.

Как получить настоящее время?

Утверждение “goto” передает управление в утверждение с соответствующей меткой в той же функции.

GotoStmt = “goto” Label .

goto Error Выполнение утверждения “goto” не должно приводить к появлению в области видимости каких-либо переменных, которые еще не находились в области видимости на момент перехода. Например, этот пример:

Что такое указатель и как с ним работать?

Указатель - это переменная, которая хранит адрес другой переменной, это обычная переменная, только вместо хранения значения, например, целочисленного значения или строкового значения, вместо этого он сохраняет местоположение адреса памяти этого значения. Под адресом памяти я подразумеваю фактическое расположение переменной в памяти нашего компьютера, и в основном это шестнадцатеричный формат, начинающийся с «0x».

Указатели имеют тот же синтаксис в других языках программирования, таких как C, C++ и Golang. В указателях используются 2 оператора:

  1. Оператор (&), известный как оператор адреса
  2. Оператор (*), известный как оператор разыменования
Как проверить, переменная ли имплементирует интерфейс?

в Go, реализация интерфейса является неявной. поэтому используется конструкция вида var _ myInterface = &myImplementation{}

Что такое embedding?

Go предлагает механизм встраивания(embedding) в качестве альтернативы механизму наследования в традиционных объектно-ориентированных языках программирования.

В Go можно объявить анонимные поля у структур. Указав только тип. Тип поля должен представлять собой именованный тип или указатель на именованный тип. После этого появляется возможность использовать поля и методы встроенного типа.

Начнем с простой структуры:

type Human struct {
   name  string
   age int
}

Тип Human встроен(embedded) в тип Student:

type Student struct {
   Human
   school string
}
Что такое memory leak? Какие способы его выявления? Как от него избавиться?

pprof and flame graphs are pretty useful to analyze application memory leaks.

Что такое race condition? Какие способы его выявления? Как от него избавиться?

A race condition is an undesirable situation that occurs when a device or system attempts to perform two or more operations at the same time, but because of the nature of the device or system, the operations must be done in the proper sequence to be done correctly.

Пишем тесты, и запускаем их с флагом -race (в этом случае рантайм будет в случайном порядке переключаться между горутинами (если не ошибаюсь), и компилятор генерирует дополнительный код, который “журналирует” обращения к памяти). Этот флаг можно использовать как для go test, так и для go run или go build.

Детектор гонки основан на библиотеке времени выполнения (runtime library) C/C++ ThreadSanitizer.

Так же предпочитаю писать тесты, провоцирующие гонку. Код в этом случае будет работать значительно медленнее, но для этапа тестирования это и не так важно. А именно для тестируемой структуры запускаю (например) 100 горутин которые читают и пишут что-то в случайном порядке.

Важно и ещё одно высказывание — “Если race detector обнаруживает состояние гонки, то оно у вас наверняка есть; если же не обнаруживает — то это не означает что его нет”.

Какое у slice zero value?

nil

Практические задачи

  1. Реализовать проверку слова на анаграмму. Написать тест и бенчмарк. Оценить сложность разработанного алгоритма.
  2. Есть код . Что выйдет на экран? Почему?
  3. Есть код . Можно ли предположить, что выводится на экран? Почему?
  4. Реализуйте Stack (LIFO).
  5. Реализуйте linked list.
  6. Задача о сумме подмножества (Subset Sum Problem). Дано: множество положительных целых чисел и значение sum. Определите, существует ли подмножество данного множества с суммой, равной значению sum. Input: set [] = { 3 , 34 , 4 , 12 , 5 , 2 }, sum = 9

Output: True

Senior

Software engineering

  1. Что такое процесс и поток? Как они соединены меж собой? Имеют ли разные процессы или потоки доступа в одну область памяти?
  2. Какие инструменты обычно используют для сбора метриков и логов? Как работает Prometheus?
  3. Как работает docker под капотом?
  4. Как работает load balancer под капотом?
  5. Зачем нужны очереди?
  6. Что такое CQRS?
  7. Какие архитектуры программного обеспечения вы знаете?
  8. Какая разница между микросервисами и монолитом? Какие преимущества и недостатки?
  9. Как выстроить межсервисную транзакцию?
  10. Что такое распределенные транзакции? Как реализовать?
  11. Какие проблемы решает паттерн Saga?
  12. Как реализовать аутентификацию в микросервисной архитектуре?
  13. Что такое Event Sourcing?
  14. Сформулируйте CAP-теорему.
  15. Расскажите о Raft Consensus Algorithm.
  16. В чем разница между императивной и декларативной парадигмой программирования? Приведите примеры языков.

Go

  1. За что отвечает переменная GOMAXPROCS? Каково ее значение по умолчанию?
  2. Кто отвечает за планирование горутин? Расскажите об алгоритме работы планировщика. Зачем он нужен, если и так есть системные потоки?
  3. Расскажите об алгоритме работы garbage collector. Mark and sweep, Reference counting и оптимизация алгоритма в языке Go. Что такое stop the world?
  4. Какие виды многозадачности вы знаете? Какой из них используется в Go?
  5. Зачем нужна рефлексия? В чем разница между рефлексией и кодогенерацией?
  6. В чем разница между value и reference типом? Назовите несколько примеров в языке Go.
  7. Как остановить горютину?
  8. Как в Go реализуется наследование?
  9. Что такое lvalue и rvalue?
  10. Какое у slice zero value?
  11. Как встроить стандартный профайлер в приложение?
  12. Что такое map-reduce? Как его продать в Go?
  13. Как в Go работает префиксный инкремент/декремент?
  14. Что будет, если преобразовать в JSON-объект, структура которого содержит поля с строчными буквами в названиях?
  15. Как прервать for/switch или for/select?
  16. Как можно оптимизировать выполнение большого количества последовательных операций чтения или записи?
  17. Можно ли вызвать метод у указателя (pointer) на структуру, если он равен nil?
  18. Что будет при попытке записи в закрытый канал?
  19. ​​Что такое взаимная блокировка (deadlock)?
  20. В чем особенность nil-каналов?
  21. В каких случаях следует использовать мютексы, а не каналы, и наоборот?
  22. Что такое билд-теги?
  23. Как продать LRU cache?
  24. Что такое SSA-представление?
  25. Что вы знаете о работе с плагинами на Go?
  26. Что такое алиас типов?
  27. Что такое падинги в структурах и на что они влияют?
  28. Что такое escape-анализ?
  29. Какая разница между стеком и грудой?
  30. Что нам дает пакет unsafe?
  31. Можно ли изменить символ в строке? А с помощью пакета unsafe?
  32. Как работать с рефлексией и что мы с ней можем сделать?
  33. Как под капотом смотрятся слайсы и карты?
  34. Как работать с copy()?
  35. Как работать с sync.Pool и sync.Map ? Какие подводные камни у них есть?
  36. Расскажите о канкаренсе-паттерне в Go.
  37. Как у Go реализована арифметика указателей?*

Вопросы по языку Golang

  1. Что из себя представляет тип данных string в языке Golang? Можно ли изменить определенный символ в строке? Что происходит при склеивании строк?

  2. Вытекающий вопрос — как эффективно склеивать множество строк?

  3. Что будет происходить при конкуррентной записи в map? Как можно решить эту проблему?

  4. Нужно ли лочить структуру мьютексом, если идет конкуррентная запись в разные поля структуры?

  5. Что выведет код?

func main() { runtime.GOMAXPROCS(1)

done := false

go func() { done = true }()

for !done { } fmt.Println(“finished”) } Как можно изменить этот код, чтобы был вывод “finished”?

  1. Как устроены каналы “под капотом”? (об этом я расказал в своих статьях “Под капотом Golang — как работают каналы. Часть 1” и “Строение каналов в Golang. Часть 2.”)

  2. Какая есть проблема в коде?

var counter int for i := 0; i < 1000; i++ { go func() { counter++ }() } Как её можно решить?

А как её можно бы было решить, если бы в языке не было пакета sync?

  1. Можно ли реализовать sync.Mutex и sync.WaitGroup на каналах? Как?

  2. Что ты использовал из пакета sync(кроме Mutex и WaitGroup)?

  3. Что выведет код

func main() { v := 5 p := &v println(*p)

changePointer(p) println(*p) }

func changePointer(p *int) { v := 3 p = &v } Почему? Как нужно изменить функцию changePointer, чтобы вывело 5 и 3 (в оригинальной версии выводится 5 и 5)?

  1. За сколько примерно выполнится приложение — за 3 секунды или за 6?

func worker() chan int { ch := make(chan int)

go func() { time.Sleep(3 * time.Second) ch <- 42 }()

return ch }

func main() { timeStart := time.Now()

_, _ = <-worker(), <-worker()

println(int(time.Since(timeStart).Seconds())) // что выведет - 3 или 6? } Что нужно изменить, чтобы код работал за 3 секунды?


OZON От своего хорошего знакомого, который там работает, слышал положительные отзывы. Да и в принципе, как я думаю, маркетплейс - это должно быть интересно: много данных, много клиентов, нужно как-то с этим справляться: шардинг, микросервисы, консистентность данных, , с этим по сути я никогда по-серьёзному не работал.

Был небольшой собес с рекрутером, потом с двумя техспецами, один из которых лид, второй - сеньор. Стандартный набор вопросов - задачка на алгоритмы, задачки на синхронизацию горутин и замыкания, запросы в БД. Спокойное сухое техническое собеседование.

Были устные задачки на алгоритмы, задачки на работу рантайма go, как устроен планировщик задач, внутреннее устройство каналов, вопросы по линуксу, по базам, по http, по деплою. В общем, приятно пообщались. Было ощущение, что я собрался со знакомыми и мы просто обсуждаем работу современных технологий.

на синхронизацию горутин - прорешеать

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

Общие вопросы:

Как хранятся переменные в Golang? Что такое “стек” и “куча”? Почему аллокация в “куче” дорогая? Во сколько раз? Как в golang освобождаетс память и можно ли отключить это поведение и зачем это делать? Что такое интерфейс и как используется? Как устроен пустой интерфейс? Как устроен слайс и чем он отличается от массива? Как создать многомерный массив в Golang? Нужно ли передавать slice по ссылке в фукнцию? Что происходит в runtime Golang? В чем различия goroutine от потока системы? Как огранить число потоков на системы при запуске Golang программы и возможно ли огранить их до 1 потока? Что такое каналы и каких видов они бывают? Что будет если писать в закрытый канал? Что будет если писать в неинициализированный канал? Расскажите о ООП в Golang Вопросы по database/sql:

https://habr.com/ru/amp/post/654569/ - Собеседование Golang разработчика (теоретические вопросы), Часть I https://habr.com/ru/post/670974/ - Собеседование Golang разработчика (теоретические вопросы), Часть II. Что там с конкурентностью?s

Реализовать LRU cache LFU

*Вопросы и ответы для собеседования Go-разработчика - огромная статья

  • Что такое ООП? Как это сделано в Golang?

Абстракция, Инкапсуляция, Наследование, Полиморфизм

SOLID, а именно:

O (open-closed principle, принцип открытости/закрытости) — классы/модули должны быть открыты для расширения, но закрыты для модификации.

S (single responsibility principle, принцип единственной ответственности) — определенный класс/модуль должен решать только определенную задачу, максимально узко но максимально хорошо (своеобразные UNIX-way). Если для выполнения своей задачи ему требуются какие-то другие ресурсы — они в него должны быть инкапсулированы (это отсылка к принципу инверсии зависимостей)

O (open-closed principle, принцип открытости/закрытости) — классы/модули должны быть открыты для расширения, но закрыты для модификации. Должна быть возможность расширить поведение, наделить новым функционалом, но при этом исходный код/логика модуля должна быть неизменной

L (Liskov substitution principle, принцип подстановки Лисков) — поведение наследующих классов не должно противоречить поведению, заданному базовым классом, то есть поведение наследующих классов должно быть ожидаемым для кода

I (interface segregation principle, принцип разделения интерфейса) — много тонких интерфейсов лучше, чем один толстый

D (dependency inversion principle, принцип инверсии зависимостей) — “завязываться” на абстракциях (интерфейсах), а не конкретных реализациях. Так же (это уже про IoC, но всё же) можно рассказать что если какому-то классу для своей работы требуется функциональность другого — то есть смысл “запрашивать” её в конструкторе нашего класса используя интерфейс, под который подходит наша зависимость. Таким образом целевая реализация опирается только на интерфейсы (не зависит от реализаций) и соответствует принципу под буквой S

В Go нет наследования. Совсем. Но есть встраивание (называемое “анонимным”, так как Foo в Bar встраивается не под каким-то именем, а без него) при этом встраиваются и свойства, и функции:

Инкапсуляция реализована на уровне пакетов. Имена, начинающиеся со строчной буквы, видны только внутри этого пакета (не являются экспортируемыми). И наоборот — всё, что начинается с заглавной буквы — доступно извне пакета. Дешево и сердито.

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

  • Как устроено инвертирование зависимостей?

Принцип инверсии зависимостей (dependency inversion principle) в Go который можно реализовывать следующим образом:

type speaker interface {
    Speak() string
}

type Foo struct {
    s speaker // s *Foo - было бы плохо
}

func NewFoo(s speaker) (*Foo, error) {
    if s == nil {
        return nil, errors.New("speaker is nil")
    }

    return &Foo{s: s}, nil
}

func (f Foo) SaySomething() string { return f.s.Speak() }

func main() {
    var foo, err = NewFoo(someSpeaker)

    if err != nil {
        panic(err)
    }

    fmt.Println(foo.SaySomething()) // depends on the speaker implementation
}

Мы объявляем интерфейс speaker не экспортируемым на нашей, принимающей стороне, и используя псевдо-конструктор NewFoo гарантируем что свойство s будет проинициализировано верным типом (дополнительно проверяя его на nil).

  • Как сделать свои методы для стороннего пакета?

Например, если мы используем логгер Zap в нашем проекте, и хотим к этому Zap-у прикрутить наши методы — то для этого нам нужно будет создать свою структуру, внутри в неё встраивать логгер Zap-а, и к этой структуре уже прикручивать требуемые методы. Просто “навесить сверху” функции на сторонний пакет мы не можем.

  • Типы данных и синтаксис

Целочисленные — 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?

В Go строка в действительности является слайсом (срезом) байт, доступным только для чтения. Строка содержит произвольные байты, и у неё нет ёмкости (cap). При преобразовании слайса байт в строку (str := string(slice)) или обратно (slice := []byte(str)) — происходит копирование массива (со всеми следствиями).

Создание подстрок работает очень эффективно. Поскольку строка предназначена только для чтения, исходная строка и строка, полученная в результате операции среза, могут безопасно совместно использовать один и тот же массив:

Go использует тип rune (алиас int32) для представления Unicode. Конструкция for … range итерирует строку посимвольно (а не побайтово, как можно было бы предположить):

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

Эффективным способом работы со строками (когда есть необходимость часто выполнять конкатенацию, например) является использование слайса байт или strings.Builder:

И ещё одну важную особенность стоит иметь в виду — это подсчет длины строки (например — для какой-нибудь валидации). Если считать по количеству байт, и строка содержит не только ASCII символы — то количество байт и фактическое количество символов будут расходиться:

HTTP 1 Основы Request Line - method, URL, protocol version Headers (обязательно только host) Request Body (не обязатель)

hht2

  • binary
  • multiplexed (streams)
  • cancel stream
  • server push

Understanding Allocations in Go

Isolation Levels

  • Read Uncommitted
  • Read Committed
  • Cursor Stability
  • Repeatable Read
  • Snapshot
  • Serializable

dirty read A transaction reads data written by a concurrent uncommitted transaction.

nonrepeatable read A transaction re-reads data it has previously read and finds that data has been modified by another transaction (that committed since the initial read).

phantom read A transaction re-executes a query returning a set of rows that satisfy a search condition and finds that the set of rows satisfying the condition has changed due to another recently-committed transaction.

serialization anomaly The result of successfully committing a group of transactions is inconsistent with all possible orderings of running those transactions one at a time.

Read Committed - базовый для постгрес

(SQL Isolation Levels)[https://blog.acolyer.org/2016/02/24/a-critique-of-ansi-sql-isolation-levels/]

Может ли структура быть ключом мапы Что такое интерфейс (это контракт), что такое пустой итерфейс Выравнивание памяти ( что такое) reciever - что это такое Виды индексов - b-tree, hash, gist? Распределенные транзакции

Меняется ли копасити если мы режем слайса

у мапы есть len но нет cap

https://github.com/loong/go-concurrency-exercises

https://github.com/luk4z7/go-concurrency-guide

https://github.com/MatthewJamesBoyle/golang-interview-prep

https://github.com/abstractart/learn-system-design

https://github.com/ByteByteGoHq/system-design-101#table-of-contents

https://github.com/cheatsnake/backend-cheats

https://github.com/vasanthk/how-web-works

Linux Performance Checklists for SREs

Linux Perf Analysis in 60s (https://netflixtechblog.com/linux-performance-analysis-in-60-000-milliseconds-accc10403c55)

  1. uptime ⟶ load averages
  2. dmesg -T | tail ⟶ kernel errors
  3. vmstat 1 ⟶ overall stats by time
  4. mpstat -P ALL 1 ⟶ CPU balance
  5. pidstat 1 ⟶ process usage
  6. iostat -xz 1 ⟶ disk I/O
  7. free -m ⟶ memory usage
  8. sar -n DEV 1 ⟶ network I/O
  9. sar -n TCP,ETCP 1 ⟶ TCP stats
  10. top ⟶ check overview

Linux Disk Checklist

  1. iostat -xz 1 ⟶ any disk I/O? if not, stop looking
  2. vmstat 1 ⟶ is this swapping? or, high sys time?
  3. df -h ⟶ are file systems nearly full?
  4. ext4slower 10 ⟶ (zfs*, xfs*, etc.) slow file system I/O?
  5. bioslower 10 ⟶ if so, check disks
  6. ext4dist 1 ⟶ check distribution and rate
  7. biolatency 1 ⟶ if interesting, check disks
  8. cat /sys/devices/…/ioerr_cnt ⟶ (if available) errors
  9. smartctl -l error /dev/sda1 ⟶ (if available) errors
  • Another short checklist. Won’t solve everything. ext4slower/dist, bioslower/latency, are from bcc/BPF tools.

Linux Network Checklist

  1. sar -n DEV,EDEV 1 ⟶ at interface limits? or use nicstat
  2. sar -n TCP,ETCP 1 ⟶ active/passive load, retransmit rate
  3. cat /etc/resolv.conf ⟶ it’s always DNS
  4. mpstat -P ALL 1 ⟶ high kernel time? single hot CPU?
  5. tcpretrans ⟶ what are the retransmits? state?
  6. tcpconnect ⟶ connecting to anything unexpected?
  7. tcpaccept ⟶ unexpected workload?
  8. netstat -rnv ⟶ any inefficient routes?
  9. check firewall config ⟶ anything blocking/throttling?
  10. netstat -s ⟶ play 252 metric pickup
  • tcp*, are from bcc/BPF tools.

Linux CPU Checklist

  1. uptime ⟶ load averages
  2. vmstat 1 ⟶ system-wide utilization, run q length
  3. mpstat -P ALL 1 ⟶ CPU balance
  4. pidstat 1 ⟶ per-process CPU
  5. CPU flame graph ⟶ CPU profiling
  6. CPU subsecond offset heat map ⟶ look for gaps
  7. perf stat -a – sleep 10 ⟶ IPC, LLC hit ratio
  • htop can do 1-4. I’m tempted to add execsnoop for short-lived processes (it’s in perf-tools or bcc/BPF tools).

https://www.brendangregg.com/blog/2016-05-04/srecon2016-perf-checklists-for-sres.html

Банк CV от AgileFluent - https://agilefluent.notion.site/CV-AgileFluent-4910d13666064c10b6e067d708055587

https://github.com/cheatsnake/backend-cheats/ - базовые вещи, просто почитать когда скучно

https://github.com/karanpratapsingh/system-design

https://book.soft-skillz.work/2018-10-23.html - все милое

https://telegra.ph/Opyt-sobesedovaniya-v-Google-na-poziciyu-SRE-02-02 - Опыт собеседования в Google на позицию SRE

Learn Makefiles - https://makefiletutorial.com

https://jepsen.io/consistency - Consistency Models

https://backendinterview.ru - Памятка PHP/GoLang разработчику для подготовки к собеседованиям

A Senior Engineer’s Guide to the System Design Interview - https://interviewing.io/guides/system-design-interview

https://architecturenotes.co

https://github.com/donnemartin/system-design-primer

https://github.com/ByteByteGoHq/system-design-101 - крутецкий очень

https://github.com/goavengers/go-interview

https://github.com/dmitryrpm/maxima-tests - тест на golang

https://github.com/luk4z7/go-concurrency-guide

https://github.com/loong/go-concurrency-exercises

https://github.com/mridul-sahu/golang-concurrency-patterns

Мои собеседования (Golang developer) - https://habr.com/ru/articles/683920/

Вопросы и ответы для собеседования Go-разработчика - https://habr.com/ru/articles/658623/