Ошибки в Go — это особенность языка, которая позволяет работать с неожиданным поведением кода в явном виде:
import "errors"
func validateName(name string) error {
if name == "" {
// errors.New создает новый объект ошибки
return errors.New("empty name")
}
if len([]rune(name)) > 50 {
return errors.New("a name cannot be more than 50 characters")
}
return nil
}
Тип error
является интерфейсом. Интерфейс — это отдельный тип данных в Go, представляющий набор методов. Любая структура реализует интерфейс неявно через структурную типизацию. Структурная типизация (в динамических языках это называют утиной типизацией) — это связывание типа с реализацией во время компиляции без явного указания связи в коде:
package main
import (
"fmt"
)
// объявление интерфейса
type Printer interface {
Print()
}
// нигде не указано, что User реализует интерфейс Printer
type User struct {
email string
}
// структура User имеет метод Print, как в интерфейсе Printer. Следовательно, во время компиляции запишется связь между User и Printer
func (u *User) Print() {
fmt.Println("My email is", u.email)
}
// функция принимает как аргумент интерфейс Printer
func TestPrint(p Printer) {
p.Print()
}
func main() {
// в функцию TestPrint передается структура User, и так как она реализует интерфейс Printer, все работает без ошибок
TestPrint(&User{email: "test@test.com"})
}
Интерфейс error
содержит только один метод Error, который возвращает строковое представление ошибки:
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
Следовательно, легко можно создавать свои реализации ошибок:
type TimeoutErr struct {
msg string
}
// структура TimeoutErr реализует интерфейс error и может быть использована как обычная ошибка
func (e *TimeoutErr) Error() string {
return e.msg
}
Следует запомнить, что если функция возвращает ошибку, то она всегда возвращается последним аргументом:
// функция возвращает несколько аргументов, и ошибка возвращается последней
func DoHTTPCall(r Request) (Response, error) {
...
}
Нулевое значение для интерфейса — это пустое значение nil
. Следовательно, когда код работает верно, возвращается nil
вместо ошибки.
Для выполнения этого задания потребуется функция json.Unmarshal
, которая декодирует JSON байты в структуру:
package main
import (
"encoding/json"
"fmt"
)
type HelloWorld struct {
Hello string
}
func main() {
hw := HelloWorld{}
// первым аргументом передается JSON-строка в виде слайса байт. Вторым аргументом указатель на структуру, в которую нужно декодировать результат.
err := json.Unmarshal([]byte("{\"hello\":\"world\"}"), &hw)
fmt.Printf("error: %s, struct: %+v\n", err, hw) // error: %!s(<nil>), struct: {Hello:world}
}
В API методах часто используются запросы с телом в виде JSON. Такие тела нужно декодировать в структуры и валидировать. Хоть это и не лучшая практика делать функции, в которых происходит несколько действий, но для простоты примера реализуйте функцию DecodeAndValidateRequest(requestBody []byte) (CreateUserRequest, error)
, которая декодирует тело запроса из JSON в структуру CreateUserRequest
и валидирует ее. Если приходит невалидный JSON или структура заполнена неверно, функция должна вернуть ошибку.
Структура запроса:
type CreateUserRequest struct {
Email string `json:"email"`
Password string `json:"password"`
PasswordConfirmation string `json:"password_confirmation"`
}
Список ошибок, которые нужно возвращать из функции:
// validation errors
var (
errEmailRequired = errors.New("email is required") // когда поле email не заполнено
errPasswordRequired = errors.New("password is required") // когда поле password не заполнено
errPasswordConfirmationRequired = errors.New("password confirmation is required") // когда поле password_confirmation не заполнено
errPasswordDoesNotMatch = errors.New("password does not match with the confirmation") // когда поля password и password_confirmation не совпадают
)
Примеры работы функции DecodeAndValidateRequest
:
DecodeAndValidateRequest([]byte("{\"email\":\"\",\"password\":\"test\",\"password_confirmation\":\"test\"}")) // CreateUserRequest{}, "email is required"
DecodeAndValidateRequest([]byte("{\"email\":\"test\",\"password\":\"\",\"password_confirmation\":\"test\"}")) // CreateUserRequest{}, "password is required"
DecodeAndValidateRequest([]byte("{\"email\":\"test\",\"password\":\"test\",\"password_confirmation\":\"\"}")) // CreateUserRequest{}, "password confirmation is required"
DecodeAndValidateRequest([]byte("{\"email\":\"test\",\"password\":\"test\",\"password_confirmation\":\"test2\"}")) // CreateUserRequest{}, "password does not match with the confirmation"
DecodeAndValidateRequest([]byte("{\"email\":\"email@test.com\",\"password\":\"passwordtest\",\"password_confirmation\":\"passwordtest\"}")) // CreateUserRequest{Email:"email@test.com", Password:"passwordtest", PasswordConfirmation:"passwordtest"}, nil
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1package solution
2
3import (
4 "github.com/stretchr/testify/assert"
5 "testing"
6)
7
8func TestDecodeAndValidateRequest(t *testing.T) {
9 a := assert.New(t)
10
11 req, err := DecodeAndValidateRequest(nil)
12 a.Error(err)
13 a.Equal(CreateUserRequest{}, req)
14
15 req, err = DecodeAndValidateRequest([]byte(""))
16 a.Error(err)
17 a.Equal(CreateUserRequest{}, req)
18
19 req, err = DecodeAndValidateRequest([]byte("{}"))
20 a.Error(err)
21 a.Equal(CreateUserRequest{}, req)
22
23 req, err = DecodeAndValidateRequest([]byte("{\"email\":\"\",\"password\":\"test\",\"password_confirmation\":\"test\"}"))
24 a.Equal(errEmailRequired, err)
25 a.Equal(CreateUserRequest{}, req)
26
27 req, err = DecodeAndValidateRequest([]byte("{\"email\":\"test\",\"password\":\"\",\"password_confirmation\":\"test\"}"))
28 a.Equal(errPasswordRequired, err)
29 a.Equal(CreateUserRequest{}, req)
30
31 req, err = DecodeAndValidateRequest([]byte("{\"email\":\"test\",\"password\":\"test\",\"password_confirmation\":\"\"}"))
32 a.Equal(errPasswordConfirmationRequired, err)
33 a.Equal(CreateUserRequest{}, req)
34
35 req, err = DecodeAndValidateRequest([]byte("{\"email\":\"test\",\"password\":\"test\",\"password_confirmation\":\"test2\"}"))
36 a.Equal(errPasswordDoesNotMatch, err)
37 a.Equal(CreateUserRequest{}, req)
38
39 req, err = DecodeAndValidateRequest([]byte("{\"email\":\"email@test.com\",\"password\":\"passwordtest\",\"password_confirmation\":\"passwordtest\"}"))
40 a.NoError(err)
41 a.Equal(CreateUserRequest{
42 Email: "email@test.com",
43 Password: "passwordtest",
44 PasswordConfirmation: "passwordtest",
45 }, req)
46}
47
Решение учителя откроется через: