HTTP Clients in Go

Go’s standard library provides powerful tools for making HTTP requests. The net/http package makes it easy to interact with web APIs and services.

Basic GET Request

The simplest way to make an HTTP request:

package main

import (
    "fmt"
    "io"
    "net/http"
)

func main() {
    // Make a GET request
    resp, err := http.Get("https://api.github.com/users/octocat")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer resp.Body.Close()
    
    // Read the response body
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Error reading body:", err)
        return
    }
    
    fmt.Println("Status:", resp.Status)
    fmt.Println("Body:", string(body))
}

Making POST Requests

Sending data to a server:

package main

import (
    "bytes"
    "fmt"
    "net/http"
)

func main() {
    // JSON data to send
    jsonData := `{"name": "John", "age": 30}`
    
    // Create a POST request
    resp, err := http.Post(
        "https://httpbin.org/post",
        "application/json",
        bytes.NewBuffer([]byte(jsonData)),
    )
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer resp.Body.Close()
    
    fmt.Println("Status:", resp.Status)
}

Using http.Client

For more control over requests, use http.Client:

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    // Create a client with timeout
    client := &http.Client{
        Timeout: 10 * time.Second,
    }
    
    // Create a request
    req, err := http.NewRequest("GET", "https://api.example.com/data", nil)
    if err != nil {
        fmt.Println("Error creating request:", err)
        return
    }
    
    // Add headers
    req.Header.Set("Authorization", "Bearer token123")
    req.Header.Set("User-Agent", "MyGoApp/1.0")
    
    // Make the request
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer resp.Body.Close()
    
    fmt.Println("Status:", resp.Status)
    fmt.Println("Content-Type:", resp.Header.Get("Content-Type"))
}

Handling JSON

Working with JSON APIs:

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    // Create user data
    user := User{Name: "Alice", Age: 25}
    
    // Encode to JSON
    jsonData, err := json.Marshal(user)
    if err != nil {
        fmt.Println("Error encoding JSON:", err)
        return
    }
    
    // Make POST request
    resp, err := http.Post(
        "https://jsonplaceholder.typicode.com/users",
        "application/json",
        bytes.NewBuffer(jsonData),
    )
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer resp.Body.Close()
    
    // Decode response
    var createdUser User
    if err := json.NewDecoder(resp.Body).Decode(&createdUser); err != nil {
        fmt.Println("Error decoding response:", err)
        return
    }
    
    fmt.Printf("Created user: %+v\n", createdUser)
}

Custom Client Configuration

Advanced client configuration:

package main

import (
    "crypto/tls"
    "net/http"
    "time"
)

func createHTTPClient() *http.Client {
    // Create transport with custom settings
    transport := &http.Transport{
        TLSClientConfig: &tls.Config{
            InsecureSkipVerify: false, // Don't skip verification in production
        },
        MaxIdleConns:        10,
        IdleConnTimeout:     30 * time.Second,
        DisableCompression:  false,
    }
    
    // Create client
    client := &http.Client{
        Transport: transport,
        Timeout:   30 * time.Second,
    }
    
    return client
}

func main() {
    client := createHTTPClient()
    
    resp, err := client.Get("https://example.com")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    
    // Use response...
}

Error Handling

Proper error handling for HTTP requests:

package main

import (
    "fmt"
    "net/http"
)

func makeRequest(url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return fmt.Errorf("failed to make request: %w", err)
    }
    defer resp.Body.Close()
    
    // Check status code
    if resp.StatusCode >= 400 {
        return fmt.Errorf("server returned error status: %s", resp.Status)
    }
    
    // Process successful response
    fmt.Println("Request successful")
    return nil
}

func main() {
    if err := makeRequest("https://httpbin.org/status/404"); err != nil {
        fmt.Println("Error:", err)
    }
}

Concurrent Requests

Making multiple requests concurrently:

package main

import (
    "fmt"
    "net/http"
    "sync"
)

func fetchURL(url string, wg *sync.WaitGroup, results chan<- string) {
    defer wg.Done()
    
    resp, err := http.Get(url)
    if err != nil {
        results <- fmt.Sprintf("Error fetching %s: %v", url, err)
        return
    }
    defer resp.Body.Close()
    
    results <- fmt.Sprintf("Fetched %s: %s", url, resp.Status)
}

func main() {
    urls := []string{
        "https://google.com",
        "https://github.com",
        "https://stackoverflow.com",
    }
    
    var wg sync.WaitGroup
    results := make(chan string, len(urls))
    
    // Start goroutines for each URL
    for _, url := range urls {
        wg.Add(1)
        go fetchURL(url, &wg, results)
    }
    
    // Wait for all requests to complete
    go func() {
        wg.Wait()
        close(results)
    }()
    
    // Collect results
    for result := range results {
        fmt.Println(result)
    }
}

Using Third-Party Libraries

For more advanced features, consider libraries like resty:

package main

import (
    "fmt"
    "github.com/go-resty/resty/v2"
)

func main() {
    client := resty.New()
    
    // Set headers for all requests
    client.SetHeader("Authorization", "Bearer token")
    client.SetHeader("Content-Type", "application/json")
    
    // Make a GET request
    resp, err := client.R().
        SetQueryParam("page", "1").
        Get("https://api.example.com/users")
    
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    
    fmt.Println("Status:", resp.Status())
    fmt.Println("Body:", resp.String())
}

Best Practices

  1. Always close response bodies: Use defer resp.Body.Close()
  2. Set timeouts: Prevent hanging requests
  3. Handle errors properly: Check both request errors and status codes
  4. Reuse clients: Create clients once and reuse them
  5. Set appropriate headers: User-Agent, Content-Type, etc.
  6. Use context for cancellation: Allow requests to be cancelled

Testing HTTP Clients

Testing your HTTP client code:

package main

import (
    "io"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
)

func TestFetchData(t *testing.T) {
    // Create a test server
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"message": "test data"}`))
    }))
    defer server.Close()
    
    // Test your function with the test server
    data, err := fetchData(server.URL)
    if err != nil {
        t.Fatalf("fetchData failed: %v", err)
    }
    
    expected := "test data"
    if !strings.Contains(data, expected) {
        t.Errorf("Expected response to contain %q, got %q", expected, data)
    }
}

HTTP clients in Go are straightforward yet powerful. The standard library provides everything you need for most use cases, with third-party libraries available for specialized needs.

For more on building HTTP servers, see our web servers tutorial. If you need to work with JSON, check out the JSON handling tutorial.

Last updated on