3.6.4 GraphQL 客户端开发

3.6.4 GraphQL 客户端开发 #

在构建了 GraphQL 服务器之后,本节将详细介绍如何开发 GraphQL 客户端应用。我们将学习如何使用 Go 语言构建 GraphQL 客户端,包括查询执行、变更操作、订阅处理、错误处理和性能优化等内容。

GraphQL 客户端库 #

Go 生态系统中有几个优秀的 GraphQL 客户端库:

  1. machinebox/graphql - 简单易用的 GraphQL 客户端
  2. shurcooL/graphql - 类型安全的 GraphQL 客户端
  3. 99designs/gqlgen - 支持客户端代码生成
  4. hasura/go-graphql-client - 功能丰富的客户端库

本节主要使用 machinebox/graphqlshurcooL/graphql 进行演示。

基础客户端实现 #

安装依赖 #

go get github.com/machinebox/graphql
go get github.com/shurcooL/graphql
go get golang.org/x/oauth2

简单客户端示例 #

// client/simple_client.go
package main

import (
    "context"
    "fmt"
    "log"

    "github.com/machinebox/graphql"
)

func main() {
    // 创建 GraphQL 客户端
    client := graphql.NewClient("http://localhost:8080/graphql")

    // 设置请求头(如果需要认证)
    client.Header.Set("Authorization", "Bearer your-jwt-token")

    // 创建查询
    req := graphql.NewRequest(`
        query {
            users {
                id
                name
                email
                role
                createdAt
            }
        }
    `)

    // 定义响应结构
    var resp struct {
        Users []struct {
            ID        string `json:"id"`
            Name      string `json:"name"`
            Email     string `json:"email"`
            Role      string `json:"role"`
            CreatedAt string `json:"createdAt"`
        } `json:"users"`
    }

    // 执行查询
    ctx := context.Background()
    if err := client.Run(ctx, req, &resp); err != nil {
        log.Fatal(err)
    }

    // 处理响应
    fmt.Printf("Found %d users:\n", len(resp.Users))
    for _, user := range resp.Users {
        fmt.Printf("- %s (%s): %s\n", user.Name, user.Role, user.Email)
    }
}

类型安全的客户端 #

使用 shurcooL/graphql 库可以实现类型安全的 GraphQL 客户端:

// client/typed_client.go
package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/shurcooL/graphql"
    "golang.org/x/oauth2"
)

// 定义 GraphQL 类型
type User struct {
    ID        graphql.ID     `graphql:"id"`
    Name      graphql.String `graphql:"name"`
    Email     graphql.String `graphql:"email"`
    Role      UserRole       `graphql:"role"`
    CreatedAt time.Time      `graphql:"createdAt"`
    Posts     []Post         `graphql:"posts"`
}

type Post struct {
    ID        graphql.ID     `graphql:"id"`
    Title     graphql.String `graphql:"title"`
    Content   graphql.String `graphql:"content"`
    Status    PostStatus     `graphql:"status"`
    CreatedAt time.Time      `graphql:"createdAt"`
    Author    User           `graphql:"author"`
    Tags      []Tag          `graphql:"tags"`
}

type Tag struct {
    ID   graphql.ID     `graphql:"id"`
    Name graphql.String `graphql:"name"`
}

type UserRole string
type PostStatus string

const (
    UserRoleAdmin UserRole = "ADMIN"
    UserRoleUser  UserRole = "USER"

    PostStatusDraft     PostStatus = "DRAFT"
    PostStatusPublished PostStatus = "PUBLISHED"
)

func main() {
    // 创建带认证的客户端
    src := oauth2.StaticTokenSource(
        &oauth2.Token{AccessToken: "your-jwt-token"},
    )
    httpClient := oauth2.NewClient(context.Background(), src)

    client := graphql.NewClient("http://localhost:8080/graphql", httpClient)

    // 查询用户列表
    queryUsers(client)

    // 查询单个用户
    queryUser(client, "1")

    // 创建用户
    createUser(client)

    // 更新用户
    updateUser(client, "1")

    // 创建文章
    createPost(client)
}

