POST request with JSON in Go: from beginner to production

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Sending JSON in a POST request in Go: from "how does this even work" to "done and forgotten"



Have you ever been stuck on Stack Overflow, trying to figure out why your POST request with JSON in Go isn't being accepted by the server? Sounds familiar: everything seems correct, but you get a 400 Bad Request or an empty body. It turns out there are several ways to send JSON in Go, and choosing the right one is not just a matter of taste, but a matter of your microservice's stability.



In this article, we won't just show code. We'll break down three approaches: from the naive one (which works but is dangerous) to a production-ready solution with proper error handling and contexts. If you write Go for APIs, bots, or integrations, this is a must-read.



Why is this topic so popular?

Go is a language for network services. Sooner or later, every developer faces the task: "send JSON in the body of a POST request." It seems simple, but there are nuances:

  • You need to correctly serialize a struct to JSON (marshaling).
  • It's important not to forget the Content-Type: application/json header.
  • You need to properly close the response body.
  • You want to avoid memory leaks and panics.

Let's break down each approach with live examples.



Approach 1: "Quick and dirty" — bytes.NewBuffer + json.Marshal

This is the simplest way. Take a struct, marshal it into bytes, put it in a buffer, and send it. Many beginner Go developers write exactly like this.



package main

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

type User struct { Name string `json:"name"` Email string `json:"email"`}

func main() { user := User{Name: "Ivan Petrov", Email: "ivan@example.com"} // Step 1: marshal the struct to JSON jsonData, err := json.Marshal(user) if err != nil { fmt.Println("Marshaling error:", err) return } // Step 2: create a POST request resp, err := http.Post( "https://api.example.com/users", "application/json", bytes.NewBuffer(jsonData), ) if err != nil { fmt.Println("Request error:", err) return } defer resp.Body.Close() fmt.Println("Status:", resp.Status)}


Pros and cons of this approach

  • Pro: Minimal code, understandable for beginners.
  • Con: No flexibility. You can't add custom headers (authorization, trace-id). No control over timeouts. If the server returns an error, you won't handle it.
  • Con: Potential issue: if json.Marshal returns an error, you'll catch it, but in real code, people often forget to check.

This method is suitable for prototypes and tests. For production — no.



Approach 2: "The right way" — http.NewRequest + json.NewEncoder

The Go community recommends using http.NewRequest and json.NewEncoder. Why? Because it gives you full control over the request and is more memory-efficient.



package main

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

type Order struct { ID int `json:"id"` Amount float64 `json:"amount"` Items []string `json:"items"`}

func sendOrder(order Order) error { // Create a buffer and encoder var buf bytes.Buffer enc := json.NewEncoder(&buf) // Encode directly into the buffer (without intermediate []byte) if err := enc.Encode(order); err != nil { return fmt.Errorf("JSON encoding error: %w", err) } // Create the request req, err := http.NewRequest("POST", "https://api.example.com/orders", &buf) if err != nil { return fmt.Errorf("request creation error: %w", err) } // Set headers req.Header.Set("Content-Type", "application/json") req.Hea

Blogs

Book Recommendations