День 19 · Строки, руны и файлы · страница 5 из 6

Файл по имени

До сих пор ввод приходил от терминала: < input.txt или конвейер. Выгрузка же лежит файлом с известным именем, и программа открывает его сама — целиком или строка за строкой. У каждого способа своя ловушка.


os.ReadFile: весь файл сразу

Ситуация. В каталоге лежит orders.csv, программа должна пройти его строки.

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

import (
	"fmt"
	"os"
	"strings"
)

func main() {
	data, err := os.ReadFile("orders.csv")
	if err != nil {
		fmt.Fprintln(os.Stderr, "ошибка:", err)
		os.Exit(1)
	}
	lines := strings.Split(string(data), "\n")
	fmt.Println("строк:", len(lines))
	for i, line := range lines {
		fmt.Printf("%d: [%s]\n", i+1, line)
	}
}

Как это читать. os.ReadFile("orders.csv") возвращает содержимое файла и ошибку. Содержимое — []byte, срез байтов со страницы 1; string(data) превращает его в строку. Дальше strings.Split режет строку по переводу строки "\n" — получается срез строк файла.

Имя без каталога — orders.csv — значит «файл в текущем каталоге»: в том, где стоит терминал, когда запускают программу.


Попробуйте сейчас: что выведет Split файла.

Цель: предсказать, сколько частей даст Split для файла из трёх строк, и сдать ответ.

1. Создайте файл из трёх строк; каждая, и последняя тоже, кончается \n — так сохраняют файлы редакторы:

▶ Выполните · выделенное замените своим
cd ~/gocourse/day19/scratch
printf 'TK-101;Болт М8;12;1500\nTK-102;Шайба;100;90\nTK-103;Гайка М10;40;350\n' > orders.csv
wc -l orders.csv

wc -l считает переводы строки и напечатает 3 orders.csv.

2. Не запуская программу, ответьте: какое число она напечатает после строк:?

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

3. Наберите программу из раздела выше и запустите:

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

import (
	"fmt"
	"os"
	"strings"
)

func main() {
	data, err := os.ReadFile("orders.csv")
	if err != nil {
		fmt.Fprintln(os.Stderr, "ошибка:", err)
		os.Exit(1)
	}
	lines := strings.Split(string(data), "\n")
	fmt.Println("строк:", len(lines))
	for i, line := range lines {
		fmt.Printf("%d: [%s]\n", i+1, line)
	}
}
▶ Выполните
go run .

Готово, когда: course answer ответил Принято: day19.q4 = … (пункт проверки q4), программа запущена.


Что увидите.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day19/scratch$ go run .
строк: 4
1: [TK-101;Болт М8;12;1500]
2: [TK-102;Шайба;100;90]
3: [TK-103;Гайка М10;40;350]
4: []

Как это читать. Split режет по каждому "\n". Последний перевод строки стоит в самом конце файла, и после него остаётся пустой кусок — четвёртая «строка», которой в файле нет. Программа отчёта сочла бы её битой: «строка 4: пустая строка».

Перевод строки в конце файла — не пустая строка. Его убирают до Split:

Код для чтения · разбираем, набирать не нужно
	text := strings.TrimSuffix(string(data), "\n")
	lines := strings.Split(text, "\n")

strings.TrimSuffix(s, "\n") убирает один "\n" в конце, если он есть. Пустая строка в середине файла остаётся — она настоящая.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day19/scratch$ go run .
строк: 3
1: [TK-101;Болт М8;12;1500]
2: [TK-102;Шайба;100;90]
3: [TK-103;Гайка М10;40;350]

bufio.Scanner по файлу

Scanner из дня 07 читал строки из os.Stdin. Файл для него то же самое: его открывают os.Open и отдают Scanner вместо os.Stdin.

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

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	f, err := os.Open("orders.csv")
	if err != nil {
		fmt.Fprintln(os.Stderr, "ошибка:", err)
		os.Exit(1)
	}
	scanner := bufio.NewScanner(f)
	n := 0
	for scanner.Scan() {
		n++
		fmt.Printf("%d: [%s]\n", n, scanner.Text())
	}
	f.Close()
	fmt.Println("строк:", n)
}

Что увидите.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day19/scratch$ go run .
1: [TK-101;Болт М8;12;1500]
2: [TK-102;Шайба;100;90]
3: [TK-103;Гайка М10;40;350]
строк: 3

