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

В конце мая прошла конференция 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.

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

Комментарии (7)


  1. funny_falcon
    03.08.2021 11:52

    Вот блин: на захвате переменной цикла споткнулся. И вроде ж известная грабля, сам при написании уже на автомате отслеживаю. А при чтении чужого кода не заметил :-(


  1. creker
    03.08.2021 11:55
    +2

    Задача 1

    Компилятор будет молчать. Это корректный код. Жаловаться может только линтер, который при компиляции сам по себе не выполняется, а тот же golangci-lint у меня на такой код не жалуется. Ну и в ответе опечатка - 42, а не 43

    Задача 2

    Плохой практикой скорее будет объявлять подобные блоки, но variable shadowing плохой практикой не является. Это повсеместная практика например для переменных ошибок. Чрезвычайно редко, когда я сталкивался с багами из-за этого. А на практике будет релевантен код с замыканием. Кстати ниодин линтер у меня на это не жалуется, в том числе go vet. Там этой проверки нет, судя по коду.

    Задача 5

    Слайсы это не ссылочный тип. Именно по этой причине в первой задаче получается такой ответ. Слайсы это value тип. При присвоении другой переменной содержимое копируется. Тот факт, что он внутри себя содержит указатель на массив, не делает его ссылочным. Ссылочный тип это канал или map, например.


    1. Irenica Автор
      03.08.2021 12:33
      +3

      Спасибо за указание на опечатку, исправила. Но такое лучше писать в личку, иначе комментарий становится неактуальным.

      Задача 1

      Вы правы, замечание сделает не компилятор, а линтер, а если конкретнее, ineffassign, который отслеживает бесполезные присваивания. Странно, что у вас golangci-lint не жалуется, ибо упомянутый линтер входит в него. Возможно, дело в настройках или версии.

      Задача 2

      Я не утверждала, что variable shadowing - плохая практика. Лишь заметила, что в Go она таковой считается. Об этом говорит хотя бы то, что core team языка разработала линтер для этого.

      Он не входит в поставку go vet по умолчанию, а оформлен в виде отдельного инструмента, который можно установить дополнительно и подключить к go vet. Об этом написано непосредственно в справке этой утилиты: go help vet:

      For example, the 'shadow' analyzer can be built and run using these commands:
      
         go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow
         go vet -vettool=$(which shadow)
      

      Задача 5

      Это терминологический спор. Можно ли считать ссылочным тип, при копировании значения которого связанные данные не копируются? Я считаю, что да.

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

      К слову, реализации ссылочных типов как раз и работают на базе указателя на данные. Так что это вполне аргумент за.


      1. creker
        03.08.2021 12:58
        +2

        Не терминологический, это по факту так. Слайс это структура из трех полей - указатель на начало массива, длина, capacity. Все три этих поля копируются. Если бы слайс был ссылочным типом, то они все передавались бы по ссылке и в вашем первом примере мы получили бы совсем другой ответ, но это не так. Это прямо противоречит тезису "если менять конкретный элемент через одну переменную, значение этого элемента изменится во всех местах, где есть копия этой ссылки". Нет, не изменится. Только если повезет и внутри слайса будет указатель на одну и туже область памяти, чего мы знать не можем без unsafe. Об этом постоянно говорят новичкам и объясняют разработчики языка. В Go все передается по значению (да, даже ссылочные типы. В их случае по значению передается указатель) и нужно понимать семантику типов. После этого все вопросы отпадут сами собой и ответы на первую и пятую задачи будут очевидны.

        Ближайший аналог слайсов это Span в C#, который имеет примерно такую же семантику и тоже value-тип. Наличие внутри него указателя точно так же не делает его ссылочным. В C# даже специальный синтаксис придумали, чтобы гарантировать это - ref struct.


  1. Ru6aKa
    03.08.2021 14:15
    -2

    Проблема с variable shadowing - это просто "упрощения" языка, а конкретно что можно объявить и инициализировать переменную без ключевого слова. И я бы сказал что такое "упрощение" пошло не на пользу языку.

    Практически во всех языках новый блок - это новый scope, и соответственно, если в блоке переменная без ключевого слова то обращаемся к переменной что объявлена в outer scope.


  1. smartello
    03.08.2021 14:29
    +1

    Решил три, но я не знаю go. Прям непонятно что отвечать.


  1. ufm
    05.08.2021 07:57

    В задаче №3 ответы 1 и 2 не могут быть правильными в любом случае.