func queryUsers(client *graphql.Client) {
    var query struct {
        Users []User `graphql:"users(role: $role)"`
    }

    variables := map[string]interface{}{
        "role": (*UserRole)(nil), // nil 表示不过滤
    }

    err := client.Query(context.Background(), &query, variables)
    if err != nil {
        log.Printf("Query users error: %v", err)
        return
    }

    fmt.Printf("Found %d users:\n", len(query.Users))
    for _, user := range query.Users {
        fmt.Printf("- %s (%s): %s\n", user.Name, user.Role, user.Email)
    }
}

func queryUser(client *graphql.Client, userID string) {
    var query struct {
        User *User `graphql:"user(id: $id)"`
    }

    variables := map[string]interface{}{
        "id": graphql.ID(userID),
    }

    err := client.Query(context.Background(), &query, variables)
    if err != nil {
        log.Printf("Query user error: %v", err)
        return
    }

    if query.User == nil {
        fmt.Printf("User with ID %s not found\n", userID)
        return
    }

    user := query.User
    fmt.Printf("User: %s (%s)\n", user.Name, user.Email)
    fmt.Printf("Posts: %d\n", len(user.Posts))
    for _, post := range user.Posts {
        fmt.Printf("  - %s (%s)\n", post.Title, post.Status)
    }
}

func createUser(client *graphql.Client) {
    var mutation struct {
        CreateUser User `graphql:"createUser(input: $input)"`
    }

    input := map[string]interface{}{
        "name":   "Bob Wilson",
        "email":  "[email protected]",
        "role":   UserRoleUser,
        "avatar": "https://example.com/bob.jpg",
    }

    variables := map[string]interface{}{
        "input": input,
    }

    err := client.Mutate(context.Background(), &mutation, variables)
    if err != nil {
        log.Printf("Create user error: %v", err)
        return
    }

    user := mutation.CreateUser
    fmt.Printf("Created user: %s (ID: %s)\n", user.Name, user.ID)
}

func updateUser(client *graphql.Client, userID string) {
    var mutation struct {
        UpdateUser User `graphql:"updateUser(id: $id, input: $input)"`
    }

    input := map[string]interface{}{
        "name": "Updated Name",
    }

    variables := map[string]interface{}{
        "id":    graphql.ID(userID),
        "input": input,
    }

    err := client.Mutate(context.Background(), &mutation, variables)
    if err != nil {
        log.Printf("Update user error: %v", err)
        return
    }

    user := mutation.UpdateUser
    fmt.Printf("Updated user: %s (ID: %s)\n", user.Name, user.ID)
}

func createPost(client *graphql.Client) {
    var mutation struct {
        CreatePost Post `graphql:"createPost(input: $input)"`
    }

    input := map[string]interface{}{
        "title":   "My New Post",
        "content": "This is the content of my new post.",
        "status":  PostStatusDraft,
        "tags":    []string{"go", "graphql"},
    }

    variables := map[string]interface{}{
        "input": input,
    }

    err := client.Mutate(context.Background(), &mutation, variables)
    if err != nil {
        log.Printf("Create post error: %v", err)
        return
    }

    post := mutation.CreatePost
    fmt.Printf("Created post: %s (ID: %s)\n", post.Title, post.ID)
    fmt.Printf("Author: %s\n", post.Author.Name)
    fmt.Printf("Tags: ")
    for i, tag := range post.Tags {
        if i > 0 {
            fmt.Print(", ")
        }
        fmt.Print(tag.Name)
    }
    fmt.Println()
}

高级客户端功能 #

1. 请求拦截器和中间件 #

// client/interceptor.go
package client

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/machinebox/graphql"
)

// RequestInterceptor 请求拦截器接口
type RequestInterceptor interface {
    Intercept(req *http.Request) error
}

// ResponseInterceptor 响应拦截器接口
type ResponseInterceptor interface {
    Intercept(resp *http.Response) error
}

// LoggingInterceptor 日志拦截器
type LoggingInterceptor struct{}

func (l *LoggingInterceptor) Intercept(req *http.Request) error {
    log.Printf("GraphQL Request: %s %s", req.Method, req.URL.String())
    return nil
}

// AuthInterceptor 认证拦截器
type AuthInterceptor struct {
    Token string
}

func (a *AuthInterceptor) Intercept(req *http.Request) error {
    if a.Token != "" {
        req.Header.Set("Authorization", "Bearer "+a.Token)
    }
    return nil
}

// RetryInterceptor 重试拦截器
type RetryInterceptor struct {
    MaxRetries int
    Delay      time.Duration
}

