Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 21 из 25 ~18 мин +50 XP

Файлы и диалоги выбора

Девятнадцатый урок закончил обещанием: всё, что больше пары килобайт, — территория файлов. Пора этот долг вернуть. Запрос «fyne файлы» — это хранилище приложения myApp.Storage(): закрытый каталог RootURI, выделенный системе по идентификатору из каркаса второго урока, Create и Open для собственных документов, List и Remove для учёта. Адрес файла здесь — не путь, а fyne.URI со схемой file://, и эта абстракция окупится на мобильных платформах двадцать второго урока, где привычных путей не видно вовсе. Вторая половина урока — диалоги выбора: dialog.NewFileOpen и dialog.NewFileSave с фильтрами по расширению, стартовым каталогом и честным nil при отмене. Канон двадцатого урока работает и здесь: колбэки диалогов доставляет в главный поток сам тулкит, а чтение большого файла ты уносишь в горутину и возвращаешься через fyne.Do — окно остаётся живым.

Содержание урока · 11 разделов

Настройки кончились — начинаются файлы#

Хранилище настроек из девятнадцатого урока рассчитано на десятки мелких значений четырёх базовых типов: тема, громкость, счётчик запусков. Как только данные перестают быть мелкими — черновик редактора, отчёт для пользователя, журнал действий, скачанная картинка — нужен файл. Теоретически можно взять os.ReadFile из стандартной библиотеки, но путь к нему не выбрать: каталог, куда приложению разрешено писать, выделяет операционная система, и на каждой платформе он свой, а на мобильных путь вообще не существует как строка. Fyne решает это встроенным хранилищем, и запрос «fyne файлы» — это именно оно: myApp.Storage() возвращает fyne.Storage — интерфейс документов приложения с четырьмя рабочими методами. Create(name) создаёт файл в каталоге приложения и отдаёт fyne.URIWriteCloser для записи; Open(name) открывает существующий как fyne.URIReadCloser; List() перечисляет имена сохранённого; Remove(name) стирает файл по имени. Запрос «fyne storage» приводит к тому же интерфейсу. Имена — относительные, без слэшей и точек-родителей: хранилище само разыменует их внутри своего корня. Отличие от ресурсов шестнадцатого урока принципиальное: ресурсы вкомпилированы в бинарник и неизменяемы, а файлы живут на диске, меняются в любой момент и переживают перезапуск так же надёжно, как preferences, — только объём у них любой.

Черновик, который переживает перезапуск

GO
package main

import (
	"io"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.NewWithID("io.gomaster.notes")
	window := myApp.NewWindow("Черновик")

	editor := widget.NewMultiLineEntry()
	editor.SetPlaceHolder("Текст сохраняется в хранилище приложения")

	if r, err := myApp.Storage().Open("draft.txt"); err == nil {
		data, _ := io.ReadAll(r)
		r.Close()
		editor.SetText(string(data))
	}

	save := widget.NewButton("Сохранить черновик", func() {
		w, err := myApp.Storage().Create("draft.txt")
		if err != nil {
			return
		}
		w.Write([]byte(editor.Text))
		w.Close()
	})

	window.SetContent(container.NewVBox(editor, save))
	window.ShowAndRun()
}

При старте Open пытается открыть прошлый черновик: первый запуск не находит файла и получает ошибку — это нормальный путь, а не сбой, поэтому проверка err == nil и дальше пустой editor. Кнопка записи берёт writer у Create, кладёт байты и закрывает. Идентификатор из app.NewWithID решает, в каком каталоге всё окажется.

Что вернёт myApp.Storage().Open("no-such.txt") при первом запуске приложения?

URI: адрес вместо пути#

