本文将为大家介绍 genapi,一个用于自动生成 Golang HTTP Client 的代码库。如果你对这个项目感兴趣,可以访问 genapi 官网GitHub 仓库 获取更多技术细节。

从手工到自动:Golang HTTP Client 的演进之路

在 Golang 开发中,调用 HTTP API 是一个非常常见的需求。本文将通过一个天气 API 的示例,介绍 HTTP Client 代码是如何从手工编写演进到自动生成的。让我们看看这个简单的天气 API:

GET /api/weather?city=shanghai
Response:
{
    "temperature": 25,
    "humidity": 60,
    "condition": "sunny"
}

原始手工编写

最初,我们可能会直接编写如下代码:

func getWeather(city string) (*Weather, error) {
    resp, err := http.Get("https://api.weather.com/api/weather?city=" + city)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    var weather Weather
    if err := json.NewDecoder(resp.Body).Decode(&weather); err != nil {
        return nil, err
    }
    
    return &weather, nil
}

这种方式简单直接,但存在以下问题:

  1. URL 硬编码在代码中
  2. 参数拼接容易产生错误
  3. 错误处理逻辑重复
  4. 响应解析代码重复

模板化请求

为了解决上述问题,我们开始对代码进行抽象和模板化改造:

type Client struct {
    baseURL string
    client  *http.Client
}

func (c *Client) doRequest(method, path string, query url.Values, result interface{}) error {
    u, _ := url.Parse(c.baseURL + path)
    u.RawQuery = query.Encode()
    
    req, err := http.NewRequest(method, u.String(), nil)
    if err != nil {
        return err
    }
    
    resp, err := c.client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    return json.NewDecoder(resp.Body).Decode(result)
}

func (c *Client) GetWeather(city string) (*Weather, error) {
    query := url.Values{}
    query.Set("city", city)
    
    var weather Weather
    err := c.doRequest("GET", "/api/weather", query, &weather)
    return &weather, err
}

这样的改进带来了以下好处:

  1. 统一的错误处理机制
  2. 更安全的参数构建方式
  3. 可复用的请求处理逻辑

然而,我们仍然需要手动编写每个 API 方法。

genapi:注解驱动的代码生成

为了进一步提高开发效率,引入 genapi。通过简单的注解,我们可以自动生成所有的 API 调用代码:

package api

import "github.com/lexcao/genapi"

//go:generate go run github.com/lexcao/genapi/cmd/genapi -file $GOFILE

// WeatherAPI 定义了天气服务的 API
// @BaseURL("https://api.weather.com")
type WeatherAPI interface {
    genapi.Interface

    // @get("/api/weather")
    // @query("city", "{city}")
    GetWeather(ctx context.Context, city string) (*Weather, error)
}

只需要定义接口和添加注解,使用 go generate genapi 就会自动生成完整的客户端代码:

// CODE GENERATED BY genapi. DO NOT EDIT.
package api

import (
	"context"
	"github.com/lexcao/genapi"
	"net/url"
)

type implWeatherAPI struct {
	client genapi.HttpClient
}

// SetHttpClient implments genapi.Interface
func (i *implWeatherAPI) SetHttpClient(client genapi.HttpClient) {
	i.client = client
}

func (i *implWeatherAPI) GetWeather(ctx context.Context, city string) (*Weather, error) {
	resp, err := i.client.Do(&genapi.Request{
		Method: "get",
		Path:   "/api/weather",
		Queries: url.Values{
			"city": []string{
				city,
			},
		},
		Context: ctx,
	})
	return genapi.HandleResponse[*Weather](resp, err)
}

func init() {
	genapi.Register[WeatherAPI, *implWeatherAPI](
		genapi.Config{
			BaseURL: "https://api.weather.com",
		},
	)
}

生成后的代码这样使用

func main() {
    client := genapi.New[api.WeatherAPI]()

    weather, err := client.GetWeather(context.TODO(), "shanghai")
}

替换不同的 HttpClient

genapi 的一个核心特性是支持在运行时动态替换 HttpClient。这给了我们极大的灵活性,可以根据需要切换不同的 HTTP 客户端实现。

替换默认的 HttpClient

import (
    http_client "net/http"
    "github.com/lexcao/genapi"
    "github.com/lexcao/genapi/pkg/clients/http"
)

func main() {
    httpClient := &http_client.Client{}
    
    // 创建时指定
    client := genapi.New[api.WeatherAPI](
        genapi.WithHttpClient(http.New(httpClient))
    )

    // 或者运行时设置
    client.SetHttpClient(httpClient)
}

使用 Resty

genapi 已经内置支持了 Resty 客户端。首先需要安装:

go get github.com/lexcao/genapi/pkg/clients/resty

然后就可以这样使用:

import (
    "github.com/lexcao/genapi"
    "github.com/lexcao/genapi/pkg/clients/resty"
    resty_client "github.com/go-resty/resty/v2"
)

func main() {
    client := genapi.New[api.WeatherAPI](
        genapi.WithHttpClient(resty.DefaultClient),           // 使用默认配置
        genapi.WithHttpClient(resty.New(resty_client.New())), // 自定义配置
    )
}

实现自己的 HttpClient

你也可以实现自己的 HttpClient,只需要实现 genapi.HttpClient 接口:

type HttpClient interface {
    SetConfig(Config)
    Do(req *Request) (*Response, error)
}

还可以使用测试套件 genapi.TestHttpClient 来验证你的实现是否覆盖基本用例:

总结

genapi 通过注解驱动的方式,让开发者可以:

  • 专注于接口定义,避免编写重复代码
  • 提高开发效率,降低维护成本
  • 使代码更加清晰可靠
  • 动态替换 HttpClient