func (r *RetryInterceptor) Intercept(req *http.Request) error {
    // 重试逻辑在客户端层面实现
    return nil
}

// EnhancedClient 增强的 GraphQL 客户端
type EnhancedClient struct {
    client               *graphql.Client
    requestInterceptors  []RequestInterceptor
    responseInterceptors []ResponseInterceptor
}

func NewEnhancedClient(endpoint string) *EnhancedClient {
    return &EnhancedClient{
        client:               graphql.NewClient(endpoint),
        requestInterceptors:  make([]RequestInterceptor, 0),
        responseInterceptors: make([]ResponseInterceptor, 0),
    }
}

func (c *EnhancedClient) AddRequestInterceptor(interceptor RequestInterceptor) {
    c.requestInterceptors = append(c.requestInterceptors, interceptor)
}

func (c *EnhancedClient) AddResponseInterceptor(interceptor ResponseInterceptor) {
    c.responseInterceptors = append(c.responseInterceptors, interceptor)
}

func (c *EnhancedClient) Run(ctx context.Context, req *graphql.Request, resp interface{}) error {
    // 应用请求拦截器
    httpReq, err := req.HTTP(ctx)
    if err != nil {
        return err
    }

    for _, interceptor := range c.requestInterceptors {
        if err := interceptor.Intercept(httpReq); err != nil {
            return err
        }
    }

    // 执行请求
    return c.client.Run(ctx, req, resp)
}

// 使用示例
func ExampleEnhancedClient() {
    client := NewEnhancedClient("http://localhost:8080/graphql")

    // 添加拦截器
    client.AddRequestInterceptor(&LoggingInterceptor{})
    client.AddRequestInterceptor(&AuthInterceptor{Token: "your-jwt-token"})

    // 创建查询
    req := graphql.NewRequest(`
        query {
            users {
                id
                name
                email
            }
        }
    `)

    var resp struct {
        Users []struct {
            ID    string `json:"id"`
            Name  string `json:"name"`
            Email string `json:"email"`
        } `json:"users"`
    }

    // 执行查询
    if err := client.Run(context.Background(), req, &resp); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Found %d users\n", len(resp.Users))
}

2. 缓存机制 #

// client/cache.go
package client

import (
    "context"
    "crypto/md5"
    "encoding/json"
    "fmt"
    "sync"
    "time"

    "github.com/machinebox/graphql"
)

// CacheEntry 缓存条目
type CacheEntry struct {
    Data      interface{}
    ExpiresAt time.Time
}

// Cache 缓存接口
type Cache interface {
    Get(key string) (interface{}, bool)
    Set(key string, value interface{}, ttl time.Duration)
    Delete(key string)
    Clear()
}

// MemoryCache 内存缓存实现
type MemoryCache struct {
    mu    sync.RWMutex
    items map[string]*CacheEntry
}

func NewMemoryCache() *MemoryCache {
    cache := &MemoryCache{
        items: make(map[string]*CacheEntry),
    }

    // 启动清理 goroutine
    go cache.cleanup()

    return cache
}

func (c *MemoryCache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()

    entry, exists := c.items[key]
    if !exists {
        return nil, false
    }

    if time.Now().After(entry.ExpiresAt) {
        delete(c.items, key)
        return nil, false
    }

    return entry.Data, true
}

func (c *MemoryCache) Set(key string, value interface{}, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()

    c.items[key] = &CacheEntry{
        Data:      value,
        ExpiresAt: time.Now().Add(ttl),
    }
}

func (c *MemoryCache) Delete(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()

    delete(c.items, key)
}

func (c *MemoryCache) Clear() {
    c.mu.Lock()
    defer c.mu.Unlock()

    c.items = make(map[string]*CacheEntry)
}

func (c *MemoryCache) cleanup() {
    ticker := time.NewTicker(time.Minute)
    defer ticker.Stop()

    for range ticker.C {
        c.mu.Lock()
        now := time.Now()
        for key, entry := range c.items {
            if now.After(entry.ExpiresAt) {
                delete(c.items, key)
            }
        }
        c.mu.Unlock()
    }
}

// CachedClient 带缓存的 GraphQL 客户端
type CachedClient struct {
    client *graphql.Client
    cache  Cache
    ttl    time.Duration
}

