Future design pattern

The Future design pattern (also called Promise) is a quick and easy way to achieve concurrent structures for asynchronous programming. We will take advantage of first class functions in Go to develop Futures.

Description

In short, we will define each possible behavior of an action before executing them in different Goroutines. Node.js uses this approach, providing event-driven programming by default. The idea here is to achieve a fire-and-forget that handles all possible results in an action.

To understand it better, we can talk about a type that has embedded the behavior in case an execution goes well or in case it fails.

Description

In the preceding diagram, the main function launches a Future within a new Goroutine. It won't wait for anything, nor will it receive any progress of the Future. It really fires and forgets it.

The interesting thing here is that we can launch a new Future within a Future and embed as many Futures as we want in the same Goroutine (or new ones). The idea is to take advantage of the result of one Future to launch the next. For example:

Description

Here, we have the same Future. In this case, if the Execute function returned a correct result, the Success function is executed, and only in this case we execute a new Goroutine with another Future inside (or even without a Goroutine).

This is a kind of lazy programming, where a Future could be calling to itself indefinitely or just until some rule is satisfied. The idea is to define the behavior in advance and let the future resolve the possible solutions.

Objectives

With the Future pattern, we can launch many new Goroutines, each with an action and its own handlers. This enables us to do the following:

  • Delegate the action handler to a different Goroutine
  • Stack many asynchronous calls between them (an asynchronous call that calls another asynchronous call in its results)

A simple asynchronous requester

We are going to develop a very simple example to try to understand how a Future works. In this example, we will have a method that returns a string or an error, but we want to execute it concurrently. We have learned ways to do this already. Using a channel, we can launch a new Goroutine and handle the incoming result from the channel.

But in this case, we will have to handle the result (string or error), and we don't want this. Instead, we will define what to do in case of success and what to do in case of error and fire-and-forget the Goroutine.

Acceptance criteria

We don't have functional requirements for this task. Instead, we will have technical requirements for it:

  • Delegate the function execution to a different Goroutine
  • The function will return a string (maybe) or an error
  • The handlers must be already defined before executing the function
  • The design must be reusable

Unit tests

So, as we mentioned, we will use first class functions to achieve this behavior, and we will need three specific types of function:

  • type SuccessFunc func(string): The SuccessFunc function will be executed if everything went well. Its string argument will be the result of the operation, so this function will be called by our Goroutine.
  • type FailFunc func(error): The FailFunc function handles the opposite result, that is, when something goes wrong, and, as you can see, it will return an error.
  • type ExecuteStringFunc func() (string, error): Finally, the ExecuteStringFunc function is a type that defines the operation we want to perform. Maybe it will return a string or an error. Don't worry if this all seems confusing; it will be clearer later.

So, we create the future object, we define a success behavior, we define a fail behavior, and we pass an ExecuteStringFunc type to be executed. In the implementation file, we'll need a new type:

type MaybeString struct {} 

We will also create two tests in the _test.go file:

package future 
 
import ( 
  "errors" 
  "testing" 
  "sync" 
) 
 
func TestStringOrError_Execute(t *testing.T) { 
  future := &MaybeString{} 
  t.Run("Success result", func(t *testing.T) { 
    ... 
  }) 
  t.Run("Error result", func(t *testing.T) { 
  ... 
  }) 
} 

We will define functions by chaining them, as you would usually see in Node.js. Code like this is compact and not particularly difficult to follow:

t.Run("Success result", func(t *testing.T) { 
    future.Success(func(s string) {
        
        t.Log(s)
    
    }).Fail(func(e error) {
        
        t.Fail()
    
    })
 
    future.Execute(func() (string, error) {
        
        return "Hello World!", nil
    
    }) 
}) 

The future.Success function must be defined in the MaybeString structure to accept a SuccessFunc function that will be executed if everything goes correctly and return the same pointer to the future object (so we can keep chaining). The Fail function must also be defined in the MaybeString structure and must accept a FailFunc function to later return the pointer. We return the pointer in both cases so we can define the Fail and the Success or vice versa.

Finally, we use the Execute method to pass an ExecuteStringFunc type (a function that accepts nothing and returns a string or an error). In this case, we return a string and nil, so we expect that the SuccessFunc function will be executed and we log the result to the console. In case that fail function is executed, the test has failed because the FailFunc function shouldn't be executed for a returned nil error.

But we still lack something here. We said that the function must be executed asynchronously in a different Goroutine, so we have to synchronize this test somehow so that it doesn't finish too soon. Again, we can use a channel or a sync.WaitGroup:

t.Run("Success result", func(t *testing.T) { 
    var wg sync.WaitGroup
    wg.Add(1) 
    future.Success(func(s string) { 
      t.Log(s) 
 
      wg.Done() 
    }).Fail(func(e error) { 
      t.Fail() 
       
      wg.Done() 
    }) 
 
    future.Execute(func() (string, error) { 
      return "Hello World!", nil 
    }) 
    wg.Wait() 
  }) 

