mardi 24 mars 2020

What's the best func signature for a function that fetches an object and might not find it?

This is a very similar question to this, but focusing Go implementation.

Let's say you have a function that fetches an object using some ID. The function might not find that object.

Something like:

func FindUserByID(id int) User {
    ....
}

How should you handle a situation where the user is not found?

There's a number of patterns and solutions to follow. Some are common, some are favorable in specific languages.

I need to choose a solution that is suitable in Go (golang), and so I want to go through all the options and get some feedback on what's the best approach here.

Go have some limitations and some features that can be helpful with this issue.

Option 1:

The obvious solution is to have the "Find" function return nil, in case the object wasn't found. As many know, returning nil is unfavorable and forces the caller do check for nil, making "nil" the magic value for "not found".

func FindUserByID(id int) User {
    ...
    if /*user not found*/ {
       return nil
    }
    ...
}

It works. And it's simple.

Option 2:

Returning an exception - "NotFound". Goland does not support exceptions. The only alternative is to have the function to return an error, and check the error in the caller code:

func FindUserByID(id int) (User, error) {
    ...
    return errors.New("NotFound")
}

func Foo() {
    User, err := FindUserByID(123)
    if err.Error() == "NotFound" {
      ...
    }
    ...
}

Since Go does not support exceptions, the above is a code smell, relying on error string.

Option 3:

Separate to 2 different functions: one will check if the object exists, and the other will return it.

func FindUserByID(id int) User {
   ....
}

func IsExist(id int) bool {
   ...
}

The problem with it is:

  1. Checking if the object exists in many cases means also fetching it. So we pay penalty for doing the same operation twice (assume no caching available).
  2. The call to "IsExist" can return true, but "Find" can fail if the object was removed by the time it was called. In high concurrency applications it can happen very often. Which again forces checking nil value from "Find".

Option 4:

Change the name of "Find" to suggest it may return nil. This is common in .Net and the name can be "TryFindByID". But many people hate this pattern and I haven't seen it anywhere else. And anyhow it's still makes the nil value the magic "not exist" mark.

Option 5:

In some languages (java, c++) there's the "Optional" pattern. This makes a clear signature and helps the caller understand she needs to call "isEmpty()" first. Unfortunately this is not available in Go. There are some projects in github (like https://github.com/markphelps/optional) but since Go is limited and does not support returning generic types without casting, it means another compilation step is required to creat an Optional struct for out object, and use that in the function signature.

func FindUserByID(id int) OptionalUser {
   ....
}

func Foo() {
    optionalUser := FindUserByID(123)
    if optionalUser.IsEmpty() {
      ...
    }
    ...
}

But it depends on 3rd parties and adds compilation complexity. It doubles the amount of structs that follow this pattern.

Option 6:

Go support returning multiple values in a function. So the "Find" function can also return a bool value if the object exists.

func FindUserByID(id int) (User, bool) {
    ...
    if /*user not found*/ {
       return nil, false
    }
    ...
}

This seems to be a favorable approach in Go. For example, casting in Go also returns a bool value saying if the operation was successful.

I wonder what's the best approach and to get some feedback on the above options.

Aucun commentaire:

Enregistrer un commentaire