func NewCachedClient(endpoint string, cache Cache, ttl time.Duration) *CachedClient {
    return &CachedClient{
        client: graphql.NewClient(endpoint),
        cache:  cache,
        ttl:    ttl,
    }
}

func (c *CachedClient) Run(ctx context.Context, req *graphql.Request, resp interface{}) error {
    // 生成缓存键
    key := c.generateCacheKey(req)

    // 尝试从缓存获取
    if cached, found := c.cache.Get(key); found {
        return c.copyResponse(cached, resp)
    }

    // 执行查询
    if err := c.client.Run(ctx, req, resp); err != nil {
        return err
    }

    // 缓存结果(只缓存查询,不缓存变更)
    if !c.isMutation(req) {
        c.cache.Set(key, resp, c.ttl)
    }

    return nil
}

func (c *CachedClient) generateCacheKey(req *graphql.Request) string {
    data, _ := json.Marshal(map[string]interface{}{
        "query":     req.Query(),
        "variables": req.Vars(),
    })
    return fmt.Sprintf("%x", md5.Sum(data))
}

func (c *CachedClient) isMutation(req *graphql.Request) bool {
    query := req.Query()
    return len(query) > 8 && query[:8] == "mutation"
}

func (c *CachedClient) copyResponse(src, dst interface{}) error {
    data, err := json.Marshal(src)
    if err != nil {
        return err
    }
    return json.Unmarshal(data, dst)
}

// 使用示例
func ExampleCachedClient() {
    cache := NewMemoryCache()
    client := NewCachedClient("http://localhost:8080/graphql", cache, 5*time.Minute)

    req := graphql.NewRequest(`
        query {
            users {
                id
                name
                email
            }
        }
    `)

    var resp struct {
        Users []struct {
            ID    string `json:"id"`
            Name  string `json:"name"`
            Email string `json:"email"`
        } `json:"users"`
    }

    // 第一次请求(从服务器获取)
    start := time.Now()
    if err := client.Run(context.Background(), req, &resp); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("First request took: %v\n", time.Since(start))

    // 第二次请求(从缓存获取)
    start = time.Now()
    if err := client.Run(context.Background(), req, &resp); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Second request took: %v\n", time.Since(start))
}

3. 批量请求 #

// client/batch.go
package client

import (
    "context"
    "fmt"
    "sync"
    "time"

    "github.com/machinebox/graphql"
)

// BatchRequest 批量请求项
type BatchRequest struct {
    Request  *graphql.Request
    Response interface{}
    Error    error
    Done     chan struct{}
}

// BatchClient 批量请求客户端
type BatchClient struct {
    client     *graphql.Client
    batchSize  int
    batchDelay time.Duration
    queue      chan *BatchRequest
    mu         sync.Mutex
    batch      []*BatchRequest
}

func NewBatchClient(endpoint string, batchSize int, batchDelay time.Duration) *BatchClient {
    client := &BatchClient{
        client:     graphql.NewClient(endpoint),
        batchSize:  batchSize,
        batchDelay: batchDelay,
        queue:      make(chan *BatchRequest, 100),
        batch:      make([]*BatchRequest, 0, batchSize),
    }

    go client.processBatch()

    return client
}

func (c *BatchClient) Run(ctx context.Context, req *graphql.Request, resp interface{}) error {
    batchReq := &BatchRequest{
        Request:  req,
        Response: resp,
        Done:     make(chan struct{}),
    }

    c.queue <- batchReq

    select {
    case <-batchReq.Done:
        return batchReq.Error
    case <-ctx.Done():
        return ctx.Err()
    }
}

func (c *BatchClient) processBatch() {
    ticker := time.NewTicker(c.batchDelay)
    defer ticker.Stop()

    for {
        select {
        case req := <-c.queue:
            c.mu.Lock()
            c.batch = append(c.batch, req)
            shouldFlush := len(c.batch) >= c.batchSize
            c.mu.Unlock()

            if shouldFlush {
                c.flushBatch()
            }

        case <-ticker.C:
            c.flushBatch()
        }
    }
}

