Projeto prático final em Go (Golang): um guia detalhado
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты учишься в школе поваров. Сначала ты учишься чистить картошку (переменные), потом жарить яичницу (условия), потом варить суп (циклы), потом печь пирожки (функции). Но в конце обучения тебе дают задание: приготовить полноценный обед из трёх блюд для целой компании гостей. Ты должен сам придумать меню, рассчитать продукты, организовать процесс так, чтобы всё было готово одновременно, и красиво подать на стол.
Вот это и есть итоговый практический проект в программировании. Это не просто очередная задача, где нужно написать один маленький кусочек кода. Это твой шанс собрать всё, что ты выучил, в одну большую, работающую программу. Ты сам решаешь, какие функции использовать, как организовать данные, как сделать так, чтобы программа не падала с ошибками и была удобной для пользователя.
Итоговый проект — это твой диплом повара. Пока ты не приготовишь этот обед, ты не сможешь назвать себя настоящим программистом.
Зачем это нужно? Во-первых, это проверка себя. Ты увидишь, что действительно знаешь, а что нужно подучить. Во-вторых, это твоё портфолио. Когда ты будешь искать работу или показывать друзьям, что ты умеешь, ты скажешь: «Вот, я написал полноценную программу на Go». В-третьих, это невероятно интересно. Создавать что-то своё, большое и полезное — это совсем другое чувство, чем решать скучные задачки из учебника.
Теория: как это работает изнутри
Любой большой проект, даже если это простая игра или калькулятор, состоит из трёх главных частей. Давай разберём их на примере программы «Учёт расходов».
1. Ввод и вывод (UI — User Interface)
Это то, как пользователь общается с программой. В консольных программах на Go это обычно fmt.Scan() для ввода и fmt.Println() для вывода. Пользователь вводит: «Купил пиццу за 500 рублей». Программа выводит: «Добавлено. Всего потрачено: 5000 рублей».
2. Обработка данных (Business Logic)
Это мозг программы. Здесь происходят все вычисления, проверки и изменения. Например: «Если сумма расхода больше 1000, то вывести предупреждение "Многовато!"». Или: «Посчитать средний расход за неделю». Вся логика, ради которой программа создаётся, живёт здесь.
3. Хранение данных (Data Storage)
Программа должна что-то запоминать. Пока она работает, данные хранятся в переменных, массивах (слайсах) и структурах. Но когда программа выключится, всё пропадёт. Чтобы данные сохранились навсегда, их нужно записывать в файл или базу данных. В нашем проекте мы будем использовать простой текстовый файл.
Запомни золотое правило: разделяй эти три части. Не пиши логику внутри кода, который рисует меню. И не сохраняй файлы внутри кода, который считает расходы. Так твою программу будет легко чинить и улучшать.
Синтаксис в Go (Golang): все формы и варианты
Для итогового проекта тебе понадобятся не просто базовые конструкции, а умение их комбинировать. Давай вспомним всё самое важное, что мы будем использовать.
Пакеты и импорт
Любая программа на Go начинается с пакета. Для проекта мы будем использовать package main. Импортируем нужные библиотеки:
package main
import (
"fmt" // Для вывода на экран
"os" // Для работы с файлами и завершения программы
"bufio" // Для удобного чтения с клавиатуры
"strconv" // Для превращения строк в числа
"strings" // Для работы с текстом (например, обрезать пробелы)
"time" // Для работы с датами и временем
)
Структуры (struct) — твои собственные типы данных
В большом проекте нельзя хранить всё в отдельных переменных. Нужно группировать данные. Структура — это как анкета с полями.
// Структура для одного расхода
type Expense struct {
Amount float64 // Сумма
Category string // Категория (Еда, Транспорт)
Date time.Time // Дата
Comment string // Комментарий
}
Слайсы (динамические массивы)
Мы не знаем заранее, сколько расходов введёт пользователь. Слайс может расти.
var expenses []Expense // Пустой слайс для хранения всех расходов
Методы — функции, привязанные к структуре
Это удобно: мы говорим структуре «Посчитай сам себя».
// Метод для структуры Expense, который выводит информацию о расходе
func (e Expense) Display() {
fmt.Printf("Дата: %s | Категория: %s | Сумма: %.2f | Комментарий: %s
",
e.Date.Format("02.01.2006"), e.Category, e.Amount, e.Comment)
}
Работа с файлами
Чтобы данные не пропадали, мы будем их сохранять. Используем пакет os и bufio.
// Сохранение данных в файл
func SaveToFile(filename string, expenses []Expense) error {
file, err := os.Create(filename) // Создаём файл
if err != nil {
return err // Если ошибка — возвращаем её
}
defer file.Close() // Обязательно закроем файл в конце
for _, exp := range expenses {
// Записываем каждую строку в формате: Сумма|Категория|Дата|Комментарий
line := fmt.Sprintf("%.2f|%s|%s|%s
", exp.Amount, exp.Category, exp.Date.Format(time.RFC3339), exp.Comment)
_, err := file.WriteString(line)
if err != nil {
return err
}
}
return nil // Всё хорошо
}
Примеры кода — от простого к сложному
Мы будем строить наш проект постепенно. Каждый пример — это шаг вперёд. В конце у нас получится полноценная программа «Учёт личных расходов».
Пример 1: Приветствие и простое меню
Начнём с самого простого: покажем пользователю меню и спросим, что он хочет сделать.
package main
import "fmt"
func main() {
// Бесконечный цикл для работы программы
for {
fmt.Println("
=== Учёт расходов ===")
fmt.Println("1. Добавить расход")
fmt.Println("2. Показать все расходы")
fmt.Println("3. Выйти")
fmt.Print("Выберите действие: ")
var choice int
fmt.Scan(&choice) // Считываем выбор пользователя
switch choice {
case 1:
fmt.Println("Вы выбрали: Добавить расход")
case 2:
fmt.Println("Вы выбрали: Показать расходы")
case 3:
fmt.Println("До свидания!")
return // Выход из программы
default:
fmt.Println("Неверный ввод. Попробуйте снова.")
}
}
}
Объяснение: Мы создали бесконечный цикл for, который не даёт программе закрыться. Внутри мы рисуем меню и ждём ввод. switch проверяет, что выбрал пользователь. Если выбрал 3 — программа завершается.
Пример 2: Добавление расхода с проверкой ввода
Теперь научимся добавлять расход. Важно: пользователь может ошибиться и ввести буквы вместо цифр. Нужно это проверить.
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin) // Создаём читателя с клавиатуры
fmt.Print("Введите сумму расхода: ")
amountStr, _ := reader.ReadString('
') // Читаем до нажатия Enter
amountStr = strings.TrimSpace(amountStr) // Убираем лишние пробелы и переносы строк
// Пробуем превратить строку в число с плавающей точкой
amount, err := strconv.ParseFloat(amountStr, 64)
if err != nil {
fmt.Println("Ошибка: нужно ввести число, а не текст!")
return
}
fmt.Printf("Вы ввели сумму: %.2f
", amount)
}
Объяснение: bufio.NewReader позволяет читать целую строку, а не одно слово. strings.TrimSpace убирает лишние символы. strconv.ParseFloat пытается превратить строку в число. Если не получается — программа не падает, а сообщает об ошибке.
Пример 3: Создание структуры и слайса расходов
Объединим данные в структуру и создадим список для хранения.
package main
import (
"fmt"
"time"
)
// Определяем структуру Expense
type Expense struct {
Amount float64
Category string
Date time.Time
Comment string
}
func main() {
// Создаём слайс расходов
expenses := []Expense{}
// Добавляем первый расход вручную для примера
firstExpense := Expense{
Amount: 150.50,
Category: "Еда",
Date: time.Now(),
Comment: "Обед в столовой",
}
// Добавляем в слайс
expenses = append(expenses, firstExpense)
// Выводим информацию
fmt.Println("Список расходов:")
for i, exp := range expenses {
fmt.Printf("%d. Сумма: %.2f | Категория: %s | Дата: %s | Комментарий: %s
",
i+1, exp.Amount, exp.Category, exp.Date.Format("02.01.2006"), exp.Comment)
}
}
Объяснение: Мы создали тип Expense с четырьмя полями. Слайс expenses хранит все расходы. Функция append добавляет новый элемент в конец слайса. Цикл for range проходит по всем элементам и выводит их.
Пример 4: Метод для отображения расхода
Сделаем код чище: пусть структура сама умеет себя показывать.
package main
import (
"fmt"
"time"
)
type Expense struct {
Amount float64
Category string
Date time.Time
Comment string
}
// Метод Display для структуры Expense
func (e Expense) Display(index int) {
fmt.Printf("%d. [%s] %s: %.2f руб. (%s)
",
index, e.Date.Format("02.01.2006"), e.Category, e.Amount, e.Comment)
}
func main() {
expenses := []Expense{
{Amount: 500, Category: "Транспорт", Date: time.Now(), Comment: "Такси"},
{Amount: 1200, Category: "Развлечения", Date: time.Now(), Comment: "Кино"},
}
fmt.Println("Мои расходы:")
for i, exp := range expenses {
exp.Display(i + 1) // Вызываем метод у каждого элемента
}
}
Объяснение: Метод Display привязан к структуре. Теперь, чтобы вывести расход, мы просто пишем exp.Display(). Код стал понятнее и короче.
Пример 5: Сохранение данных в файл
Теперь самое важное — научимся сохранять данные, чтобы они не пропадали.
package main
import (
"fmt"
"os"
"time"
)
type Expense struct {
Amount float64
Category string
Date time.Time
Comment string
}
// Функция для сохранения в файл
func SaveExpenses(filename string, expenses []Expense) error {
// Создаём файл (если существует — перезаписываем)
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("не удалось создать файл: %w", err)
}
// Отложенное закрытие файла (выполнится при выходе из функции)
defer file.Close()
// Записываем каждый расход в отдельную строку
for _, exp := range expenses {
// Формат: Сумма|Категория|Дата|Комментарий
line := fmt.Sprintf("%.2f|%s|%s|%s
",
exp.Amount, exp.Category, exp.Date.Format(time.RFC3339), exp.Comment)
_, err := file.WriteString(line)
if err != nil {
return fmt.Errorf("ошибка записи: %w", err)
}
}
return nil // Всё хорошо
}
func main() {
expenses := []Expense{
{Amount: 250, Category: "Еда", Date: time.Now(), Comment: "Завтрак"},
{Amount: 800, Category: "Одежда", Date: time.Now(), Comment: "Носки"},
}
err := SaveExpenses("my_expenses.txt", expenses)
if err != nil {
fmt.Println("Ошибка сохранения:", err)
} else {
fmt.Println("Данные успешно сохранены в файл my_expenses.txt")
}
}
Объяснение: os.Create создаёт файл. defer file.Close() гарантирует, что файл закроется, даже если произойдёт ошибка. Мы записываем данные в формате CSV-like (разделитель — вертикальная черта). fmt.Errorf с %w позволяет «обернуть» ошибку, добавив свой комментарий.
Пример 6: Загрузка данных из файла
Если мы научились сохранять, нужно научиться и загружать.
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"time"
)
type Expense struct {
Amount float64
Category string
Date time.Time
Comment string
}
// Функция загрузки из файла
func LoadExpenses(filename string) ([]Expense, error) {
// Открываем файл для чтения
file, err := os.Open(filename)
if err != nil {
// Если файла нет — это не ошибка, просто возвращаем пустой список
if os.IsNotExist(err) {
return []Expense{}, nil
}
return nil, fmt.Errorf("не удалось открыть файл: %w", err)
}
defer file.Close()
var expenses []Expense
scanner := bufio.NewScanner(file) // Создаём сканер для чтения строк
for scanner.Scan() {
line := scanner.Text() // Читаем одну строку
parts := strings.Split(line, "|") // Разделяем по символу |
// Проверяем, что у нас ровно 4 части
if len(parts) != 4 {
continue // Пропускаем битые строки
}
// Преобразуем строки обратно в числа и даты
amount, _ := strconv.ParseFloat(parts[0], 64)
category := parts[1]
date, _ := time.Parse(time.RFC3339, parts[2])
comment := parts[3]
// Создаём структуру и добавляем в слайс
expense := Expense{
Amount: amount,
Category: category,
Date: date,
Comment: comment,
}
expenses = append(expenses, expense)
}
// Проверяем, не было ли ошибок при сканировании
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("ошибка чтения файла: %w", err)
}
return expenses, nil
}
func main() {
expenses, err := LoadExpenses("my_expenses.txt")
if err != nil {
fmt.Println("Ошибка загрузки:", err)
return
}
fmt.Printf("Загружено %d расходов.
", len(expenses))
for _, exp := range expenses {
fmt.Printf(" - %s: %.2f руб.
", exp.Category, exp.Amount)
}
}
Объяснение: bufio.NewScanner читает файл построчно. strings.Split разбивает строку на части. time.Parse превращает строку обратно в дату. Мы проверяем, что в строке ровно 4 части, иначе пропускаем её (это защита от битых данных).
Пример 7: Полный цикл программы с меню
Соберём всё вместе. Это уже почти полноценный проект.
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"time"
)
type Expense struct {
Amount float64
Category string
Date time.Time
Comment string
}
const dataFile = "expenses.txt"
func main() {
// Загружаем данные при старте
expenses, err := LoadExpenses(dataFile)
if err != nil {
fmt.Println("Ошибка загрузки данных, начинаем с чистого листа:", err)
expenses = []Expense{}
}
reader := bufio.NewReader(os.Stdin)
for {
fmt.Println("
=== Учёт расходов ===")
fmt.Println("1. Добавить расход")
fmt.Println("2. Показать все расходы")
fmt.Println("3. Показать итог")
fmt.Println("4. Выйти и сохранить")
fmt.Print("Выберите действие: ")
choiceStr, _ := reader.ReadString('
')
choiceStr = strings.TrimSpace(choiceStr)
choice, _ := strconv.Atoi(choiceStr)
switch choice {
case 1:
// Добавление расхода
fmt.Print("Введите сумму: ")
amountStr, _ := reader.ReadString('
')
amountStr = strings.TrimSpace(amountStr)
amount, err := strconv.ParseFloat(amountStr, 64)
if err != nil || amount <= 0 {
fmt.Println("Ошибка: введите положительное число.")
continue
}
fmt.Print("Введите категорию (Еда, Транспорт и т.д.): ")
category, _ := reader.ReadString('
')
category = strings.TrimSpace(category)
fmt.Print("Введите комментарий: ")
comment, _ := reader.ReadString('
')
comment = strings.TrimSpace(comment)
expense := Expense{
Amount: amount,
Category: category,
Date: time.Now(),
Comment: comment,
}
expenses = append(expenses, expense)
fmt.Println("Расход добавлен!")
case 2:
// Показ всех расходов
if len(expenses) == 0 {
fmt.Println("Расходов пока нет.")
} else {
fmt.Println("
Список расходов:")
for i, exp := range expenses {
fmt.Printf("%d. [%s] %s: %.2f руб. — %s
",
i+1, exp.Date.Format("02.01.2006"), exp.Category, exp.Amount, exp.Comment)
}
}
case 3:
// Подсчёт итога
var total float64
for _, exp := range expenses {
total += exp.Amount
}
fmt.Printf("Всего потрачено: %.2f руб.
", total)
case 4:
// Сохранение и выход
err := SaveExpenses(dataFile, expenses)
if err != nil {
fmt.Println("Ошибка при сохранении:", err)
} else {
fmt.Println("Данные сохранены. До свидания!")
}
return
default:
fmt.Println("Неверный ввод. Попробуйте снова.")
}
}
}
// Функции SaveExpenses и LoadExpenses — такие же, как в примерах 5 и 6
func SaveExpenses(filename string, expenses []Expense) error {
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("не удалось создать файл: %w", err)
}
defer file.Close()
for _, exp := range expenses {
line := fmt.Sprintf("%.2f|%s|%s|%s
",
exp.Amount, exp.Category, exp.Date.Format(time.RFC3339), exp.Comment)
_, err := file.WriteString(line)
if err != nil {
return fmt.Errorf("ошибка записи: %w", err)
}
}
return nil
}
func LoadExpenses(filename string) ([]Expense, error) {
file, err := os.Open(filename)
if err != nil {
if os.IsNotExist(err) {
return []Expense{}, nil
}
return nil, fmt.Errorf("не удалось открыть файл: %w", err)
}
defer file.Close()
var expenses []Expense
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
parts := strings.Split(line, "|")
if len(parts) != 4 {
continue
}
amount, _ := strconv.ParseFloat(parts[0], 64)
category := parts[1]
date, _ := time.Parse(time.RFC3339, parts[2])
comment := parts[3]
expenses = append(expenses, Expense{
Amount: amount,
Category: category,
Date: date,
Comment: comment,
})
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("ошибка чтения файла: %w", err)
}
return expenses, nil
}
Объяснение: Это полноценная программа. Она загружает данные из файла при старте, показывает меню, позволяет добавлять расходы, смотреть их и считать итог. При выходе всё сохраняется. Обрати внимание на обработку ошибок: если пользователь ввёл не число, программа не падает, а просит ввести снова.
Сравнение с Python: чем похоже и чем отличается
| Характеристика | Go (Golang) | Python |
|---|---|---|
| Типизация | Строгая статическая. Тип переменной указывается явно и не меняется. | Динамическая. Тип определяется сам, может меняться. |
| Скорость | Очень быстрый. Компилируется в машинный код. | Медленнее. Интерпретируется. |
| Обработка ошибок | Явная. Функции возвращают ошибку, которую нужно проверить. | Исключения (try/except). Можно «поймать» ошибку. |
| Циклы | Только for. Нет while. |
Есть for и while. |
| Структуры данных | Структуры (struct), слайсы, мапы (map). | Словари, списки, кортежи, классы. |
| Параллельность | Горутины и каналы. Встроено в язык. | Библиотеки threading, asyncio. Сложнее. |
| Простота для новичка | Средняя. Нужно писать больше кода, но он понятный. | Высокая. Код очень короткий и читаемый. |
Если ты знаешь Python, то Go покажется тебе более строгим и требовательным. Но за эту строгость ты получаешь скорость и надёжность. Программа на Go редко «падает» неожиданно, если ты правильно обработал все ошибки.
Типичные ошибки и подводные камни
Даже опытные программисты на Go иногда попадают в эти ловушки. Вот самые частые:
1. Игнорирование ошибок
Самая популярная ошибка новичков — написать _, _ := fmt.Scan(...) и забыть проверить, что вернулось. Если пользователь введёт букву вместо числа, программа может повести себя странно.
Как избежать: Всегда проверяй ошибки. Если тебе не нужно значение, используй _, но хотя бы проверь, что ошибки нет.
2. Забыл закрыть файл
Если ты открыл файл и не закрыл его, программа может «съесть» всю память или не дать другим программам работать с этим файлом.
Как избежать: Используй defer file.Close() сразу после открытия файла.
3. Путаница с указателями
В Go есть указатели (* и &). Если ты передаёшь структуру в функцию без указателя, создаётся её копия. Изменения в функции не повлияют на оригинал.
Как избежать: Если функция должна изменить структуру, передавай указатель: func (e *Expense) SetAmount(a float64).
4. Бесконечный цикл без выхода
Если в цикле for нет условия выхода или break, программа зависнет.
Как избежать: Всегда предусматривай способ выхода из цикла (например, пункт меню «Выйти» с return).
5. Неправильный формат даты
В Go даты форматируются не через YYYY-MM-DD, а через специальное число 2006-01-02 15:04:05. Это очень легко забыть.
Как избежать: Запомни магическое число: 2006-01-02 15:04:05 — это 1 час 2 минуты 3 секунды 4 года 5 дня 6 месяца. Или просто копируй из документации.
Лучшие практики и советы профессионалов
Чтобы твой проект был не просто работающим, а красивым и удобным для других, следуй этим советам:
1. Разделяй код на файлы
Не пиши всё в одном main.go. Создай отдельные файлы:
main.go— только запуск программыexpense.go— структура и методыstorage.go— работа с файламиui.go— меню и ввод/вывод
Так код легче читать и чинить.
2. Используй константы для магических чисел
Вместо if choice == 1 лучше написать:
const (
MenuAdd = 1
MenuShow = 2
MenuTotal = 3
MenuExit = 4
)
Если захочешь поменять цифры в меню, не придётся искать их по всему коду.
3. Пиши комментарии на русском, но не перебарщивай
Комментируй почему ты сделал так, а не что ты сделал. Код и так показывает, что он делает.
Плохо: // Прибавляем единицу к i
Хорошо: // Смещаем индекс на 1, потому что пользователи считают с 1, а программисты с 0
4. Тестируй граничные случаи
Проверь, что будет, если:
- Файл с данными пустой
- Пользователь ввёл отрицательную сумму
- Пользователь ввёл очень большое число
- Файл повреждён (например, в нём не 4 части, а 2)
Твоя программа должна обрабатывать всё это без паники.
5. Используй gofmt
В Go есть встроенный инструмент для форматирования кода. Запусти gofmt -w . в папке с проектом, и код станет красивым и единообразным.
Вопросы для самопроверки
-
Вопрос: Зачем в итоговом проекте нужно разделять код на логику, ввод/вывод и хранение данных?
Ответ: Чтобы код было легко читать, изменять и тестировать. Если логика смешана с выводом на экран, то при смене интерфейса (например, с консоли на веб) придётся переписывать всё.
-
Вопрос: Что такое
deferи зачем он нужен при работе с файлами?Ответ:
deferоткладывает выполнение функции до выхода из текущей функции. Это гарантирует, что файл закроется, даже если произойдёт ошибка. -
Вопрос: Почему в Go нужно явно проверять ошибки, а не использовать try/except как в Python?
Ответ: Go создавался для написания надёжных программ. Явная провер