本文将为大家介绍 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
}
这种方式简单直接,但存在以下问题:
- URL 硬编码在代码中
- 参数拼接容易产生错误
- 错误处理逻辑重复
- 响应解析代码重复
模板化请求
为了解决上述问题,我们开始对代码进行抽象和模板化改造:
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
}
这样的改进带来了以下好处:
- 统一的错误处理机制
- 更安全的参数构建方式
- 可复用的请求处理逻辑
然而,我们仍然需要手动编写每个 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