func (c *BatchClient) flushBatch() {
    c.mu.Lock()
    if len(c.batch) == 0 {
        c.mu.Unlock()
        return
    }

    batch := c.batch
    c.batch = make([]*BatchRequest, 0, c.batchSize)
    c.mu.Unlock()

    // 并发执行批量请求
    var wg sync.WaitGroup
    for _, req := range batch {
        wg.Add(1)
        go func(batchReq *BatchRequest) {
            defer wg.Done()
            defer close(batchReq.Done)

            batchReq.Error = c.client.Run(context.Background(), batchReq.Request, batchReq.Response)
        }(req)
    }

    wg.Wait()
}

4. 订阅支持 #

// client/subscription.go
package client

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/url"

    "github.com/gorilla/websocket"
)

// SubscriptionClient WebSocket 订阅客户端
type SubscriptionClient struct {
    url    string
    conn   *websocket.Conn
    subs   map[string]chan interface{}
    mu     sync.RWMutex
}

func NewSubscriptionClient(endpoint string) *SubscriptionClient {
    u, _ := url.Parse(endpoint)
    if u.Scheme == "http" {
        u.Scheme = "ws"
    } else if u.Scheme == "https" {
        u.Scheme = "wss"
    }
    u.Path = "/graphql"

    return &SubscriptionClient{
        url:  u.String(),
        subs: make(map[string]chan interface{}),
    }
}

func (c *SubscriptionClient) Connect() error {
    conn, _, err := websocket.DefaultDialer.Dial(c.url, nil)
    if err != nil {
        return err
    }

    c.conn = conn

    // 发送连接初始化消息
    initMsg := map[string]interface{}{
        "type": "connection_init",
    }

    if err := c.conn.WriteJSON(initMsg); err != nil {
        return err
    }

    // 启动消息处理 goroutine
    go c.handleMessages()

    return nil
}

func (c *SubscriptionClient) Subscribe(query string, variables map[string]interface{}) (<-chan interface{}, error) {
    if c.conn == nil {
        return nil, fmt.Errorf("not connected")
    }

    id := fmt.Sprintf("sub_%d", time.Now().UnixNano())

    subMsg := map[string]interface{}{
        "id":   id,
        "type": "start",
        "payload": map[string]interface{}{
            "query":     query,
            "variables": variables,
        },
    }

    if err := c.conn.WriteJSON(subMsg); err != nil {
        return nil, err
    }

    ch := make(chan interface{}, 10)
    c.mu.Lock()
    c.subs[id] = ch
    c.mu.Unlock()

    return ch, nil
}

func (c *SubscriptionClient) Unsubscribe(id string) error {
    if c.conn == nil {
        return fmt.Errorf("not connected")
    }

    stopMsg := map[string]interface{}{
        "id":   id,
        "type": "stop",
    }

    if err := c.conn.WriteJSON(stopMsg); err != nil {
        return err
    }

    c.mu.Lock()
    if ch, exists := c.subs[id]; exists {
        close(ch)
        delete(c.subs, id)
    }
    c.mu.Unlock()

    return nil
}

func (c *SubscriptionClient) handleMessages() {
    for {
        var msg map[string]interface{}
        if err := c.conn.ReadJSON(&msg); err != nil {
            log.Printf("WebSocket read error: %v", err)
            break
        }

        msgType, ok := msg["type"].(string)
        if !ok {
            continue
        }

        switch msgType {
        case "data":
            c.handleDataMessage(msg)
        case "error":
            c.handleErrorMessage(msg)
        case "complete":
            c.handleCompleteMessage(msg)
        }
    }
}

func (c *SubscriptionClient) handleDataMessage(msg map[string]interface{}) {
    id, ok := msg["id"].(string)
    if !ok {
        return
    }

    payload, ok := msg["payload"]
    if !ok {
        return
    }

    c.mu.RLock()
    ch, exists := c.subs[id]
    c.mu.RUnlock()

    if exists {
        select {
        case ch <- payload:
        default:
            // Channel is full, skip this message
        }
    }
}

func (c *SubscriptionClient) handleErrorMessage(msg map[string]interface{}) {
    log.Printf("Subscription error: %v", msg)
}

func (c *SubscriptionClient) handleCompleteMessage(msg map[string]interface{}) {
    id, ok := msg["id"].(string)
    if !ok {
        return
    }

    c.mu.Lock()
    if ch, exists := c.subs[id]; exists {
        close(ch)
        delete(c.subs, id)
    }
    c.mu.Unlock()
}

func (c *SubscriptionClient) Close() error {
    if c.conn != nil {
        return c.conn.Close()
    }
    return nil
}

