День 12 · Несколько результатов и ошибка как результат · страница 2 из 7

Где живёт переменная: блок

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


Правило одной фразы

Имя видно от строки, где оно объявлено, до закрывающей } того блока, в котором объявлено.

Блок — это всё, что заключено в фигурные скобки: тело функции, тело if, тело for. Вы уже встречали это правило дважды: в дне 06 — с переменной внутри if, в дне 08 — с переменной цикла. Сегодня оно же объясняет, почему функции не видят переменных друг друга.

Код для чтения · разбираем, набирать не нужно
package main

import "fmt"

func main() {
	stock := 40
	if stock > 10 {
		keep := stock / 4
		fmt.Println("страховой запас:", keep)
	}
	fmt.Println("запас был:", keep)
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day12/scratch$ go run .
# day12/scratch
./main.go:11:36: undefined: keep

keep объявлена в строке 8, внутри блока if. Её блок кончается закрывающей } в строке 10, и в строке 11 имени keep уже не существует. Колонка 36 — в байтах: кириллица занимает по два, как в дне 01.

Обратное неверно: изнутри блока видно всё, что объявлено снаружи, — keep спокойно считается из stock. Вложенный блок видит внешние имена, внешний блок внутренние — нет.


Функция не видит чужих переменных

Ситуация. В main есть stock, в функции — параметр qty. Кажется, раз они в одном файле, то и видны друг другу.

Код для чтения · разбираем, набирать не нужно
package main

import "fmt"

func pack(qty int, size int) (int, int) {
	boxes := qty / size
	return boxes, qty % size
}

