In this article, I’m excited to introduce genapi, a code generator for Golang HTTP clients. For comprehensive documentation and implementation details, you can explore the genapi website or check out our GitHub repository.
From Manual to Automatic: Evolution of Golang HTTP Client
In Golang development, making HTTP API calls is a very common requirement. Through a weather API example, this article will demonstrate how HTTP client code evolves from manual writing to automatic generation. Let’s look at this simple weather API:
GET /api/weather?city=shanghai
Response:
{
"temperature": 25,
"humidity": 60,
"condition": "sunny"
}
Initial Manual Approach
Initially, we might write code directly like this:
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
}
This approach is straightforward but has several issues:
- Hardcoded URLs in the code
- Error-prone parameter concatenation
- Duplicate error handling logic
- Repetitive response parsing code
Templated Requests
To address these issues, we begin abstracting and templatizing the code:
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
}
These improvements bring several benefits:
- Unified error handling mechanism
- Safer parameter construction
- Reusable request handling logic
However, we still need to manually write each API method.
genapi: Annotation-Driven Code Generation
To further improve development efficiency, introducing genapi. Through simple annotations, we can automatically generate all API call code:
package api
import "github.com/lexcao/genapi"
//go:generate go run github.com/lexcao/genapi/cmd/genapi -file $GOFILE
// WeatherAPI defines the weather service 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)
}
By just defining the interface and adding annotations, genapi will automatically generate the complete client code using go generate:
// CODE GENERATED BY genapi. DO NOT EDIT.
package api
import (
"context"
"github.com/lexcao/genapi"
"net/url"
)
type implWeatherAPI struct {
client genapi.HttpClient
}
// SetHttpClient implements 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",
},
)
}
Using the Generated Code
func main() {
client := genapi.New[api.WeatherAPI]()
weather, err := client.GetWeather(context.TODO(), "shanghai")
}
Replacing Different HttpClients
One of the core features of genapi is its support for dynamically replacing HttpClient at runtime. This gives us great flexibility to switch between different HTTP client implementations as needed.
Replacing the Default HttpClient
import (
http_client "net/http"
"github.com/lexcao/genapi"
"github.com/lexcao/genapi/pkg/clients/http"
)
func main() {
httpClient := &http_client.Client{}
// Specify when creating
client := genapi.New[api.WeatherAPI](
genapi.WithHttpClient(http.New(httpClient))
)
// Or set at runtime
client.SetHttpClient(httpClient)
}
Using Resty
genapi has built-in support for Resty client. First, you need to install:
go get github.com/lexcao/genapi/pkg/clients/resty
Then you can use it like this:
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), // Use default configuration
genapi.WithHttpClient(resty.New(resty_client.New())), // Custom configuration
)
}
Implementing Your Own HttpClient
You can also implement your own HttpClient by implementing the genapi.HttpClient
interface:
type HttpClient interface {
SetConfig(Config)
Do(req *Request) (*Response, error)
}
You can also use the test suite genapi.TestHttpClient
to verify if your implementation covers the basic use cases:
Summary
Through annotation-driven approach, genapi enables developers to:
- Focus on interface definition, avoiding repetitive code
- Improve development efficiency and reduce maintenance costs
- Make code clearer and more reliable
- Dynamically replace HttpClient implementations