GithubHelp home page GithubHelp logo

ulasakdeniz / go-typed-promise Goto Github PK

View Code? Open in Web Editor NEW
9.0 3.0 0.0 35 KB

A type-safe generic promise library for Golang.

Home Page: https://pkg.go.dev/github.com/ulasakdeniz/go-typed-promise#section-readme

License: Apache License 2.0

Go 100.00%
concurrency generics go golang promise

go-typed-promise's Introduction

go-typed-promise

An experimental type-safe generic promise library for Go 1.19.

Why?

Why

Go has powerful concurrency tools in the form of channels and goroutines. However, the introduction of generics allows for the creation of abstractions, such as the Promise pattern, on top of these basic concurrency tools.

Promises can be useful when you have a task that you want to run asynchronously and reuse the result of that task. They can also make it easier to retrieve data from goroutines. Additionally, if still needed, the result of a promise can be sent to a channel using ToChannel. See Piping promises to channels section.

A use case

Think about this sample task:

  • Make a request to a remote server
  • Then save the result to a database and publish to a queue at the same time
  • Run each operation (http call, saving in db and publishing to queue) in separate goroutines.
  • If any of these operations fail, terminate the task.
  • Implement a timeout of 1 second for the entire task.

These network operations are made in a goroutine to allow for the possibility of running this flow concurrently multiple times in the future.

Implementing with errgroup and promise

We will see the implementation of the sample task using both the errgroup and promise packages. While golang.org/x/sync/errgroup package is well-suited for tasks of this nature, the promise implementation may offer a simpler and more straightforward approach.

First, let's set the stage with mock functions for our tasks (uncomment the time.Sleep to see the timeout works):

// make http request
func httpCall() (string, error) {
    //time.Sleep(2 * time.Second)
    return "Hello World", nil
}

// publish message to a queue
func publishMessage(_ string) (string, error) {
    //time.Sleep(2 * time.Second)
    return "success", nil
}

// save to database
func saveToDB(_ string) (string, error) {
    //time.Sleep(2 * time.Second)
    return "success", nil
}
Click to see implementation with errgroup
package main

import (
    "context"
    "fmt"
    "time"

    "golang.org/x/sync/errgroup"
)

func main() {
	timeoutCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()
	g, ctx := errgroup.WithContext(timeoutCtx)

	httpResChan := make(chan string, 2)
	publishChan := make(chan string)
	saveChan := make(chan string)

	// make http request
	g.Go(func() error {
		defer close(httpResChan)

		internalResChan := make(chan string)
		internalErrChan := make(chan error)

		// make http request and send to internal channels so that it can timeout and if result is available sent to other channels
		go func() {
			defer close(internalResChan)
			defer close(internalErrChan)

			res, err := httpCall()
			if err != nil {
				internalErrChan <- err
				return
			}
			internalResChan <- res
		}()

		select {
		case <-ctx.Done():
			return ctx.Err()
		case res := <-internalResChan:
			httpResChan <- res
			httpResChan <- res // broadcast
			return nil
		case err := <-internalErrChan:
			return err
		}
	})

	// use this function to publish message and save to db, they both receive the http result and pretty much do the same thing
	runWithHttpRes := func(out chan<- string, task func(string) (string, error)) {
		// publish message to a queue
		g.Go(func() error {
			defer close(out)

			internalResChan := make(chan string)
			internalErrChan := make(chan error)

			go func() {
				defer close(internalResChan)
				defer close(internalErrChan)

				res, err := task(<-httpResChan)
				if err != nil {
					internalErrChan <- err
					return
				}
				internalResChan <- res
			}()

			select {
			case <-ctx.Done():
				return ctx.Err()
			case res := <-internalResChan:
				out <- res
				return nil
			case err := <-internalErrChan:
				return err
			}
		})
	}

	// publish message to a queue
	runWithHttpRes(publishChan, publishMessage)
	// save to database
	runWithHttpRes(saveChan, saveToDB)

	results := make([]string, 2)

	// collect results
	g.Go(func() error {
		var counter int32
		for counter < 2 {
			select {
			case <-ctx.Done():
				return ctx.Err()
			case publishRes := <-publishChan:
				counter++
				results[0] = publishRes
				publishChan = nil
			case saveRes := <-saveChan:
				counter++
				results[1] = saveRes
				saveChan = nil
			}
		}
		return nil
	})

	// wait for all goroutines to finish.
	if err := g.Wait(); err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(results)
	}
}
Click to see implementation using promise
func main() {
    ctx, cancel := context.WithTimeout(context.TODO(), 1*time.Second)
    defer cancel()

    httpCallPromise, _ := promise.New(ctx, httpCall)

    publishMessagePromise := httpCallPromise.Map(func (data string, httpErr error) (string, error) {
        if httpErr != nil {
            return "", httpErr
        }
        return publishMessage(data)
    })

    saveToDBPromise := httpCallPromise.Map(func (data string, httpErr error) (string, error) {
        if httpErr != nil {
            return "", httpErr
        }
        return saveToDB(data)
    })

    resultPromise, _ := promise.All(ctx, publishMessagePromise, saveToDBPromise)
    result, err := resultPromise.Await()
    if err != nil {
        fmt.Println(err)
    }

    fmt.Println(result)
}

