3
\$\begingroup\$

I try to implement the features of the js Promise using Go. Currently, I had implemented Promise.all(), Promise.allSettled() and Promise.race() static methods.

Code here:

package go_promise

import (
    "sort"
)

type Promiser interface {
    AllSettled(iterable []Workload) []Result
    All(iterable []Workload) []Result
    Race(iterable []Workload) *Result
}

type promise struct {
}

type Workload func(args ...interface{}) interface{}
type Result struct {
    Idx int
    R   interface{}
}

func NewPromise() *promise {
    return &promise{}
}

func (p *promise) AllSettled(iterable []Workload) []Result {
    if len(iterable) == 0 {
        return nil
    }
    c := make(chan struct{}, len(iterable))
    r := make([]Result, len(iterable))
    for idx, iter := range iterable {
        iter := iter
        idx := idx
        go func() {
            r[idx].Idx = idx
            r[idx].R = iter()
            c <- struct{}{}
        }()
    }
    for i := 0; i < len(iterable); i++ {
        <-c
    }
    sort.Slice(r, func(i, j int) bool {
        return r[i].Idx < r[j].Idx
    })
    close(c)
    return r
}

func (p *promise) All(iterable []Workload) []Result {
    if len(iterable) == 0 {
        return nil
    }
    c := make(chan Result, len(iterable))
    errc := make(chan Result, 1)
    r := []Result{}
    for idx, iter := range iterable {
        iter := iter
        idx := idx
        go func() {
            result := iter()
            if err, ok := result.(error); ok {
                errc <- Result{Idx: idx, R: err}
            } else {
                c <- Result{Idx: idx, R: result}
            }
        }()
    }
    for {
        select {
        case result := <-errc:
            return []Result{result}
        case result := <-c:
            r = append(r, result)
            if len(r) == len(iterable) {
                sort.Slice(r, func(i, j int) bool {
                    return r[i].Idx < r[j].Idx
                })
                return r
            }
        }
    }
}

func (p *promise) Race(iterable []Workload) *Result {
    if len(iterable) == 0 {
        return nil
    }
    c := make(chan *Result, len(iterable))
    for _, iter := range iterable {
        iter := iter
        go func() {
            r := Result{R: iter()}
            c <- &r
        }()
    }
    return <-c
}

unit test:

package go_promise_test

import (
    "errors"
    "reflect"
    "testing"
    "time"

    go_promise "github.com/mrdulin/golang/src/go-promise"
)

func workloadGen(i interface{}, options ...interface{}) go_promise.Workload {
    return func(args ...interface{}) interface{} {
        var sec time.Duration = 1
        if len(options) != 0 {
            sec = options[0].(time.Duration)
        }
        time.Sleep(time.Second * sec)
        return i
    }
}

func TestPromise_AllSettled(t *testing.T) {
    t.Run("should settle all workloads and return results", func(t *testing.T) {
        p := go_promise.NewPromise()
        w1 := workloadGen(1)
        w2 := workloadGen(2)
        w3 := workloadGen(3)
        ws := []go_promise.Workload{w1, w2, w3}
        got := p.AllSettled(ws)
        want := []go_promise.Result{{Idx: 0, R: 1}, {Idx: 1, R: 2}, {Idx: 2, R: 3}}
        if !reflect.DeepEqual(got, want) {
            t.Fatalf("got: %+v, want: %+v", got, want)
        }
    })
    t.Run("should settle empty workloads and return nil", func(t *testing.T) {
        p := go_promise.NewPromise()
        ws := []go_promise.Workload{}
        got := p.AllSettled(ws)
        var want []go_promise.Result
        if !reflect.DeepEqual(got, want) {
            t.Fatalf("got: %#v, want: %#v", got, want)
        }
    })
}

