15 вопросов с собеседований Golang из LinkedIn Assessment — подробные пояснения
Недавно я наткнулся на тест LinkedIn Golang, интересную возможность для разработчиков получить значок на сайте, демонстрирующий их знание языка (этот значок не только демонстрирует ваши навыки, но и помогает вам выделиться из толпы на конкурентном рынке труда).
Я нашёл вопросы в тесте довольно интересными, и я уже писал о некоторых из них ранее, так как они дают ценную информацию о различных аспектах языка программирования Go.
@golang_interview/ – разбор практических вопросов Go в нашем телеграм канале.
1. Что нужно, чтобы две функции были одного типа?
A. They should share the same signatures, including parameter types and return types.
B. They should share the same parameter types but can return different types.
C. All functions should be the same type.
D. The functions should not be a first class type.
Если мы хотим, чтобы две функции в Go были одного типа, они должны иметь одинаковую сигнатуру функции .
type sigFunc func(a int, b float64) (bool, error)
func functionA(a int, b float64) (bool, error) {
return true, nil
}
func functionB(a int, b float64) (bool, error) {
return false, nil
}
func main() {
var x sigFunc = functionA
x = functionB
print(x)
}
Это означает, что они должны иметь соответствующие параметры (количество, типы) и возвращаемые значения.
2. Что возвращает функция len()
, если ей передаётся строка в кодировке UTF-8?
A. the number of characters
B. the number of bytes
C. It does not accept string types.
D. the number of code points
Вот вам небольшая идея: в Go строки на самом деле представляют собой последовательности байтов. Это означает, что когда вы передаёте строку в кодировке UTF-8 функции len(), она считает байты, а не символы:
func main() {
s := "世界"
fmt.Println("Byte length:", len(s)) // 6 bytes
fmt.Println("Rune count:", utf8.RuneCountInString(s)) // 2 letters
}
3. Каково значение Read
?
const (
Write = iota
Read
Execute
)
A. 0
B. 1
C. 2
D. a random value
Идентификатор iota в Go — интересная функция — она представляет целочисленное значение, которое начинается с 0 и увеличивается на 1 для каждого элемента в блоке const.
4. Как вы выведите запущенные тесты с помощью go test
?
A. go test
B. go test -x
C. go test --verbose
D. go test -v
При использовании команды go test
вы можете включить флаг -v (“verbose”) для более подробного вывода.
go test -v
Запустив go test
с флагом -v, вы увидите название каждого теста, его результат и продолжительность теста по мере его выполнения:
=== RUN TestAdd
=== RUN TestAdd/case_1_2_3
=== RUN TestAdd/case_-1_-2
=== RUN TestAdd/case_0
=== RUN TestAdd/case_-1_2
--- PASS: TestAdd (0.00s)
--- PASS: TestAdd/case_1_2_3 (0.00s)
--- PASS: TestAdd/case_-1_-2 (0.00s)
--- PASS: TestAdd/case_0 (0.00s)
--- PASS: TestAdd/case_-1_2 (0.00s)
PASS
ok command-line-arguments 0.523s
5. Что делает блок sync.Mutex
, пока он заблокирован?
A. all goroutines
B. any other call to lock that Mutex
C. any reads or writes of the variable it is locking
D. any writes to the variable it is locking
В Go a sync.Mutex
служит механизмом взаимного исключения, гарантируя, что только одна горутина может одновременно получить доступ к критическому разделу кода.
Фраза « любое чтение или запись переменной, которую он блокирует » соответствует нашему пониманию, поскольку мы используем a sync.Mutex
для защиты доступа к общим переменным.
var mtx = sync.Mutex{}
func Add() {
mtx.Lock()
defer mtx.Unlock()
a++
}
6. Каков идиоматический способ приостановить выполнение текущей области до тех пор, пока не будет возвращено произвольное количество подпрограмм?
A. Pass an int and Mutex to each and count when they return.
B. Loop over a select statement.
C. Sleep for a safe amount of time.
D. sync.WaitGroup
Если у вас была возможность изучить мою статью о пакете Go Sync, вы, возможно, помните, что сначала я использовал функцию time.Sleep
, прежде чем перейти к более эффективному sync.WaitGroup
:
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 500; i++ {
wg.Add(1)
go func() {
defer wg.Done()
Add()
}()
}
wg.Wait()
fmt.Println(a)
}
In sync.WaitGroup
Go — это удобный инструмент, который позволяет вам дождаться завершения группы горутин, прежде чем приступить к выполнению.
Он работает с использованием простого счётчика и эффективно блокирует текущую область действия до тех пор, пока все рабочие горутины не завершат свои задачи, а счётчик WaitGroup не достигнет нуля.
7. Каков побочный эффект использования time.After
в утверждении select
?
A. It blocks the other channels.
B. It is meant to be used in select statements without side effects.
C. It blocks the select statement until the time has passed.
D. The goroutine does not end until the time passes.
Если вы не знакомы с time.After
, это функция в пакете Go time, которая возвращает набор каналов для отправки текущего времени после указанной продолжительности.
func After(d Duration) <-chan Time
Обычно она используется в операторах select для реализации тайм-аутов или задержек. Например, представьте, что вы ждёте 3 секунды, прежде чем вывести что-то на экране:
func main() {
timeout := 3 * time.Second
start := time.Now()
done := make(chan bool)
select {
case <-done:
fmt.Println("Operation completed.")
return
case <-time.After(timeout):
fmt.Printf("Timeout after %v\n", time.Since(start))
}
}
Теперь поговорим о побочном эффекте.
Для кратковременных time.After
периодов это может не иметь большого значения, но рассмотрим сценарий, в котором тайм-аут установлен на 1 час, а работа завершается до истечения времени ожидания. В этой ситуации таймер всё ещё задерживается в памяти:
func main() {
done := make(chan bool)
go func() {
time.Sleep(500 * time.Millisecond)
done <- true
}()
for {
select {
case <-done:
fmt.Println("Operation completed.")
return
case <-time.After(time.Hour):
fmt.Println("Still waiting...")
}
}
}
Как следствие, горутина, созданная с помощью time.After
, не завершится, пока не истечёт полный час, даже если операция завершится раньше.
8. Какие ограничения есть на тип var
для компиляции i := myVal.(int)?
A. myVal must be an integer type, such as int, int64, int32, etc.
B. myVal must be able to be asserted as an int.
C. myVal must be an interface.
D. myVal must be a numeric type, such as float64 or int64.
В контексте утверждения типа i := myVal.(int)
переменная myVal
должна быть типом интерфейса для успешной компиляции кода.
Однако использование её таким образом может быть рискованным и неэффективным, так как это может вызвать панику во время выполнения, если myVal
не типа int
. Чтобы более изящно справиться с этой ситуацией, лучше использовать форму утверждения типа с двумя значениями, которая обеспечивает резервный механизм:
// BAD
i := myVal.(int)
// BETTER
i, ok := myVal.(int)
// ... doing something with ok
9. Как правильно передать это как тело запроса HTTP POST?
data := "A group of Owls is called a parliament"
A. resp, err := http.Post("https://httpbin.org/post", "text/plain", []byte(data))
B. resp, err := http.Post("https://httpbin.org/post", "text/plain", data)
C. resp, err := http.Post("https://httpbin.org/post", "text/plain", strings.NewReader(data))
D. resp, err := http.Post("https://httpbin.org/post", "text/plain", &data)
Чтобы отправить данные в виде тела запроса HTTP POST, важно знать тип содержимого. Поскольку это необработанный текст, мы будем использовать тип содержимого «текст/обычный». Функция http.Post
требует io.Reader
в качестве тела, а не строку или байты:
Post(url string, contentType string, body io.Reader) (resp *http.Response, err error)
Интерфейс Reader
определяется следующим образом:
type Reader interface {
Read(p []byte) (n int, err error)
}
Чтобы выполнить требования, мы преобразуем тело в буфер, реализующий этот интерфейс:
func main() {
data := "A group of Owls is called a parliament"
contentType := "text/plain"
body := strings.NewReader(data)
// or
// body := bytes.NewBufferString(data)
resp, err := http.Post("https://example.com", contentType, body)
// ....
}
Таким образом, правильный способ отправки данных в виде HTTP-запроса POST:
resp, err := http.Post("https://httpbin.org/post", "text/plain", strings.NewReader(data))
10. Каким должно быть идиоматическое имя для интерфейса с одним методом и сигнатурой Save() error
?
A. Saveable
B. SaveInterface
C. ISave
D. Saver
Основываясь на эффективном соглашении об именах Go для интерфейсов, интерфейсы с одним методом обычно именуются путём добавления суффикса «-er» или аналогичной модификации к имени метода, что приводит к Reader
, Writer
, Formatter
и CloseNotifier
..
В этом случае идиоматическое название интерфейса будет Saver .
О, возможно, вы также сталкивались с интерфейсом Stringer()
, который определяется следующим образом:
type Stringer interface {
String() string
}
Это уловка, когда вы передаёте значение, реализующее интерфейс Stringer
таким функциям, как fmt.Println()
или fmt.Printf()
, метод
String()
автоматически вызывается для получения строкового представления значения.
11. Какова чувствительность к регистру по умолчанию для функции JSON Unmarshal
?
A. The default behavior is case insensitive, but it can be overridden.
B. Fields are matched case sensitive.
C. Fields are matched case insensitive.
D. The default behavior is case sensitive, but it can be overridden.
Функция JSON Unmarshal в Go по умолчанию нечувствительна к регистру. Однако это поведение можно переопределить с помощью тегов JSON. Что происходит, когда есть два поля, такие как «title» и «Title»?
Давайте рассмотрим это на примере, чтобы понять, почему сначала не учитывается регистр:
type Post struct {
Title string `json:"Title"`
SubTitle string
}
func main() {
p := Post{}
json.Unmarshal([]byte(`{"title":"hello","subtitle":"world"}`), &p)
fmt.Println(p)
}
// {hello world}
В этом примере поле Title
имеет явный тег поля json:"Title"
, предписывающий функции json.Unmarshal
сопоставлять поле JSON «Title» (с учётом регистра) с полем структуры Title
. Поле SubTitle
не имеет тега поля, поэтому оно будет использовать поведение по умолчанию без учёта регистра, чтобы сопоставить поле «subtitle» JSON с полем структуры SubTitle
.
«Но мне интересно, почему поле «title» всё ещё совпадает, даже если оно перекрывается «Title»?»
Это связано с тем, что пакет encoding/json
использует резервный механизм во время демаршалинга:
- Сначала он ищет точное совпадение между именами полей JSON и именами полей или тегами структуры.
- Если он не находит точного совпадения, он возвращается к линейному поиску без учёта регистра (который может быть дорогостоящим для больших наборов данных).
type Post struct {
Title string
SubTitle string
TitleSm string `json:"title"`
}
func main() {
p := Post{}
json.Unmarshal([]byte(`{"title":"hello","subtitle":"world"}`), &p)
fmt.Println(p)
}
// { world hello}
// p.Title is empty
12. Где полезен встроенный метод recover
?
A. in the main function
B. immediately after a line that might panic
C. inside a deferred function
D. at the beginning of a function that might panic
Встроенный метод recover
действительно полезен в отложенных функциях , но не рекомендуется вызывать его напрямую с помощью ключевого слова defer.
// BAD
func main() {
defer recover()
panicCode() // <-- crash
}
// BETTER
func handlePanic() {
if panicInfo := recover(); panicInfo != nil {
fmt.Println(panicInfo)
}
}
func main() {
defer handlePanic()
panicCode()
}
13. В чём разница между time
пакетами Time.Sub()
и методами Time.Add()
?
A. Time.Add() is for performing addition while Time.Sub() is for nesting timestamps.
B. Time.Add() always returns a later time while time.Sub always returns an earlier time.
C. They are opposites. Time.Add(x) is the equivalent of Time.Sub(-x).
D. Time.Add() accepts a Duration parameter and returns a Time while Time.Sub() accepts a Time parameter and returns a Duration.
Основное различие между методами Time.Add()
и Time.Sub()
в пакете time
заключается в их параметрах и возвращаемых значениях. Time.Add()
принимает параметр Duration
и возвращает значение Time
, а Time.Sub()
принимает параметр Time
и возвращает Duration
.
«Почему они оба не принимают
Duration
и не возвращаютсяTime
?»
Причина в том, что Time.Add()
может обрабатывать отрицательные аргументы, эффективно функционируя как операция вычитания. Следовательно, не имеет смысла иметь другой метод Time.Sub()
, который также принимает файлы Duration
.
Методы Time.Add()
и Time.Sub()
служат разным целям и имеют разные сигнатуры для конкретных вариантов использования:
func main() {
now := time.Now()
newTime := now.Add(2 * time.Hour)
fmt.Println("Time after 2 hours:", newTime)
newTime = now.Add(2 * time.Hour)
fmt.Println("Time before 2 hours:", newTime)
duration := newTime.Sub(now)
fmt.Println("Duration newTime to now:", duration)
}
Time after 2 hours: 2023-05-09 03:05:03.177199 +0700 +07 m=+7200.000587876
Time before 2 hours: 2023-05-09 03:05:03.177199 +0700 +07 m=+7200.000587876
Duration newTime to now: 2h0m0s
Как показано в этом примере, Time.Add()
используется для добавления или вычитания продолжительности из значения времени, а Time.Sub()
используется для вычисления продолжительности между двумя значениями времени.
14. Каков риск использования нескольких тегов полей в одной структуре?
A. Every field must have all tags to compile.
B. It tightly couples different layers of your application.
C. Any tags after the first are ignored.
D. Missing tags panic at runtime.
Основная проблема с использованием нескольких тегов полей в одной структуре заключается в том, что это может привести к тесной связи между различными уровнями вашего приложения . Чтобы проиллюстрировать эту концепцию, давайте рассмотрим пример:
Но что такое « пары разных слоёв»?
type Post struct {
Title string `json:"title" bson:"title"`
SubTitle string `json:"subtitle" bson:"subtitle"`
}
В этом примере структура Post
имеет два тега поля для каждого поля: json
и bson
. Эти теги могут использоваться для различных целей, таких как отправка HTTP-ответов (с помощью json
) и обработка демаршалинга MongoDB (с помощью bson
).
Когда используется несколько таких тегов, уровень ответа HTTP (веб-сервер) и уровень хранения (MongoDB) становятся тесно связанными. Если вы хотите изменить title
, например, на shortTitle
, вам потребуется обновить как ответ HTTP (что также может повлиять на клиентов, обрабатывающих ответ), так и хранилище MongoDB.
Важно отметить, что использование нескольких тегов полей таким образом не является неправильным по своей сути.
15. Если вы перебираете мапу в цикле for range, в каком порядке будет осуществляться доступ к парам ключ:значение?
A. in pseudo-random order that cannot be predicted
B. in reverse order of how they were added, last in first out
C. sorted by key in ascending order
D. in the order they were added, first in first out
При переборе мапы с использованием цикла for range в Go порядок доступа к парам ключ-значение не обязательно соответствует какой-либо конкретной последовательности и не обязательно соответствует порядку вставки:
func main() {
m := map[string]int{}
m["a"] = 1
m["b"] = 2
m["c"] = 3
for key, value := range m {
fmt.Printf("Key: %s, Value: %d\n", key, value)
}
for key, value := range m {
fmt.Printf("Key: %s, Value: %d\n", key, value)
}
}
Key: a, Value: 1
Key: b, Value: 2
Key: c, Value: 3
Key: c, Value: 3
Key: a, Value: 1
Key: b, Value: 2
В результате вы можете столкнуться с разными порядками каждый раз, когда перебираете одну и ту же мапу. Такое поведение связано с реализацией карт в Go как хэш-карт, что приводит к недетерминированному порядку итераций.