Complete Guide to Sending JSON POST Requests in Go

Nov 09, 2025 · Programming · 17 views · 7.8

Keywords: Go Language | JSON | POST Request | HTTP Client | REST API

Abstract: This article provides a comprehensive guide to sending JSON-formatted POST requests in Go, focusing on standard implementations using the net/http package. Through comparison of original problematic code and optimized solutions, it deeply explores key technical aspects including JSON serialization, HTTP request construction, header configuration, and offers complete code examples with best practice recommendations.

Problem Background and Original Code Analysis

When developing REST API clients, sending JSON-formatted data to servers is a common requirement. The original code used the third-party library napping but encountered issues with incomplete JSON transmission. The core problem lies in:

var data map[string]json.RawMessage
err := json.Unmarshal(jsonStr, &data)

This approach deserializes the JSON string into RawMessage but fails to properly reserialize it back to JSON format when sending, resulting in the server being unable to correctly parse the request body.

Standard net/http Solution

Go's standard library net/http provides complete HTTP client functionality, enabling JSON POST requests without relying on third-party libraries.

Basic POST Request Implementation

The simplest approach uses the http.Post function:

package main

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

func main() {
    url := "http://restapi3.apiary.io/notes"
    
    // Directly use JSON string
    jsonStr := []byte(`{"title":"Buy cheese and bread for breakfast."}`)
    
    resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonStr))
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    
    body, _ := io.ReadAll(resp.Body)
    fmt.Printf("Status: %s, Body: %s\n", resp.Status, string(body))
}

Advanced Request Control

When custom headers or more control options are needed, use http.NewRequest and http.Client:

func main() {
    url := "http://restapi3.apiary.io/notes"
    fmt.Println("URL:", url)

    // JSON data preparation
    var jsonStr = []byte(`{"title":"Buy cheese and bread for breakfast."}`)
    
    // Create request
    req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
    if err != nil {
        panic(err)
    }
    
    // Set request headers
    req.Header.Set("X-Custom-Header", "myvalue")
    req.Header.Set("Content-Type", "application/json")
    
    // Send request
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    // Handle response
    fmt.Println("response Status:", resp.Status)
    fmt.Println("response Headers:", resp.Header)
    body, _ := io.ReadAll(resp.Body)
    fmt.Println("response Body:", string(body))
}

Dynamic JSON Data Generation

In practical applications, JSON data is typically generated dynamically. Use the encoding/json package for programmatic JSON construction:

Using Structs for JSON Generation

type Note struct {
    Title       string `json:"title"`
    Description string `json:"description,omitempty"`
}

func main() {
    // Create struct instance
    note := Note{
        Title:       "Morning task",
        Description: "Buy cheese and bread for breakfast.",
    }
    
    // Serialize to JSON
    jsonData, err := json.Marshal(note)
    if err != nil {
        log.Fatalf("JSON marshal failed: %s", err)
    }
    
    // Send request
    resp, err := http.Post("https://example.com/endpoint", 
                          "application/json", 
                          bytes.NewBuffer(jsonData))
    if err != nil {
        log.Fatalf("POST request failed: %s", err)
    }
    defer resp.Body.Close()
    
    log.Printf("Request completed with status: %d", resp.StatusCode)
}

Using Maps for Dynamic JSON

When data structure is not fixed, use map[string]interface{}:

func sendDynamicJSON(url string, data map[string]interface{}) error {
    jsonData, err := json.Marshal(data)
    if err != nil {
        return fmt.Errorf("marshal JSON: %w", err)
    }
    
    req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
    if err != nil {
        return fmt.Errorf("create request: %w", err)
    }
    
    req.Header.Set("Content-Type", "application/json")
    
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("send request: %w", err)
    }
    defer resp.Body.Close()
    
    if resp.StatusCode >= 400 {
        return fmt.Errorf("server returned status: %d", resp.StatusCode)
    }
    
    return nil
}

Error Handling and Best Practices

Comprehensive Error Handling

Production environments require robust error handling:

func sendJSONRequest(url string, jsonData []byte) (*http.Response, error) {
    req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
    if err != nil {
        return nil, fmt.Errorf("creating request: %w", err)
    }
    
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("User-Agent", "MyGoClient/1.0")
    
    client := &http.Client{
        Timeout: 30 * time.Second,
    }
    
    resp, err := client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("sending request: %w", err)
    }
    
    return resp, nil
}

Response Handling Best Practices

func handleResponse(resp *http.Response) error {
    defer resp.Body.Close()
    
    // Check HTTP status code
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        body, _ := io.ReadAll(resp.Body)
        return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
    }
    
    // Parse response body
    var result map[string]interface{}
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return fmt.Errorf("reading response: %w", err)
    }
    
    if err := json.Unmarshal(body, &result); err != nil {
        return fmt.Errorf("parsing response JSON: %w", err)
    }
    
    fmt.Printf("Success: %+v\n", result)
    return nil
}

Performance Optimization Recommendations

Connection Reuse

For high-frequency requests, reuse http.Client instances:

var (
    httpClient = &http.Client{
        Timeout: 30 * time.Second,
        Transport: &http.Transport{
            MaxIdleConns:        100,
            MaxIdleConnsPerHost: 10,
            IdleConnTimeout:     90 * time.Second,
        },
    }
)

JSON Serialization Optimization

For large data serialization, consider using json.Encoder:

func encodeJSONDirectly(w io.Writer, data interface{}) error {
    encoder := json.NewEncoder(w)
    encoder.SetEscapeHTML(false) // Avoid HTML escaping
    return encoder.Encode(data)
}

Conclusion

For sending JSON POST requests in Go, using the standard library's net/http package is recommended over third-party libraries. Key steps include properly setting the Content-Type header to application/json, wrapping JSON data with bytes.Buffer, and implementing comprehensive error handling. For dynamic JSON data, use map[string]interface{} or custom structs with json.Marshal. Through proper connection management and serialization optimization, high-performance HTTP clients can be built.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.