Introduction to the

Resty is an HTTP client library for the Go language. Resty is powerful and feature-rich. It supports almost all of the HTTP method (GET/POST/PUT/DELETE/OPTION/HEAD/PATCH, etc.), and provides a simple and easy to use API.

Quick to use

The code in this article uses Go Modules.

Create directory and initialize:

$ mkdir resty && cd resty
$ go mod init github.com/darjun/go-daily-lib/resty
Copy the code

Install the Resty library:

$ go get -u github.com/go-resty/resty/v2
Copy the code

Let’s get baidu home page information:

package main

import (
  "fmt"
  "log"

  "github.com/go-resty/resty/v2"
)

func main(a) {
  client := resty.New()

  resp, err := client.R().Get("https://baidu.com")

  iferr ! =nil {
    log.Fatal(err)
  }

  fmt.Println("Response Info:")
  fmt.Println("Status Code:", resp.StatusCode())
  fmt.Println("Status:", resp.Status())
  fmt.Println("Proto:", resp.Proto())
  fmt.Println("Time:", resp.Time())
  fmt.Println("Received At:", resp.ReceivedAt())
  fmt.Println("Size:", resp.Size())
  fmt.Println("Headers:")
  for key, value := range resp.Header() {
    fmt.Println(key, "=", value)
  }
  fmt.Println("Cookies:")
  for i, cookie := range resp.Cookies() {
    fmt.Printf("cookie%d: name:%s value:%s\n", i, cookie.Name, cookie.Value)
  }
}
Copy the code

Resty is simpler to use.

  • First, call oneresty.New()To create aclientObject;
  • callclientThe object’sR()Method to create a request object;
  • Invoking the request objectGet()/Post()And so on, pass in the parameter URL, you can send HTTP request to the corresponding URL. Returns a response object;
  • The response object provides many methods to check the status, headers, cookies, and so on of the response.

In the above program we get:

  • StatusCode(): Status code, for example, 200.
  • Status(): Status code and status information, for example, 200 OK.
  • Proto(): protocol, such as HTTP/1.1;
  • Time(): Time from sending a request to receiving a response;
  • ReceivedAt(): The time when the response is received;
  • Size(): Response size;
  • Header(): Responds to the header message withhttp.HeaderThe type returns, i.emap[string][]string;
  • Cookies(): The server passes.Set-CookieCookie information set in the header.

Basic response information output by the running program:

Response Info:
Status Code: 200
Status: 200 OK
Proto: HTTP/1.1
Time: 415.774352ms
Received At: 2021- 0626 - 11:42:45.307157 +0800 CST m=+0.416547795
Size: 302456
Copy the code

Header Information:

Headers:
Server = [BWS/1.1]
Date = [Sat, 26 Jun 2021 03:42:45 GMT]
Connection = [keep-alive]
Bdpagetype = [1]
Bdqid = [0xf5a61d240003b218]
Vary = [Accept-Encoding Accept-Encoding]
Content-Type = [text/html;charset=utf- 8 -]
Set-Cookie = [BAIDUID=BF2EE47AAAF7A20C6971F1E897ABDD43:FG=1; expires=Thu, 31-Dec37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com BIDUPSID=BF2EE47AAAF7A20C6971F1E897ABDD43; expires=Thu, 31-Dec37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com PSTM=1624678965; expires=Thu, 31-Dec37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com BAIDUID=BF2EE47AAAF7A20C716E90B86906D6B0:FG=1; max-age=31536000; expires=Sun, 26-Jun- 22 03:42:45 GMT; domain=.baidu.com; path=/; version=1; comment=bd BDSVRTM=0; path=/ BD_HOME=1; path=/ H_PS_PSSID=34099_31253_34133_34072_33607_34135_26350; path=/; domain=.baidu.com]
Traceid = [1624678965045126810617700867425882583576]
P3p = [CP=" OTI DSP COR IVA OUR IND COM " CP=" OTI DSP COR IVA OUR IND COM "]
X-Ua-Compatible = [IE=Edge,chrome=1]
Copy the code

Note that there is a set-cookie header, which appears in the Cookie section:

Cookies:
cookie0: name:BAIDUID value:BF2EE47AAAF7A20C6971F1E897ABDD43:FG=1
cookie1: name:BIDUPSID value:BF2EE47AAAF7A20C6971F1E897ABDD43
cookie2: name:PSTM value:1624678965
cookie3: name:BAIDUID value:BF2EE47AAAF7A20C716E90B86906D6B0:FG=1
cookie4: name:BDSVRTM value:0
cookie5: name:BD_HOME value:1
cookie6: name:H_PS_PSSID value:34099_31253_34133_34072_33607_34135_26350
Copy the code

Automatic Unmarshal

