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();
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 yourPost
together.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.PostManager
methods must take a pointer receiver to avoid copying the mutex insidesync.Map
.And instead of fetching Posts directly, we use the PostManager.
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 tofetchPost
with a given ID to happen at a time.