---
title: "Хранение настроек (preferences)"
description: "Настройки Fyne: fyne.Preferences с SetString, SetFloat и SetBool, значения WithFallback, StoragePath и preferences.json, загрузка при старте."
course: "Fyne: десктопные и мобильные GUI на Go"
url: "https://golangmaster.ru/courses/fyne/lessons/19-preferences/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# Хранение настроек (preferences)

*Курс «Fyne: десктопные и мобильные GUI на Go», урок 19 из 25. ~16 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/fyne/lessons/19-preferences/*

Восемнадцатый урок поставил фильтр на вход данных, но до сих пор всё, что приложение знало о пользователе, умирало вместе с окном: источники семнадцатого урока жили в памяти, и перезапуск обнулял счётчики, темы и черновики. Запрос «fyne preferences» — это fyne.Preferences из myApp.Preferences(): простое хранилище ключ-значение с SetString, SetFloat, SetBool и SetInt, вариантами WithFallback для честных значений по умолчанию и записью на диск без единой строчки кода работы с файлами. Русский запрос «настройки fyne» ведёт сюда же. Урок собирает полный цикл: сохранить, прочитать при старте со значением по умолчанию, услышать изменение слушателем и подключить хранилище к привязкам — так выбор темы из пятнадцатого урока переживёт закрытие окна, а экран настроек из полей восемнадцатого станет постоянным.

### Настройки переживают перезапуск

Приложение, которое каждый раз начинает с нуля, злит тихо, но надёжно: пользователь второй раз выбирает ту же тему, второй раз вводит имя, второй раз двигает громкость. Причина проста — все значения жили в памяти: источники семнадцатого урока умирают с процессом, и на диск не попадает ничего. Fyne решает это встроенным хранилищем: у каждого приложения есть fyne.Preferences, доступный как myApp.Preferences(). Запрос «fyne preferences» — это интерфейс с четырьмя парами для базовых типов: SetString(key, value) и String(key), SetBool(key, v) и Bool(key), SetFloat(key, v) и Float(key), SetInt(key, v) и Int(key). Плюс RemoveValue(key), который стирает ключ — на следующий запуск его будто никогда и не было. Хранилище ключ-значение: ключ — строка, значение — один из четырёх типов; никаких списков и структур, сложное сериализуй сам — об этом в разделе про практику. Главная ценность в том, что запись и чтение выглядят как работа со словарём, а сохранение на диск Fyne делает сам: ни путей, ни файлов, ни форматов. Русский запрос «fyne настройки» приводит в то же место. Вторая ценность — предсказуемость чтения: пустое хранилище возвращает пустые значения без ошибок и паник, а для честных значений по умолчанию есть семейство WithFallback, которое сейчас покажем в коде. Осталось собрать первый пример: приветствие, которое помнит имя.

### Имя, которое запомнили

```go
package main

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

func main() {
	myApp := app.NewWithID("io.gomaster.notepad")
	window := myApp.NewWindow("Блокнот")

	prefs := myApp.Preferences()
	name := prefs.StringWithFallback("user.name", "Гость")

	greeting := widget.NewLabel("С возвращением, " + name)

	field := widget.NewEntry()
	field.SetPlaceHolder("Как к вам обращаться?")
	field.OnSubmitted = func(s string) {
		prefs.SetString("user.name", s)
		greeting.SetText("Запомнил: " + s)
	}

	window.SetContent(container.NewVBox(greeting, field))
	window.ShowAndRun()
}
```

Приложение создаётся с идентификатором — app.NewWithID из каркаса второго урока; на нём держится вся персистентность. При старте имя читается с запасным значением: первый запуск покажет «Гость», а после SetString каждый следующий — сохранённое имя. Ни одного импорта файловых пакетов: хранение настроек — работа хранилища.

**Вопрос:** Что вернёт prefs.String("never.set"), если ключ никогда не записывали?

- Ошибку «key not found» — чтение отсутствующего ключа это сбой
- ✅ Пустую строку — без ошибки и без паники
- Панику: чтение до первой записи запрещено
- nil — и код обязан проверить на nil перед использованием

Чтение отсутствующего ключа — штатный случай первого запуска: String вернёт пустую строку, Bool — false, Float и Int — нули, всё без ошибок. Когда пустое значение неудобно, бери StringWithFallback и братьев — они вернут запасное значение, если ключа нет.

### Четыре типа и WithFallback

Семейство WithFallback — главный инструмент загрузки при старте: StringWithFallback(key, fallback) вернёт сохранённое значение, а если его нет — запасное. Аналогично устроены BoolWithFallback(key, def), FloatWithFallback(key, def) и IntWithFallback(key, def). Семантика строгая: запасное значение подставляется только при отсутствии ключа; записанный false или ноль вернутся как есть. Отсюда правило выбора: для значений, где пустота и ноль — легитимное состояние, обычных String и Bool достаточно; для значений, где нужна разумная стартовая точка — громкость 0.5, тема «тёмная», язык «ru» — бери WithFallback и не пиши if-ы. Ключи именуй как пути в конфиге: «user.name», «theme.dark», «audio.volume» — группировка по префиксу спасает, когда ключей становится пара десятков; строковые константы для ключей собери в одном месте и не повторяй литералы по коду. Четыре типа — целые, дробные, булевы и строки — покрывают настройки как класс, но не данные: список задач или структуру профиля в preferences не положишь. Сериализуй в JSON-строку и храни её одним ключом — приемлемо для небольших блобов; всё, что больше пары килобайт, — территория файлов и хранилища приложения, это двадцать первый урок. И помни объём: preferences — про десятки мелких значений, читаемых быстро, а не про базу данных.

### Два запуска, один язык

```go
prefs := myApp.Preferences()
lang := prefs.StringWithFallback("lang", "ru")
fmt.Println("язык:", lang)

prefs.SetString("lang", "en")
// приложение закрыли и запустили снова
// что напечатается при втором запуске?
```

**Что выведет программа?**

- «язык: ru» — SetString живёт только до закрытия окна
- ✅ «язык: en» — значение записано в хранилище и пережило перезапуск
- Ничего: второй запуск упадёт на чтении незнакомого ключа
- «язык: ru» и «язык: en» — обе строки, потому что хранятся оба значения

Первый запуск напечатал «ru» — сработал запасной вариант, ключа ещё не было. SetString записал «en» в хранилище на диске, и второй запуск прочитал уже сохранённое значение: WithFallback вернул бы запасное только при отсутствии ключа. В этом весь цикл сохранение-загрузка.

### Где живут настройки: StoragePath и идентификатор

Настройки пишутся в JSON-файл в каталоге пользователя, и путь определяется идентификатором приложения. Поэтому каркас начинается с app.NewWithID("io.gomaster.notepad") — по convention идентификатор пишется в обратном доменном стиле, как пакет для сторов. Если создать приложение через app.New() без идентификатора, Fyne выдаст ему случайный на каждый запуск: значения будут работать в памяти, но файл настроек каждый раз окажется новым, и всё написанное утонет. Это самый частый вопрос новичков — «почему настройки не сохраняются» — и самый частый ответ: добавь идентификатор. Каталог зависит от платформы и берётся из правил системы: на Linux это внутри ~/.config, на macOS — в ~/Library/Application Support, на Windows — в %AppData%; точное место подскажет метод StoragePath() хранилища. Внутри лежит preferences.json с твоими ключами — можно открыть и посмотреть, что именно сохранено, полезно при отладке. Запись на диск не мгновенная: Fyne копит изменения и сбрасывает файл с небольшой задержкой, поэтому не рассчитывай прочитать файл в ту же миллисекунду после Set — работай через API хранилища. На мобильных платформах из двадцать второго урока идентификатор обязателен вдвойне: там он становится именем пакета в сторе, а настройки живут в изолированном каталоге приложения. Один идентификатор — один набор настроек: смена ID обнулит видимые значения, это удобный способ получить «чистый профиль» для теста.

### Счётчик запусков в двух вариантах

```go
// вариант A
myApp := app.New()
// вариант B
myApp := app.NewWithID("io.gomaster.notes")

prefs := myApp.Preferences()
n := prefs.IntWithFallback("launches", 0)
prefs.SetInt("launches", n+1)
fmt.Println("запуск номер:", n+1)

// оба варианта закрывали и открывали по три раза
// что покажет каждый на третьем запуске?
```

**Что выведет программа?**

- Оба показывают «запуск номер: 3» — счётчик работает и без идентификатора
- ✅ Вариант A всегда «запуск номер: 1», вариант B честно растёт до трёх
- Вариант A растёт, вариант B всегда единица — WithFallback мешает SetInt
- Оба всегда показывают единицу — IntWithFallback затирает записанное

Без идентификатора каждый запуск получает новый случайный ID и новый пустой файл настроек: счётчик читает запасной ноль и пишет единицу — вечно. Вариант B с постоянным ID читает тот же файл и накапливает значения. WithFallback тут ни при чём: он отвечает только за чтение отсутствующего ключа.

**Вопрос:** Что обязательно для того, чтобы настройки пережили перезапуск на десктопе?

- Вызов prefs.Save() перед закрытием окна
- ✅ Создание приложения через app.NewWithID с постоянным идентификатором
- Хранение ключей в отдельном файле рядом с программой
- Ничего: Fyne сохраняет настройки автоматически даже без ID

Идентификатор определяет путь к файлу настроек; без него каждый запуск получает случайный ID и новый пустой файл. Метода Save у хранилища нет — запись Fyne выполняет сам, но адресовать файл умеет только постоянный ID.

### Слушатели изменений: хранилище тоже уведомляет

Хранилище умеет то же, что источники семнадцатого урока, — сообщать об изменениях. AddChangeListener(func()) вешает колбэк, который вызывается, когда значения меняются; RemoveChangeListener(listener) снимает подписку. Сигнатура обманчиво пустая — func() без аргументов: хранилище не говорит, какой именно ключ изменился, оно сообщает факт перемены. Что делать колбэку? Перечитать интересующие ключи и обновить интерфейс — дешёвые чтения из памяти, так что перечитать можно хоть всё. Такой слушатель — удобное место для глобальных реакций: тема сменилась — перекрась самописные примитивы из седьмого урока, ведь виджеты перекрасятся сами, а canvas-примитивы хранят цвет значением и требуют Refresh. Порядок и группировка не гарантированы: несколько быстрых Set могут прийти одним уведомлением или двумя — не строй логику на счёте вызовов, перечитывай состояние целиком. Правила жизни те же, что у DataListener: снимаешь ненужного, не создаёшь циклов — колбэк, который сам пишет в хранилище, обязан избегать бесконечной эхо-петли: изменяй условно или проверяй, что значение реально поменялось, прежде чем писать ответ. На живом примере видно, как одна кнопка меняет булев ключ, а слушатель перерисовывает подпись — интерфейс и данные снова не встретились напрямую.

### Слушатель хранилища в деле

```go
package main

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

func main() {
	myApp := app.NewWithID("io.gomaster.settings")
	window := myApp.NewWindow("Тема")

	prefs := myApp.Preferences()

	mode := widget.NewLabel("")
	update := func() {
		if prefs.BoolWithFallback("theme.dark", false) {
			mode.SetText("Тема: тёмная")
		} else {
			mode.SetText("Тема: светлая")
		}
	}
	update()
	prefs.AddChangeListener(update)

	toggle := widget.NewButton("Переключить", func() {
		prefs.SetBool("theme.dark", !prefs.BoolWithFallback("theme.dark", false))
	})

	window.SetContent(container.NewVBox(mode, toggle))
	window.ShowAndRun()
}
```

Функция update перечитывает булев ключ и пишет подпись; её вызов при старте заполняет метку до всяких изменений, а AddChangeListener делает её живой на будущее. Кнопка меняет только значение в хранилище — интерфейс реагирует через слушателя. Тот же ключ прочитает и полноценная тема из пятнадцатого урока.

**Вопрос:** Какая сигнатура у колбэка в Preferences().AddChangeListener?

- func(key string) — колбэк получает имя изменённого ключа
- func(key string, value interface{}) — ключ и новое значение
- ✅ func() — без аргументов; что изменилось, узнавай перечитыванием
- func(err error) — колбэк сообщает об ошибках записи

Слушатель хранилища получает только факт перемены: func(). Имена ключа и значения в колбэк не приходят — перечитай нужные ключи сам. Чтения дешёвые, а перечитывание целиком устойчиво к группировке нескольких изменений в одно уведомление.

### Мост к привязкам: BindPreferenceString

Семнадцатый урок оставил мост недостроенным — пора достроить. binding.BindPreferenceString(key, prefs) создаёт источник, живущий прямо в хранилище: Get читает ключ preferences, Set пишет в него же. Дальше всё знакомо: Entry.Bind(source) — и поле из [девятого урока](/courses/fyne/lessons/09-text-entry/) двусторонне связано с хранилищем; печать пользователя сохраняет значение, а загрузка при старте происходит сама, ведь источник читает ключ уже при подключении. Аналогично работают BindPreferenceBool для Check, BindPreferenceFloat для Slider и BindPreferenceInt. Получается самый короткий путь к экрану настроек: один Bind заменяет и поле ввода с обработчиком, и чтение при старте, и запись при изменении. Когда что выбирать? Прямые String и WithFallback удобны, когда значение читается в нескольких местах и записывается по событию — приветствие из первого примера; привязка удобна, когда значение — собственность одного контрола: имя пользователя в поле, громкость в слайдере, тема в галочке. Осторожность одна: привязка пишет на каждую правку поля, а не по кнопке «Сохранить» — для настроек это обычно и нужно, но если требуется явное подтверждение, оставь обычный источник семнадцатого урока и пиши в preferences по OnSubmit формы из восемнадцатого. Мост между уроками работает в обе стороны: проверка гарантирует, что в хранилище не попадёт мусор, а хранилище делает привязки персистентными без единой новой строки кода.

### Экран настроек целиком на привязках

```go
package main

import (
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/data/binding"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.NewWithID("io.gomaster.prefs")
	window := myApp.NewWindow("Настройки")

	prefs := myApp.Preferences()

	name := widget.NewEntry()
	name.SetPlaceHolder("Имя пользователя")
	name.Bind(binding.BindPreferenceString("user.name", prefs))

	dark := widget.NewCheck("Тёмная тема", nil)
	dark.Bind(binding.BindPreferenceBool("theme.dark", prefs))

	volume := widget.NewSlider(0, 1)
	volume.Bind(binding.BindPreferenceFloat("audio.volume", prefs))

	window.SetContent(container.NewVBox(name, dark, volume))
	window.ShowAndRun()
}
```

Три контрола, три привязки — и ни одного обработчика: поле пишет имя в user.name, галочка управляет theme.dark, слайдер двигает audio.volume, всё сохраняется само и загружается при старте, потому что источники читают ключи прямо из хранилища. Закрой и открой приложение — состояние вернётся.

### Каталог API настроек

| API | Что делает | Когда звать |
| --- | --- | --- |
| myApp.Preferences() | получить хранилище приложения | один раз при старте, дальше держи ссылку |
| app.NewWithID(id) | создать приложение с постоянным идентификатором | всегда, если настройки должны жить |
| SetString / String | записать и прочитать строку | имена, пути, сериализованные блобы |
| StringWithFallback(key, def) | чтение с запасным значением | загрузка при первом запуске |
| SetBool / Bool и SetFloat / Float | булевы и дробные значения | галочки, слайдеры, громкость |
| SetInt / Int | целые значения | счётчики, размеры, лимиты |
| BoolWithFallback / FloatWithFallback / IntWithFallback | чтение с запасным значением | разумные стартовые точки |
| RemoveValue(key) | стереть ключ | сброс отдельной настройки |
| AddChangeListener / RemoveChangeListener | подписка на факты изменения | глобальные реакции: тема, заголовки |
| StoragePath() | путь, где лежат сохранённые значения | отладка и бэкап вручную |
| binding.BindPreferenceString(key, prefs) | источник привязки из хранилища | экран настроек на Bind |

### Собери загрузку при старте

Восстанови пять пропусков: получение хранилища, привязка к нему поля и три чтения с запасными значениями.

```go
prefs := myApp.___()

name := widget.NewEntry()
name.Bind(binding.___("user.name", prefs))

title := prefs.___("doc.title", "Без названия")
dark := prefs.___("theme.dark", true)
volume := prefs.___("audio.volume", 0.5)
```

Подсказка: Хранилище даёт myApp.Preferences(), источником привязки служит BindPreferenceString, а три WithFallback-метода соответствуют типам значений: строка, булево, дробное.

Правильные ответы в пропусках: `Preferences()`, `BindPreferenceString`, `StringWithFallback`, `BoolWithFallback`, `FloatWithFallback`

### Путь настройки от слайдера до файла на диске

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

Правильный порядок строк:

1. `пользователь двигает слайдер — виджет сообщает новое значение`
2. `Slider.Bind вызывает Set у источника binding.Float`
3. `источник — BindPreferenceFloat: Set пишет в ключ хранилища`
4. `хранилище обновляет значение и уведомляет слушателей`
5. `подпись через AddChangeListener перечитывает ключ и показывает проценты`
6. `Fyne сбрасывает изменения в preferences.json внутри StoragePath`

> **Честно о запуске кода.** Хранилищу нужен настоящий экран — в браузерной песочнице курса GUI не исполняется. Собери примеры по второму уроку и проверь главный сценарий урока: закрой приложение и открой снова — значения должны вернуться; без идентификатора в app.NewWithID они не вернутся, и это тоже стоит увидеть своими глазами.

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

### Практика состояния: что стоит сохранять

Соберём короткий список того, что в реальных приложениях живёт в настройках. Выбор темы: галочка тёмного режима из примера выше читается при старте до построения интерфейса, и тема из пятнадцатого урока применяется сразу — обещание того урока исполнено; меняется — слушатель перекрашивает остатки. Черновики: текст редактора в String по таймеру или OnChanged — пользователь перезапустил приложение и нашёл текст на месте; для больших текстов — файл. Флаги «больше не показывать» и «первый запуск»: Bool-ключ превращает онбординг в одноразовый, а итоговый счётчик запусков удобно показать в ShowInformation из [тринадцатого урока](/courses/fyne/lessons/13-dialogi/). Счётчики: число запусков из предсказания, номер последней открытой вкладки, геометрия окна — размеры и позицию Fyne сам не помнит, два Int-ключа решают дело. Что НЕ стоит: всё, что изменяется часто и требует истории — журнал действий; всё, что должно жить на сервере и путешествовать между машинами — preferences локальны по определению; всё, что пользователь не выбирал сам, — кеш не настройка. Проверь связку при старте: читать настройки лучше до ShowAndRun из второго урока, чтобы первый кадр уже отражал сохранённое состояние, — мигание «дефолт, потом тёмная тема» выглядит как баг, даже если это просто порядок инициализации.

### Что дальше: потоки и fyne.Do

Теперь приложение помнит пользователя: значения переживают перезапуск, тема возвращается, экран настроек собран из трёх привязок без обработчиков. Опора урока — [второй](/courses/fyne/lessons/02-pervoe-okno/) с каркасом запуска и идентификатором, [семнадцатый](/courses/fyne/lessons/17-binding/) с источниками и Bind, [восемнадцатый](/courses/fyne/lessons/18-validaciya/) с проверкой значений на входе, [пятнадцатый](/courses/fyne/lessons/15-temy/) с темой, выбор которой теперь сохраняется. Следующая тема — многопоточность в GUI: что случится, если Set из семнадцатого урока позвать из горутины, почему интерфейс трогать можно только из главного потока и как fyne.Do делает bridges безопасными. Домашнее задание соберёт экран настроек, который переживает перезапуск целиком. Полный маршрут — [на странице курса Fyne](/courses/fyne/).

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

- **fyne.Preferences** — Хранилище ключ-значение приложения из myApp.Preferences(): строки, булевы, дробные и целые значения с записью на диск без ручного кода файлов.
- **WithFallback-семейство** — StringWithFallback, BoolWithFallback, FloatWithFallback, IntWithFallback: возвращают запасное значение, только если ключа нет; записанный ноль и false вернутся как есть.
- **app.NewWithID** — Создание приложения с постоянным идентификатором — обязательное условие персистентности: путь к настройкам строится из ID, а app.New() выдаёт случайный ID на каждый запуск.
- **StoragePath и preferences.json** — Значения лежат в JSON внутри системного каталога пользователя: Linux — ~/.config, macOS — Application Support, Windows — %AppData%; путь подскажет StoragePath().
- **AddChangeListener(func())** — Подписка на факт изменения без имени ключа: колбэк перечитывает нужные значения сам. Снимается RemoveChangeListener, счёт вызовов не гарантирован.
- **BindPreferenceString** — Источник привязки, живущий в хранилище: Get читает ключ, Set пишет в него. Entry.Bind с ним даёт настройки, которые сохраняются и загружаются сами.

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

- myApp.Preferences() — хранилище ключ-значение: SetString, SetBool, SetFloat, SetInt и чтение без ошибок: отсутствующий ключ даёт пустое значение.
- WithFallback-семейство решает первый запуск: запасное значение возвращается только при отсутствии ключа, записанные ноль и false — как есть.
- Персистентность держится на идентификаторе: app.NewWithID задаёт постоянный путь настройкам, app.New() выдаёт случайный ID и теряет всё.
- Значения лежат в preferences.json в системном каталоге пользователя; точное место показывает StoragePath(), запись на диск — пакетная, не мгновенная.
- AddChangeListener(func()) сообщает о переменах без имени ключа — перечитывай состояние; снимай слушателя и не строй эхо-петель.
- BindPreferenceString/Bool/Float подключают хранилище напрямую к виджетам: экран настроек собирается без единого обработчика.
- Храним выбор темы, черновики, флаги онбординга и счётчики; не храним секреты, кеши и большие данные — им место в файлах двадцать первого урока.

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

Собери экран настроек, который переживает перезапуск целиком: app.NewWithID, имя пользователя через BindPreferenceString и Entry, тёмная тема через BindPreferenceBool и Check, громкость через BindPreferenceFloat и Slider. Добавь подпись с процентом громкости на слушателе AddChangeListener и заголовок окна, который берёт имя из настроек при старте. Реализуй счётчик запусков на IntWithFallback и покажи его в ShowInformation из тринадцатого урока при третьем и далее запуске. Смоделируй выбор темы: флаг из настроек читается до ShowAndRun, и первое, что видит пользователь, — уже выбранная тема из пятнадцатого урока. Проверь обе стороны медали: закрой и открой приложение трижды — значения растут; затем запусти копию без идентификатора и убедись, что счётчик застрял на единице. Ловушка для самопроверки: попробуй записать в настройку большой JSON со списком задач и оцени, что произойдёт при частых правках; сделай вывод, где настоящая граница между настройками и данными, и запомни её для двадцать первого урока.

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

### Почему настройки не сохраняются между запусками?

Почти всегда приложение создано через app.New() без идентификатора: Fyne выдаёт случайный ID на каждый запуск, файл настроек каждый раз новый. Создавай приложение через app.NewWithID с постоянной строкой в обратном доменном стиле — и значения начнут жить.

### Можно ли хранить в preferences структуру или список?

Напрямую нет — только четыре базовых типа. Небольшую структуру сериализуй в JSON-строку и держи одним ключом; для больших данных используй файлы хранилища приложения. Помни, что значения перечитываются часто — не превращай настройку в базу данных.

### Узнает ли слушатель, какой именно ключ изменился?

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