Где живёт переменная: блок
Пока в программе была одна 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: keepkeep объявлена в строке 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.gopackage 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 2boxes в 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.Cut2. Наберите ту же программу в
scratch/main.go:✎ Наберите в файлscratch/main.gopackage 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.txt2. Напишите
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