Many web sites now offer apis that return structured data in JSON/XML formats. Resty can automatically Unmarshal the response data into the corresponding structure object. Here is an example. We know that many JS files are hosted on CDN. We can get the basic information of these libraries by going to api.cdnjs.com/libraries and return a JSON data in the following format:

Next, we define the structure and then use Resty to pull the information, automatically Unmarshal:

type Library struct {
  Name   string
  Latest string
}

type Libraries struct {
  Results []*Library
}

func main(a) {
  client := resty.New()

  libraries := &Libraries{}
  client.R().SetResult(libraries).Get("https://api.cdnjs.com/libraries")
  fmt.Printf("%d libraries\n".len(libraries.Results))

  for _, lib := range libraries.Results {
    fmt.Println("first library:")
    fmt.Printf("name:%s latest:%s\n", lib.Name, lib.Latest)
    break}}Copy the code

As you can see, all we need to do is create an object of the result type, then call the SetResult() method on the request object, and Resty will automatically Unmarshal the data for the response into the incoming object. The request information is set up in a chained invocation, where multiple Settings are done in a single line.

Run:

$ go run main.go
4040 libraries
first library:
name:vue latest:https:/ / cdnjs.cloudflare.com/ajax/libs/vue/3.1.2/vue.min.js
Copy the code

There are 4,040 libraries, the first of which is Vue✌️. We request https://api.cdnjs.com/libraries/vue can obtain Vue detailed information:

You can use Resty to pull this information yourself.

For a typical request, Resty will infer the data format based on the Content-Type in the response. But sometimes the response does not have a content-Type header or does not conform to the Content format, we can force Resty to parse the response in a specific format by calling ForceContentType() on the request object:

client.R().
  SetResult(result).
  ForceContentType("application/json")
Copy the code

Request information

Resty provides a wealth of ways to set up request information. We can set the query string in two ways. One is to call the request object’s SetQueryString() to set our concatenated query string:

client.R().
  SetQueryString("name=dj&age=18").
  Get(...)
Copy the code

The other is to call SetQueryParams() of the request object, pass in the map[string]string, and let Resty concatenate it for us. Obviously this is more convenient:

client.R().
  SetQueryParams(map[string]string{
    "name": "dj"."age": "18",
  }).
  Get(...)
Copy the code

Resty also provides a very useful interface for setting path parameters. We call SetPathParams() and pass in the map[string]string parameter, and then use the map key in the subsequent URL path:

client.R().
  SetPathParams(map[string]string{
    "user": "dj",
  }).
  Get("/v1/users/{user}/details")
Copy the code

Note that the keys in the path need to be wrapped in {}.

Setting header:

client.R().
  SetHeader("Content-Type"."application/json").
  Get(...)
Copy the code

Set the request message body:

client.R().
  SetHeader("Content-Type"."application/json").
  SetBody(`{"name": "dj", "age":18}`).
  Get(...)
Copy the code

Message bodies can be of many types: string, []byte, object, map[string]interface{}, and so on.

Set to carry a Content-Length header that resty automatically calculates:

client.R().
  SetBody(User{Name:"dj", Age:18}).
  SetContentLength(true).
  Get(...)
Copy the code

Some sites need to obtain a token before they can access its API. Set the token:

client.R().
  SetAuthToken("youdontknow").
  Get(...)
Copy the code

case

Finally, we use a case study to tie together the above. Now we want to access the organization’s repository information through the GitHub API, which is linked to the API documentation below. The GitHub API request address is https://api.github.com, and the request format is as follows:

GET /orgs/{org}/repos
Copy the code

We can also set the following parameters:

  • accept:The first, this must be set toapplication/vnd.github.v3+json;
  • org: Organization name,The path parameter;
  • type: Warehouse type,Query parameters, e.g.Public /private/forks(forks)And so on;
  • sort: sorting rules for the warehouse,Query parameters, e.g.created/updated/pushed/full_nameAnd so on. Sort by creation time by default;
  • direction: ascendingascOr descending orderdsc.Query parameters;
  • per_page: Number of entries per page, maximum 100, default 30,Query parameters;
  • page: Current request page, andper_pageDo paging together, default 1,Query parameters.

You must set a token to access the GitHub API. Log in to your GitHub account, click your profile picture in the upper right corner, and select Settings:

Then, select Developer Settings:

Select Personal Access Tokens and click Generate New Tokens in the upper right corner:

Note indicates the purpose of the token. You can fill in the Note according to your own situation. The following check boxes are used to select which permissions the token has and do not need to be checked:

To Generate a token, click the Generate Token button below:

Note that this token can only be seen now, close the page and enter the next time will not be seen. So keep it and don’t use my token. I will delete token😭 after testing the program.

The JSON data in the response looks like this:

There are a lot of fields. For convenience, I will deal with a few fields here:

