From c76b5266e26740ce4edcb42a1e67f16b63740e18 Mon Sep 17 00:00:00 2001 From: Julian Ospald Date: Wed, 30 Aug 2017 00:27:42 +0200 Subject: [PATCH] Initial commit --- .gitignore | 1 + go-challenge.txt | 88 +++++++++++++ src/numbers/numbers.go | 231 ++++++++++++++++++++++++++++++++++ src/numbers/numbers_test.go | 112 +++++++++++++++++ src/numbers/sort/sort.go | 100 +++++++++++++++ src/numbers/sort/sort_test.go | 92 ++++++++++++++ src/numbers/thoughts.md | 103 +++++++++++++++ src/testserver/testserver.go | 42 +++++++ 8 files changed, 769 insertions(+) create mode 100644 .gitignore create mode 100644 go-challenge.txt create mode 100644 src/numbers/numbers.go create mode 100644 src/numbers/numbers_test.go create mode 100644 src/numbers/sort/sort.go create mode 100644 src/numbers/sort/sort_test.go create mode 100644 src/numbers/thoughts.md create mode 100644 src/testserver/testserver.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ + diff --git a/go-challenge.txt b/go-challenge.txt new file mode 100644 index 0000000..0cba5d8 --- /dev/null +++ b/go-challenge.txt @@ -0,0 +1,88 @@ +travel audience Go challenge +============================ + +Task +---- + +Write an HTTP service that exposes an endpoint "/numbers". This endpoint receives a list of URLs +though GET query parameters. The parameter in use is called "u". It can appear +more than once. + + http://yourserver:8080/numbers?u=http://example.com/primes&u=http://foobar.com/fibo + +When the /numbers is called, your service shall retrieve each of these URLs if +they turn out to be syntactically valid URLs. Each URL will return a JSON data +structure that looks like this: + + { "numbers": [ 1, 2, 3, 5, 8, 13 ] } + +The JSON data structure will contain an object with a key named "numbers", and +a value that is a list of integers. After retrieving each of these URLs, the +service shall merge the integers coming from all URLs, sort them in ascending +order, and make sure that each integer only appears once in the result. The +endpoint shall then return a JSON data structure like in the example above with +the result as the list of integers. + +The endpoint needs to return the result as quickly as possible, but always +within 500 milliseconds. It needs to be able to deal with error conditions when +retrieving the URLs. If a URL takes too long to respond, it must be ignored. It +is valid to return an empty list as result only if all URLs returned errors or +took too long to respond. + +Example +------- + +The service receives an HTTP request: + + >>> GET /numbers?u=http://example.com/primes&u=http://foobar.com/fibo HTTP/1.0 + +It then retrieves the URLs specified as parameters. + +The first URL returns this response: + + >>> GET /primes HTTP/1.0 + >>> Host: example.com + >>> + <<< HTTP/1.0 200 OK + <<< Content-Type: application/json + <<< Content-Length: 34 + <<< + <<< { "number": [ 2, 3, 5, 7, 11, 13 ] } + +The second URL returns this response: + + >>> GET /fibo HTTP/1.0 + >>> Host: foobar.com + >>> + <<< HTTP/1.0 200 OK + <<< Content-Type: application/json + <<< Content-Length: 40 + <<< + <<< { "number": [ 1, 1, 2, 3, 5, 8, 13, 21 ] } + +The service then calculates the result and returns it. + + <<< HTTP/1.0 200 OK + <<< Content-Type: application/json + <<< Content-Length: 44 + <<< + <<< { "number": [ 1, 2, 3, 5, 7, 8, 11, 13, 21 ] } + + +Completion Conditions +--------------------- + +Solve the task described above using Go. Only use what's provided in the Go +standard library. The resulting program must run stand-alone with no other +dependencies than the Go compiler. + +Document your source code, both using comments and in a separate text file that +describes the intentions and rationale behind your solution. Also write down +any ambiguities that you see in the task description, and describe you how you +interpreted them and why. If applicable, write automated tests for your code. + +For testing purposes, you will be provided with an example server that, when +run, listens on port 8090 and provides the endpoints /primes, /fibo, /odd and +/rand. + +Please return your working solution within 7 days of receiving the challenge. diff --git a/src/numbers/numbers.go b/src/numbers/numbers.go new file mode 100644 index 0000000..67b4866 --- /dev/null +++ b/src/numbers/numbers.go @@ -0,0 +1,231 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "numbers/sort" + "sync" + "time" +) + +// Helper struct for JSON decoding. +type Numbers struct { + Numbers []int +} + +// The maximum response time of the handlers. +var MaxResponseTime time.Duration = 500 * time.Millisecond + +// The main entry point of the backend. +func main() { + listenAddr := flag.String("http.addr", ":8090", "http listen address") + flag.Parse() + + http.HandleFunc("/numbers", func(w http.ResponseWriter, r *http.Request) { + numbersHandler(w, r) + }) + + log.Fatal(http.ListenAndServe(*listenAddr, nil)) +} + +// The main handler. The expected request is of the form: +// GET /numbers?u=http://example.com/primes&u=http://foobar.com/fibo HTTP/1.0 +// The parameter 'u' will be parsed and all urls will be fetched, which must +// return a valid JSON that looks like e.g.: +// { "Numbers": [ 1, 2, 3, 5, 8, 13 ] } +// +// Then these lists are merged, deduplicated and sorted and the response will +// be a JSON of the same form. +// The handler is guaranteed to respond within a timeframe of 500ms. If +// URLs take too long to load or return garbage, they are skipped. +// If all URLs take too long to load or return garbage, an empty JSON list +// is returned. +func numbersHandler(w http.ResponseWriter, r *http.Request) { + // timeout channel for the handler as a whole + timeout := make(chan bool, 1) + go func() { + time.Sleep(MaxResponseTime) + timeout <- true + }() + + var rurl []string = r.URL.Query()["u"] + // if no parameters, return 400 + if len(rurl) == 0 { + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Bad request: Missing 'u' parameters")) + return + } + + // Non-blocking input channel for URL results. + // We will read as much as we can at once. + inputChan := make(chan []int, len(rurl)) + var wg sync.WaitGroup + + // fetch all URLs asynchronously + for i := range rurl { + wg.Add(1) + go func(url string) { + defer wg.Done() + n, e := getNumbers(url) + + if e == nil { + if n != nil && len(n) > 0 { + inputChan <- n + } else { + log.Printf("Received empty list of numbers from endpoint") + } + } else { + log.Printf("Got an error: %s", e) + } + }(rurl[i]) + } + // master routine closing the inputChan + go func() { + wg.Wait() + close(inputChan) + }() + + // channel for sorting process, so we can short-circuit in + // case sorting takes too long + sortChan := make(chan []int, 1) + // aggregate numbers from URLs + var numberBuffer []int = []int{} + // these are actually sorted + var sortedNumbers []int = []int{} + + // aggregate and sort loop, + // breaks if all URLs have been processed or the timeout + // has been reached + done := false + for done != true { + select { + case <-timeout: + log.Printf("Waiting for URL took too long, finishing response anyway") + finishResponse(w, sortedNumbers) + return + case res, more := <-inputChan: + if more { // still URLs to fetch + numberBuffer = append(numberBuffer, res...) + // continue to aggregate numbers from the buffer + continue + } else { // all URLs fetched, sort and be done + log.Printf("Nothing else to fetch") + done = true + } + // non-blocking branch that sorts what we already have + // we are not done here yet + default: + // only sort if we have new results + if len(numberBuffer) == 0 { + continue + } + } + + // sort fallthrough, either the inputChan is currently "empty" + // or we fetched all URLs already + go func(n []int) { + res, err := sort.SortedAndDedup(timeout, n) + if err != nil { + return + } + sortChan <- res + }(append(sortedNumbers, numberBuffer...)) + numberBuffer = []int{} + + select { + case merged := <-sortChan: + sortedNumbers = merged + case <-timeout: + log.Printf("Sorting took too long, finishing response anyway") + finishResponse(w, sortedNumbers) + return + } + } + + log.Printf("Result is complete, finishing response") + finishResponse(w, sortedNumbers) +} + +// Finalizes the JSON response with the given numbers. This always +// sends a 200 HTTP status code. +func finishResponse(w http.ResponseWriter, numbers []int) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{"Numbers": numbers}) +} + +// Gets the numbers from the given url. +// 'resp' is always nil if there was an error. Errors can +// be url parse errors, HTTP response errors, io errors from reading the +// body or json decoding errors. +func getNumbers(rawurl string) (resp []int, err error) { + // validate url + u_err := validateURL(rawurl) + if u_err != nil { + return nil, u_err + } + + // retrieve response + r, r_err := http.Get(rawurl) + if r_err != nil { + return nil, r_err + } + if r.StatusCode != 200 { + return nil, fmt.Errorf("HTTP: Status code is not 200, but %d", + r.StatusCode) + } + + // open body + defer r.Body.Close() + body, b_err := ioutil.ReadAll(r.Body) + if b_err != nil { + return nil, b_err + } + + // parse json + return parseJson(body) +} + +// Parse the given raw JSON bytes into a list of numbers. The JSON +// is expected to be of the form: +// {"Numbers": [1,2,5]} +// +// Not particularly strict. +func parseJson(body []byte) (res []int, err error) { + dec := json.NewDecoder(bytes.NewReader(body)) + var n Numbers + j_err := dec.Decode(&n) + if j_err != nil { + return nil, j_err + } else { + if n.Numbers == nil { + return nil, fmt.Errorf("JSON: missing key 'Numbers'") + } + return n.Numbers, nil + } +} + +// Validate the URL. The URL has to be syntactically valid, +// has to have a scheme of either 'https' or 'http' and a hostname. +// An error is returned on invalid URLs. +func validateURL(rawurl string) error { + u, u_err := url.Parse(rawurl) + if u_err != nil { + return u_err + } + if u.Scheme != "https" && u.Scheme != "http" { + return fmt.Errorf("URL: not a valid HTTP/HTTPS scheme in %s", rawurl) + } + if u.Host == "" { + return fmt.Errorf("URL: not a valid host in %s", rawurl) + } + + return nil +} diff --git a/src/numbers/numbers_test.go b/src/numbers/numbers_test.go new file mode 100644 index 0000000..a50f5e2 --- /dev/null +++ b/src/numbers/numbers_test.go @@ -0,0 +1,112 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +// Tests valid and invalid URLs via 'validateURL'. +func TestValidateURL(t *testing.T) { + urls_valid := []string{ + "http://www.bar.com", + "https://86.31.3.9.de", + "http://localhost:8080", + "https://baz.org", + } + + urls_invalid := []string{ + "http:/www.bar.com", + "ftp://86.31.3.9.de", + "localhost:8080", + "ssh://foo.bar", + } + + for _, url := range urls_valid { + err := validateURL(url) + if err != nil { + t.Errorf("URL %s invalid\nError was: %s", url, err) + } + } + + for _, url := range urls_invalid { + err := validateURL(url) + if err == nil { + t.Errorf("URL %s valid", url) + } + } +} + +// Tests specific JSON that is accepted by 'parseJson', e.g. +// {"Numbers": [1,2,5]} +func TestParseJson(t *testing.T) { + validJSON := [][]byte{ + []byte("{\"Numbers\": []}"), + []byte("{\"Numbers\": [7]}"), + []byte("{\"Numbers\": [1,2,5]}"), + []byte("{\"Numbers\" : [1 , 2 ,5]}"), + } + + invalidJSON := [][]byte{ + []byte("{\"Numbers\": [}"), + []byte("\"Numbers\": [7]}"), + []byte("{\"umbers\": [1,2,5]}"), + []byte("{\"umbers\": [1,2,5]"), + []byte("{\"Numbers\" [1,2,5]}"), + } + + for _, json := range validJSON { + _, err := parseJson(json) + if err != nil { + t.Errorf("JSON \"%s\" invalid\nError was: %s", json, err) + } + } + + for _, json := range invalidJSON { + res, err := parseJson(json) + if err == nil { + t.Errorf("JSON \"%s\" valid\nResult was: %s", json, res) + } + } +} + +// Test the actual backend handler. Because we have no mocking framework, +// we just do a few very basic tests. +func TestHandler(t *testing.T) { + // no url parameters => status code 400 + { + req, err := http.NewRequest("GET", "/numbers", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(numbersHandler) + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusBadRequest { + t.Errorf("Handler returned status code %v, expected %v", status, http.StatusOK) + } + } + + // invalid url => empty result, status code 200 + { + req, err := http.NewRequest("GET", "/numbers?u=ftp://a.b.c.d.e.f", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(numbersHandler) + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("Handler returned status code %v, expected %v", status, http.StatusOK) + } + body := rr.Body.String() + expected := "{\"Numbers\":[]}\n" + if body != expected { + t.Errorf("Body not as expected, got %s, expected %s", body, expected) + } + } +} diff --git a/src/numbers/sort/sort.go b/src/numbers/sort/sort.go new file mode 100644 index 0000000..da90cb2 --- /dev/null +++ b/src/numbers/sort/sort.go @@ -0,0 +1,100 @@ +// Sorting algorithms. May also contain deduplication operations. +package sort + +import ( + "fmt" +) + +// Mergesorts and deduplicates the list. +func SortedAndDedup(timeout <-chan bool, list []int) (res []int, err error) { + sorted, err := Mergesort(timeout, list) + if err != nil { + return nil, err + } + + deduped := dedupSortedList(sorted) + return deduped, nil +} + +// Deduplicate the sorted list and return a new one with a potentially different +// size. +func dedupSortedList(list []int) []int { + newList := []int{} + if len(list) <= 1 { + return list + } + var prev int = list[0] + newList = append(newList, list[0]) + for i := 1; i < len(list); i++ { + if prev != list[i] { + newList = append(newList, list[i]) + } + prev = list[i] + } + + return newList +} + +// Mergesorts the given list and returns it as a result. The input list +// is not modified. +// The algorithm is a bottom-up iterative version and not explained +// in detail here. +func Mergesort(timeout <-chan bool, list []int) (res []int, err error) { + newList := append([]int{}, list...) + temp := append([]int{}, list...) + n := len(newList) + + for m := 1; m < (n - 1); m = 2 * m { + for i := 0; i < (n - 1); i += 2 * m { + select { + case <-timeout: + return nil, fmt.Errorf("Sorting timed out") + default: + } + from := i + mid := i + m - 1 + to := min(i+2*m-1, n-1) + + merge(timeout, newList, temp, from, mid, to) + } + } + + return newList, nil +} + +// The merge part of the mergesort. +func merge(timeout <-chan bool, list []int, temp []int, from int, mid int, to int) { + k := from + i := from + j := mid + 1 + + for i <= mid && j <= to { + if list[i] < list[j] { + temp[k] = list[i] + i++ + } else { + temp[k] = list[j] + j++ + } + k++ + } + + for i <= mid && i < len(temp) { + temp[k] = list[i] + i++ + k++ + } + + for i := from; i <= to; i++ { + list[i] = temp[i] + } +} + +// Get the minimum of two integers. +func min(l int, r int) int { + if l < r { + return l + } else { + return r + } +} diff --git a/src/numbers/sort/sort_test.go b/src/numbers/sort/sort_test.go new file mode 100644 index 0000000..7ce4092 --- /dev/null +++ b/src/numbers/sort/sort_test.go @@ -0,0 +1,92 @@ +package sort + +import ( + "testing" +) + +// Test the mergesort and deduplication with a predefined set of slices. +func TestSortAndDedup(t *testing.T) { + to_sort := [][]int{ + {}, + {7}, + {1, 4, 5, 6, 3, 2}, + {1, 2, 3, 4, 5, 6, 7}, + {1, 1, 1, 3, 3, 2, 1}, + {84, 32, 32, 7, 1, 2, 1}, + {1, 3, 5, 5, 7, 8, 10, 17, 19, 24, 27, 34, 76, 1, 1, 2, 3, 5, 8, 13, 21}, + {1, 3, 5, 5, 7, 8, 10, 17, 19, 24, 27, 34, 76, 1, 1, 2, 3, 5, 8, 13, 21, 1, 3, 5, 5, 7, 8, 10, 17, 19, 24, 27, 34, 76, 1, 1, 2, 3, 5, 8, 13, 21, 1, 3, 5, 5, 7, 8, 10, 17, 19, 24, 27, 34, 76, 1, 1, 2}, + } + + result := [][]int{ + {}, + {7}, + {1, 2, 3, 4, 5, 6}, + {1, 2, 3, 4, 5, 6, 7}, + {1, 2, 3}, + {1, 2, 7, 32, 84}, + {1, 2, 3, 5, 7, 8, 10, 13, 17, 19, 21, 24, 27, 34, 76}, + {1, 2, 3, 5, 7, 8, 10, 13, 17, 19, 21, 24, 27, 34, 76}, + } + + for i := range to_sort { + sorted, _ := SortedAndDedup(make(chan bool, 1), to_sort[i]) + if slice_equal(sorted, result[i]) != true { + t.Errorf("Failure in sorting + dedup, expected %s got %s", result[i], sorted) + } + } + +} + +// Test the mergesort with a predefined set of slices. +func TestSort(t *testing.T) { + // ok + to_sort := [][]int{ + {}, + {7}, + {1, 4, 5, 6, 3, 2}, + {1, 2, 3, 4, 5, 6, 7}, + {1, 1, 1, 3, 3, 2, 1}, + {84, 32, 32, 7, 1, 2, 1}, + {1, 3, 5, 5, 7, 8, 10, 17, 19, 24, 27, 34, 76, 1, 1, 2, 3, 5, 8, 13, 21}, + } + + result := [][]int{ + {}, + {7}, + {1, 2, 3, 4, 5, 6}, + {1, 2, 3, 4, 5, 6, 7}, + {1, 1, 1, 1, 2, 3, 3}, + {1, 1, 2, 7, 32, 32, 84}, + {1, 1, 1, 2, 3, 3, 5, 5, 5, 7, 8, 8, 10, 13, 17, 19, 21, 24, 27, 34, 76}, + } + + for i := range to_sort { + sorted, _ := Mergesort(make(chan bool, 1), to_sort[i]) + if slice_equal(sorted, result[i]) != true { + t.Errorf("Failure in sorting, expected %s got %s", result[i], sorted) + } + } +} + +// Helper function to compare int slices for equality. +func slice_equal(s1 []int, s2 []int) bool { + if s1 == nil && s2 == nil { + return true + } + + if s1 == nil || s2 == nil { + return false + } + + if len(s1) != len(s2) { + return false + } + + for i := range s1 { + if s1[i] != s2[i] { + return false + } + } + + return true +} diff --git a/src/numbers/thoughts.md b/src/numbers/thoughts.md new file mode 100644 index 0000000..31a401f --- /dev/null +++ b/src/numbers/thoughts.md @@ -0,0 +1,103 @@ +## Workflow approach taken + +1. get a minimal version of a handler working that responds with unsorted and undeduplicated JSON lists, no go-routines +2. add asynchronous fetching of the URLs for performance +3. ensure the 500ms timeout is met +4. implement the sorting algorithm +5. review manually for undefined behavior, memory leaks, possible security vulnerabilities and consistent error handling +6. write tests + +## Retrieving of URLs and timing guarantee + +URLs are retrieved asynchronously for performance reasons. Then +the handler enters an aggregation/sort loop in order to gather as much +data as is already available and process it immediately for sorting. +This is repeated as long as there is still potential data to be discovered +(as in: not all URLs fetched). +If the timeout is reached at any point (while aggregating data or sorting) +the handler returns immediately with the sorted data it has at that point. + +That means that sorting is potentially done multiple times. That decision +assumes that the highest time fluctuation comes from the HTTP requests +and not from the sorting algorithm. It also helps with stopping early +and further hardens the 500ms timeout, because we already +have at least parts of the results sorted instead of none. + +This rules out the case that we aggregate all data close before the timeout, +but fail to sort the huge list within the rest of the timeframe and exceed it. + +This might be tweaked, changed or optimized in case there is more +information on the data that the backend regularly processes or has +to expect. + +## Sorting/Merging of the numbers + +Initially I wanted to write a lot of custom code that would +switch the algorithm depending on the endpoint used, as in: only do expensive +sorting when `/random` was retrieved and otherwise assume sorted lists +that can easily be merged and deduplicated. + +However, the actual endpoints are not in the task description and the +example testserver is not a specification of the expected/valid +endpoints and the properties of their responses. + +Instead I use an iterative version of bottom-up mergesort. The reasons being: + +* relatively easy to implement +* because it's iterative, it doesn't potentially blow up the stack +* faster on partially sorted lists than quicksort +* better worst case time complexity than quicksort + +Parallelizing the merge sort algorithm can be considered, but isn't +implemented, because there is no hard evidence that it is particularly useful +for the expected input, nor is it expressed in the task. + +The sorting procedure also stops early when the input timeout channel is +triggered. Apart from a maybe small performance benefit, this is mainly done to +avoid potential DoS attacks with overly huge inputs +that get fetched within the 500ms timeframe. This is necessary, because +the handler cannot kill the "sort go-routine", which would +continue to run even after the handler has responded and consume CPU and +memory. This is very basic and might be extended or removed in the future. +Up for discussion. + +## Error handling + +If URL parameters are missing, then HTTP code 400 is issued, +instead of an empty 200 response. This kinda hijacks the HTTP protocol, +but allows to signal wrong use of backend API. Some Google APIs also seem +to use this method. + +No package level error variables are used since the specific +errors in `getNumbers` are irrelevant for the handler to proceed. +Instead they will just be printed and the response +integer list will always be set to `nil` on any such error. + +The task does not exactly say how to handle the case of a single +URL responding with garbage. It is assumed that it will be ignored +just like URLs that take too long to respond. + +## JSON parsing/response + +There are a few inconsistencies in the task and the testserver given about +the exact form of the JSON input and output. + +I decided that the input has to have an uppercase 'Numbers' key and +the response of the handler as well. + +The JSON parser isn't overly strict. In case I was allowed to use +3rd party packages, I'd choose a parser-combinator library to stricten +the allowed input (e.g. only one Numbers key allowed, no garbage at the end of the input etc.). + +## TODO + +* make 'handler' function more slim (could be deduplicated) +* could use contex.Context for more abstraction +* more general types in some cases +* proper mocking for the handler (no 3rd party libs allowed) + - maybe use a Docker environment for that + +## Remarks + +* time used for task: ~2.5 days +* no prior Go experience diff --git a/src/testserver/testserver.go b/src/testserver/testserver.go new file mode 100644 index 0000000..d9228f4 --- /dev/null +++ b/src/testserver/testserver.go @@ -0,0 +1,42 @@ +package main + +import ( + "encoding/json" + "flag" + "log" + "math/rand" + "net/http" + "time" +) + +func main() { + listenAddr := flag.String("http.addr", ":8080", "http listen address") + flag.Parse() + + http.HandleFunc("/primes", handler([]int{2, 3, 5, 7, 11, 13})) + http.HandleFunc("/fibo", handler([]int{1, 1, 2, 3, 5, 8, 13, 21})) + http.HandleFunc("/odd", handler([]int{1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23})) + http.HandleFunc("/rand", handler([]int{5, 17, 3, 19, 76, 24, 1, 5, 10, 34, 8, 27, 7})) + + log.Fatal(http.ListenAndServe(*listenAddr, nil)) +} + +func handler(numbers []int) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + waitPeriod := rand.Intn(550) + log.Printf("%s: waiting %dms.", r.URL.Path, waitPeriod) + + time.Sleep(time.Duration(waitPeriod) * time.Millisecond) + + x := rand.Intn(100) + if x < 10 { + http.Error(w, "service unavailable", http.StatusServiceUnavailable) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + json.NewEncoder(w).Encode(map[string]interface{}{"Numbers": numbers}) + } +}