
How to Make HTTP Requests in Go – A Simple Guide
Making HTTP requests is one of the most fundamental skills you’ll need when working with Go, especially when building backend services, APIs, or automation scripts on your servers. Whether you’re setting up monitoring systems, integrating with third-party services, or building microservices that need to communicate with each other, Go’s HTTP client capabilities are incredibly powerful and surprisingly simple to use. This guide will walk you through everything from basic GET requests to advanced techniques like custom headers, timeouts, and error handling – giving you the tools to build robust network applications that can handle real-world server environments.
How HTTP Requests Work in Go
Go’s net/http
package provides everything you need to make HTTP requests right out of the box. Under the hood, Go uses a default HTTP client that handles connection pooling, keep-alive connections, and other optimizations automatically. The beauty of Go’s approach is that it gives you simple methods for common tasks while still allowing deep customization when needed.
The core components you’ll work with are:
- http.Client – The main client struct that handles requests
- http.Request – Represents an HTTP request with headers, body, and method
- http.Response – Contains the server’s response including status, headers, and body
- Transport – Handles the low-level HTTP protocol details
What makes Go particularly great for server-side HTTP work is its built-in concurrency support. You can easily fire off hundreds of HTTP requests concurrently using goroutines without worrying about thread management or callback hell.
Quick Setup and Basic Examples
Let’s start with the simplest possible HTTP request and build up from there. First, make sure you have Go installed on your server – if you’re running a VPS or dedicated server, you’ll want to install it system-wide.
# Install Go on Ubuntu/Debian
sudo apt update
sudo apt install golang-go
# Verify installation
go version
Now let’s create our first HTTP client:
package main
import (
"fmt"
"io"
"net/http"
"log"
)
func main() {
// Simple GET request
resp, err := http.Get("https://httpbin.org/get")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Status: %s\n", resp.Status)
fmt.Printf("Body: %s\n", body)
}
This basic example demonstrates the essential pattern: make the request, check for errors, close the response body, and read the data. The defer resp.Body.Close()
is crucial – forgetting this will cause connection leaks that can crash your server under load.
Real-World Examples and Use Cases
Let’s dive into practical scenarios you’ll encounter when managing servers and building backend systems.
API Integration with Custom Headers
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
type APIResponse struct {
Status string `json:"status"`
Data string `json:"data"`
}
func makeAPIRequest(apiKey string) (*APIResponse, error) {
// Create custom client with timeout
client := &http.Client{
Timeout: 10 * time.Second,
}
// Prepare JSON payload
payload := map[string]string{
"message": "Hello from Go",
"timestamp": time.Now().Format(time.RFC3339),
}
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, err
}
// Create request with custom headers
req, err := http.NewRequest("POST", "https://api.example.com/data", bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("User-Agent", "MyServer/1.0")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var apiResp APIResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
return nil, err
}
return &apiResp, nil
}
Health Check Monitoring System
Here’s a practical example for monitoring multiple services – perfect for server maintenance automation:
package main
import (
"context"
"fmt"
"net/http"
"sync"
"time"
)
type HealthCheck struct {
URL string
Name string
Status string
Duration time.Duration
Error error
}
func checkHealth(ctx context.Context, url, name string) HealthCheck {
start := time.Now()
client := &http.Client{
Timeout: 5 * time.Second,
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return HealthCheck{URL: url, Name: name, Status: "ERROR", Error: err, Duration: time.Since(start)}
}
resp, err := client.Do(req)
if err != nil {
return HealthCheck{URL: url, Name: name, Status: "DOWN", Error: err, Duration: time.Since(start)}
}
defer resp.Body.Close()
status := "UP"
if resp.StatusCode >= 400 {
status = "UNHEALTHY"
}
return HealthCheck{
URL: url,
Name: name,
Status: status,
Duration: time.Since(start),
}
}
func monitorServices() {
services := map[string]string{
"Web Server": "https://example.com/health",
"API Gateway": "https://api.example.com/health",
"Database API": "https://db.example.com/ping",
}
var wg sync.WaitGroup
results := make(chan HealthCheck, len(services))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
for name, url := range services {
wg.Add(1)
go func(name, url string) {
defer wg.Done()
results <- checkHealth(ctx, url, name)
}(name, url)
}
go func() {
wg.Wait()
close(results)
}()
fmt.Println("Health Check Results:")
fmt.Println("=====================")
for result := range results {
fmt.Printf("%-15s %-10s %v\n", result.Name, result.Status, result.Duration)
if result.Error != nil {
fmt.Printf(" Error: %v\n", result.Error)
}
}
}
Comparison: Go vs Other Languages
Feature | Go | Python (requests) | Node.js (axios) |
---|---|---|---|
Built-in HTTP client | ✅ Standard library | ❌ Requires external lib | ❌ Requires external lib |
Concurrent requests | ✅ Goroutines (native) | ⚠️ asyncio or threading | ✅ Native async/await |
Memory usage | ~2MB per goroutine | ~50MB per thread | ~20MB per request |
Performance | ~100k req/sec | ~10k req/sec | ~80k req/sec |
Advanced Error Handling and Retry Logic
package main
import (
"context"
"fmt"
"net"
"net/http"
"time"
)
type RetryableClient struct {
client *http.Client
maxRetries int
retryDelay time.Duration
}
func NewRetryableClient() *RetryableClient {
return &RetryableClient{
client: &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
DisableKeepAlives: false,
},
},
maxRetries: 3,
retryDelay: 1 * time.Second,
}
}
func (rc *RetryableClient) Get(url string) (*http.Response, error) {
var lastErr error
for attempt := 0; attempt <= rc.maxRetries; attempt++ {
if attempt > 0 {
fmt.Printf("Retry attempt %d for %s\n", attempt, url)
time.Sleep(rc.retryDelay * time.Duration(attempt))
}
resp, err := rc.client.Get(url)
if err != nil {
lastErr = err
// Check if it's a retryable error
if isRetryable(err) {
continue
}
return nil, err
}
// Check HTTP status codes
if resp.StatusCode >= 500 {
resp.Body.Close()
lastErr = fmt.Errorf("server error: %s", resp.Status)
continue
}
return resp, nil
}
return nil, fmt.Errorf("max retries exceeded, last error: %v", lastErr)
}
func isRetryable(err error) bool {
// Network timeouts
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
return true
}
// Connection refused
if opErr, ok := err.(*net.OpError); ok {
return opErr.Op == "dial"
}
return false
}
Performance Optimization and Best Practices
When you're running HTTP clients on production servers, performance matters. Here are some key optimizations:
Connection Pooling Configuration
package main
import (
"net/http"
"time"
)
func createOptimizedClient() *http.Client {
transport := &http.Transport{
MaxIdleConns: 100, // Total idle connections
MaxIdleConnsPerHost: 10, // Idle connections per host
IdleConnTimeout: 90 * time.Second, // How long to keep idle connections
DisableKeepAlives: false, // Enable HTTP keep-alive
// Timeouts for different phases
DialTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
return &http.Client{
Transport: transport,
Timeout: 60 * time.Second, // Overall request timeout
}
}
Batch Processing with Worker Pools
package main
import (
"fmt"
"net/http"
"sync"
"time"
)
type URLJob struct {
URL string
ID int
}
type Result struct {
ID int
URL string
StatusCode int
Duration time.Duration
Error error
}
func createWorkerPool(numWorkers int, jobs <-chan URLJob, results chan<- Result) {
client := createOptimizedClient()
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
start := time.Now()
resp, err := client.Get(job.URL)
result := Result{
ID: job.ID,
URL: job.URL,
Duration: time.Since(start),
Error: err,
}
if err == nil {
result.StatusCode = resp.StatusCode
resp.Body.Close()
}
results <- result
}
}()
}
wg.Wait()
close(results)
}
func processBatchRequests(urls []string) {
jobs := make(chan URLJob, len(urls))
results := make(chan Result, len(urls))
// Start worker pool (adjust based on your server capacity)
go createWorkerPool(10, jobs, results)
// Send jobs
for i, url := range urls {
jobs <- URLJob{URL: url, ID: i}
}
close(jobs)
// Collect results
for i := 0; i < len(urls); i++ {
result := <-results
if result.Error != nil {
fmt.Printf("❌ %s failed: %v\n", result.URL, result.Error)
} else {
fmt.Printf("✅ %s -> %d (%v)\n", result.URL, result.StatusCode, result.Duration)
}
}
}
Integration with Popular Tools and Frameworks
Go's HTTP client integrates beautifully with various tools commonly used in server environments:
Prometheus Metrics Integration
package main
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"net/http"
"strconv"
"time"
)
var (
httpRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests made",
},
[]string{"method", "status"},
)
httpRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
},
[]string{"method", "status"},
)
)
type MetricsTransport struct {
Transport http.RoundTripper
}
func (t *MetricsTransport) RoundTrip(req *http.Request) (*http.Response, error) {
start := time.Now()
resp, err := t.Transport.RoundTrip(req)
duration := time.Since(start).Seconds()
status := "error"
if err == nil {
status = strconv.Itoa(resp.StatusCode)
}
httpRequestsTotal.WithLabelValues(req.Method, status).Inc()
httpRequestDuration.WithLabelValues(req.Method, status).Observe(duration)
return resp, err
}
func createMonitoredClient() *http.Client {
return &http.Client{
Transport: &MetricsTransport{
Transport: http.DefaultTransport,
},
}
}
Logging and Debugging
package main
import (
"bytes"
"fmt"
"io"
"log"
"net/http"
"net/http/httputil"
)
type DebugTransport struct {
Transport http.RoundTripper
Logger *log.Logger
}
func (t *DebugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Log request
reqDump, _ := httputil.DumpRequestOut(req, true)
t.Logger.Printf("REQUEST:\n%s\n", reqDump)
resp, err := t.Transport.RoundTrip(req)
if err != nil {
t.Logger.Printf("ERROR: %v\n", err)
return nil, err
}
// Log response
respDump, _ := httputil.DumpResponse(resp, true)
t.Logger.Printf("RESPONSE:\n%s\n", respDump)
return resp, nil
}
Common Pitfalls and How to Avoid Them
Here are the most common mistakes I see when people start using Go's HTTP client in production:
- Forgetting to close response bodies - This will leak connections and crash your server
- Not setting timeouts - Default client has no timeout, requests can hang forever
- Ignoring status codes - A 404 or 500 isn't an error in Go, you need to check manually
- Not handling context cancellation - Essential for graceful shutdowns
The "Wrong" Way vs "Right" Way
// ❌ BAD: Will leak connections and hang
func badExample() {
resp, _ := http.Get("https://example.com")
// Missing: defer resp.Body.Close()
// Missing: error handling
// Missing: timeout
}
// ✅ GOOD: Proper error handling and resource management
func goodExample(ctx context.Context) error {
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequestWithContext(ctx, "GET", "https://example.com", nil)
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("making request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("HTTP error: %s", resp.Status)
}
return nil
}
Automation and Scripting Possibilities
Go's HTTP client opens up incredible possibilities for server automation. Here are some real-world scenarios:
- Deployment webhooks - Trigger builds and deployments via API calls
- Service discovery - Query service registries and update configurations
- Log aggregation - Send logs to centralized systems like ELK stack
- Backup verification - Check backup systems and notify on failures
- SSL certificate monitoring - Alert before certificates expire
SSL Certificate Checker Example
package main
import (
"crypto/tls"
"fmt"
"net/http"
"time"
)
func checkSSLExpiry(domain string) error {
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Get("https://" + domain)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.TLS == nil {
return fmt.Errorf("no TLS connection info available")
}
for _, cert := range resp.TLS.PeerCertificates {
daysUntilExpiry := int(cert.NotAfter.Sub(time.Now()).Hours() / 24)
fmt.Printf("Certificate for %s expires in %d days\n", domain, daysUntilExpiry)
if daysUntilExpiry < 30 {
fmt.Printf("⚠️ WARNING: Certificate expires soon!\n")
}
}
return nil
}
The statistics speak for themselves: Go's HTTP client can handle over 100,000 concurrent requests on a modest server, making it perfect for high-throughput scenarios. Companies like Uber, Netflix, and Docker rely on Go for their critical infrastructure, and HTTP client performance is a big reason why.
Related Tools and Utilities
While Go's standard library is comprehensive, these tools can enhance your HTTP client workflows:
- Resty - Simple HTTP client with more convenience methods
- go-retryablehttp - Drop-in replacement with automatic retries
- GoRequest - jQuery-style HTTP client
- Prometheus client - For metrics and monitoring
Conclusion and Recommendations
Go's HTTP client is a powerhouse that strikes the perfect balance between simplicity and performance. For server administrators and backend developers, it's an invaluable tool that can handle everything from simple API calls to complex distributed system communication.
Use Go's HTTP client when you need:
- High-performance HTTP communication (APIs, microservices)
- Reliable automation scripts for server management
- Monitoring and health-check systems
- Integration with cloud services and third-party APIs
- Building robust backend services that need to make external calls
Best practices to remember:
- Always set timeouts and handle contexts properly
- Close response bodies religiously
- Use connection pooling for better performance
- Implement proper retry logic for production systems
- Monitor your HTTP client metrics in production
Whether you're running a simple VPS with a few services or managing a complex dedicated server infrastructure, Go's HTTP client will serve you well. Its combination of performance, reliability, and ease of use makes it the go-to choice for modern server-side development.
Start with the simple examples in this guide, then gradually incorporate the advanced patterns as your needs grow. The learning curve is gentle, but the performance benefits are immediate – you'll wonder how you ever managed HTTP requests without Go's elegant approach.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.