我创建了一个模块来发出 HTTP 请求,其使用方式如下:
response, err := Get[ResponseType]("https://example.com").
Query("foo", "bar").
Header("Accept", "text/plain").
Do(context.Background())
它使用构建器和泛型来简化 HTTP 请求。
虽然(IMO)构建器模式读起来很好,但我现在尝试在编译时强制执行一些检查。例如,需要
http.Client
才能进行实际的 HTTP 调用。我想让用户注入自己的 http.Client
或指示构建器使用默认的。像这样:
response, err := Get[ResponseType]("https://example.com").
Client(myHttpClient).
Do(context.Background())
// or
response, err := Get[ResponseType]("https://example.com").
UseDefaultClient().
Do(context.Background())
如果没有使用这两种指定客户端的方法,那么我希望构建器在编译时失败。我尝试使用泛型来做到这一点,但我还没有找到一个完全令人满意的解决方案。这是我到目前为止所得到的:
// R is for response type, C is for config state
type Builder[R any, C any] struct {
httpClient *http.Client
}
type NoClient struct{}
type WithClient struct{}
func Get[R any](requestUrl string) *Builder[R, NoClient] {
return &Builder[R, NoClient]{ /* ... */ }
}
func (b *Builder[R, C]) Client(httpClient *http.Client) *Builder[R, WithClient] {
b.httpClient = httpClient
return (*Builder[R, WithClient])(b)
}
func (b *Builder[R, C]) UseDefaultClient() *Builder[R, WithClient] {
b.httpClient = &http.Client{}
return (*Builder[R, WithClient])(b)
}
func (b *Builder[R, C]) Header(key string, value string) *Builder[R, C] {
/* add header */
return b
}
func (b *Builder[R, C]) Do(ctx context.Context) (Response[R], error) {
/* execute request */
}
如果类型为
Do
,我希望对方法 Builder[R, NoClient]
的调用在编译时失败,否则成功。但是,我相信这是不可能的。编译器完全没问题:
response, err := Get[ResponseType]("https://example.com").
Do(context.Background())
因为方法
Do
的接收者指针没有限制(据我所知,不可能添加一个)。
我发现的一个潜在解决方案是使用函数而不是方法:
func ExecuteRequest[R any](
ctx context.Context,
builder *Builder[R, WithClient],
) (Response[R], error) {
/* ... */
}
现在,根据需要,这在编译时失败了:
response, err := ExecuteRequest(
context.Background(),
Get[ResponseType]("https://example.com")
)
我想知道是否真的可以在保持
Get[R](...).Do()
方法的同时解决这个问题。虽然我使用了一个具体的示例(希望)可以更容易地理解问题,但我认为为构建器强制执行编译时保证在许多情况下都是有用的。
因此,您基本上希望强制构建器的用户在发出任何请求之前专门设置客户端。然后用只有这两个方法的东西来限制他们的接口。
实现目标的一种方法是使用仅允许设置客户端的中间构建器,这里是一个示例:
type Response[T any] struct{}
type ClientBuilder[R any] struct {
httpClient *http.Client
}
func (b *ClientBuilder[R]) WithClient(httpClient *http.Client) *Builder[R] {
b.httpClient = httpClient
return (*Builder[R])(b)
}
func (b *ClientBuilder[R]) DefaultClient() *Builder[R] {
b.httpClient = &http.Client{}
return (*Builder[R])(b)
}
type Builder[R any] struct {
httpClient *http.Client
}
func Get[R any](requestUrl string) *ClientBuilder[R] {
return &ClientBuilder[R]{ /* ... */ }
}
func (b *Builder[R]) Header(key string, value string) *Builder[R] {
/* add header */
return b
}
func (b *Builder[R]) Do(ctx context.Context) (Response[R], error) {
/* execute request */
return Response[R]{}, nil
}
func main() {
// This will fail on compile
response, err := Get[string]("https://example.com").
Do(context.Background())
// This wont
response, err := Get[string]("https://example.com").
DefaultClient().
Do(context.Background())
fmt.Println(response, err)
}
但我认为默认客户端设置要求有点太冗长了,按照您的初始代码示例,我将完全删除
UseDefaultClient
并在用户决定不设置自定义方法时仅在 Do 方法中使用一个:
func (b *Builder[R, C]) Do(ctx context.Context) (Response[R], error) {
client := b.httpClient
if client == nil {
client = http.DefaultClient
}
/* execute request */
}