We have seen WaitGroups before in the previous channel. This WaitGroup is configured to wait for one signal (wg.Add(1)). The Success and Fail methods will trigger the Done() method of the WaitGroup to allow execution to continue and finish testing (that is why the Wait() method is at the end). Remember that each Done() method will subtract one from the WaitGroup, and we have added only one, so our Wait() method will only block until one Done() method is executed.

Using what we know of making a Success result unit test, it's easy to make a Failed result unit test by swapping the t.Fail() method call from the error to success so that the test fails if a call to success is done:

t.Run("Failed result", func(t *testing.T) { 
    var wg sync.WaitGroup
  
    wg.Add(1)
  
    future.Success(func(s string) {
    
        t.Fail()
    
        wg.Done()
  
    }).Fail(func(e error) {
    
        t.Log(e.Error())
    
        wg.Done()
  
    })
  
    future.Execute(func() (string, error) {
    
        return "", errors.New("Error ocurred")
  
    })
  
    wg.Wait() 
}) 

If you are using an IDE like me, your Success, Fail, and Execute method calls must be in red. This is because we lack our method's declaration in the implementation file:

package future 
 
type SuccessFunc func(string) 
type FailFunc func(error) 
type ExecuteStringFunc func() (string, error) 
 
type MaybeString struct { 
  ... 
} 
 
func (s *MaybeString) Success(f SuccessFunc) *MaybeString { 
  return nil 
} 
 
func (s *MaybeString) Fail(f FailFunc) *MaybeString { 
  return nil 
} 
 
func (s *MaybeString) Execute(f ExecuteStringFunc) { 
  ... 
} 

Our test seems ready to execute. Let's try it out:

go test -v .
=== RUN   TestStringOrError_Execute
=== RUN   TestStringOrError_Execute/Success_result
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
testing.(*T).Run(0xc4200780c0, 0x5122e9, 0x19, 0x51d750, 0xc420041d30)
        /usr/lib/go/src/testing/testing.go:647 +0x316
testing.RunTests.func1(0xc4200780c0)
        /usr/lib/go/src/testing/testing.go:793 +0x6d
testing.tRunner(0xc4200780c0, 0xc420041e20)
        /usr/lib/go/src/testing/testing.go:610 +0x81
testing.RunTests(0x51d758, 0x5931e0, 0x1, 0x1, 0x50feb4)
        /usr/lib/go/src/testing/testing.go:799 +0x2f5
testing.(*M).Run(0xc420041ee8, 0xc420014550)
        /usr/lib/go/src/testing/testing.go:743 +0x85
main.main()
        go-design-patterns/future/_test/_testmain.go:54 +0xc6
...continue

Well... the tests have failed, yes... but not in a controllable way. Why is this? We don't have any implementation yet, so no Success or Fail functions are being executed either. Our WaitGroup is waiting forever for a call to the Done() method that will never arrive, so it can't continue and finish the test. That's the meaning of All Goroutines are asleep - deadlock!. In our specific example, it would mean Nobody is going to call Done(), so we are dead!.

Note

Thanks to the Go compiler and the runtime executor, we can detect deadlocks easily. Imagine if Go runtime couldn't detect deadlocks--we would be effectively stuck in a blank screen without knowing what was wrong.

So how can we solve this? Well, an easy way would be with a timeout that calls the Done() method after waiting a while for completion. For this code, it's safe to wait for 1 second because it's not doing long-running operations.

We will declare a timeout function within our test file that waits for a second, then prints a message, sets the test as failed, and lets the WaitGroup continue by calling its Done() method:

func timeout(t *testing.T, wg *sync.WaitGroup) { 
  time.Sleep(time.Second) 
  t.Log("Timeout!") 
 
  t.Fail() 
  wg.Done() 
} 

The final look of each subtest is similar to our previous example of the "Success result":

t.Run("Success result", func(t *testing.T) { 
  var wg sync.WaitGroup 
  wg.Add(1) 
 
  //Timeout! 
  go timeout(t, wg) 
  // ... 
}) 

Let's see what happens when we execute our tests again:

go test -v .
=== RUN   TestStringOrError_Execute
=== RUN   TestStringOrError_Execute/Success_result
=== RUN   TestStringOrError_Execute/Failed_result
--- FAIL: TestStringOrError_Execute (2.00s)
    --- FAIL: TestStringOrError_Execute/Success_result (1.00s)
        future_test.go:64: Timeout!
    --- FAIL: TestStringOrError_Execute/Failed_result (1.00s)
        future_test.go:64: Timeout!
FAIL
exit status 1
FAIL

Our tests failed, but in a controlled way. Look at the end of the FAIL lines--notice how the elapsed time is 1 second because it has been triggered by the timeout, as we can see in the logging messages.

It's time to pass to the implementation.

Implementation

According to our tests, the implementation must take a SuccessFunc, a FailFunc, and an ExecuteStringFunc function in a chained fashion within the MaybeString type and launches the ExecuteStringFunc function asynchronously to call SuccessFunc or FailFunc functions according to the returned result of the ExecuteStringFunc function.