func TestPromise_All(t *testing.T) {
    t.Run("should handle all workloads", func(t *testing.T) {
        p := go_promise.NewPromise()
        w1 := workloadGen(1)
        w2 := workloadGen(2)
        w3 := workloadGen(3)
        ws := []go_promise.Workload{w1, w2, w3}
        got := p.All(ws)
        want := []go_promise.Result{{Idx: 0, R: 1}, {Idx: 1, R: 2}, {Idx: 2, R: 3}}
        if !reflect.DeepEqual(got, want) {
            t.Fatalf("got: %#v, want: %#v", got, want)
        }
    })

    t.Run("should reject the promise when any workload is rejected", func(t *testing.T) {
        p := go_promise.NewPromise()
        err := errors.New("network")
        w1 := workloadGen(1)
        w2 := workloadGen(2)
        w3 := workloadGen(err)
        ws := []go_promise.Workload{w1, w2, w3}
        got := p.All(ws)
        want := []go_promise.Result{{Idx: 2, R: err}}
        if !reflect.DeepEqual(got, want) {
            t.Fatalf("got: %+v, want: %+v", got, want)
        }
    })
}

func TestPromise_Race(t *testing.T) {
    t.Run("should race all resolved promise", func(t *testing.T) {
        p := go_promise.NewPromise()
        var t1 time.Duration = 2
        var t2 time.Duration = 1
        w1 := workloadGen(1, t1)
        w2 := workloadGen(2, t1)
        w3 := workloadGen(3, t2)
        ws := []go_promise.Workload{w1, w2, w3}
        got := p.Race(ws)
        want := &go_promise.Result{Idx: 0, R: 3}
        if !reflect.DeepEqual(got, want) {
            t.Fatalf("got: %+v, want: %+v", got, want)
        }
    })

    t.Run("should race promises with a rejected promise", func(t *testing.T) {
        p := go_promise.NewPromise()
        err := errors.New("network")
        var t1 time.Duration = 2
        var t2 time.Duration = 1
        w1 := workloadGen(1, t1)
        w2 := workloadGen(2, t1)
        w3 := workloadGen(err, t2)
        ws := []go_promise.Workload{w1, w2, w3}
        got := p.Race(ws)
        want := &go_promise.Result{Idx: 0, R: err}
        if !reflect.DeepEqual(got, want) {
            t.Fatalf("got: %+v, want: %+v", got, want)
        }
    })
}

unit test result:

☁  go-promise [master] ⚡  go test -v -race 
=== RUN   TestPromise_AllSettled
=== RUN   TestPromise_AllSettled/should_settle_all_workloads_and_return_results
=== RUN   TestPromise_AllSettled/should_settle_empty_workloads_and_return_nil
--- PASS: TestPromise_AllSettled (1.00s)
    --- PASS: TestPromise_AllSettled/should_settle_all_workloads_and_return_results (1.00s)
    --- PASS: TestPromise_AllSettled/should_settle_empty_workloads_and_return_nil (0.00s)
=== RUN   TestPromise_All
=== RUN   TestPromise_All/should_handle_all_workloads
=== RUN   TestPromise_All/should_reject_the_promise_when_any_workload_is_rejected
--- PASS: TestPromise_All (2.01s)
    --- PASS: TestPromise_All/should_handle_all_workloads (1.00s)
    --- PASS: TestPromise_All/should_reject_the_promise_when_any_workload_is_rejected (1.00s)
=== RUN   TestPromise_Race
=== RUN   TestPromise_Race/should_race_all_resolved_promise
=== RUN   TestPromise_Race/should_race_promises_with_a_rejected_promise
--- PASS: TestPromise_Race (2.01s)
    --- PASS: TestPromise_Race/should_race_all_resolved_promise (1.00s)
    --- PASS: TestPromise_Race/should_race_promises_with_a_rejected_promise (1.00s)
PASS
ok      github.com/mrdulin/golang/src/go-promise        5.354s

Although there is no data race condition problem, I am not sure if there are goroutine memory leaks, performance issues, and other issues that have not been considered. Please help me review my code, thanks!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.