func main() {
	fmt.Println(pack(17, 5))
	fmt.Println(qty, boxes)
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day12/scratch$ go run .
# day12/scratch
./main.go:12:14: undefined: qty
./main.go:12:19: undefined: boxes

Параметр qty и локальная переменная boxes живут в теле pack и исчезают, как только функция закончилась. Для main их не существует — не «они пустые», а именно нет таких имён.

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

Образец · набирать не нужно
        что приходит                      что уходит
            ↓                                 ↓
   func pack(qty int, size int)          (int, int)

Поэтому вопрос «как передать данные в функцию» имеет ровно один ответ — аргументом, а «как забрать» — результатом. Сегодня результатов может быть несколько, и этого хватает почти всегда.


Попробуйте сейчас: что доживёт до печати.

Цель: предсказать число, которое напечатает программа, и сдать ответ.

1. Прочитайте программу. Не запуская, ответьте себе: что лежит в keep после блока if и существует ли там bonus.

Код для чтения · разбираем, набирать не нужно
package main

import "fmt"

func keepStock(stock int) int {
	keep := stock / 4
	return keep
}

func main() {
	stock := 40
	keep := 0
	if stock > 10 {
		keep = keepStock(stock)
		bonus := 5
		keep = keep + bonus
	}
	fmt.Println(keep)
}

Обратите внимание: внутри if стоит keep =, а не keep :=. Почему это важно — на странице 5.

2. Сдайте число:

▶ Выполните · выделенное замените своим
course answer day12.q2 ЧИСЛО

3. Проверьте запуском — наберите ту же программу в scratch/main.go:

✎ Наберите в файл scratch/main.go
package main

import "fmt"

func keepStock(stock int) int {
	keep := stock / 4
	return keep
}

func main() {
	stock := 40
	keep := 0
	if stock > 10 {
		keep = keepStock(stock)
		bonus := 5
		keep = keep + bonus
	}
	fmt.Println(keep)
}
▶ Выполните
cd ~/gocourse/day12/scratch
go run .

4. Потом допишите в печать bonus: fmt.Println(keep, bonus). Должно получиться:

Вывод · это печатает программа, набирать не нужно
# day12/scratch
./main.go:18:20: undefined: bonus

Готово, когда: пункт q2 в course check 12 зелёный, а bonus в печати дал undefined.


Одно имя в двух функциях — это две переменные

В pack есть boxes, и в main можно объявить свою boxes. Это разные ячейки: совпадение имён ничего не связывает.

Код для чтения · разбираем, набирать не нужно
package main

import "fmt"

func pack(qty int, size int) (int, int) {
	boxes := qty / size
	boxes = boxes + 100
	return boxes, qty % size
}

func main() {
	boxes := 0
	full, rest := pack(17, 5)
	fmt.Println(boxes, full, rest)
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day12/scratch$ go run .
0 103 2

boxes в main осталась нулём, хотя внутри pack переменная с тем же именем доросла до 103. Ячейки разные, имена совпали случайно.

Отсюда практический вывод: имена переменных внутри функции — её личное дело. Переименовали boxes в count — на вызывающих это никак не повлияет, менять ничего не нужно. А вот порядок и типы результатов менять больно: их видят все, кто вызывает.


Три результата: strings.Cut

Готовые функции тоже бывают с несколькими результатами. В дне 04 была strings.CutPrefix с двумя; её родственница strings.Cut разрезает строку по разделителю и возвращает три значения — они пригодятся в сегодняшнем задании.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day12/scratch$ go doc strings.Cut
package strings // import "strings"

func Cut(s, sep string) (before, after string, found bool)
    Cut slices s around the first instance of sep, returning the text before and
    after sep. The found result reports whether sep appears in s. If sep does
    not appear in s, cut returns s, "", false.

В заголовке из go doc у результатов есть имена — before, after, found. Это подсказка читателю: что до разделителя, что после и нашёлся ли разделитель вообще.

Код для чтения · разбираем, набирать не нужно
package main

import (
	"fmt"
	"strings"
)

func main() {
	code, qty, ok := strings.Cut("TK-4821:17", ":")
	fmt.Println(code, qty, ok)
	code, qty, ok = strings.Cut("TK-4821", ":")
	fmt.Println("["+code+"]", "["+qty+"]", ok)
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day12/scratch$ go run .
TK-4821 17 true
[TK-4821] [] false

Вторая строка показывает, что бывает, когда разделителя нет: первая часть — вся строка целиком, вторая — пустая, found — false. Если не смотреть на третий результат, программа спокойно пойдёт дальше с пустой строкой вместо количества.

Резать strings.Cut умеет по первому вхождению: в строке A:B:C разделится на A и B:C.


Попробуйте сейчас: три результата у strings.Cut.

Цель: прочитать описание strings.Cut в go doc и увидеть своими глазами, что она отдаёт на строке без разделителя.

1. Прочитайте описание:

▶ Выполните
go doc strings.Cut

2. Наберите ту же программу в scratch/main.go:

✎ Наберите в файл scratch/main.go
package main

import (
	"fmt"
	"strings"
)

func main() {
	code, qty, ok := strings.Cut("TK-4821:17", ":")
	fmt.Println(code, qty, ok)
	code, qty, ok = strings.Cut("TK-4821", ":")
	fmt.Println("["+code+"]", "["+qty+"]", ok)
}

Запустите:

▶ Выполните
cd ~/gocourse/day12/scratch
go run .

3. Поменяйте вторую строку на strings.Cut(" TK-1 : 12 ", ":") и запустите ещё раз. Посмотрите на квадратные скобки: пробелы остались внутри частей, Cut их не убирает — это работа strings.TrimSpace.

Готово, когда: вы видели обе строки вывода и знаете, что вернёт Cut, если разделителя в строке нет; пункт t_godoc в course check 12 зелёный.


Что может пойти не так

Что видите Что это значит Что делать
undefined: keep на строке после } переменная объявлена внутри блока if или for объявить до блока, внутри — =
undefined: qty в main qty — параметр функции, снаружи его нет передать значение аргументом или вернуть результатом
undefined: boxes в main локальная переменная функции наружу не видна вернуть её результатом
declared and not used: keep переменную объявили в блоке и в нём же ни разу не прочитали использовать в блоке или объявить снаружи
функция «не видит» изменений переменной из main ничего не видит: она получила копию аргумента (день 11) вернуть новое значение результатом
в main и в функции переменные с одним именем ведут себя по-разному это две разные ячейки нормально; связывать их не нужно

Попробуйте сейчас: задание split — разбор строк заказа.

Цель: split/main.go читает строки заказа и печатает по строке на позицию и итог.

1. Прочитайте условие — вместимость коробки в нём ваша:

▶ Выполните
cd ~/gocourse/day12/split
cat TASK.txt

2. Напишите main.go. План: прочитать строку, убрать пробелы по краям, разрезать strings.Cut по двоеточию, превратить количество в число через strconv.Atoi, посчитать коробки и остаток своей функцией pack с двумя результатами, напечатать. Пустые строки пропускаются — continue, как в дне 08.

3. Проверьте на примере из условия и на пустом вводе:

▶ Выполните
go run . < example.txt
go run . < /dev/null

На пустом вводе должна быть одна строка:

Вывод · это печатает программа, набирать не нужно
Позиций не было

4. Отформатируйте код:

▶ Выполните
gofmt -w .

5. Задание готово — закоммитьте:

▶ Выполните
cd ~/gocourse/day12
git add .
git commit -m "split: разбор строк заказа"

Готово, когда: вывод на example.txt совпал с примером в TASK.txt, на пустом вводе — Позиций не было, gofmt -l . ничего не печатает; пункт split в course check 12 зелёный. Пункт code_fmt смотрит и split, и intake со страницы 4 — зелёным он станет после обоих заданий.


Дальше: ошибка как результат своей функции — course next