Ферма

Ферма

Навчальна вправа

Вступ

Інтерфейс error

У Go обробка помилок не відбувається через винятки. Натомість помилки - це звичайні значення типів, які реалізують вбудований інтерфейс error. Інтерфейс error дуже мінімальний. Він містить лише один метод Error(), який повертає повідомлення про помилку як рядок тексту (англ. string).

type error interface {
  Error() string
}

Щоразу, коли ми визначаємо функцію, під час виконання якої може статися помилка, яку потрібно донести до того, хто викликав функцію, треба включити error до типів, які функція повертає. Якщо функція має кілька повернених значень, за домовленістю error завжди стоїть останнім.

func DoSomething() (int, error) {
  // ...
}

Створення і повернення простої помилки

Не обовʼязково щоразу самостійно реалізовувати інтерфейс error. Щоб створити просту помилку, можна використати функцію errors.New() зі стандартного пакета errors. Єдине, що потрібно передати, - це повідомлення про помилку як рядок тексту, а errors.New() подбає про створення значення, яке містить наше повідомлення й реалізує інтерфейс error.

Якщо функція повертає помилку, гарна практика - повертати нульове значення для решти значень, які вона повертає:

func DoSomething() (SomeStruct, int, error) {
  // ...
  return SomeStruct{}, 0, errors.New("failed to calculate result")
}
Caution

Не варто припускати, що всі функції повертають нульові значення для решти значень, якщо помилка є. Найкраще вважати, що користуватися жодним з інших повернених значень небезпечно, якщо сталася помилка. Єдині винятки - це випадки, коли документація чітко зазначає, що інші повернені значення мають сенс у разі помилки.

Якщо таку просту помилку потрібно використати в кількох місцях, краще оголосити для неї змінну, а не викликати errors.New безпосередньо в коді. За домовленістю назва такої змінної має починатися з Err або err (залежно від того, експортована вона чи ні). Такі змінні для помилок часто називають вартовими помилками.

import "errors"

var ErrNotFound = errors.New("resource was not found")

func DoSomething() error {
  // ...
  return ErrNotFound
}

Повернімо nil як помилку, щоб показати, що під час виконання функції помилок не було:

func Foo() (int, error) {
  return 10, nil
}

Перевірка помилок

Якщо ми викликаємо функцію, яка повертає помилку, зазвичай зберігаємо значення помилки у змінній з назвою err. Перш ніж використати справжній результат функції, потрібно перевірити, що помилки не було.

Щоб не вкладати «щасливий шлях» нашого коду, випадки з помилками слід обробляти першими. Ми можемо використати == і !=, щоб порівняти помилку з nil, і знаємо, що сталася помилка, коли err не дорівнює nil.

func processUserFile() error {
	file, err := os.Open("./users.csv")
	if err != nil {
		return err
	}

	// do something with file
}

Найчастіше помилку повертають далі вгору стеком викликів, як показано в прикладі вище. Інший спосіб обробити помилку - записати її в журнал і продовжити з якоюсь іншою операцією. Гарна практика - або повернути помилку, або записати її в журнал, і ніколи не робити й того, й іншого.

Оскільки більшість функцій у Go повертають помилку як одне з повернених значень, ми всюди в коді Go будемо бачити та використовувати конструкцію if err != nil.

Власні типи помилок

Якщо потрібно, щоб помилка містила більше інформації, ніж просто рядок тексту з повідомленням про помилку, можна створити власний тип помилки. Як уже згадувалося, усе, що реалізує інтерфейс error (тобто має метод Error() string), може бути помилкою в Go.

Зазвичай для створення власного типу помилки використовують структуру. За домовленістю назви власних типів помилок мають закінчуватися на Error. Крім того, метод Error() string найкраще визначати з приймачем-вказівником; про причини цього читайте в цьому коментарі на Stack Overflow. Зауважмо, що тоді потрібно повертати вказівник на власну помилку, інакше вона не вважатиметься error, бо значення без вказівника не надає методу Error() string.

type MyCustomError struct {
  message string
  details string
}

func (e *MyCustomError) Error() string {
  return fmt.Sprintf("%s, details: %s", e.message, e.details)
}

func someFunction() error {
  // ...
  return &MyCustomError{
    message: "...",
    details: "...",
  }
}

Вказівки

Нарешті настав день, на який ми так довго чекали, і тепер ми - щасливі власники прекрасної ферми в Альпах.

Ми й досі не любимо прокидатися надто рано вранці, щоб погодувати своїх корів. Оскільки ми - чудові інженери, ми створюємо дозатор корму FEED-M-ALL.

