There’s a package called axios in the JavaScript world that is commonly used for making network requests. It got me thinking about what steps are needed to make a request in Go with all the bells and whistles. So in this article, we will make a simple function that will make a request to the httpstat.us API so we can test different responses. Let’s get to it.
The code for this can be found on Gitlab.
First, let’s just make a simple request.
func getTypeOfError(err *url.Error) string {
if err.Timeout() {
return "a timeout"
}
return "an error"
}
func makeGetRequestToStatusApi(urlToGet string) (string, error) {
res, err := http.Get(urlToGet)
if err != nil {
urlErr := err.(*url.Error)
message := "there was %s when attempting to request the URL %s: %s"
return "", fmt.Errorf(message, getTypeOfError(urlErr), urlErr.URL, err)
}
defer res.Body.Close()
if res.StatusCode/100 == 4 || res.StatusCode/100 == 5 {
return "", fmt.Errorf("the response was returned with a %d", res.StatusCode)
}
body, _ := ioutil.ReadAll(res.Body)
return string(body), nil
}
We create a function that takes a string as the URL and then returns a string or an error. We make a request using the .Get function on the http module. If there’s an error, we cast it to a *url.Error type. According to the docs, that is the type of error that .Get always returns. If there’s no error, we defer the closing of the body. We check to make sure it isn’t a 400 or 500 level HTTP error. An error isn’t created when these status codes are returned. So we have to check for them ourselves. Lastly, we read in the whole body and return it as a string.
This API allows us to send an Accept header of application/json, and it will return the string in JSON format. However, we can’t pass any headers to the .Get function. Instead, we have to create a new request, update the headers on it, and then use the default Client in the http package to send the request. That would make our function look like this.
func makeGetRequestToStatusApi(urlToGet string) (string, error) {
req, err := http.NewRequest(http.MethodGet, urlToGet, nil)
if err != nil {
return "", err
}
req.Header.Set("Accept", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
urlErr := err.(*url.Error)
message := "there was %s when attempting to request the URL %s: %s"
return "", fmt.Errorf(message, getTypeOfError(urlErr), urlErr.URL, err)
}
defer res.Body.Close()
if res.StatusCode/100 == 4 || res.StatusCode/100 == 5 {
return "", fmt.Errorf("the response was returned with a %d", res.StatusCode)
}
body, _ := ioutil.ReadAll(res.Body)
return string(body), nil
}
We access the Header property on the Request and set the Accept header. Then we send the request using the .Do function on the DefaultClient.
Now that we know it will be returning a JSON string, we should decode that into a Go string so we can return it from this function.
func makeGetRequestToStatusApi(urlToGet string) (string, error) {
req, err := http.NewRequest(http.MethodGet, urlToGet, nil)
if err != nil {
return "", err
}
req.Header.Set("Accept", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
urlErr := err.(*url.Error)
message := "there was %s when attempting to request the URL %s: %s"
return "", fmt.Errorf(message, getTypeOfError(urlErr), urlErr.URL, err)
}
defer res.Body.Close()
if res.StatusCode/100 == 4 || res.StatusCode/100 == 5 {
return "", fmt.Errorf("the response was returned with a %d", res.StatusCode)
}
var resBody string
if err = json.NewDecoder(res.Body).Decode(&resBody); err != nil {
return "", err
}
return resBody, nil
}
Nothing crazy here. Basic JSON decoding.
Last thing we should do is set a timeout on our request to make sure we don’t have any requests take super long. We do this by creating a new context, adding a timeout to it, and then putting it on the request.
func makeGetRequestToStatusApi(urlToGet string) (string, error) {
req, err := http.NewRequest(http.MethodGet, urlToGet, nil)
if err != nil {
return "", err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
req = req.WithContext(ctx)
req.Header.Set("Accept", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
urlErr := err.(*url.Error)
message := "there was %s when attempting to request the URL %s: %s"
return "", fmt.Errorf(message, getTypeOfError(urlErr), urlErr.URL, err)
}
defer res.Body.Close()
if res.StatusCode/100 == 4 || res.StatusCode/100 == 5 {
return "", fmt.Errorf("the response was returned with a %d", res.StatusCode)
}
var resBody string
if err = json.NewDecoder(res.Body).Decode(&resBody); err != nil {
return "", err
}
return resBody, nil
}
If the request takes too long, the context forces it to cancel. We then receive that error and handle it.
We can test this function out by creating a simple main function like the one below and then running the program.
func main() {
urls := []string{"http://httpstat.us/200", "http://httpstat.us/404", "http://httpstat.us/201?sleep=2000", "http://httpstat.us/201"}
for _, url := range urls {
if resBody, err := makeGetRequestToStatusApi(url); err != nil {
fmt.Printf("there was an error when getting %s: %v\n", url, err)
} else {
fmt.Printf("the response from %s: %s\n", url, resBody)
}
}
}
The output should look something like this.
the response from http://httpstat.us/200: 200 OK
there was an error when getting http://httpstat.us/404: the response was returned with a 404
there was an error when getting http://httpstat.us/201?sleep=2000: there was a timeout when attempting to request the URL http://httpstat.us/201?sleep=2000: Get http://httpstat.us/201?sleep=2000: context deadline exceeded
the response from http://httpstat.us/201: 201 Created
That’s all there is to it. Let me know if there are any other ways to improve this function. Potentially errors that have gone unhandled or something like that.