问题
I have some code that makes 3 requests to fill 3 variables now. Two requests are same. I want to share one http request between two different functions (in real world, these functions are splitted into two different modules).
Let me describe the problem what I have based on much simpler example than I have in real world.
At the moment, I have the following main function and Post data structure:
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)
}
There are three goroutines, so I have 3 concurrent requests, I sync everything using sync.Workgroup
. The following code is implementation of the requests:
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")
}
Here is implementation of functions which pulls needed information from fetched posts:
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
}
So, at the moment I have 3 concurrent requests, this works perfectly. The problem is I don't want 2 absolutely same separate HTTP requests to fetch the second post. One would be enough. So, what I want to achieve is 2 concurrent requests for post 1 and post 2. I want second call to makeSecondPostRequest
not to create new HTTP request, but share the existing one (which was sent by the first call).
How I can achieve this?
Note: the following code is how this can be done using JavaScript, for example.
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:
While you could put something together like promises, in this case it's not necessary.
Your code is written in a procedural fashion. You've written very specific functions which pull specific little bits off the Post
and throw out the rest. Instead, keep your Post
together.
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)
}
Now instead of caching responses you can cache Posts. We can do this by adding a PostManager to handle fetching and caching Posts.
Note that normal map
is not safe for concurrent use, so we use sync.Map for our cache.
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
methods must take a pointer receiver to avoid copying the mutex inside sync.Map
.
And instead of fetching Posts directly, we use the 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's caching would be improved by using conditional requests to check if the cached Post has changed or not.
Its locking can also be improved, as written its possible to fetch the same Post at the same time. We can fix this using singleflight to allow only one call to fetchPost
with a given ID to happen at a time.
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)
}
来源:https://stackoverflow.com/questions/60197499/how-to-share-one-http-request-instance-beween-two-goroutines