go-axios (HTTP Client) 入门

go-axios入门

前言

日常开发中,各服务主要都是REST的形式提供接口服务,因此HTTP Client则是开发中的重中之重。 golang
中自带的HTTP Client已经能满足各类的场景,但是在使用的时候,各依赖服务的调用都基于同一模块,调整相关代码时影响较大,一些老旧系统的出错响应不规范,导致出错处理流程复杂难懂, go-axios
则由此而生。

go-axios
整体思路沿用(抄袭?) axios
,主要提供实例化的参数配置,提交数据与响应数据的 transform
,发送与响应的拦截器以及可自定义的 Adapter
(用于mock测试)。

实例化配置

go-axios
不提供默认的实例,所有的调用服务都需要自己去实例化,如我们有一个调用百度服务的实例:

package main

import (
    "fmt"

    "github.com/vicanso/go-axios"
)

func main() {
    ins := axios.NewInstance(&axios.InstanceConfig{
        BaseURL: "https://www.baidu.com/",
    })
    resp, err := ins.Get("/")
    fmt.Println(err)
    fmt.Println(resp.Status)
}

压缩提交数据

一般客户端比较少提交大数据的场景,但是在内部服务间的调用,有部分场景经常需要提交大量的数据,如应用系统的统计汇总,下面的则是针对大于1KB的提交数据进行gzip压缩(还可选择snappy等更快速的压缩算法)的例子:

package main

import (
    "bytes"
    "compress/gzip"
    "fmt"
    "math/rand"
    "net/http"
    "time"

    "github.com/vicanso/go-axios"
)

func init() {
    rand.Seed(time.Now().UnixNano())
}

var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func randStringRunes(n int) string {
    b := make([]rune, n)
    for i := range b {
        b[i] = letterRunes[rand.Intn(len(letterRunes))]
    }
    return string(b)
}

// doGzip gzip
func doGzip(buf []byte, level int) ([]byte, error) {
    var b bytes.Buffer
    if level <= 0 {
        level = gzip.DefaultCompression
    }
    w, _ := gzip.NewWriterLevel(&b, level)
    _, err := w.Write(buf)
    if err != nil {
        return nil, err
    }
    w.Close()
    return b.Bytes(), nil
}

func main() {
    transformRequest := make([]axios.TransformRequest, 0)
    // 默认的transform request将提交的数据转换为字节
    transformRequest = append(transformRequest, axios.DefaultTransformRequest...)
    transformRequest = append(transformRequest, func(body interface{}, headers http.Header) (data interface{}, err error) {
        key := "Content-Encoding"
        // 已做处理的跳过
        if headers.Get(key) != "" {
            return body, nil
        }
        buf, ok := body.([]byte)
        if !ok {
            return body, nil
        }
        // 少于1KB,不压缩
        if len(buf) < 1024 {
            return body, nil
        }
        gzipBuf, err := doGzip(buf, 0)
        // 压缩失败,则不处理
        if err != nil {
            return body, nil
        }
        headers.Set(key, "gzip")
        return gzipBuf, nil
    })
    ins := axios.NewInstance(&axios.InstanceConfig{
        BaseURL:          "http://localhost:3000/",
        TransformRequest: transformRequest,
    })
    data := map[string]string{
        "account":  randStringRunes(1024),
        "password": randStringRunes(1024),
    }
    resp, err := ins.Post("/", data)
    fmt.Println(err)
    fmt.Println(resp.Status)
}

请求拦截

如果需要对某个服务停止调用,则可以在请求拦截中处理。我们在管理后台中能针对各接入的服务设置可用的时间段,方便管理,简化的示例代码如下:

package main

import (
    "errors"
    "fmt"
    "sync/atomic"
    "time"

    "github.com/vicanso/go-axios"
)

const (
    // ServiceDisabled service disalbed
    ServiceDisabled = iota
    // ServiceEnabled service enabled
    ServiceEnabled
)

func main() {
    var baiduServerStatus int32
    // 如果时间戳为偶数则设置为可用(实际定时从数据库中相关配置中更新)
    if time.Now().Unix()%2 == 0 {
        atomic.StoreInt32(&baiduServerStatus, ServiceEnabled)
    }

    ins := axios.NewInstance(&axios.InstanceConfig{
        BaseURL: "https://www.baidu.com/",
        RequestInterceptors: []axios.RequestInterceptor{
            func(config *axios.Config) (err error) {
                if atomic.LoadInt32(&baiduServerStatus) != ServiceEnabled {
                    err = errors.New("service isn't enabled")
                    return
                }
                return
            },
        },
    })
    resp, err := ins.Get("/")
    fmt.Println(err)
    fmt.Println(resp)
}

