diff --git a/concepts/error-wrapping/.meta/config.json b/concepts/error-wrapping/.meta/config.json deleted file mode 100644 index 179d146ca..000000000 --- a/concepts/error-wrapping/.meta/config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "blurb": "TODO: add blurb", - "authors": ["ErikSchierboom"], - "contributors": [] -} diff --git a/concepts/error-wrapping/about.md b/concepts/error-wrapping/about.md deleted file mode 100644 index e787f792e..000000000 --- a/concepts/error-wrapping/about.md +++ /dev/null @@ -1,3 +0,0 @@ -# About - -TODO diff --git a/concepts/error-wrapping/introduction.md b/concepts/error-wrapping/introduction.md deleted file mode 100644 index 7e55e9aad..000000000 --- a/concepts/error-wrapping/introduction.md +++ /dev/null @@ -1,23 +0,0 @@ -# Introduction - -TODO - -You can compare an error using the `errors.Is` function: - -```go -var ErrCustom = errors.New("custom error") -// .. -if errors.Is(someErr, ErrCustom) { - // do something about the custom error -} -``` - -And you can convert an error to a particular error type using the `errors.As` function: - -```go -var someErr *SomeError -// returns true if err is a SomeError -if errors.As(err, &someErr) { - // someErr now holds the value -} -``` \ No newline at end of file diff --git a/concepts/error-wrapping/links.json b/concepts/error-wrapping/links.json deleted file mode 100644 index fe51488c7..000000000 --- a/concepts/error-wrapping/links.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/concepts/errors/.meta/config.json b/concepts/errors/.meta/config.json index f6547ea90..401d00aa8 100644 --- a/concepts/errors/.meta/config.json +++ b/concepts/errors/.meta/config.json @@ -1,5 +1,5 @@ { "blurb": "Error handling is NOT done via exceptions in Go. Instead, errors are normal values that are returned as the last return value of a function.", - "authors": ["micuffaro", "brugnara"], - "contributors": ["jmrunkle", "junedev"] + "authors": ["micuffaro", "brugnara", "junedev"], + "contributors": ["jmrunkle"] } diff --git a/concepts/errors/about.md b/concepts/errors/about.md index 78ba39967..f443ff2aa 100644 --- a/concepts/errors/about.md +++ b/concepts/errors/about.md @@ -1,109 +1,101 @@ # About -Error handling is **not** done via exceptions in Go. -Instead, errors are normal values of the interface type `error`. - -## Creating and Returning Errors +## The error interface -You do not have to always implement the error interface yourself. -To create a simple error, you can use the `errors.New()` function that is part of the standard library package `errors`. -These error variables are also called _sentinel errors_ and by convention their names should start with `Err` or `err` (depending on whether they are exported or not). -You should use error variables instead of directly writing `errors.New` in cases where you use an error multiple times or where you want the consumer of your package to be able to check for the specific error. +Error handling is **not** done via exceptions in Go. +Instead, errors are normal _values_ of types that implement the built-in `error` interface. +The `error` interface is very minimal. +It contains only one method `Error()` that returns the error message as a string. ```go -import "errors" - -var ErrSomethingWrong = errors.New("something went wrong") -ErrSomethingWrong.Error() // returns "something went wrong" +type error interface { + Error() string +} ``` -An error is by convention the last value returned in a function with multiple return values. -If the function returns an error, it should always return the zero value for other returned values: +Every time you define a function in which an error could happen during the execution that needs to reach the caller, you need to include `error` as one of the return types. +If the function has multiple return values, by convention `error` is always the last one. ```go -import "errors" - -// Do this: -func GoodFoo() (int, error) { - return 0, errors.New("Error") -} - -// Not this: -func BadFoo() (int, error) { - return 10, errors.New("Error") +func DoSomething() (int, error) { + // ... } ``` -Return `nil` for the error when there are no errors: +## Creating and returning a simple error + +You do not have to always implement the error interface yourself. +To create a simple error, you can use the `errors.New()` function that is part of the standard library package `errors`. +The only thing you need to pass in is the error message as a string, and `errors.New()` will take care of creating a value that contains your message and implements the `error` interface. + +If the function returns an error, it is good practice to return the zero value for all other return parameters: ```go -func Foo() (int, error) { - return 10, nil +func DoSomething() (SomeStruct, int, error) { + // ... + return SomeStruct{}, 0, errors.New("failed to calculate result") } ``` -## Custom Error Types +~~~~exercism/caution +You should not assume that all functions return zero values for other return values if an error is present. +It is best practice to assume that it is not safe to use any of the other return values if an error is returned. -If you want your error to include more information than just the error message string, you can create a custom error type. -As mentioned before, everything that implements the `error` interface (i.e. has an `Error() string` method) can serve as an error in Go. +As an example, imagine a function was trying to read from a file and returns a half-filled byte slice and an error. +If you would re-use that returned byte slice in your code, assuming it is always empty because there was error, you can run into subtle bugs. -Usually, a struct is used to create a custom error type. -By convention, custom error type names should end with `Error`. -Also, it is best to set up the `Error() string` method with a pointer receiver, see this [Stackoverflow comment][stackoverflow-errors] to learn about the reasoning. -Note that this means you need to return a pointer to your custom error otherwise it will not count as `error` because the non-pointer value does not provide the `Error() string` method. +The only exceptions are cases where the function documentation clearly states that other returns values are meaningful in case of an error. +Look at the [documentation for `Write` on a buffer][buffer-write] for an example. +~~~~ + +Make sure the error message you provide is as specific as possible as errors do not include any stack traces by default. +By convention, the error message should start with a lowercase letter and not end with a period. + +If you want to use such an error in multiple places (or you want to make the error available to the consumer of your package), you should declare a variable for the error instead of using `errors.New` in-line. +By convention, the name of the variable should start with `Err` or `err` (depending on whether it is exported or not). These error variables are often called _sentinel errors_. ```go -type MyCustomError struct { - message string - details string -} +import "errors" -func (e *MyCustomError) Error() string { - return fmt.Sprintf("%s, Details: %s", e.message, e.details) -} +var ErrNotFound = errors.New("resource was not found") -func someFunction() error { +func DoSomething() error { // ... - return &MyCustomError{ - message: "...", - details: "...", - } + return ErrNotFound } ``` -## Checking for Errors - -Errors can be checked against `nil`: +Return `nil` for the error to signal that there were no errors during the function execution: ```go -func myFunc() error { - file, err := os.Open("./users.csv") - if err != nil { - // handle the error - return err // or e.g. log it and continue - } - // do something with file +func Foo() (int, error) { + return 10, nil } ``` -Since most functions in Go include an error as one of the return values, you will see/use this `if err != nil` pattern all over the place in Go code. +## Including variables in the error message -You can compare error variables with the equality operator `==`: +If you need to construct a more complex error message from some variables, you can make use of string formatting. +Go has a special `fmt.Errorf` function for this purpose. +It constructs the error message string and returns an error. ```go -var ErrResourceNotFound = errors.New("resource not found") -// ... -if err == ErrResourceNotFound { - // do something about the resource-not-found error -} +input := 123 +action := "UPDATE" + +err := fmt.Errorf("invalid input %d for action %s", input, action) +err.Error() +// => "invalid input 123 for action UPDATE" ``` -How to check for errors of a specific custom error type will be covered in later concepts. +## Error checking -## Best Practice: Return Early +If you call a function that returns an error, it is common to store the error value in a variable called `err`. +Before you use the actual result of the function, you need to check that there was no error. +We can use `==` and `!=` to compare the error against `nil` and we know there was an error when `err` is not `nil`. -It is best practice to return early when an error is encountered. -That avoids nesting the "happy path" code, see this [article by Mat Ryer][line-of-sight] for more details. +To avoid nesting the "happy path" of your code, error cases should be handled first, usually via an early return. +See this famous [article by Mat Ryer][line-of-sight] for more details. ```go // Do this: @@ -135,29 +127,42 @@ func myFunc() error { } ``` -## Scope of Errors +Most of the time, the error will be returned up the function stack as shown in first example above. +Another way of handling the error could be to log it and continue with some other operation. +It is good practice to either return or log the error, never both. -In Go, it is also a best practice to avoid declaring variables in a scope if these are not required later. +Since most functions in Go include an error as one of the return values, you will see/use the `if err != nil` pattern all over the place in Go code. + +## Custom error types + +If you want your error to include more information than just the error message string, you can create a custom error type. +As mentioned before, everything that implements the `error` interface (i.e. has an `Error() string` method) can serve as an error in Go. -For the sake of this example, we want to check for errors only: +Usually, a struct is used to create a custom error type. +By convention, custom error type names should end with `Error`. +Also, it is best to set up the `Error() string` method with a pointer receiver, see this [Stackoverflow comment][stackoverflow-errors] to learn about the reasoning. +Note that this means you need to return a pointer to your custom error otherwise it will not count as `error` because the non-pointer value does not provide the `Error() string` method. ```go -_, err := os.Getwd() -if err != nil { - return nil, err +type MyCustomError struct { + message string + details string } -// at this point, err is still visible. -``` -So this is the preferred way to consume this error: +func (e *MyCustomError) Error() string { + return fmt.Sprintf("%s, details: %s", e.message, e.details) +} -```go -if _, err := os.Getwd(); err != nil { - return +func someFunction() error { + // ... + return &MyCustomError{ + message: "...", + details: "...", + } } -// err is now out of this scope. ``` [stackoverflow-errors]: https://stackoverflow.com/a/50333850 [line-of-sight]: https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea88 +[buffer-write]: https://pkg.go.dev/bytes#Buffer.Write diff --git a/concepts/errors/introduction.md b/concepts/errors/introduction.md index ea0e1ae7b..0479311ef 100644 --- a/concepts/errors/introduction.md +++ b/concepts/errors/introduction.md @@ -1,49 +1,64 @@ # Introduction +## The error interface + Error handling is **not** done via exceptions in Go. Instead, errors are normal _values_ of types that implement the built-in `error` interface. +The `error` interface is very minimal. +It contains only one method `Error()` that returns the error message as a string. ```go type error interface { - Error() string + Error() string } ``` -This means that any type which implements an one simple method `Error()` that returns a `string` implements the `error` interface. -This allows a function with return type `error` to return values of different types as long as all of them satisfy the `error` interface. +Every time you define a function in which an error could happen during the execution that needs to reach the caller, you need to include `error` as one of the return types. +If the function has multiple return values, by convention `error` is always the last one. + +```go +func DoSomething() (int, error) { + // ... +} +``` -## Creating and Returning Errors +## Creating and returning a simple error You do not have to always implement the error interface yourself. To create a simple error, you can use the `errors.New()` function that is part of the standard library package `errors`. -These error variables are also called _sentinel errors_ and by convention their names should start with `Err` or `err` (depending on whether they are exported or not). -You should use error variables instead of directly writing `errors.New` in cases where you use an error multiple times or where you want the consumer of your package to be able to check for the specific error. +The only thing you need to pass in is the error message as a string, and `errors.New()` will take care of creating a value that contains your message and implements the `error` interface. -```go -import "errors" +If the function returns an error, it is good practice to return the zero value for all other return parameters: -var ErrSomethingWrong = errors.New("something went wrong") -ErrSomethingWrong.Error() // returns "something went wrong" +```go +func DoSomething() (SomeStruct, int, error) { + // ... + return SomeStruct{}, 0, errors.New("failed to calculate result") +} ``` -An error is by convention the last value returned in a function with multiple return values. -If the function returns an error, it should always return the zero value for other returned values: +~~~~exercism/caution +You should not assume that all functions return zero values for other return values if an error is present. +It is best practice to assume that it is not safe to use any of the other return values if an error occurred. +The only exceptions are cases where the documentation clearly states that other returns values are meaningful in case of an error. +~~~~ + +If you want to use such a simple error in multiple places, you should declare a variable for the error instead of using `errors.New` in-line. +By convention, the name of the variable should start with `Err` or `err` (depending on whether it is exported or not). +These error variables are often called _sentinel errors_. ```go import "errors" -// Do this: -func GoodFoo() (int, error) { - return 0, errors.New("Error") -} +var ErrNotFound = errors.New("resource was not found") -// Not this: -func BadFoo() (int, error) { - return 10, errors.New("Error") +func DoSomething() error { + // ... + return ErrNotFound } ``` -Return `nil` for the error when there are no errors: +Return `nil` for the error to signal that there were no errors during the function execution: ```go func Foo() (int, error) { @@ -51,7 +66,32 @@ func Foo() (int, error) { } ``` -## Custom Error Types +## Error checking + +If you call a function that returns an error, it is common to store the error value in a variable called `err`. +Before you use the actual result of the function, you need to check that there was no error. + +To avoid nesting the "happy path" of your code, error cases should be handled first. +We can use `==` and `!=` to compare the error against `nil` and we know there was an error when `err` is not `nil`. + +```go +func processUserFile() error { + file, err := os.Open("./users.csv") + if err != nil { + return err + } + + // do something with file +} +``` + +Most of the time, the error will be returned up the function stack as shown in the example above. +Another way of handling the error could be to log it and continue with some other operation. +It is good practice to either return or log the error, never both. + +Since most functions in Go include an error as one of the return values, you will see/use the `if err != nil` pattern all over the place in Go code. + +## Custom error types If you want your error to include more information than just the error message string, you can create a custom error type. As mentioned before, everything that implements the `error` interface (i.e. has an `Error() string` method) can serve as an error in Go. @@ -68,7 +108,7 @@ type MyCustomError struct { } func (e *MyCustomError) Error() string { - return fmt.Sprintf("%s, Details: %s", e.message, e.details) + return fmt.Sprintf("%s, details: %s", e.message, e.details) } func someFunction() error { @@ -80,36 +120,4 @@ func someFunction() error { } ``` -## Checking for Errors - -Errors can be checked against `nil`. -It is recommended to return early in case of an error to avoid nesting the "happy path" of your code. - -```go -func myFunc() error { - file, err := os.Open("./users.csv") - if err != nil { - // handle the error - return err // or e.g. log it and continue - } - - // do something with file -} -``` - -Since most functions in Go include an error as one of the return values, you will see/use this `if err != nil` pattern all over the place in Go code. - -You can compare error variables with the equality operator `==`: - -```go -var ErrResourceNotFound = errors.New("resource not found") -// ... -if err == ErrResourceNotFound { - // do something about the resource-not-found error -} -``` - -How to check for errors of a specific custom error type will be covered in later concepts. - [stackoverflow-errors]: https://stackoverflow.com/a/50333850 - diff --git a/config.json b/config.json index 45104e446..0e0163d5f 100644 --- a/config.json +++ b/config.json @@ -2195,11 +2195,6 @@ "slug": "pointers", "uuid": "57477550-cecf-4334-903a-85a6c32d61d0" }, - { - "name": "Error Wrapping", - "slug": "error-wrapping", - "uuid": "a118d8e5-d9ed-4612-a0a4-97bcbfa9ab4c" - }, { "name": "Variables", "slug": "variables", diff --git a/exercises/concept/the-farm/.docs/hints.md b/exercises/concept/the-farm/.docs/hints.md index 56220d3c7..f7abf9947 100644 --- a/exercises/concept/the-farm/.docs/hints.md +++ b/exercises/concept/the-farm/.docs/hints.md @@ -1,25 +1,35 @@ # Hints -## 1. Get the amount of fodder from the `weightFodder` function - -- read fodder weight and error from the `FodderAmount` method on the supplied `weightFodder` -- if there is an error, use the equality operator `==` to check if it is `ErrScaleMalfunction` -- for any other error, return that back to the caller, with no computed value for division - -## 2. Return an error for negative fodder - -- use `errors.New(string)` to return a custom error for this case - -## 3. Prevent division by zero - -- use `errors.New(string)` to return a custom error for this case - -## 4. Handle negative cows - -- start by defining a `SillyNephewError` struct type -- add a field to the struct to hold the number of cows -- implement a method `Error() string` with a pointer receiver that returns the the correct text -- [string formatting][concept-string-formatting] can help with creating the error message -- if negative cows are supplied, return a pointer of a `SillyNephewError` error that contains the number of cows - -[concept-string-formatting]: /tracks/go/concepts/string-formatting +## 1. Divide the food evenly + +- Start by writing the function signature of `DivideFood`. + It should accept 2 parameters of type `FodderCalculator` and `int` and return two values of types `float64` and `error`. + Revisit the [functions concept][concept-functions] if you need more information on how to define functions. +- In the function body, call the `FodderAmount` [method][concept-methods] on `FodderCalculator` to fetch the default total amount of fodder for the cows. + It will return the actual result and an error. + Handle the error via an if-statement as it was explained in the introduction. +- After that, call the `FatteningFactor` method and handle the error return value as before. +- Now that you have the fodder amount and the factor, you can calculate the final result. + You need to divide the fodder by the number of cows (revisit [numbers] for hints on type conversion) and multiply with the factor. Check the introduction for what to return as the error value in case of success. + +## 2. Check the number of cows + +- `ValidateInputAndDivideFood` has the same function signature as `DivideFood`. +- Since you want to return early in case of an error in Go, you first check whether the number of cows is less or equal than 0 with an if-statement. +- If it is, you return an error that you created with `errors.New`. + Make sure the message matches the instructions. +- If the number of cows is valid, you can proceed to call the existing `DivideFood` function from task 1. + +## 3. Improve the error handling + +- Start by creating the `InvalidCowsError` [struct][concept-structs] with two unexported fields that hold the number of cows and the message. +- Next, define the `Error` method on that struct (with a pointer receiver). Revisit the exercise introduction for help on how to do this. +- Now you can work on the `ValidateNumberOfCows` function. + Depending on the number of cows ([if-statement][concept-conditionals]), it should create and return a new instance of the `InvalidCowsError` and set the correct message while doing so. + If the number of cows was valid, `nil` should be returned. + +[concept-methods]: /tracks/go/concepts/methods +[concept-functions]: /tracks/go/concepts/functions +[concept-numbers]: /tracks/go/concepts/numbers +[concept-structs]: /tracks/go/concepts/structs +[concept-conditionals]: /tracks/go/concepts/conditionals-if \ No newline at end of file diff --git a/exercises/concept/the-farm/.docs/instructions.md b/exercises/concept/the-farm/.docs/instructions.md index 0aa69f45f..b0b789aab 100644 --- a/exercises/concept/the-farm/.docs/instructions.md +++ b/exercises/concept/the-farm/.docs/instructions.md @@ -2,73 +2,96 @@ The day you waited so long finally came and you are now the proud owner of a beautiful farm in the Alps. -You still do not like to wake up too early in the morning to feed your cows and because you are an excellent engineer, you build a food dispenser, the `FEED-M-ALL`. +You still do not like waking up too early in the morning to feed your cows. +Because you are an excellent engineer, you build a food dispenser, the `FEED-M-ALL`. -The last thing required in order to finish your project, is a piece of code that, given the number of cows and the amount of fodder for the day, does a division so each cow has the same quantity: you need to avoid conflicts, cows are very sensitive. +The last thing required in order to finish your project, is a piece of code that calculates the amount of fodder that each cow should get. +It is important that each cow receives the same amount, you need to avoid conflicts. +Cows are very sensitive. -Depending on the day, some cows prefer to eat fresh grass instead of fodder, sometime no cows at all want to eat fodder. -While this is good for your pocket, you want to catch the division by zero returning an error. - -Also, your silly nephew (who has just learned about negative numbers) sometimes will say that there are a negative number of cows. -You love your nephew so you want to return a helpful error when he does that. - -## 1. Get the amount of fodder from the `FodderAmount` method - -You will be passed a value that fulfills the `WeightFodder` interface. -`WeightFodder` includes a method called `FodderAmount` that returns the amount of fodder available and possibly an error. +Luckily, you don't need to work out all the formulas for calculating fodder amounts yourself. +You use some mysterious external library that you found on the internet. +It is supposed to result in the happiest cows. +The library exposes a type that fulfils the following interface. +You will rely on this in the code you write yourself. ```go -// twentyFodderNoError says there are 20.0 fodder -fodder, err := DivideFood(twentyFodderNoError, 10) -// fodder == 2.0 -// err == nil +type FodderCalculator interface { + FodderAmount(int) (float64, error) + FatteningFactor() (float64, error) +} ``` -If `ErrScaleMalfunction` is returned by `FodderAmount` and the fodder amount is positive, double the fodder amount returned by `FodderAmount` before dividing it equally between the cows. -For any other error besides `ErrScaleMalfunction`, return 0 and the error. +As you work on your code, you will improve the error handling to make it more robust and easier to debug later on when you use it in your daily farm live. -```go -// twentyFodderWithErrScaleMalfunction says there are 20.0 fodder and a ErrScaleMalfunction -fodder, err := DivideFood(twentyFodderWithErrScaleMalfunction, 10) -// fodder == 4.0 -// err == nil -``` +## 1. Divide the food evenly + +First of all, you focus on writing the code that is needed to calculate the amount of fodder per cow. + +Implement a function `DivideFood` that accepts a `FodderCalculator` and a number of cows as an integer as arguments. +*For this task, you assume the number of cows passed in is always greater than zero.* +The function should return the amount of food per cow as a `float64` or an error if one occurred. -## 2. Return an error for negative fodder +To make the calculation, you first need to retrieve the total amount of fodder for all the cows. +This is done by calling the `FodderAmount` method and passing the number of cows. +Additionally, you need a factor that this amount needs to be multiplied with. +You get this factor via calling the `FatteningFactor` method. +With these two values and the number of cows, you can now calculate the amount of food per cow (as a `float64`). +That is what should be returned from the `DivideFood` function. -If the scale is broken and returning negative amounts of fodder, return an error saying "negative fodder" only if `FodderAmount` returned `ErrScaleMalfunction` or nil : +If one of the methods you call returns an error, the execution should stop and that error should be returned (as is) from the `DivideFood` function. ```go -// negativeFiveFodder says there are -5.0 fodder -fodder, err := DivideFood(negativeFiveFodder, 10) -// fodder == 0.0 -// err.Error() == "negative fodder" +// For this example, we assume FodderAmount returns 50 +// and FatteningFactor returns 1.5. +DivideFood(fodderCalculator, 5) +// => 15 + +// Now assuming FodderAmount returns an error with message "something went wrong". +DivideFood(fodderCalculator, 5) +// => 0 "something went wrong" ``` -## 3. Prevent division by zero +## 2. Check the number of cows -After getting the fodder amount from `weightFodder`, prevent a division by zero when there are no cows at all by returning an error saying "division by zero": +While working on the first task above, you realized that the external library you use is not as high-quality as you thought it would be. +For example, it cannot properly handle invalid inputs. +You want to work around this limitation by adding a check for the input value in your own code. + +Write a function `ValidateInputAndDivideFood` that has the same signature as `DivideFood` above. + +- If the number of cows passed in is greater than 0, the function should call `DivideFood` and return the results of that call. +- If the number of cows is 0 or less, the function should return an error with message `"invalid number of cows"`. ```go -// twentyFodderNoError says there are 20.0 fodder -fodder, err := DivideFood(twentyFodderNoError, 0) -// fodder == 0.0 -// err.Error() == "division by zero" +ValidateInputAndDivideFood(fodderCalculator, 5) +// => 15 + +ValidateInputAndDivideFood(fodderCalculator, -2) +// => 0 "invalid number of cows" ``` -## 4. Handle negative cows +## 3. Improve the error handling -Define a custom error type called `SillyNephewError`. -It should be returned in case the number of cows is negative. +Checking the number of cows before passing it along was a good move but you are not quite happy with the unspecific error message. +You decide to do better by creating a custom error type called `InvalidCowsError`. -The error message should include the number of cows that was passed as argument. -You can see the format of the error message in the example below. +The custom error should hold the number of cows (`int`) and a custom message (`string`) and the `Error` method should serialize the data in the following format: +```txt +{number of cows} cows are invalid: {custom message} +``` + +Equipped with your custom error, implement a function `ValidateNumberOfCows` that accepts the number of cows as an integer and returns an error (or nil). -Note that if both a "negative fodder" error and a `SillyNephewError` could be returned, the "negative fodder" error takes precedence. +- If the number of cows is less than 0, the function returns an `InvalidCowsError` with the custom message set to `"there are no negative cows"`. +- If the number of cows is 0, the function returns an `InvalidCowsError` with the custom message set to `"no cows don't need food"`. +- Otherwise, the function returns `nil` to indicate that the validation was successful. ```go -// twentyFodderNoError says there are 20.0 fodder -fodder, err := DivideFood(twentyFodderNoError, -5) -// fodder == 0.0 -// err.Error() == "silly nephew, there cannot be -5 cows" +err := ValidateNumberOfCows(-5) +err.Error() +// => "-5 cows are invalid: there are no negative cows" ``` + +After the hard work of setting up this validation function, you notice it is already evening and you leave your desk to enjoy the sunset over the mountains. +You leave the task of actually adding the new validation function to your code for another day. \ No newline at end of file diff --git a/exercises/concept/the-farm/.docs/introduction.md b/exercises/concept/the-farm/.docs/introduction.md index ea0e1ae7b..0479311ef 100644 --- a/exercises/concept/the-farm/.docs/introduction.md +++ b/exercises/concept/the-farm/.docs/introduction.md @@ -1,49 +1,64 @@ # Introduction +## The error interface + Error handling is **not** done via exceptions in Go. Instead, errors are normal _values_ of types that implement the built-in `error` interface. +The `error` interface is very minimal. +It contains only one method `Error()` that returns the error message as a string. ```go type error interface { - Error() string + Error() string } ``` -This means that any type which implements an one simple method `Error()` that returns a `string` implements the `error` interface. -This allows a function with return type `error` to return values of different types as long as all of them satisfy the `error` interface. +Every time you define a function in which an error could happen during the execution that needs to reach the caller, you need to include `error` as one of the return types. +If the function has multiple return values, by convention `error` is always the last one. + +```go +func DoSomething() (int, error) { + // ... +} +``` -## Creating and Returning Errors +## Creating and returning a simple error You do not have to always implement the error interface yourself. To create a simple error, you can use the `errors.New()` function that is part of the standard library package `errors`. -These error variables are also called _sentinel errors_ and by convention their names should start with `Err` or `err` (depending on whether they are exported or not). -You should use error variables instead of directly writing `errors.New` in cases where you use an error multiple times or where you want the consumer of your package to be able to check for the specific error. +The only thing you need to pass in is the error message as a string, and `errors.New()` will take care of creating a value that contains your message and implements the `error` interface. -```go -import "errors" +If the function returns an error, it is good practice to return the zero value for all other return parameters: -var ErrSomethingWrong = errors.New("something went wrong") -ErrSomethingWrong.Error() // returns "something went wrong" +```go +func DoSomething() (SomeStruct, int, error) { + // ... + return SomeStruct{}, 0, errors.New("failed to calculate result") +} ``` -An error is by convention the last value returned in a function with multiple return values. -If the function returns an error, it should always return the zero value for other returned values: +~~~~exercism/caution +You should not assume that all functions return zero values for other return values if an error is present. +It is best practice to assume that it is not safe to use any of the other return values if an error occurred. +The only exceptions are cases where the documentation clearly states that other returns values are meaningful in case of an error. +~~~~ + +If you want to use such a simple error in multiple places, you should declare a variable for the error instead of using `errors.New` in-line. +By convention, the name of the variable should start with `Err` or `err` (depending on whether it is exported or not). +These error variables are often called _sentinel errors_. ```go import "errors" -// Do this: -func GoodFoo() (int, error) { - return 0, errors.New("Error") -} +var ErrNotFound = errors.New("resource was not found") -// Not this: -func BadFoo() (int, error) { - return 10, errors.New("Error") +func DoSomething() error { + // ... + return ErrNotFound } ``` -Return `nil` for the error when there are no errors: +Return `nil` for the error to signal that there were no errors during the function execution: ```go func Foo() (int, error) { @@ -51,7 +66,32 @@ func Foo() (int, error) { } ``` -## Custom Error Types +## Error checking + +If you call a function that returns an error, it is common to store the error value in a variable called `err`. +Before you use the actual result of the function, you need to check that there was no error. + +To avoid nesting the "happy path" of your code, error cases should be handled first. +We can use `==` and `!=` to compare the error against `nil` and we know there was an error when `err` is not `nil`. + +```go +func processUserFile() error { + file, err := os.Open("./users.csv") + if err != nil { + return err + } + + // do something with file +} +``` + +Most of the time, the error will be returned up the function stack as shown in the example above. +Another way of handling the error could be to log it and continue with some other operation. +It is good practice to either return or log the error, never both. + +Since most functions in Go include an error as one of the return values, you will see/use the `if err != nil` pattern all over the place in Go code. + +## Custom error types If you want your error to include more information than just the error message string, you can create a custom error type. As mentioned before, everything that implements the `error` interface (i.e. has an `Error() string` method) can serve as an error in Go. @@ -68,7 +108,7 @@ type MyCustomError struct { } func (e *MyCustomError) Error() string { - return fmt.Sprintf("%s, Details: %s", e.message, e.details) + return fmt.Sprintf("%s, details: %s", e.message, e.details) } func someFunction() error { @@ -80,36 +120,4 @@ func someFunction() error { } ``` -## Checking for Errors - -Errors can be checked against `nil`. -It is recommended to return early in case of an error to avoid nesting the "happy path" of your code. - -```go -func myFunc() error { - file, err := os.Open("./users.csv") - if err != nil { - // handle the error - return err // or e.g. log it and continue - } - - // do something with file -} -``` - -Since most functions in Go include an error as one of the return values, you will see/use this `if err != nil` pattern all over the place in Go code. - -You can compare error variables with the equality operator `==`: - -```go -var ErrResourceNotFound = errors.New("resource not found") -// ... -if err == ErrResourceNotFound { - // do something about the resource-not-found error -} -``` - -How to check for errors of a specific custom error type will be covered in later concepts. - [stackoverflow-errors]: https://stackoverflow.com/a/50333850 - diff --git a/exercises/concept/the-farm/.meta/config.json b/exercises/concept/the-farm/.meta/config.json index 4114c5635..861bb1f62 100644 --- a/exercises/concept/the-farm/.meta/config.json +++ b/exercises/concept/the-farm/.meta/config.json @@ -1,11 +1,10 @@ { "authors": [ "brugnara", - "jmrunkle" - ], - "contributors": [ + "jmrunkle", "junedev" ], + "contributors": [], "files": { "solution": [ "the_farm.go" @@ -23,5 +22,8 @@ "go.mod" ] }, - "blurb": "Learn about errors and interfaces while feeding cows on the farm." + "blurb": "Learn about error handling in Go while feeding cows on the farm.", + "custom": { + "taskIdsEnabled": true + } } diff --git a/exercises/concept/the-farm/.meta/design.md b/exercises/concept/the-farm/.meta/design.md index d566e65be..f949d6b4b 100644 --- a/exercises/concept/the-farm/.meta/design.md +++ b/exercises/concept/the-farm/.meta/design.md @@ -2,40 +2,33 @@ ## Learning objectives -- Know how to create an error variable -- Know how to create a custom error type -- Know how to return an error -- Know how to check for errors -- Learn about the "return early" best practice +- Understand what an error is in Go and that error handling is not done via exceptions +- Know how and where to return an error from a function and how to signal "no error" and what to set for other return values +- Using `errors.New` to create a simple error +- Checking for errors and returning early +- Creating a custom error type ## Out of Scope -The following topics will be introduced later and should therefore not be part of this concept exercise. +This exercise is deliberately kept simple (many aspects left for later exercises) to make sure students properly digest what errors are and the basic mechanics of how to work with them. This is very different in Go compared to many other languages so it is important is sticks. The exercise still has enough content to unlock -- `error-wrapping` (`errors.Is`, `errors.As`) -- `type-assertions` +The following topics will be introduced later and should therefore not be part of this concept exercise: + +- checking for specific errors or error types +- error wrapping (`fmt.Errorf("...%w")`, `errors.Is` vs. `==`, `errors.As` vs. type assertion) ## Concepts The Concepts this exercise unlocks are: -- `interfaces` - `errors` ## Prerequisites +- `interfaces` to understand the error interface - `conditionals-if` for the error checking - `functions` for learning about multiple return values - `structs` to be able to create a custom error type - `methods` to understand interfaces - `string-formatting` to implement the `Error() string` method - `packages` to understand how to import `errors` - -## Analyzer - -This exercise could benefit from the following rules in the [analyzer][analyzer]. - -- Check that the student actually implemented a custom error type `SillyNephewError` -- Check that the `Error() string` method has a pointer receiver - -[analyzer]: https://github.com/exercism/go-analyzer diff --git a/exercises/concept/the-farm/.meta/exemplar.go b/exercises/concept/the-farm/.meta/exemplar.go index 68515b936..ac4bcb61f 100644 --- a/exercises/concept/the-farm/.meta/exemplar.go +++ b/exercises/concept/the-farm/.meta/exemplar.go @@ -5,34 +5,60 @@ import ( "fmt" ) -// SillyNephewError should be returned if your nephew thinks there are negative cows. -type SillyNephewError struct { - Cows int +// DivideFood uses FodderCalculator methods to determine the amount of fodder each cow +// should get. +func DivideFood(fodderCalculator FodderCalculator, cows int) (float64, error) { + fodder, err := fodderCalculator.FodderAmount(cows) + if err != nil { + return 0, err + } + + factor, err := fodderCalculator.FatteningFactor() + if err != nil { + return 0, err + } + + return fodder / float64(cows) * factor, nil +} + +// ValidateInputAndDivideFood does the same as DivideFood but it checks that +// the number of cows is positive first and returns an error if not. +func ValidateInputAndDivideFood(fodderCalculator FodderCalculator, cows int) (float64, error) { + if cows <= 0 { + return 0, errors.New("invalid number of cows") + } + + return DivideFood(fodderCalculator, cows) +} + +// InvalidCowsError is a custom error type that can be used +// to signal invalid input. +type InvalidCowsError struct { + cows int + message string } // Error implements the error interface. -func (sn *SillyNephewError) Error() string { - return fmt.Sprintf("silly nephew, there cannot be %d cows", sn.Cows) +func (e *InvalidCowsError) Error() string { + return fmt.Sprintf("%d cows are invalid: %s", e.cows, e.message) } -// DivideFood computes the fodder amount per cow for the given cows. -func DivideFood(weightFodder WeightFodder, cows int) (float64, error) { - fodder, err := weightFodder.FodderAmount() - if err != nil && err != ErrScaleMalfunction { - return 0, err - } - if err == ErrScaleMalfunction { - fodder *= 2 - } - if fodder < 0 { - return 0, errors.New("negative fodder") +// ValidateNumberOfCows returns an InvalidCowsError when the number of +// cows passed in was not positive and nil otherwise. +func ValidateNumberOfCows(cows int) error { + if cows < 0 { + return &InvalidCowsError{ + cows: cows, + message: "there are no negative cows", + } } + if cows == 0 { - return 0, errors.New("division by zero") - } - if cows < 0 { - return 0, &SillyNephewError{Cows: cows} + return &InvalidCowsError{ + cows: cows, + message: "no cows don't need food", + } } - return fodder / float64(cows), nil + return nil } diff --git a/exercises/concept/the-farm/the_farm.go b/exercises/concept/the-farm/the_farm.go index bb496155d..7b5a0985b 100644 --- a/exercises/concept/the-farm/the_farm.go +++ b/exercises/concept/the-farm/the_farm.go @@ -1,10 +1,16 @@ package thefarm -// See types.go for the types defined for this exercise. +// TODO: define the 'DivideFood' function -// TODO: Define the SillyNephewError type here. +// TODO: define the 'ValidateInputAndDivideFood' function -// DivideFood computes the fodder amount per cow for the given cows. -func DivideFood(weightFodder WeightFodder, cows int) (float64, error) { - panic("Please implement DivideFood") -} +// TODO: define the 'ValidateNumberOfCows' function + +// Your first steps could be to read through the tasks, and create +// these functions with their correct parameter lists and return types. +// The function body only needs to contain `panic("")`. +// +// This will make the tests compile, but they will fail. +// You can then implement the function logic one by one and see +// an increasing number of tests passing as you implement more +// functionality. diff --git a/exercises/concept/the-farm/the_farm_test.go b/exercises/concept/the-farm/the_farm_test.go index dcaa4a3ac..dc8319563 100644 --- a/exercises/concept/the-farm/the_farm_test.go +++ b/exercises/concept/the-farm/the_farm_test.go @@ -2,238 +2,251 @@ package thefarm import ( "errors" + "math" + "reflect" "testing" ) -type testWeightFodder struct { - fodder float64 - err error +const precision = 1e-5 + +var errDeterminingAmount = errors.New("amount could not be determined") +var errDeterminingFactor = errors.New("factor could not be determined") + +type testFodderCalculator struct { + amount float64 + amountErr error + factor float64 + factorErr error +} + +func (fc testFodderCalculator) FodderAmount(int) (float64, error) { + return fc.amount, fc.amountErr } -func (wf testWeightFodder) FodderAmount() (float64, error) { - return wf.fodder, wf.err +func (fc testFodderCalculator) FatteningFactor() (float64, error) { + return fc.factor, fc.factorErr } +// testRunnerTaskID=1 func TestDivideFood(t *testing.T) { - nonScaleError := errors.New("non-scale error") tests := []struct { - description string - weightFodder WeightFodder - weightFodderDescription string - cows int - wantAmount float64 - wantErr error + name string + fodderCalculator FodderCalculator + cows int + wantAmount float64 + wantErr error }{ { - description: "100 fodder for 10 cows", - weightFodder: testWeightFodder{fodder: 100, err: nil}, - weightFodderDescription: "100 fodder, no error", - cows: 10, - wantAmount: 10, - wantErr: nil, - }, - { - description: "10 fodder for 10 cows", - weightFodder: testWeightFodder{fodder: 10, err: nil}, - weightFodderDescription: "10 fodder, no error", - cows: 10, - wantAmount: 1, - wantErr: nil, + name: "success, simple inputs", + fodderCalculator: testFodderCalculator{ + amount: 100, + factor: 1, + }, + cows: 10, + wantAmount: 10, + wantErr: nil, }, { - description: "10.5 fodder for 2 cows", - weightFodder: testWeightFodder{fodder: 10.5, err: nil}, - weightFodderDescription: "10.5 fodder, no error", - cows: 2, - wantAmount: 5.25, - wantErr: nil, + name: "success, decimal inputs", + fodderCalculator: testFodderCalculator{ + amount: 60.5, + factor: 1.3, + }, + cows: 5, + wantAmount: 15.73, + wantErr: nil, }, { - description: "5 fodder for 2 cows", - weightFodder: testWeightFodder{fodder: 5, err: nil}, - weightFodderDescription: "5 fodder, no error", - cows: 2, - wantAmount: 2.5, - wantErr: nil, + name: "error when retrieving fodder amount", + fodderCalculator: testFodderCalculator{ + amountErr: errDeterminingAmount, + }, + cows: 10, + wantAmount: 0, + wantErr: errDeterminingAmount, }, { - description: "0 fodder for 2 cows", - weightFodder: testWeightFodder{fodder: 0, err: nil}, - weightFodderDescription: "0 fodder, no error", - cows: 2, - wantAmount: 0, - wantErr: nil, - }, - { - description: "Generic error from the scale is returned", - weightFodder: testWeightFodder{fodder: 10, err: nonScaleError}, - weightFodderDescription: "10 fodder, generic error", - cows: 2, - wantAmount: 0, - wantErr: nonScaleError, + name: "error when retrieving fattening factor", + fodderCalculator: testFodderCalculator{ + factorErr: errDeterminingFactor, + }, + cows: 10, + wantAmount: 0, + wantErr: errDeterminingFactor, }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotAmount, gotErr := DivideFood(tt.fodderCalculator, tt.cows) + + if gotErr != tt.wantErr { + msg := "expected error %q but got %q\n" + + "(if expected and actual look the same that means the message matches but the errors are not identical)" + t.Fatalf(msg, tt.wantErr, gotErr) + } + + if math.Abs(gotAmount-tt.wantAmount) > precision { + t.Fatalf("expected amount %v but got %v", tt.wantAmount, gotAmount) + } + }) + } +} + +// testRunnerTaskID=2 +func TestValidateInputAndDivideFood(t *testing.T) { + tests := []struct { + name string + fodderCalculator FodderCalculator + cows int + wantAmount float64 + wantErr error + }{ { - description: "Negative fodder with generic error from the scale is returned", - weightFodder: testWeightFodder{fodder: -10, err: nonScaleError}, - weightFodderDescription: "-10 fodder, generic error", - cows: 2, - wantAmount: 0, - wantErr: nonScaleError, + name: "negative cows are invalid", + fodderCalculator: testFodderCalculator{ + amount: 10, + factor: 1, + }, + cows: -10, + wantAmount: 0, + wantErr: errors.New("invalid number of cows"), }, { - description: "Scale returns 10 with ErrScaleMalfunction for 2 cows", - weightFodder: testWeightFodder{fodder: 10, err: ErrScaleMalfunction}, - weightFodderDescription: "10 fodder, ErrScaleMalfunction", - cows: 2, - wantAmount: 10, - wantErr: nil, + name: "zero cows are invalid", + fodderCalculator: testFodderCalculator{ + amount: 10, + factor: 1, + }, + cows: 0, + wantAmount: 0, + wantErr: errors.New("invalid number of cows"), }, { - description: "Scale returns 1 with ErrScaleMalfunction for 10 cows", - weightFodder: testWeightFodder{fodder: 5, err: ErrScaleMalfunction}, - weightFodderDescription: "5 fodder, ErrScaleMalfunction", - cows: 10, - wantAmount: 1, - wantErr: nil, + name: "success, simple inputs", + fodderCalculator: testFodderCalculator{ + amount: 100, + factor: 1, + }, + cows: 10, + wantAmount: 10, + wantErr: nil, }, { - description: "Negative fodder", - weightFodder: testWeightFodder{fodder: -1, err: nil}, - weightFodderDescription: "-1 fodder, no error", - cows: 2, - wantAmount: 0, - wantErr: errors.New("negative fodder"), + name: "success, decimal inputs", + fodderCalculator: testFodderCalculator{ + amount: 60.5, + factor: 1.3, + }, + cows: 5, + wantAmount: 15.73, + wantErr: nil, }, { - description: "Negative fodder with ScaleError", - weightFodder: testWeightFodder{fodder: -1, err: ErrScaleMalfunction}, - weightFodderDescription: "-1 fodder, ScaleError", - cows: 2, - wantAmount: 0, - wantErr: errors.New("negative fodder"), + name: "error when retrieving fodder amount", + fodderCalculator: testFodderCalculator{ + amountErr: errDeterminingAmount, + }, + cows: 10, + wantAmount: 0, + wantErr: errDeterminingAmount, }, { - description: "Zero cows", - weightFodder: testWeightFodder{fodder: 100, err: nil}, - weightFodderDescription: "100 fodder, no error", - cows: 0, - wantAmount: 0, - wantErr: errors.New("division by zero"), + name: "error when retrieving fattening factor", + fodderCalculator: testFodderCalculator{ + factorErr: errDeterminingFactor, + }, + cows: 10, + wantAmount: 0, + wantErr: errDeterminingFactor, }, } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - gotAmount, gotErr := DivideFood(test.weightFodder, test.cows) - switch { - case gotAmount != test.wantAmount: - t.Errorf( - "DivideFood(weightFodder(%v), %v) got (%v, %v) wanted (%v, %v)", - test.weightFodderDescription, - test.cows, - gotAmount, - gotErr, - test.wantAmount, - test.wantErr, - ) - case gotErr != nil && test.wantErr == nil: - t.Errorf( - "DivideFood(weightFodder(%v), %v) got an unexpected error (%v)", - test.weightFodderDescription, - test.cows, - gotErr, - ) - case gotErr == nil && test.wantErr != nil: - t.Errorf( - "DivideFood(weightFodder(%v), %v) got no error, but wanted an error (%v)", - test.weightFodderDescription, - test.cows, - test.wantErr, - ) - case !(gotErr == test.wantErr || gotErr.Error() == test.wantErr.Error()): - t.Errorf( - "DivideFood(weightFodder(%v), %v) got error (%v), but wanted error (%v)", - test.weightFodderDescription, - test.cows, - gotErr, - test.wantErr, - ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotAmount, gotErr := ValidateInputAndDivideFood(tt.fodderCalculator, tt.cows) + + if tt.wantErr != nil && gotErr != nil && tt.wantErr.Error() != gotErr.Error() { + t.Fatalf("expected error %q but got %q", tt.wantErr, gotErr) + } + + if tt.wantErr == nil && gotErr != nil { + t.Fatalf("expected nil but got error %q", gotErr) + } + + if tt.wantErr != nil && gotErr == nil { + t.Fatalf("expected error %q but got nil", tt.wantErr) + } + + if math.Abs(gotAmount-tt.wantAmount) > precision { + t.Fatalf("expected amount %v but got %v", tt.wantAmount, gotAmount) } }) } } -func TestDivideFoodSillyNephewError(t *testing.T) { +// testRunnerTaskID=3 +func TestValidateNumberOfCows(t *testing.T) { tests := []struct { - description string - cows int - wantErrMsg string + name string + cows int + errorExpected bool + wantErrMsg string }{ { - description: "Negative ten cows", - cows: -10, - wantErrMsg: "silly nephew, there cannot be -10 cows", + name: "big positive number of cows", + cows: 80, + errorExpected: false, + }, + { + name: "small positive number of cows", + cows: 2, + errorExpected: false, }, { - description: "Negative seven cows", - cows: -7, - wantErrMsg: "silly nephew, there cannot be -7 cows", + name: "big negative number of cows", + cows: -20, + errorExpected: true, + wantErrMsg: "-20 cows are invalid: there are no negative cows", + }, + { + name: "small negative number of cows", + cows: -1, + errorExpected: true, + wantErrMsg: "-1 cows are invalid: there are no negative cows", + }, + { + name: "zero cows", + cows: 0, + errorExpected: true, + wantErrMsg: "0 cows are invalid: no cows don't need food", }, } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - weightFodder := testWeightFodder{fodder: 100, err: nil} - gotAmount, gotErr := DivideFood(weightFodder, test.cows) - if gotAmount != 0 { - t.Errorf( - "DivideFood(weightFodder(%v), %v) got amount %v, but wanted amount 0", - "100 fodder, no error", - test.cows, - gotAmount, - ) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotErr := ValidateNumberOfCows(tt.cows) + + if tt.errorExpected && gotErr == nil { + t.Fatalf("an error was expected but got nil") } - if gotErr.Error() != test.wantErrMsg { - t.Errorf( - "DivideFood(weightFodder(%v), %v) got error msg %q, but wanted error msg %q", - "100 fodder, no error", - test.cows, - gotErr.Error(), - test.wantErrMsg, - ) + + if tt.errorExpected && tt.wantErrMsg != gotErr.Error() { + t.Fatalf("want error %q but got %q", tt.wantErrMsg, gotErr) + } + + if !tt.errorExpected && gotErr != nil { + t.Fatalf("expected nil but got %q", gotErr) } }) } } -func TestDivideFoodNegativeFodderErrorPrecedence(t *testing.T) { - tests := []struct { - description string - weightFodder WeightFodder - weightFodderDescription string - cows int - wantAmount float64 - wantErr error - }{ - { - description: "Negative fodder and negative cows", - cows: -5, - wantAmount: 0, - wantErr: errors.New("negative fodder"), - weightFodder: testWeightFodder{fodder: -1, err: nil}, - weightFodderDescription: "-1 fodder, no error", - }, - } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - _, gotErr := DivideFood(test.weightFodder, test.cows) - if !(gotErr == test.wantErr || gotErr.Error() == test.wantErr.Error()) { - t.Errorf( - "DivideFood(weightFodder(%v), %v) got error (%v), but wanted error (%v)", - test.weightFodderDescription, - test.cows, - gotErr, - test.wantErr, - ) - } - }) +// testRunnerTaskID=3 +func TestValidateNumberOfCows_PointerReturned(t *testing.T) { + gotErr := ValidateNumberOfCows(-10) + + if reflect.ValueOf(gotErr).Kind() != reflect.Ptr { + t.Fatalf("expected pointer but got %v", reflect.ValueOf(gotErr).Kind()) } } diff --git a/exercises/concept/the-farm/types.go b/exercises/concept/the-farm/types.go index de1030949..361fac454 100644 --- a/exercises/concept/the-farm/types.go +++ b/exercises/concept/the-farm/types.go @@ -1,15 +1,10 @@ package thefarm -import ( - "errors" -) +// This file contains types used in the exercise and tests and should not be modified. -// This file contains types used in the exercise but should not be modified. - -// WeightFodder returns the amount of available fodder. -type WeightFodder interface { - FodderAmount() (float64, error) +// FodderCalculator provides helper methods to determine the optimal +// amount of fodder to feed cows. +type FodderCalculator interface { + FodderAmount(int) (float64, error) + FatteningFactor() (float64, error) } - -// ErrScaleMalfunction indicates an error with the scale. -var ErrScaleMalfunction = errors.New("sensor error")