Installation

It is still experimental and not intended to be used in production.

go get github.com/ulasakdeniz/go-typed-promise

Usage

Errors are ignored in the examples below for simplicity.

Creating a promise

Create a promise with New function. It is a generic function that takes a context.Context and a func() (T, error) as parameters. The function returns a *Promise[T].

package main

import (
	"context"
	"fmt"
	"time"
	"github.com/ulasakdeniz/go-typed-promise"
)

func main() {
	// the type of p is Promise[string]
	p, err := promise.New(context.TODO(), func() (string, error) {
		// simulate a long running task that makes a network call
		time.Sleep(1 * time.Second)
		return "Hello", nil
	})

	// err is not nil if an error occurs while creating the promise
	if err != nil {
		// handle error
	}

	// await the result
	result, err := p.Await()
	if err != nil {
		// handle error
	}

	fmt.Println(result) // prints "Hello"
}

Using Await

Await function returns the result of the promise. Calling Await multiple times will return the same result. The promise will be resolved only once. Note that Await is a blocking call. It waits until the promise is resolved.

package main

import (
	"context"
	"fmt"
	"io"
	"net/http"

	"github.com/ulasakdeniz/go-typed-promise"
)

func main() {
    ipPromise, _ := promise.New(context.TODO(), func() (string, error) {
		res, _ := http.Get("https://api.ipify.org")
		ip, _ := io.ReadAll(res.Body)
		return string(ip), nil
	})

	ip, err := ipPromise.Await()
	if err != nil {
		fmt.Println("error", err)
	}

	fmt.Println("ip", ip)
	// Output: ip <YOUR_IP>
}

Creating a promise with a timeout

A context.Context is used to create a promise with a timeout. If the promise is not resolved within the timeout, the promise will be rejected with an error.

package main

import (
    "context"
    "fmt"
    "time"

    "github.com/ulasakdeniz/go-typed-promise"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	timeoutPromise, _ := promise.New(ctx, func() (string, error) {
		time.Sleep(2 * time.Second)
		return "Hello", nil
	})

	_, timeoutErr := timeoutPromise.Await()
	fmt.Println(timeoutErr)
	// Output: context error while waiting for promise: context deadline exceeded
}

Chaining promises

Promises can be chained with Map function. It takes a func(T) (T, error) as a parameter. The function returns a Promise[T] which is a new promise created from the result of the previous promise.

Note: Because of Golang generics limitations, you cannot change the type of the promise with Map. If you want to change the type of the promise, you can use promise.FromPromise function.

Using Promise.All

Promise.All takes a number of promises and returns a promise that resolves when all the promises in the slice are resolved. The result of the promise is a slice of the results of the promises.

Using Promise.Any

Promise.Any takes a number of promises and returns a promise that resolves when a promise successfully completes. The result of the promise is the result of the first promise that completes. If all the promises fail, the promise will have error.

Callbacks: OnComplete, OnSuccess, OnFailure

OnComplete is called when the promise is resolved or rejected. OnSuccess is called when the promise is resolved. OnFailure is called when the promise is rejected.

Piping promises to channels

ToChannel function pipes the result of the promise to a channel.

package main

import (
    "context"
    "fmt"
    "time"

    "github.com/ulasakdeniz/go-typed-promise"
)

func main() {
	stringChannel := make(chan string)
	errChannel := make(chan error)
	p, _ := promise.New(context.TODO(), func() (string, error) {
		time.Sleep(1 * time.Second)
		return "Hello", nil
	})

	p.ToChannel(stringChannel, errChannel)

    fmt.Println(<-stringChannel) // Output: "Hello"
}

If the promise results in error, the error will be sent to the error channel.

package main

import (
    "context"
    "fmt"
    "time"

    "github.com/ulasakdeniz/go-typed-promise"
)

func main() {
    stringChannel := make(chan string)
    errChannel := make(chan error)
    p, _ := promise.New(context.TODO(), func() (string, error) {
        time.Sleep(1 * time.Second)
        return "", fmt.Errorf("error")
    })

    p.ToChannel(stringChannel, errChannel)

    fmt.Println(<-errChannel)
	// Output: "error"
}

go-typed-promise's People

Contributors

ulasakdeniz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.