性能统计

go-axios
可启用性能跟踪,包括DNS,TCP连接,首字节等各时间点的统计指标,可在 ResponseInterceptor
中获取这些指标写入统计数据库,示例如下:

package main

import (
    "fmt"

    "github.com/vicanso/go-axios"
)

var (
    aslant = axios.NewInstance(&axios.InstanceConfig{
        BaseURL:     "https://aslant.site/",
        // 启用性能跟踪
        EnableTrace: true,
        ResponseInterceptors: []axios.ResponseInterceptor{
            httpStats,
        },
    })
)

func httpStats(resp *axios.Response) (err error) {
    stats := make(map[string]interface{})
    config := resp.Config
    stats["url"] = config.URL
    stats["status"] = resp.Status

    ht := config.HTTPTrace
    if ht != nil {
        stats["timeline"] = config.HTTPTrace.Stats()
        stats["addr"] = ht.Addr
        stats["reused"] = ht.Reused
    }
    // 可以将相应的记录写入统计数据
    fmt.Println(stats)
    return nil
}

func main() {
    resp, err := aslant.Get("/")
    fmt.Println(err)
    fmt.Println(resp.Status)
}

出错转换

我们的REST服务出错是返回的HTTP状态码为4xx,5xx,而axios默认只为请求出错时才会返回Error,因此我们需要针对各服务将出错的响应直接转换为相应的Error,简化编码流程,也保证针对出错的正常处理(因为开发者有时会只判断Error,而未判断状态码),示例如下:

package main

import (
    "errors"
    "fmt"

    "github.com/vicanso/go-axios"

    jsoniter "github.com/json-iterator/go"
)

var (
    standardJSON = jsoniter.ConfigCompatibleWithStandardLibrary
)
var (
    aslant = axios.NewInstance(&axios.InstanceConfig{
        BaseURL: "https://ip.aslant.site/",
        ResponseInterceptors: []axios.ResponseInterceptor{
            convertResponseToError,
        },
    })
)

// convertResponseToError convert http response(4xx, 5xx) to error
func convertResponseToError(resp *axios.Response) (err error) {
    if resp.Status >= 400 {
        // 我们标准的响应出错消息记录至message中
        message := standardJSON.Get(resp.Data, "message").ToString()
        if message == "" {
            message = "Unknown Error"
        }
        // 也可自定义出错类
        err = errors.New(message)
    }
    return
}

func main() {
    _, err := aslant.Get("/ip-locations/json/123")
    fmt.Println(err)
}

Mock测试

系统依赖于各种服务,最需要处理的就是如何在测试中不受其它系统的影响,因为需要简单易用的mock方式,示例如下:

package main

import (
    "fmt"

    "github.com/vicanso/go-axios"
)

type (
    // UserInfo user info
    UserInfo struct {
        Account string `json:"account,omitempty"`
        Name    string `json:"name,omitempty"`
    }
)

var (
    aslant = axios.NewInstance(&axios.InstanceConfig{
        BaseURL: "https://aslant.site/",
    })
)

// getUserInfo get user info from aslant.site
func getUserInfo() (userInfo *UserInfo, err error) {
    resp, err := aslant.Get("/users/me")
    if err != nil {
        return
    }
    userInfo = new(UserInfo)
    err = resp.JSON(userInfo)
    if err != nil {
        return
    }
    return
}

// mockUserInfo mock user info
func mockUserInfo(data []byte) (done func()) {
    originalAdapter := aslant.Config.Adapter
    aslant.Config.Adapter = func(config *axios.Config) (resp *axios.Response, err error) {
        resp = &axios.Response{
            Data:   data,
            Status: 200,
        }
        return
    }

    done = func() {
        aslant.Config.Adapter = originalAdapter
    }
    return
}

func main() {
    mockUserInfo([]byte(`{"account":"tree", "name":"tree.xie"}`))
    userInfo, err := getUserInfo()
    fmt.Println(err)
    fmt.Println(userInfo)
}

小结

go-axios
的总体实现较为简单,总体上还是依赖于 http.Client
,更新详细的文档可至github上查阅,如果使用中有任何疑问,欢迎提issue。