Deep Engineering

ЗАМЕР

bench/goiter/runs/mechanics.txt

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/go/iteration/range-over-func

Запись прогона

У этого замера записи прогона нет — только скрипт.

Скрипт

175 строк
go1.24.7 linux/amd64 | цикл по функции изнутри
ни одного измерения времени: проверяется порядок, а не скорость

1. ЦИКЛ ПО ФУНКЦИИ — ЭТО ВЫЗОВ ЭТОЙ ФУНКЦИИ
------------------------------------------------------------------
  for v := range talking { ... }

    итератор: начал работу
    итератор: зову yield(0)
  тело цикла: получил 0
    итератор: yield вернул true
    итератор: зову yield(1)
  тело цикла: получил 1
    итератор: yield вернул true
    итератор: зову yield(2)
  тело цикла: получил 2
    итератор: yield вернул true
    итератор: значения кончились, возвращаюсь сам

  Печать итератора и печать тела чередуются: тело выполнилось
  МЕЖДУ двумя строками итератора. Так может быть только в
  одном случае — если тело вызвал сам итератор.

  Стек вызовов из тела двух вложенных циклов по функциям
  (внешний идёт по one, внутренний по other):

      0  main.frames
      1  main.block1.one.block1-range2-range4
      2  main.other
      3  main.block1-range2
      4  main.one
      5  main.block1
      6  main.main
      7  runtime.main
      8  runtime.goexit

  Читать снизу вверх. Кадры чередуются: итератор, тело,
  итератор, тело. У тела есть собственное имя, и придумал его
  компилятор — суффикс -rangeN и означает «тело N-го цикла по
  функции в этой функции».

2. BREAK — ЭТО RETURN FALSE, CONTINUE — ЭТО RETURN TRUE
------------------------------------------------------------------
  тело доработало до конца     шаг 0: yield вернул true
  тело доработало до конца     шаг 1: yield вернул true
  тело доработало до конца     шаг 2: yield вернул true

  тело сделало continue        шаг 0: yield вернул true
  тело сделало continue        шаг 1: yield вернул true
  тело сделало continue        шаг 2: yield вернул true

  тело сделало break           шаг 0: yield вернул false

  Итератор не знает слов break и continue: он видит только
  булев результат yield. Всё управление циклом проходит
  через это единственное значение.

3. RETURN ИЗ ТЕЛА НЕ ВЫХОДИТ НЕМЕДЛЕННО
------------------------------------------------------------------
  Вызываем функцию, у которой внутри цикла стоит return:

    итератор: открыл ресурс
    тело: получил 0
    тело: получил 1
    тело: получил 2
    тело: делаю return
    итератор: yield вернул false, возвращаюсь
    итератор: ЗАКРЫЛ ресурс (defer)

  функция вернула: "значение из тела цикла"

  Порядок строк — главное здесь. После слова «делаю return»
  управление НЕ ушло из функции: сначала yield вернул false,
  итератор доработал и отработал свой defer, и только потом
  вернулась вызывающая функция.

  Иначе и быть не может: тело — это функция, из неё нельзя
  вернуться за вызывающего. return в теле означает «останови
  цикл и запомни, что после него надо выйти»; сам выход
  происходит уже после того, как итератор вернул управление.

  Практическое следствие: уборка в defer у итератора
  выполняется при ЛЮБОМ выходе из тела, включая return и
  панику. Файл будет закрыт.

4. ПАНИКА ПРИХОДИТ К ИТЕРАТОРУ НЕ ТАК, КАК RETURN
------------------------------------------------------------------
  Тот же опыт, что в блоке 3, с двумя отличиями: у итератора
  есть строка ПОСЛЕ yield, а тело вместо return паникует.

    итератор: открыл ресурс
    тело: паникую
    итератор: ЗАКРЫЛ ресурс (defer)
    вызывающий: поймал панику: паника тела

  Строки «ПОСЛЕ yield» в выводе нет: при панике yield не
  возвращает управление вовсе — паника разматывает кадр
  итератора насквозь. Из кода итератора выполняются только
  его defer.

  Отсюда правило: уборку класть в defer, а не в строку после
  yield. Строка после yield при панике тела не выполнится,
  defer выполнится и там и там.

5. ЧЕТЫРЕ ОШИБКИ ИТЕРАТОРА, КОТОРЫЕ ЛОВИТ РАНТАЙМ
------------------------------------------------------------------
  yield после break:       runtime error: range function continued iteration after function for loop body returned false
  yield после паники:      runtime error: range function continued iteration after loop body panic
  yield после цикла:       runtime error: range function continued iteration after whole loop exit
  итератор съел панику:    runtime error: range function recovered a loop body panic and did not resume panicking

  Все четыре — не ошибки компиляции, а проверки во время
  работы: цикл держит при себе переменную состояния и сверяет
  с ней каждый вход в тело. Поэтому итератор, который
  сохранил yield и зовёт его позже или из другой горутины,
  падает предсказуемо, а не портит данные молча.

  Четвёртая — самая неочевидная. Итератор с общим
  defer recover() выглядит как разумная защита, но паника из
  ТЕЛА цикла проходит через кадр итератора, и такой recover
  съедает чужую панику. Рантайм это замечает.

6. ВЛОЖЕННЫЕ ЦИКЛЫ И BREAK С МЕТКОЙ
------------------------------------------------------------------
  Два цикла по функциям, break с меткой из внутреннего:

      тело: A1
      тело: A2
      тело: A3
      внутренний итератор: вернулся
      тело: B1
      тело: B2
      тело: break Loop
      внутренний итератор: yield вернул false
      внутренний итератор: вернулся
    внешний итератор: yield вернул false
    внешний итератор: вернулся

  Оба итератора получили false и оба доработали — сначала
  внутренний, потом внешний. Выход через два уровня не
  прыжок: это две остановки подряд, каждая по своему yield.
  Уборка обоих итераторов при этом выполняется.

7. ITER.SEQ2 И СТАНДАРТНАЯ БИБЛИОТЕКА
------------------------------------------------------------------
  Два типа, и оба — просто имена для функций:

      type Seq[V any]     func(yield func(V) bool)
      type Seq2[K, V any] func(yield func(K, V) bool)

  slices.Sorted(maps.Keys(m)): [груша слива яблоко]
  slices.Sorted(maps.Values(m)): [1 2 3]

  Метод All() коллекции возвращает iter.Seq2, и по нему
  ходят обычным for ... range с двумя переменными:

      груша    3
      слива    2
      яблоко   1

  slices.Collect собирает последовательность обратно в срез:
      slices.Collect((set{m}).All() по ключам) -> [груша слива яблоко]
      slices.Collect(slices.Values([]int{4,1,3})) -> [4 1 3]

  slices.All даёт пары «индекс, значение» для среза:
      0 a
      1 b
      2 c

  Ничего нового в язык для этого не добавляли: iter.Seq —
  это имя для типа функции, а не новая сущность. Поэтому
  свой итератор ничем не отличается от библиотечного и
  работает с теми же slices.Collect и slices.Sorted.