// 使用示例
func ExampleSubscriptionClient() {
    client := NewSubscriptionClient("ws://localhost:8080/graphql")

    if err := client.Connect(); err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // 订阅用户创建事件
    ch, err := client.Subscribe(`
        subscription {
            userCreated {
                id
                name
                email
                createdAt
            }
        }
    `, nil)
    if err != nil {
        log.Fatal(err)
    }

    // 处理订阅消息
    for data := range ch {
        fmt.Printf("New user created: %v\n", data)
    }
}

错误处理和重试机制 #

// client/error_handling.go
package client

import (
    "context"
    "fmt"
    "log"
    "math"
    "time"

    "github.com/machinebox/graphql"
)

// GraphQLError GraphQL 错误类型
type GraphQLError struct {
    Message    string                 `json:"message"`
    Locations  []GraphQLErrorLocation `json:"locations"`
    Path       []interface{}          `json:"path"`
    Extensions map[string]interface{} `json:"extensions"`
}

type GraphQLErrorLocation struct {
    Line   int `json:"line"`
    Column int `json:"column"`
}

// GraphQLResponse GraphQL 响应类型
type GraphQLResponse struct {
    Data   interface{}    `json:"data"`
    Errors []GraphQLError `json:"errors"`
}

// RetryConfig 重试配置
type RetryConfig struct {
    MaxRetries      int
    InitialDelay    time.Duration
    MaxDelay        time.Duration
    BackoffFactor   float64
    RetryableErrors []string
}

// DefaultRetryConfig 默认重试配置
func DefaultRetryConfig() *RetryConfig {
    return &RetryConfig{
        MaxRetries:    3,
        InitialDelay:  100 * time.Millisecond,
        MaxDelay:      5 * time.Second,
        BackoffFactor: 2.0,
        RetryableErrors: []string{
            "NETWORK_ERROR",
            "TIMEOUT_ERROR",
            "INTERNAL_ERROR",
        },
    }
}

// ResilientClient 具有错误处理和重试机制的客户端
type ResilientClient struct {
    client      *graphql.Client
    retryConfig *RetryConfig
}

func NewResilientClient(endpoint string, config *RetryConfig) *ResilientClient {
    if config == nil {
        config = DefaultRetryConfig()
    }

    return &ResilientClient{
        client:      graphql.NewClient(endpoint),
        retryConfig: config,
    }
}

func (c *ResilientClient) Run(ctx context.Context, req *graphql.Request, resp interface{}) error {
    var lastErr error

    for attempt := 0; attempt <= c.retryConfig.MaxRetries; attempt++ {
        if attempt > 0 {
            delay := c.calculateDelay(attempt)
            log.Printf("Retrying GraphQL request (attempt %d/%d) after %v",
                attempt, c.retryConfig.MaxRetries, delay)

            select {
            case <-time.After(delay):
            case <-ctx.Done():
                return ctx.Err()
            }
        }

        err := c.client.Run(ctx, req, resp)
        if err == nil {
            return nil
        }

        lastErr = err

        // 检查是否应该重试
        if !c.shouldRetry(err) {
            break
        }
    }

    return fmt.Errorf("GraphQL request failed after %d attempts: %w",
        c.retryConfig.MaxRetries+1, lastErr)
}

func (c *ResilientClient) calculateDelay(attempt int) time.Duration {
    delay := float64(c.retryConfig.InitialDelay) * math.Pow(c.retryConfig.BackoffFactor, float64(attempt-1))

    if delay > float64(c.retryConfig.MaxDelay) {
        delay = float64(c.retryConfig.MaxDelay)
    }

    return time.Duration(delay)
}

func (c *ResilientClient) shouldRetry(err error) bool {
    // 这里可以根据错误类型判断是否应该重试
    // 例如网络错误、超时错误等可以重试
    // 而语法错误、认证错误等不应该重试

    // 简化实现:检查错误消息
    errMsg := err.Error()
    for _, retryableError := range c.retryConfig.RetryableErrors {
        if contains(errMsg, retryableError) {
            return true
        }
    }

    return false
}

func contains(s, substr string) bool {
    return len(s) >= len(substr) && s[:len(substr)] == substr
}