Вся файловая часть Fyne говорит на fyne.URI — интерфейсе-адресе с тремя методами. String() возвращает полную строку вида file:///home/user/.local/share/io.gomaster.notes/draft.txt; Scheme() — только схему, для файлов это file; Extension() — расширение вместе с точкой: «.txt», «.csv». Запрос «file:// uri» — это он и есть. Внутри хранилища хватает имён, но как только файл выходит наружу — в диалогах этого урока, в canvas.NewImageFromURI из шестнадцатого, в фильтрах, — повсюду ходят URI. Построить адрес можно двумя функциями пакета storage: NewFileURI(путь) оборачивает обычный путь файла вне хранилища, NewURI(строка) разбирает готовый адрес обратно. Зачем интерфейс вместо строки пути? Во-первых, мобильные платформы двадцать второго урока: там файл пользователя — дескриптор с URI, и настоящего пути ты не увидишь никогда; код, написанный против интерфейса, переезжает туда без единой правки. Во-вторых, самодокументирование: Scheme и Extension читаются программой, фильтр диалога сравнивает uri.Extension() со своим списком, отладочная печать String() показывает, где данные на самом деле. Правило гигиены отсюда: не склеивай пути конкатенацией и не храни абсолютный путь в preferences из девятнадцатого — он привязан к машине и идентификатору; храни имя файла или строку URI и открывай через хранилище.

Схема и расширение

Прочитайте код и выберите, что он выведет:

u := storage.NewFileURI("/tmp/report.csv")
fmt.Println(u.Scheme(), "|", u.Extension())

RootURI: закрытая территория приложения#

RootURI() возвращает URI каталога, который система выделила приложению по идентификатору: на Linux это внутри ~/.local/share, на macOS — Application Support, на Windows — %AppData%; те же места, где лежит preferences.json из девятнадцатого урока, только рядом. Каталог создаётся при первом обращении к хранилищу и принадлежит приложению: чужие программы в него не заглядывают, при удалении приложения система его сносит. Из этого два следствия. Первое — объём: пишите сколько нужно, кеш на сто мегабайт или черновик в три строки, хранилище не ограничивает; границы задаёт диск и здравый смысл, а не API. Второе — идентификатор решает всё: сменишь io.gomaster.notes на другое имя, и старые файлы окажутся в чужом каталоге — та же история, что с потерянными настройками в девятнадцатом, где именно из-за app.New() вместо app.NewWithID файл каждый раз оказывался новым. Перечень содержимого даёт List(): срез имён строками, по каждому работают Open и Remove. Для отладки полезно один раз напечатать myApp.Storage().RootURI().String() и глазами увидеть, где живут данные, — на десктопе этот каталог можно открыть в проводнике и посмотреть своими глазами.

Инвентаризация и очистка хранилища

GO
package main

import (
	"sort"
	"strings"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.NewWithID("io.gomaster.notes")
	window := myApp.NewWindow("Файлы приложения")

	label := widget.NewLabel("")
	refresh := func() {
		names := myApp.Storage().List()
		sort.Strings(names)
		if len(names) == 0 {
			label.SetText("Хранилище пусто")
		} else {
			label.SetText("Файлы:\n" + strings.Join(names, "\n"))
		}
	}
	refresh()

	clean := widget.NewButton("Удалить всё", func() {
		for _, name := range myApp.Storage().List() {
			myApp.Storage().Remove(name)
		}
		refresh()
	})

	window.SetContent(container.NewVBox(label, clean))
	window.ShowAndRun()
}

List возвращает имена строками, а не URI — сортируй и показывай как удобно; Remove стирает по имени и не спрашивает подтверждения, поэтому в настоящем приложении кнопку «Удалить всё» прячут за dialog.ShowConfirm из тринадцатого урока. Обновление подписи через локальную функцию refresh — то же правило одного источника правды, что и в списках десятого урока.

Что возвращает myApp.Storage().List()?

Диалоги выбора: решение за пользователем#

Хранилище приложения — твоя территория, но исходник лежит в загрузках пользователя, а отчёт уезжает в его документы. Для этого в пакете dialog из тринадцатого урока есть файловая пара: dialog.NewFileOpen(callback, window) и dialog.NewFileSave(callback, window); короткие обёртки ShowFileOpen и ShowFileSave делают то же без промежуточной переменной. Колбэк открытия получает пару (fyne.URIReadCloser, error): reader, из которого читай байты, и ошибку. Колбэк сохранения — (fyne.URIWriteCloser, error): writer для записи результата. Запрос «filedialog fyne» в поиске приводит именно к этой паре. Три факта, которые надо знать заранее. Первый: колбэк доставляет главный поток — тулкит позвал тебя из своего цикла, поэтому менять интерфейс можно напрямую, без fyne.Do из двадцатого урока. Второй: отмена — это nil и nil: пользователь закрыл диалог, ошибки нет, а reader nil; проверяй на nil всегда, иначе первая же отмена кончится паникой на Write. Третий: чтение большого файла внутри колбэка заморозит окно — уноси в горутину и возвращайся с готовой строкой через fyne.Do, как загрузчик из двадцатого.

