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.
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.
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:
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.
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:
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.
We don't have functional requirements for this task. Instead, we will have technical requirements for it:
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!.
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.
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.
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!
18.191.218.84