Анализ теста по Go с PHDays

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

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

В конце мая прошла конференция PHDays, на которой был тест как раз с такими задачками. К моему сожалению, я провалила этот тест, но затем разобралась что, как и почему, и хочу поделиться с вами.

Итак, 5 картинок с кодом, к каждому дается 4 варианта ответа.

Задача 1

package main

import "fmt"

func f(slice []int) {
    slice = append(slice, 84)
}

func main() {
    s := []int{23, 42}
    f(s)
    fmt.Println(s)
}

Варианты ответа:

1. [23 43]
2. [23 42 84]
3. Программа не скомпилилируется
4. runtime error: slice out of range

Ответ

Если попробовать скомпилировать код, то компилятор обратит ваше внимание на строку slice = append(slice, 84) и не просто так.

Да, слайсы имеют ссылочный тип. Но если они ссылочные, что не так-то? Должно работать!

А проблема в том, как работает функция append — она не меняет старый слайс, а возвращает новый. А из функции f мы результат не получаем.

Соответственно, правильный ответ будет под номером 1: [23 43].

Чтобы получить ответ из пункта 2 ([23 42 84]), нам нужно этот результат как-то передать в main. Сделать это можно двумя способами.

Во-первых, можно вернуть новый слайс из функции. Поправим немного код.

package main

import "fmt"

func f(slice []int) []int {
    return append(slice, 84)
}

func main() {
    s := []int{23, 42}
    s = f(s)
    fmt.Println(s)
}

(Go Playground)

Во-вторых, можно вместо слайса передавать указатель на слайс:

package main

import "fmt"

func f(slice *[]int) {
    *slice = append(*slice, 84)
}

func main() {
    s := []int{23, 42}
    f(&s)
    fmt.Println(s)
}

(Go Playground)

Задача 2

package main

import "fmt"

func main() {
    s := "bar"
    {
        s := "foo"
        fmt.Print(s)
    }
    fmt.Print(s)
}

Варианты ответа:

1. foofoo
2. foobar
3. compilation error: s redeclared in this block
4. compilation error: s undefined: s

Ответ

Вы когда-нибудь видели, чтобы кто-то вот так использовал скобки? Я — нет. Однако давайте разберёмся, что тут происходит.

Имеем две области видимости: внутри скобок и за их пределами. При этом с толку сбивают две одноимённых переменных. Такое называется variable shadowing и считается плохой практикой в Go. Существуют различные линтеры, которые выдают на подобный код предупреждение (например, этот линтер, который используется вместе с go vet, см. go help vet). Собственно, поэтому сначала программа напишет foo, а затем bar. Следовательно, правильный ответ будет под номером 2.

Задача 3

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            fmt.Println(i)
            wg.Done()
        }()
    }
    wg.Wait()
}

Варианты ответа:

1. Цифры от 1 до 99 по возрастанию
2. Цифры от 1 до 99 в произвольном порядке
3. Вывод не определен
4. Ничего

Ответ

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

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

Следовательно, правильный ответ будет под номером 3 — вывод не определен.

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

Во-первых, можно сделать копию счётчика цикла (i := i) перед созданием замыкания:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        i := i
        go func() {
            fmt.Println(i)
            wg.Done()
        }()
    }
    wg.Wait()
}

(Go Playground)

Во-вторых, можно сделать в анонимной функции параметр i int и передавать счётчик цикла в замыкание через этот параметр (тогда он будет копироваться при этой передаче):

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(i int) {
            fmt.Println(i)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

(Go Playground)

Задача 4

package main

import "fmt"

func main() {
    s := "Hello"

    defer func() {
        s = "World"
    }()
    fmt.Println(s)
}

Варианты ответа:

1. World
2. Hello
3. Hello World
4. compilation error: (func literal)() used as value

Ответ

Чтобы понять, что происходит в этом коде, надо знать особенности defer. Эта конструкция выполнится только по завершению функции. Тут нас сбивает с толку Println, которы стоит после defer. Однако, если мы попробуем упросить код и убрать всё, что может нас запутать, то получим что-то такое:

package main

import "fmt"

func main() {
    s := "Hello"
    fmt.Println(s)

    s = "World"
}

(Go Playground)

Теперь правильный ответ очевиден, он под номером 2.

Задача 5

package main

import "fmt"

func main() {
    a := []string{"a", "b", "c"}
    b := a[1:2]
    b[0] = "q"
    fmt.Println(a)
}

Варианты ответа:

1. [a b c]
2. [a q c]
3. [b c]
4. output is undefined

Ответ

На первый взгляд хочется спросить, а зачем тут вообще эта b? Мы же a печатаем.

Вот теперь самое время вспомнить, что слайсы в Go — это ссылочный тип данных. Следовательно, на строке b := a[1:2], мы присваиваем в переменную b ссылку на область памяти из переменной a. А затем (b[0] = "q") редактируем эту самую область памяти. В итоге имеем в результате правильный ответ под номером 2.

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Сколько задач вы решили?
25% 5 2
0% 4 0
25% 3 2
12.5% 2 1
12.5% 1 1
25% Не знаю Go 2
Проголосовали 8 пользователей. Воздержались 2 пользователя.
Источник: https://habr.com/ru/post/571002/


Интересные статьи

Интересные статьи

17 июня состоится Online LoGeek Night QA. На нем наши тестировщики расскажут о разных способах хранения данных в автотестах с примерами на Java, а также об автоматизации ...
Инструменты Business Intelligence (BI) за последние несколько лет проникли почти во все виды бизнеса, а изучению данных уделяется все больше внимания и выделяется больше ресурсов. Если го...
Добрый день уважаемые читатели! Данная статья является продолжением публикации "Повторяем когортный анализ, выполненный в Power BI, силами Python" (ссылка). Настоятельно ...
Тема безопасности машинного обучения довольно хайповая последнее время и хотелось затронуть именно практическую ее сторону. А тут повод крутой — PHDays, где собираются самые разные специалисты из...
База данных временных рядов (TSDB, time series database) в Prometheus 2 – это отличный пример инженерного решения, которое предлагает серьёзные улучшения в сравнении с хранилищем v2 в Prometh...