Sending type-safe HTTP requests with Go
Tags: go, webdev, programming, opensource
Published: 2022-05-23 04:58:37

## Scenarios

For Gophers, we basically write code for clients to request. Sometimes we need to request RESTful APIs provided by third parties. At this time, we feel that it's hard to assemble the requests, not difficult, but error-prone.

For example, if we want to send a request like this, it looks very simple, but it's still tedious to actually write it.

```http
POST /articles/5/update?device=ios HTTP/1.1
Host: go-zero.dev
Authorization: Bearer <jwt-token>

{"author": "kevin", "body": "this is not important!", "title": "my title", "type":6}
```

## Go native way

This API is actually quite simple and we can write it directly from scratch.

```go
func main() {
    var buf bytes.
    encoder := json.NewEncoder(&buf)
    params := map[string]interface{}{
        "title":  "my title",
        "body":   "this is not important!",
        "author": "kevin",
        "type":   6,
    }
    if err := encoder.Encode(params); err ! = nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }

    url := fmt.Sprintf("http://go-zero.dev/articles/%d/update?device=%s", 5, "ios")
    req, err := http.NewRequest(http.MethodPost, url, &buf)
    if err ! = nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }

    req.Header.Add("Authorization", "Bearer <jwt-token>")
    cli := http.Client{}
    resp, err := cli.Do(req)
    if err ! = nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }

    io.Copy(os.Stdout, resp.Body)
}
```

We ran a test and found that we didn't get `200 OK`, dumped the packet and the request looks as follows. Can you think of the reason for the failure?

```http
POST /articles/5/update?device=ios HTTP/1.1
Host: go-zero.dev
User-Agent: Go-http-client/1.1
Content-Length: 79
Authorization: Bearer <jwt-token>
Accept-Encoding: gzip

{"author": "kevin", "body": "this is not important!", "title": "my title", "type":6}
```

The specific reasons for the failure will be discussed below, so let's explain this code first. You can see that the `map[string]interface{}` is used for the splicing parameters, for each field we can't check if the type matches. Only when we send it out and receive `200 OK` from the server, we can confirm that it is passed correctly. For example, the `type` parameter is used here as `int` type, we may write it as `string` type by mistake. But it is still hard to find out that this parameter is written incorrectly without requesting it.

So let's see how the `httpc` package in `go-zero` is used for type safety.

## `httpc` implementation

Let's see how the code for requesting with the `httpc` package is written.

```go
const url = "http://go-zero.dev/articles/:id/update"

type UpdateArticle struct {
    ID            int    `path: "id"`
    Device        string `form: "device,options=ios,android,web,desktop"`
    Authorization string `header: "Authorization"`
    Title         string `json: "title"`
    Body          string `json: "body"`
    Author        string `json: "author"`
    Type          int    `json: "type"`
}

func main() {
    data := &UpdateArticle{
        ID:            5,
        Device:        "ios",
        Authorization: "Bearer <jwt-token>",
        Title:         "my title",
        Body:          "this is not important!",
        Author:        "kevin",
        Type:          6,
    }

    resp, err := httpc.Do(context.Background(), http.MethodPost, url, data)
    if err ! = nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }

    io.Copy(os.Stdout, resp.Body)
}
```

Let's verify the code by sending a request, the result is as expected.

```http
POST /articles/5/update?device=ios HTTP/1.1
Host: go-zero.dev
User-Agent: Go-http-client/1.1
Content-Length: 79
Content-Type: application/json; charset=utf-8
Authorization: Bearer <jwt-token>
Accept-Encoding: gzip

{"author": "kevin", "body": "this is not important!", "title": "my title", "type":6}
```

Did you find out that, in contrast to the previous one, there is one more header was set, `Content-Type: application/json; charset=utf-8`, and we forgot to set `Content-Type` in the previous code.

`httpc` implementation is very easy to understand by defining the type of the request and sending with `Do`. With support for `path`, `form`, `header` and `json` as shown in our code, it is very easy and type-safe to send `HTTP` requests.

## More Capabilities

In addition to the ease of use and type-safety shown above, the `httpc` package has the following features.

1. timeout control with `context`. You can pass `ctx` for the requests.
2. automatic integration of `OpenTelemetry`. The `trace-id`, `span-id` returned by the server will be automatically written in the log, so that the client and server can work together to investigate the problems.
3. use `httpc.Service` to get the circuit breaker ability. When the server side has problems, it will automatically stop sending the requests, to avoid useless requests and reduce the pressure on the server side.

## Project address

[https://github.com/zeromicro/go-zero](https://github.com/zeromicro/go-zero)

Welcome to use `go-zero` and **star** to support us!