Останнє, що потрібно, аби завершити наш проєкт, - це фрагмент коду, який обчислює кількість корму, яку має отримати кожна корова. Важливо, щоб кожна корова отримала однакову кількість. Потрібно уникати конфліктів. Корови дуже вразливі.

На щастя, нам не потрібно самостійно виводити всі формули для обчислення кількості корму. Ми використовуємо якусь загадкову зовнішню бібліотеку, яку знайшли в інтернеті. За задумом, вона має зробити корів найщасливішими. Бібліотека надає тип, що реалізує такий інтерфейс. На це ми будемо покладатися у коді, який пишемо самі.

type FodderCalculator interface {
	FodderAmount(int) (float64, error)
	FatteningFactor() (float64, error)
}

Працюючи над своїм кодом, ми будемо вдосконалювати обробку помилок, щоб зробити її надійнішою та легшою для налагодження згодом, коли будемо користуватися нею в щоденному житті на фермі.

1. Розділіть корм порівну

Перш за все, ми зосереджуємося на написанні коду, потрібного для обчислення кількості корму на одну корову.

Реалізуйте функцію DivideFood, яка приймає як аргументи FodderCalculator і кількість корів (ціле число). Для цього завдання ми припускаємо, що передана кількість корів завжди більша за нуль. Функція має повернути кількість корму на одну корову як float64 або помилку, якщо вона сталася.

Щоб виконати обчислення, спершу потрібно отримати загальну кількість корму для всіх корів. Це робиться викликом методу FodderAmount, якому передають кількість корів. Крім того, потрібен множник, на який цю кількість треба помножити. Цей множник ми отримуємо, викликавши метод FatteningFactor. Маючи ці два значення та кількість корів, ми тепер можемо обчислити кількість корму на одну корову (як float64). Саме це й має повертати функція DivideFood.

Якщо один із викликаних методів повертає помилку, виконання має зупинитися, і цю помилку слід повернути (як є) з функції DivideFood.

// For this example, we assume FodderAmount returns 50
// and FatteningFactor returns 1.5.
DivideFood(fodderCalculator, 5)
// => 15 <nil>

// Now assuming FodderAmount returns an error with message "something went wrong".
DivideFood(fodderCalculator, 5)
// => 0 "something went wrong"

2. Перевірте кількість корів

Працюючи над першим завданням вище, ми зрозуміли, що зовнішня бібліотека, яку ми використовуємо, не така якісна, як здавалося. Наприклад, вона не вміє належно обробляти некоректні вхідні дані. Ми хочемо обійти це обмеження, додавши перевірку вхідного значення у власному коді.

Напишіть функцію ValidateInputAndDivideFood з тією ж сигнатурою, що й DivideFood вище.

  • Якщо передана кількість корів більша за 0, функція має викликати DivideFood і повернути результати цього виклику.
  • Якщо кількість корів дорівнює 0 або менша, функція має повернути помилку з повідомленням "invalid number of cows".
ValidateInputAndDivideFood(fodderCalculator, 5)
// => 15 <nil>

ValidateInputAndDivideFood(fodderCalculator, -2)
// => 0 "invalid number of cows"

3. Покращте обробку помилок

Перевірка кількості корів перед передаванням була вдалим кроком, але нас не зовсім влаштовує неконкретне повідомлення про помилку. Ми вирішуємо зробити краще й створити власний тип помилки під назвою InvalidCowsError.

Власна помилка має містити кількість корів (int) і власне повідомлення (string), а метод Error має серіалізувати дані в такому форматі:

{number of cows} cows are invalid: {custom message}

Озброївшись власною помилкою, реалізуйте функцію ValidateNumberOfCows, яка приймає кількість корів як ціле число й повертає помилку (або nil).

  • Якщо кількість корів менша за 0, функція повертає InvalidCowsError з власним повідомленням "there are no negative cows".
  • Якщо кількість корів дорівнює 0, функція повертає InvalidCowsError з власним повідомленням "no cows don't need food".
  • В іншому разі функція повертає nil, що означає, що перевірка була успішною.
err := ValidateNumberOfCows(-5)
err.Error()
// => "-5 cows are invalid: there are no negative cows"

Після нелегкої роботи з налаштування цієї функції перевірки ми помічаємо, що вже вечір, і залишаємо робоче місце, щоб помилуватися заходом сонця над горами. Ми відкладаємо додавання нової функції перевірки до нашого коду на інший день.

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
Go Exercism

Час розпочати Ферма?

Зареєструйтеся на Exercism, щоб вивчати й опановувати Go, а також 34 концепції166 вправ та справжнє наставництво від людей, і все це безкоштовно.