type Repository struct {
  ID              int        `json:"id"`
  NodeID          string     `json:"node_id"`
  Name            string     `json:"name"`
  FullName        string     `json:"full_name"`
  Owner           *Developer `json:"owner"`
  Private         bool       `json:"private"`
  Description     string     `json:"description"`
  Fork            bool       `json:"fork"`
  Language        string     `json:"language"`
  ForksCount      int        `json:"forks_count"`
  StargazersCount int        `json:"stargazers_count"`
  WatchersCount   int        `json:"watchers_count"`
  OpenIssuesCount int        `json:"open_issues_count"`
}

type Developer struct {
  Login      string `json:"login"`
  ID         int    `json:"id"`
  NodeID     string `json:"node_id"`
  AvatarURL  string `json:"avatar_url"`
  GravatarID string `json:"gravatar_id"`
  Type       string `json:"type"`
  SiteAdmin  bool   `json:"site_admin"`
}
Copy the code

Then use resty to set path parameters, query parameters, headers, tokens, etc., and initiate a request:

func main(a) {
  client := resty.New()

  var result []*Repository
  client.R().
    SetAuthToken("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").
    SetHeader("Accept"."application/vnd.github.v3+json").
    SetQueryParams(map[string]string{
      "per_page":  "3"."page":      "1"."sort":      "created"."direction": "asc",
    }).
    SetPathParams(map[string]string{
      "org": "golang",
    }).
    SetResult(&result).
    Get("https://api.github.com/orgs/{org}/repos")

  for i, repo := range result {
    fmt.Printf("repo%d: name:%s stars:%d forks:%d\n", i+1, repo.Name, repo.StargazersCount, repo.ForksCount)
  }
}
Copy the code

The above program pulls three warehouses in ascending order of creation time:

$ go run main.go
repo1: name:gddo stars:1097 forks:289
repo2: name:lint stars:3892 forks:518
repo3: name:glog stars:2738 forks:775
Copy the code

Trace

Now that we’ve covered the main features of Resty, let’s take a look at one of the ancillary features resty provides: Trace. We call the EnableTrace() method on the request object to EnableTrace. Enable trace to record time and other information for each step of the request. Resty supports chained calls, which means we can create the request in a single line, enable trace, and initiate the request:

client.R().EnableTrace().Get("https://baidu.com")
Copy the code

After completing the request, we get the information by calling the TraceInfo() method of the request object:

ti := resp.Request.TraceInfo()
fmt.Println("Request Trace Info:")
fmt.Println("DNSLookup:", ti.DNSLookup)
fmt.Println("ConnTime:", ti.ConnTime)
fmt.Println("TCPConnTime:", ti.TCPConnTime)
fmt.Println("TLSHandshake:", ti.TLSHandshake)
fmt.Println("ServerTime:", ti.ServerTime)
fmt.Println("ResponseTime:", ti.ResponseTime)
fmt.Println("TotalTime:", ti.TotalTime)
fmt.Println("IsConnReused:", ti.IsConnReused)
fmt.Println("IsConnWasIdle:", ti.IsConnWasIdle)
fmt.Println("ConnIdleTime:", ti.ConnIdleTime)
fmt.Println("RequestAttempt:", ti.RequestAttempt)
fmt.Println("RemoteAddr:", ti.RemoteAddr.String())
Copy the code

We can obtain the following information:

  • DNSLookup: DNS query time. If a domain name is provided instead of an IP address, you need to query the CORRESPONDING IP address in the DNS system before performing subsequent operations.
  • ConnTime: The time it takes to obtain a connection, either from the connection pool or from a new one.
  • TCPConnTime: Indicates the TCP connection time. The time is from the end of DNS query to the establishment of TCP connection.
  • TLSHandshake: TLS handshake time;
  • ServerTime: Server processing time, calculating the interval between the establishment of the connection and the receipt of the first byte by the client;
  • ResponseTime: Response time, the interval between receiving the first response byte and receiving the complete response;
  • TotalTime: Time consuming of the whole process;
  • IsConnReused: Whether the TCP connection is multiplexed.
  • IsConnWasIdle: Whether the connection was obtained from an idle connection pool;
  • ConnIdleTime: Connection idle time;
  • RequestAttempt: Request number of requests in the execution process, including retries.
  • RemoteAddr: Remote service address,IP:PORTFormat.

Resty makes a fine distinction between these. Resty actually uses functionality provided by the standard library net/ HTTP/httpTrace, which provides a structure for setting callback functions for each phase:

