Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions posts/unkiwii/01.simple-is-not-easy.md
Original file line number Diff line number Diff line change
@@ -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),
)
}
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.