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

Kate

Administrator
Команда форума
7cea88c3c93a222778e45a39fe56db4f.jpg

Думаю, многие из вас сталкивались с замысловатыми задачками, которые в реальной практике встретить почти невозможно, но которые очень любят давать во всяких тестах и на собеседованиях.
В конце мая прошла конференция 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 42]
2. [23 42 84]
3. Программа не скомпилилируется
4. runtime error: slice out of range
Ответ
Если попробовать скомпилировать код, то компилятор обратит ваше внимание на строку slice = append(slice, 84) и не просто так.
Да, слайсы имеют ссылочный тип. Но если они ссылочные, что не так-то? Должно работать!
А проблема в том, как работает функция append — она не меняет старый слайс, а возвращает новый. А из функции f мы результат не получаем.
Соответственно, правильный ответ будет под номером 1: [23 42].
Чтобы получить ответ из пункта 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.


 
Сверху