Как это читать. os.Open не читает файл, а открывает и отдаёт f. Scanner берёт из него строку за строкой и сам знает, что перевод строки в конце — не новая строка. Номер строки считаем сами, счётчиком n.

f.Close() закрывает файл: программа, которая открывает тысячи файлов и не закрывает, однажды получит отказ. Закрывать нужно на каждом пути выхода; удобнее это делает defer — блок 6.

os.ReadFile + Split os.Open + Scanner + Close
как читает весь файл в память сразу по строке
перевод строки в конце нужен TrimSuffix учитывает сам
номер строки i+1 из range свой счётчик
когда хуже файл на гигабайты нужно помнить про Close

Для выгрузок этого дня годятся оба.


Текущий каталог

Та же программа, запущенная из каталога выше:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day19/scratch$ cd ~/gocourse/day19
stagiaire@lab:~/gocourse/day19$ go run ./scratch
ошибка: open orders.csv: no such file or directory
exit status 1

go run ./scratch собрал программу из scratch, а запустил в текущем каталоге — ~/gocourse/day19, где orders.csv нет. Файл ищется там, где стоит терминал. После ошибка: — ошибка os.Open целиком: что делали, с каким файлом и что не так.


Попробуйте сейчас: файл через Scanner.

Цель: прочитать тот же файл вторым способом и увидеть ошибку открытия.

1. Наберите программу из раздела «bufio.Scanner по файлу»:

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

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	f, err := os.Open("orders.csv")
	if err != nil {
		fmt.Fprintln(os.Stderr, "ошибка:", err)
		os.Exit(1)
	}
	scanner := bufio.NewScanner(f)
	n := 0
	for scanner.Scan() {
		n++
		fmt.Printf("%d: [%s]\n", n, scanner.Text())
	}
	f.Close()
	fmt.Println("строк:", n)
}
▶ Выполните
cd ~/gocourse/day19/scratch
go run .
cd ~/gocourse/day19
go run ./scratch

Готово, когда: в scratch программа напечатала три строки и строк: 3, а из ~/gocourse/day19 — ошибка: open orders.csv: no such file or directory. Проверка дня этот опыт не сверяет.


Попробуйте сейчас: починка fix2.

Цель: fix2 не считает конец файла битой строкой, а настоящие битые строки по-прежнему находит.

1. Посмотрите на файл и запустите программу:

▶ Выполните
cd ~/gocourse/day19/fix2
cat stock.txt
go run .
echo $?

Что должно получиться:

Вывод · это печатает программа, набирать не нужно
Болт М8;120
Шайба;450
Гайка М10;75
Bolt-M8;30
Болт М8: 120 шт.
Шайба: 450 шт.
Гайка М10: 75 шт.
Bolt-M8: 30 шт.
строка 5: нужно 2 поля, а их 1
exit status 1
1

В файле четыре строки, а программа жалуется на пятую.

2. Почините — до трёх изменённых строк. Пустая строка в середине файла по-прежнему битая: проверка подаст и такой файл.

▶ Выполните
go run .
echo $?
diff ~/.course/orig/day19/fix2/main.go main.go

3. Закоммитьте:

▶ Выполните
cd ~/gocourse/day19
git add .
git commit -m "fix2: конец файла не пустая строка"

Готово, когда: четыре строки остатков, поток ошибок пуст, echo $? печатает 0 (пункты fix2 и fixsmall).


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

Что видите Что это значит Что делать
open orders.csv: no such file or directory в текущем каталоге файла нет: программа запущена не там или в имени опечатка cd в каталог с файлом; ls покажет, что там лежит
open orders.csv: permission denied файл есть, но читать его этому пользователю нельзя ls -l orders.csv покажет права
cannot use data (variable of type []byte) as string value in argument to strings.Split ReadFile отдаёт байты, а Split ждёт строку strings.Split(string(data), "\n")
declared and not used: f файл открыли, но не читали и не закрыли bufio.NewScanner(f) и f.Close()
лишняя битая строка с номером за концом файла Split после последнего перевода строки дал пустой кусок strings.TrimSuffix(string(data), "\n") до Split
пустая строка в середине файла не замечена пустые строки пропускаются все подряд пропускать только кусок после последнего перевода строки
exit status 1 под выводом go run сообщает код возврата программы ничего: при ошибках так и должно быть

Дальше: отчёт по выгрузке целиком — course next