Открыть текст и не заморозить окно

GO
package main

import (
	"io"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/dialog"
	"fyne.io/fyne/v2/storage"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.NewWithID("io.gomaster.notes")
	window := myApp.NewWindow("Импорт")

	status := widget.NewLabel("Файл не выбран")
	editor := widget.NewMultiLineEntry()

	open := widget.NewButton("Открыть текст", func() {
		fd := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) {
			if err != nil {
				dialog.ShowError(err, window)
				return
			}
			if r == nil {
				return // пользователь отменил выбор
			}
			label := r.Uri().String()
			go func() {
				data, readErr := io.ReadAll(r)
				r.Close()
				fyne.Do(func() {
					if readErr != nil {
						status.SetText("Ошибка чтения")
						return
					}
					editor.SetText(string(data))
					status.SetText("Загружен: " + label)
				})
			}()
		}, window)
		fd.SetFilter(storage.NewExtensionFileFilter([]string{".txt", ".md"}))
		fd.Resize(fyne.NewSize(600, 400))
		fd.Show()
	})

	window.SetContent(container.NewVBox(status, editor, open))
	window.ShowAndRun()
}

Диалог собирается с фильтром: покажет только .txt и .md — сравнение идёт с точкой по uri.Extension(). Проверки идут по порядку: err — ошибка выбора, nil — отмена. Дальше канон двадцатого урока в действии: чтение уезжает в горутину, а в главный поток возвращается один fyne.Do с готовой строкой. Адрес файла для подписи берётся у r.Uri() до горутины — читаемое значение зафиксировано, в замыкание уезжает готовая строка.

Где исполняется колбэк файлового диалога?

Фильтры, каталоги и выбор папки#

Фильтр задаётся SetFilter с любым fyne.FileFilter; стандартный storage.NewExtensionFileFilter принимает список расширений с точками и прячет всё остальное — для бытовых задач этого достаточно. Стартовый каталог — SetLocation(uri): сюда передают fyne.ListableURI, адрес каталога, у которого можно спрашивать содержимое методом List() ([]fyne.URI, error). Не всякий URI перечислим, поэтому перед передачей проверь storage.CanList и приведи тип: lu, ok := uri.(fyne.ListableURI). Откуда взять первый ListableURI? Из хранилища: root := myApp.Storage().RootURI() после проверки CanList приводится и диалог откроется прямо в территории приложения; запомни выбранный пользователем каталог как URI — в следующий раз SetLocation предложит то же место, и пользователь скажет спасибо. Отдельный сценарий — выбор папки целиком: dialog.NewFolderOpen(callback, window) с колбэком (fyne.ListableURI, error); получил каталог — перебирай List() и импортируй пачку файлов. Сохранению нужны свои настройки: SetFileName("otchet.csv") подсказывает имя, пользователь его поправит перед записью. И Resize(fyne.NewSize(640, 480)) — на экранах с крупным масштабом из пятнадцатого урока системный диалог по умолчанию бывает обидно маленьким.

Отмена выбора

Прочитайте код и выберите, что он выведет:

dialog.ShowFileSave(func(w fyne.URIWriteCloser, err error) {
	fmt.Println(w == nil, err == nil)
}, window)
// пользователь закрыл диалог кнопкой «Отмена»

Экспорт отчёта

GO
package main

import (
	"time"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/dialog"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.NewWithID("io.gomaster.notes")
	window := myApp.NewWindow("Экспорт")

	export := widget.NewButton("Экспорт отчёта", func() {
		fd := dialog.NewFileSave(func(w fyne.URIWriteCloser, err error) {
			if err != nil {
				dialog.ShowError(err, window)
				return
			}
			if w == nil {
				return // отмена
			}
			if _, writeErr := w.Write([]byte("все заметки, строка за строкой\n")); writeErr != nil {
				dialog.ShowError(writeErr, window)
				return
			}
			address := w.Uri().String()
			w.Close()
			dialog.ShowInformation("Готово", "Отчёт сохранён: "+address, window)
		}, window)
		fd.SetFileName("otchet-" + time.Now().Format("2006-01-02") + ".txt")
		fd.Show()
	})

	window.SetContent(container.NewVBox(export))
	window.ShowAndRun()
}