// src/net/http/httptrace.go
type ClientTrace struct {
  GetConn func(hostPort string)
  GotConn func(GotConnInfo)
  PutIdleConn func(err error)
  GotFirstResponseByte func(a)
  Got100Continue func(a)
  Got1xxResponse func(code int, header textproto.MIMEHeader) error // Go 1.11
  DNSStart func(DNSStartInfo)
  DNSDone func(DNSDoneInfo)
  ConnectStart func(network, addr string)
  ConnectDone func(network, addr string, err error)
  TLSHandshakeStart func(a) // Go 1.8
  TLSHandshakeDone func(tls.ConnectionState, error) // Go 1.8
  WroteHeaderField func(key string, value []string) // Go 1.11
  WroteHeaders func(a)
  Wait100Continue func(a)
  WroteRequest func(WroteRequestInfo)
}
Copy the code

You can get a simple idea of what a callback means from the field name. Resty set the following callback after enabling trace:

// src/github.com/go-resty/resty/trace.go
func (t *clientTrace) createContext(ctx context.Context) context.Context {
  return httptrace.WithClientTrace(
    ctx,
    &httptrace.ClientTrace{
      DNSStart: func(_ httptrace.DNSStartInfo) {
        t.dnsStart = time.Now()
      },
      DNSDone: func(_ httptrace.DNSDoneInfo) {
        t.dnsDone = time.Now()
      },
      ConnectStart: func(_ _string) {
        if t.dnsDone.IsZero() {
          t.dnsDone = time.Now()
        }
        if t.dnsStart.IsZero() {
          t.dnsStart = t.dnsDone
        }
      },
      ConnectDone: func(net, addr string, err error) {
        t.connectDone = time.Now()
      },
      GetConn: func(_ string) {
        t.getConn = time.Now()
      },
      GotConn: func(ci httptrace.GotConnInfo) {
        t.gotConn = time.Now()
        t.gotConnInfo = ci
      },
      GotFirstResponseByte: func(a) {
        t.gotFirstResponseByte = time.Now()
      },
      TLSHandshakeStart: func(a) {
        t.tlsHandshakeStart = time.Now()
      },
      TLSHandshakeDone: func(_ tls.ConnectionState, _ error) {
        t.tlsHandshakeDone = time.Now()
      },
    },
  )
}
Copy the code

When obtaining TraceInfo, calculate the time at each point in time:

// src/github.com/go-resty/resty/request.go
func (r *Request) TraceInfo(a) TraceInfo {
  ct := r.clientTrace

  if ct == nil {
    return TraceInfo{}
  }

  ti := TraceInfo{
    DNSLookup:      ct.dnsDone.Sub(ct.dnsStart),
    TLSHandshake:   ct.tlsHandshakeDone.Sub(ct.tlsHandshakeStart),
    ServerTime:     ct.gotFirstResponseByte.Sub(ct.gotConn),
    IsConnReused:   ct.gotConnInfo.Reused,
    IsConnWasIdle:  ct.gotConnInfo.WasIdle,
    ConnIdleTime:   ct.gotConnInfo.IdleTime,
    RequestAttempt: r.Attempt,
  }

  if ct.gotConnInfo.Reused {
    ti.TotalTime = ct.endTime.Sub(ct.getConn)
  } else {
    ti.TotalTime = ct.endTime.Sub(ct.dnsStart)
  }

  if! ct.connectDone.IsZero() { ti.TCPConnTime = ct.connectDone.Sub(ct.dnsDone) }if! ct.gotConn.IsZero() { ti.ConnTime = ct.gotConn.Sub(ct.getConn) }if! ct.gotFirstResponseByte.IsZero() { ti.ResponseTime = ct.endTime.Sub(ct.gotFirstResponseByte) }ifct.gotConnInfo.Conn ! =nil {
    ti.RemoteAddr = ct.gotConnInfo.Conn.RemoteAddr()
  }

  return ti
}
Copy the code

Run output:

$ go run main.go
Request Trace Info:
DNSLookup: 2.815171ms
ConnTime: 941.635171ms
TCPConnTime: 269.069692ms
TLSHandshake: 669.276011ms
ServerTime: 274.623991ms
ResponseTime: 112.216(including s TotalTime:1.216276906s
IsConnReused: false
IsConnWasIdle: false
ConnIdleTime: 0s
RequestAttempt: 1
RemoteAddr: 18.235124.214.:443
Copy the code

We saw that TLS consumed nearly half of the time.

conclusion

This article introduces the Go language a very convenient and easy to use HTTP Client library. Resty provides a very useful, rich API. Chain call, automatic Unmarshal, request parameter/path Settings these features are very convenient to use, let us work twice the result with half the effort. Due to lack of space, many advanced features, such as submitting forms, uploading files, etc., are not covered in detail. That’s for anyone interested to explore.

If you find a fun and useful Go library, please Go to GitHub and submit issue😄

reference

  1. GitHub: github.com/darjun/go-d…
  2. Resty GitHub:github.com/go-resty/re…
  3. Making API:docs.github.com/en/rest/ove…

I

My blog is darjun.github. IO

Welcome to follow my wechat public account [GoUpUp], learn together, progress together ~