diff --git a/posts/unkiwii/01.simple-is-not-easy.md b/posts/unkiwii/01.simple-is-not-easy.md new file mode 100644 index 00000000..906dc626 --- /dev/null +++ b/posts/unkiwii/01.simple-is-not-easy.md @@ -0,0 +1,198 @@ +--- +title: 'Go is simple, not easy' +published: false +description: Why simple code matters +tags: 'go, golang, programming, refactoring' +cover_image: ./assets/simple-is-not-easy-cover.png +--- + +When you use Go you will find out that is easy to read, write and run your programs. But that is only the beginning and keeping that idea that "this is an easy language" would backfire. + +## Go is not easy + +> The function of good software is to make the complex appear to be simple. +> -- Grady Booch + +As almost everything in life: hard problems are hard to solve and easy problems are easy to solve. Same with Go. + +If the code you wrote was easy then you solved easy problems. Almost any programming language would look easy in that case. + +With Go, hard problems are hard to solve, but writing simple solutions are even harder. + +## Go can be simple + +> If I had more time I would have wrote a smaller post. + +Many things in Go try to be as simple as possible, most features of the language took time to design and implement in the most simple way possible. + +Features that keep Go simple: + + 1. Only one loop: `for` + 2. Few keywords (25 at the time of writing) + 3. No implicit conversions between types + 4. Interfaces are satisfied implicitly, there's no *implements* statement + 5. `go fmt`: No one's favorite, yet everyone's favorite + 6. Automatic memory management thought garbage collection + +But the Go is also a general purpose language and any user can bend it to his will. We need to be aware of that and work hard to keep our code simple. + +## Make it simple + +The next example is an exaggeration but not so different from production code I saw in the past. + +```go +func NewUser(name string, age int) (User, error) { + var user User + var nameError error + var ageError error + var resultError error + + if nameRegex.MatchString(name) { + user.name = name + } else { + nameError = fmt.Errorf("invalid user name %s", name) + } + + if age >= 0 && age <= 150 { + user.age = age + } else { + ageError = fmt.Errorf("invlid user age %d", age) + } + + if nameError != nil && ageError != nil { + var errlist ErrList // custom error list + errlist.Add(nameError) + errlist.Add(ageError) + resultError = errlist.Result() + } else if nameError != nil { + resultError = nameError + } else if ageError != nil { + resultError = ageError + } else { + resultError = nil + } + + return user, resultError +} +``` + +It was easy to write and you can have arguments saying is easy to read, as we did only this: + + 1. Define variables we need + 2. Check for a valid name + 3. Check for a valid age + 4. Handle any error + 5. Return the result + +Easy right? But there's something wrong about this, something *smells bad* + +Let's do some refactoring to make it simple. + +But before we refactor, a note on variables and checks: + + 1. Go can help you with variables: you don't need to define variables before you use them. **Define variables when and where you need them.** + 2. You don't need to write every check and validation by hand everywhere. **Write simple functions and combine them if you must.** + +### First refactor + +We could move some code to their own functions and use early returns to avoid all that `if, else` mess. + +```go +func NewUser(name string, age int) (User, error) { + var user User + + if !isValidName(name) { + return user, fmt.Errorf("invalid user name %s", name) + } + user.name = name + + if !isValidAge(age) { + return user, fmt.Errorf("invlid user age %d", age) + } + user.age = age + + return user, nil +} + +func isValidName(name string) bool { + return nameRegex.MatchString(name) +} + +func isValidAge(age int) bool { + return age >= 0 && age <= 140 +} + +func isValidUser(user User) bool { + return isValidName(user.name) && isValidAge(user.age) +} +``` + +But we changed the behavior, now it's not the same code anymore. We need to return every error we found, not just the first one. + +### Errors are values + +> Fix the cause, not the symptom. +> -- Steve Maguire + +A common complaint from new Go programmers is that the `if err != nil` idiom is everywhere. They are right, and that's because of code that don't use errors as what they are: [Errors are values](https://www.youtube.com/watch?v=PAAkCSZUG1c&t=973s) + +You don't need to use errors as boolean values (the `err != nil` part) you can pass them around and call methods on them as any other thing. + +```go +func validateName(name string) error { + if !nameRegex.MatchString(name) { + return fmt.Errorf("invalid name %s", name) + } + return nil +} + +func validateAge(age int) error { + if age < 0 && age > 140 { + return fmt.Errorf("invalid age %d", age) + } + return nil +} + +func validateUser(user User) error { + var err ErrList + err.Add(validateName(user.name)) // no need for if err != nil + err.Add(validateAge(user.age)) + return err.Result() +} +``` + +### Final refactor + +Putting everything together this is what we did: + +- Moved validations to their own reusable functions +- Improved error handling code by removing unnecessary `if err != nil` checks + +```go +func NewUser(name string, age int) (User, error) { + user := User{name: name, age: age} + return user, validateUser(user) +} +``` + +## Conclusion + +> Perfection is achieved not when there is nothing more to add, but rather when there is nothing more to take away. +> -- Antoine de Saint-Exupery + +This was a small example on how to write simple code. In real code you may find other kind of complex code and may not be so easy to refactor. + +Simple code is hard to write but a joy to read. Try to write simple code but if you can't do it the first time around that's fine, you are not alone. + +### Bonus + +In Go 1.20 and beyond you can combine errors. Instead of using an custom error list implementation you can use `errors.Join`: + +```go +func validateUser(user User) error { + return errors.Join( + validateName(user.name), + validateAge(user.age), + ) +} +``` diff --git a/posts/unkiwii/assets/simple-is-not-easy-cover.png b/posts/unkiwii/assets/simple-is-not-easy-cover.png new file mode 100644 index 00000000..e7d715ee Binary files /dev/null and b/posts/unkiwii/assets/simple-is-not-easy-cover.png differ