SetFileName подсказывает осмысленное имя с датой вместо пустого поля. Проверки по порядку: err, nil, ошибка записи — каждая ошибка показывается через ShowError из тринадцатого урока, успех — через ShowInformation. Адрес сохранённого файла достаётся у w.Uri() и попадает в отчёт пользователю: он видит не абстрактное «готово», а куда именно лёг файл.

Каталог файлового API#

APIЧто делаетКогда звать
myApp.Storage()получить хранилище приложенияодин раз при старте, дальше держи ссылку
Storage.RootURI()URI каталога приложениястартовый каталог диалогов, отладка
Storage.Create(name)создать файл, отдать fyne.URIWriteCloserсохранение данных приложения
Storage.Open(name)открыть файл, отдать fyne.URIReadCloserзагрузка при старте с проверкой err
Storage.List()срез имён файлов строкамиэкран «мои документы», автосохранение
Storage.Remove(name)удалить файл по имениочистка, сброс данных
storage.NewFileURI(путь)обернуть внешний путь в fyne.URIфайлы вне хранилища
storage.NewURI(строка)разобрать готовый адресURI хранится как строка в настройках
URI.Scheme() / Extension() / String()схема, расширение с точкой, полный адресфильтры, ветвления, отладка
dialog.NewFileOpen / ShowFileOpenдиалог открытия файлаимпорт данных пользователя
dialog.NewFileSave / ShowFileSaveдиалог сохранения файлаэкспорт отчётов
dialog.NewFolderOpenдиалог выбора папкипакетный импорт каталога
SetFilter + storage.NewExtensionFileFilterограничить расширения«только .txt и .md»
SetLocation / SetFileName / Resizeнастройка диалогастартовый каталог, имя, размер окна

Собери сохранение и чтение

Восстанови пять пропусков: хранилище, создание файла, освобождение writer, диалог открытия и его фильтр.

st := myApp.() w, err := st.("cache.bin") if err == nil { w.Write(data) w.() } fd := dialog.(func(r fyne.URIReadCloser, err error) { if r != nil { io.ReadAll(r) r.Close() } }, window) fd.SetFilter(storage.([]string{".bin"})) fd.Show()

Путь экспорта от кнопки до диска

Расставь по порядку, что происходит, когда пользователь экспортирует отчёт из приложения.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Практика: что хранить файлами#

Черновики и документы: текст редактора больше пары килобайт — файл draft.txt, а не ключ в preferences; автосохранение по таймеру — готовый тикер из двадцатого урока, интервал — Int-ключ в настройках. Экспорт и импорт: отчёты пользователя через ShowFileSave с SetFileName, входные данные через ShowFileOpen с фильтром по нужным расширениям. Кеш изображений: выбранную в диалоге картинку можно сразу показать — canvas.NewImageFromURI из шестнадцатого урока принимает тот же адрес, а тяжёлую обработку уноси в горутину по канону двадцатого. Журналы: один файл-история вместо сотен мелких файлов — иначе List() превратится в свалку, в которой ничего не найти. Что хранить файлами НЕ нужно: настройки — их место в preferences из девятнадцатого; иконки и шрифты, вкомпилированные при сборке, — их место в ресурсах шестнадцатого; общие документы пользователя — их показывают диалогами и читают по URI, а не копируют к себе. Именуй файлы по делу: draft.txt, export-2024-05-01.csv, cache/avatars — имена это твой интерфейс к List(). И помни про совместимость: формат файла меняется — старую версию читай терпеливо и конвертируй при загрузке, данные пользователя дороже красоты кода.

Что дальше: телефон в кармане#

Теперь приложение владеет данными: черновик живёт в RootURI, экспорт уезжает через диалог с фильтром, импорт читается в фоне без заморозки окна. Опора урока — девятнадцатый с границей «настройки против данных» и StoragePath, двадцатый с каноном «колбэки — в главном потоке, чтение — в горутину, возврат — через fyne.Do», тринадцатый с семейством dialog и диалогами ошибок, шестнадцатый с canvas.NewImageFromURI, девятый с многострочным полем. Следующая тема — сборка для мобильных: тот же код, fyne package -os android, палец вместо мыши, и файловый диалог из этого урока превращается в системный пикер. Полный маршрут — на странице курса Fyne.