// 使用示例
func ExampleResilientClient() {
    config := &RetryConfig{
        MaxRetries:    5,
        InitialDelay:  200 * time.Millisecond,
        MaxDelay:      10 * time.Second,
        BackoffFactor: 2.0,
        RetryableErrors: []string{
            "network",
            "timeout",
            "internal",
        },
    }

    client := NewResilientClient("http://localhost:8080/graphql", config)

    req := graphql.NewRequest(`
        query {
            users {
                id
                name
                email
            }
        }
    `)

    var resp struct {
        Users []struct {
            ID    string `json:"id"`
            Name  string `json:"name"`
            Email string `json:"email"`
        } `json:"users"`
    }

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := client.Run(ctx, req, &resp); err != nil {
        log.Printf("Request failed: %v", err)
        return
    }

    fmt.Printf("Successfully retrieved %d users\n", len(resp.Users))
}

性能优化 #

1. 连接池 #

// client/pool.go
package client

import (
    "context"
    "sync"

    "github.com/machinebox/graphql"
)

// ClientPool GraphQL 客户端池
type ClientPool struct {
    clients chan *graphql.Client
    factory func() *graphql.Client
    mu      sync.Mutex
}

func NewClientPool(size int, factory func() *graphql.Client) *ClientPool {
    pool := &ClientPool{
        clients: make(chan *graphql.Client, size),
        factory: factory,
    }

    // 预填充池
    for i := 0; i < size; i++ {
        pool.clients <- factory()
    }

    return pool
}

func (p *ClientPool) Get() *graphql.Client {
    select {
    case client := <-p.clients:
        return client
    default:
        return p.factory()
    }
}

func (p *ClientPool) Put(client *graphql.Client) {
    select {
    case p.clients <- client:
    default:
        // Pool is full, discard the client
    }
}

func (p *ClientPool) Run(ctx context.Context, req *graphql.Request, resp interface{}) error {
    client := p.Get()
    defer p.Put(client)

    return client.Run(ctx, req, resp)
}

2. 查询优化 #

// client/optimization.go
package client

import (
    "fmt"
    "strings"
)

// QueryOptimizer 查询优化器
type QueryOptimizer struct{}

func NewQueryOptimizer() *QueryOptimizer {
    return &QueryOptimizer{}
}

// OptimizeQuery 优化 GraphQL 查询
func (o *QueryOptimizer) OptimizeQuery(query string) string {
    // 移除不必要的空白字符
    query = strings.TrimSpace(query)
    query = strings.ReplaceAll(query, "\n", " ")
    query = strings.ReplaceAll(query, "\t", " ")

    // 移除多余的空格
    for strings.Contains(query, "  ") {
        query = strings.ReplaceAll(query, "  ", " ")
    }

    return query
}

// GenerateFieldSelection 生成字段选择
func (o *QueryOptimizer) GenerateFieldSelection(fields []string) string {
    return strings.Join(fields, "\n    ")
}

// BuildQuery 构建优化的查询
func (o *QueryOptimizer) BuildQuery(operation, name string, args map[string]interface{}, fields []string) string {
    var query strings.Builder

    query.WriteString(operation)
    query.WriteString(" {\n  ")
    query.WriteString(name)

    if len(args) > 0 {
        query.WriteString("(")
        var argParts []string
        for key, value := range args {
            argParts = append(argParts, fmt.Sprintf("%s: %v", key, value))
        }
        query.WriteString(strings.Join(argParts, ", "))
        query.WriteString(")")
    }

    query.WriteString(" {\n    ")
    query.WriteString(o.GenerateFieldSelection(fields))
    query.WriteString("\n  }\n}")

    return o.OptimizeQuery(query.String())
}

// 使用示例
func ExampleQueryOptimizer() {
    optimizer := NewQueryOptimizer()

    // 构建优化的查询
    query := optimizer.BuildQuery(
        "query",
        "users",
        map[string]interface{}{
            "first": 10,
            "role":  "USER",
        },
        []string{
            "id",
            "name",
            "email",
            "posts { id title }",
        },
    )

    fmt.Println("Optimized query:")
    fmt.Println(query)
}

通过这些高级功能和优化技术,你可以构建出功能强大、性能优异的 GraphQL 客户端应用。这些技术包括请求拦截、缓存机制、批量处理、订阅支持、错误处理和性能优化等,能够满足各种复杂的业务需求。