The chain is implemented by storing the functions within the type and returning the pointer to the type. We are talking about our previously declared type methods, of course:

type MaybeString struct { 
  successFunc SuccessFunc 
  failFunc    FailFunc 
} 
 
func (s *MaybeString) Success(f SuccessFunc) *MaybeString { 
  s.successFunc = f 
  return s 
} 
 
func (s *MaybeString) Fail(f FailFunc) *MaybeString { 
  s.failFunc = f 
  return s 
} 

We needed two fields to store the SuccessFunc and FailFunc functions, which are named the successFunc and failFunc fields respectively. This way, calls to the Success and Fail methods simply store their incoming functions to our new fields. They are simply setters that also return the pointer to the specific MaybeString value. These type methods take a pointer to the MaybeString structure, so don't forget to put "*" on MaybeString after the func declaration.

Execute takes the ExecuteStringFunc method and executes it asynchronously. This seems quite simple with a Goroutine, right?

func (s *MaybeString) Execute(f ExecuteStringFunc) { 
  go func(s *MaybeString) { 
    str, err := f() 
    if err != nil { 
      s.failFunc(err) 
    } else { 
      s.successFunc(str) 
    } 
  }(s) 
} 

Looks quite simple because it is simple! We launch the Goroutine that executes the f method (an ExecuteStringFunc) and takes its result--maybe a string and maybe an error. If an error is present, we call the field failFunc in our MaybeString structure. If no error is present, we call the successFunc field. We use a Goroutine to delegate a function execution and error handling so our Goroutine doesn't have to do it.

Let's run unit tests now:

go test -v .
=== RUN   TestStringOrError_Execute
=== RUN   TestStringOrError_Execute/Success_result
=== RUN   TestStringOrError_Execute/Failed_result
--- PASS: TestStringOrError_Execute (0.00s)
    --- PASS: TestStringOrError_Execute/Success_result (0.00s)
        future_test.go:21: Hello World!
    --- PASS: TestStringOrError_Execute/Failed_result (0.00s)
        future_test.go:49: Error ocurred
PASS
ok 

Great! Look how the execution time is now nearly zero, so our timeouts have not been executed (actually, they were executed, but the tests already finished and their result was already stated).

What's more, now we can use our MaybeString type to asynchronously execute any type of function that accepts nothing and returns a string or an error. A function that accepts nothing seems a bit useless, right? But we can use closures to introduce a context into this type of function.

Let's write a setContext function that takes a string as an argument and returns an ExecuteStringFunc method that returns the previous argument with the suffix Closure!:

func setContext(msg string) ExecuteStringFunc { 
  msg = fmt.Sprintf("%d Closure!
", msg) 
   
  return func() (string, error){ 
    return msg, nil 
  } 
} 

So, we can write a new test that uses this closure:

t.Run("Closure Success result", func(t *testing.T) { 
    var wg sync.WaitGroup 
    wg.Add(1) 
    //Timeout! 
    go timeout(t, &wg) 
 
    future.Success(func(s string) { 
      t.Log(s) 
      wg.Done() 
    }).Fail(func(e error) { 
      t.Fail() 
      wg.Done() 
    }) 
    future.Execute(setContext("Hello")) 
    wg.Wait() 
  }) 

The setContext function returns an ExecuteStringFunc method it can pass directly to the Execute function. We call the setContext function with an arbitrary text that we know will be returned.

Let's execute our tests again. Now everything has to go well!

go test -v .
=== RUN   TestStringOrError_Execute
=== RUN   TestStringOrError_Execute/Success_result
=== RUN   TestStringOrError_Execute/Failed_result
=== RUN   TestStringOrError_Execute/Closure_Success_result
--- PASS: TestStringOrError_Execute (0.00s)
    --- PASS: TestStringOrError_Execute/Success_result (0.00s)
        future_test.go:21: Hello World!
    --- PASS: TestStringOrError_Execute/Failed_result (0.00s)
        future_test.go:49: Error ocurred
    --- PASS: TestStringOrError_Execute/Closure_Success_result (0.00s)
        future_test.go:69: Hello Closure!
PASS
ok

It gave us an OK too. Closure test shows the behavior that we explained before. By taking a message "Hello" and appending it with something else ("Closure!"), we can change the context of the text we want to return. Now scale this to a HTTP GET call, a call to a database, or anything you can imagine. It will just need to end by returning a string or an error. Remember, however, that everything within the setContext function but outside of the anonymous function that we are returning is not concurrent, and will be executed asynchronously before calling execute, so we must try to put as much logic as possible within the anonymous function.

Putting the Future together

We have seen a good way to achieve asynchronous programming by using a function type system. However, we could have done it without functions by setting an interface with Success, Fail, and Execute methods and the types that satisfy them, and using the Template pattern to execute them asynchronously, as we have previously seen in this chapter. It is up to you!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.218.84