Термины урока

Термин

fyne.Storage

нажмите, чтобы перевернуть
Определение

Хранилище документов приложения из myApp.Storage(): Create, Open, List и Remove внутри закрытого каталога RootURI, выделенного системе по идентификатору.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Виртуальный экран

HTML-модель виджет-дерева fyne
Файлы и диалогиfyne · мок

draft.txt

app.Storage() → fyne.Storage

Open/Create возвращают URIReadCloser и URIWriteCloser

Журнал событий

Экран собран: «Файлы и диалоги»

Коротко: что запомнить

  • myApp.Storage() — хранилище документов приложения: Create и Open с проверкой err, List с именами строками, Remove по имени.
  • Файлы начинаются там, где preferences заканчиваются: черновики, отчёты, кеш и журналы — данные, а не настройки.
  • Всюду fyne.URI: String() даёт адрес, Scheme() — file, Extension() — расширение с точкой; внешние пути оборачивает storage.NewFileURI.
  • RootURI() — закрытый каталог по идентификатору: объём не ограничен, смена ID означает другой каталог.
  • Файловые диалоги живут в пакете dialog: NewFileOpen и NewFileSave, фильтр NewExtensionFileFilter, SetFileName, SetLocation, Resize.
  • Колбэк диалога приходит в главный поток; отмена — (nil, nil): сначала err, потом nil, и только потом работа с файлом.
  • Большой файл читай в горутине и возвращайся через fyne.Do — канон двадцатого урока; за Open и Create всегда следует Close.

Домашнее задание

Собери «Мой документы» целиком. Блокнот с многострочным Entry из девятого урока, черновик которого живёт в draft.txt через Storage().Create и Open с проверкой err, автосохранение — тикер из двадцатого урока раз в минуту. Добавь импорт: ShowFileOpen с фильтром «.txt» и «.md», чтение файла в горутине и возврат через fyne.Do, подпись со строкой r.Uri(). Добавь экспорт: ShowFileSave с SetFileName вида «notes-дата.txt», запись с проверками err, nil и ошибки записи, отчёт об успехе через ShowInformation из тринадцатого урока. Внизу — экран файлов: List(), отсортированные имена в List из десятого урока и кнопка удаления выбранного через Remove, спрятанная за ShowConfirm. Стартовый каталог диалога задай SetLocation из RootURI после проверки storage.CanList. Ловушка для самопроверки: отмени диалог посреди экспорта и убедись, что приложение не упало; попробуй сохранить путь к файлу в настройку, сменить идентификатор приложения и понять, почему путь умер, а имя и строка URI — живы.

Личные заметкиПусто
Хранятся только в вашем браузере (Fyne · Файлы и диалоги выбора)

Вопросы и ответы по уроку

Можно ли работать с файлами напрямую через os и filepath?

Технически да, но путь приложения не выбираешь: каталог выделяет система, и на мобильных платформах пути как строки вообще нет — есть URI. Хранилище абстрагирует это: один код работает и на десктопе, и в изолированном каталоге телефона. os.* оставь для системных задач вне данных приложения.

Почему после отмены диалога колбэк всё равно вызывается?

Так устроен контракт: колбэк — единственная точка продолжения, и отмена обязана через неё пройти. Поэтому отмена приходит как (nil, nil), а не как отсутствие вызова. Всегда проверяй сначала err, затем reader или writer на nil — и только потом работай с файлом.

Где физически лежат файлы, сохранённые через хранилище?

В каталоге приложения, построенном из идентификатора: RootURI().String() напечатает точное место. Путь зависит от платформы, поэтому не хардкодь его — адресуй файлы именами через Create и Open, а полный адрес получай от URI в рантайме.

Термины урока

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

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Cobra25 уроков~10 чпрактика25 уроков уже доступны

Cobra: CLI-приложения на Go

Интерактивный курс Cobra — фреймворка для CLI на Go: команды, флаги, конфиги Viper, автодополнение. 25 уроков с примерами кода, тестами и итоговым приложением.

  • Создавать CLI-приложения с деревом команд на Cobra
  • Работать с аргументами, флагами и их привязкой к конфигу
  • Управлять конфигурацией через Viper: файлы, env, приоритеты
  • Писать хуки PreRun/PostRun и корректно обрабатывать ошибки