Как разделить один экземпляр HTTP-запроса между двумя программами? - PullRequest
0 голосов
/ 13 февраля 2020

У меня есть код, который делает 3 запроса для заполнения 3 переменных. Два запроса одинаковы. Я хочу разделить один http-запрос между двумя разными функциями (в реальном мире эти функции разделены на два разных модуля).

Позвольте мне описать проблему, основанную на гораздо более простом примере, чем в реальном world.

На данный момент у меня есть следующая основная функция и структура данных Post:

type Post struct {
    ID          int    `json:"id"`
    Title       string `json:"title"`
    UserID      int    `json:"userId"`
    isCompleted bool   `json:"completed"`
}

func main() {
    var wg sync.WaitGroup

    fmt.Println("Hello, world.")

    wg.Add(3)

    var firstPostID int
    var secondPostID int
    var secondPostName string

    go func() {
        firstPostID = getFirstPostID()
        defer wg.Done()
    }()

    go func() {
        secondPostID = getSecondPostID()
        defer wg.Done()
    }()

    go func() {
        secondPostName = getSecondPostName()
        defer wg.Done()
    }()

    wg.Wait()

    fmt.Println("first post id is", firstPostID)
    fmt.Println("second post id is", secondPostID)
    fmt.Println("second post title is", secondPostName)
}

Есть три процедуры, поэтому у меня есть 3 одновременных запроса, я синхронизирую c все используя sync.Workgroup. Следующий код является реализацией запросов:

func makeRequest(url string) Post {
    resp, err := http.Get(url)
    if err != nil {
        // handle error
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    var post Post

    json.Unmarshal(body, &post)

    return post
}

func makeFirstPostRequest() Post {
    return makeRequest("https://jsonplaceholder.typicode.com/todos/1")
}

func makeSecondPostRequest() Post {
    return makeRequest("https://jsonplaceholder.typicode.com/todos/2")
}

Вот реализация функций, которая извлекает необходимую информацию из полученных сообщений:

func getFirstPostID() int {
    var result = makeFirstPostRequest()
    return result.ID
}

func getSecondPostID() int {
    var result = makeSecondPostRequest()

    return result.ID
}

func getSecondPostName() string {
    var result = makeSecondPostRequest()

    return result.Title
}

Итак, на данный момент у меня есть 3 одновременных запроса это работает отлично. Проблема в том, что я не хочу, чтобы два абсолютно одинаковых HTTP-запроса извлекали второй пост. Одного было бы достаточно. Итак, чего я хочу добиться, так это 2 одновременных запроса на сообщение 1 и сообщение 2. Я хочу, чтобы второй вызов makeSecondPostRequest не создавал новый HTTP-запрос, а делился существующим (который был отправлен первым вызовом).

Как мне этого добиться?

Примечание: следующий код показывает, как это можно сделать, например, с помощью JavaScript.

let promise = null;
function makeRequest() {
    if (promise) {
        return promise;
    }

    return promise = fetch('https://jsonplaceholder.typicode.com/todos/1')
      .then(result => result.json())
      // clean up cache variable, so any next request in the future will be performed again
      .finally(() => (promise = null))

}

function main() {
    makeRequest().then((post) => {
        console.log(post.id);
    });
    makeRequest().then((post) => {
        console.log(post.title);
    });
}

main();

1 Ответ

3 голосов
/ 13 февраля 2020

Хотя вы можете собрать что-то вроде обещаний, в этом случае в этом нет необходимости.

Ваш код написан процедурным образом. Вы написали очень специфические c функции, которые извлекают определенные c биты из Post и отбрасывают остальные. Вместо этого держите свои Post вместе.

package main

import(
    "fmt"
    "encoding/json"
    "net/http"
    "sync"
)

type Post struct {
    ID          int    `json:"id"`
    Title       string `json:"title"`
    UserID      int    `json:"userId"`
    isCompleted bool   `json:"completed"`
}

func fetchPost(id int) Post {
    url := fmt.Sprintf("https://jsonplaceholder.typicode.com/todos/%v", id)
    resp, err := http.Get(url)
    if err != nil {
        panic("HTTP error")
    }
    defer resp.Body.Close()

    // It's more efficient to let json Decoder handle the IO.
    var post Post
    decoder := json.NewDecoder(resp.Body)
    err = decoder.Decode(&post)
    if err != nil {
        panic("Decoding error")
    }

    return post
}

func main() {
    var wg sync.WaitGroup

    wg.Add(2)

    var firstPost Post
    var secondPost Post

    go func() {
        firstPost = fetchPost(1)
        defer wg.Done()
    }()

    go func() {
        secondPost = fetchPost(2)
        defer wg.Done()
    }()

    wg.Wait()

    fmt.Println("First post ID is", firstPost.ID)
    fmt.Println("Second post ID is", secondPost.ID)
    fmt.Println("Second post title is", secondPost.Title)
}

Теперь вместо кэширования ответов вы можете кэшировать сообщения. Мы можем сделать это, добавив PostManager для обработки извлечения и кэширования сообщений.

Обратите внимание, что обычный map небезопасен для одновременного использования, поэтому мы используем syn c .Map для наш кеш.

type PostManager struct {
    sync.Map
}

func (pc *PostManager) Fetch(id int) Post {
    post, ok := pc.Load(id)
    if ok {
        return post.(Post)
    }
    post = pc.fetchPost(id)
    pc.Store(id, post)

    return post.(Post)
}

func (pc *PostManager) fetchPost(id int) Post {    
    url := fmt.Sprintf("https://jsonplaceholder.typicode.com/todos/%v", id)
    resp, err := http.Get(url)
    if err != nil {
        panic("HTTP error")
    }
    defer resp.Body.Close()

    // It's more efficient to let json Decoder handle the IO.
    var post Post
    decoder := json.NewDecoder(resp.Body)
    err = decoder.Decode(&post)
    if err != nil {
        panic("Decoding error")
    }

    return post
}

PostManager методы должны получать указатель приемника, чтобы избежать копирования мьютекса внутри sync.Map.

И вместо прямой загрузки сообщений мы используем PostManager.

func main() {
    var postManager PostManager

    var wg sync.WaitGroup

    wg.Add(2)

    var firstPost Post
    var secondPost Post

    go func() {
        firstPost = postManager.Fetch(1)
        defer wg.Done()
    }()

    go func() {
        secondPost = postManager.Fetch(2)
        defer wg.Done()
    }()

    wg.Wait()

    fmt.Println("First post ID is", firstPost.ID)
    fmt.Println("Second post ID is", secondPost.ID)
    fmt.Println("Second post title is", secondPost.Title)
}

Кэширование PostManager было бы улучшено с помощью условных запросов , чтобы проверить, изменилось ли кэшированное сообщение.

Его блокировка также может быть улучшена, как написано можно получить один и тот же пост одновременно. Мы можем исправить это, используя singleflight, чтобы разрешить только один вызов fetchPost с указанным идентификатором за один раз.

type PostManager struct {
    group singleflight.Group
    cached sync.Map
}

func (pc *PostManager) Fetch(id int) Post {
    post,ok := pc.cached.Load(id)
    if !ok {
        // Multiple calls with the same key at the same time will only run the code once, but all calls get the result.
        post, _, _ = pc.group.Do(strconv.Itoa(id), func() (interface{}, error) {
            post := pc.fetchPost(id)
            pc.cached.Store(id, post)
            return post, nil
        })
    }
    return post.(Post)
}
...