Introduction to Working with JSON in Go
JSON (JavaScript Object Notation) is a lightweight data interchange format widely used in web applications, APIs, and microservices. The Go language (Golang) provides a powerful built-in library encoding/json that makes it easy to parse (deserialize) and serialize data. In this article, we will thoroughly examine how to work with JSON in Go, covering the main functions, structures, and best practices.
Basics of Serialization and Deserialization
Serialization is the process of converting a Go data structure (e.g., a struct or slice) into a JSON string. Deserialization is the reverse process — converting JSON into a Go structure.
Serialization (Marshal)
For serialization, the function json.Marshal() is used. It takes a Go value and returns a byte slice ([]byte) with the JSON representation.
package main
import ( "encoding/json" "fmt")
type User struct { Name string `json:"name"` Age int `json:"age"` Email string `json:"email,omitempty"`}
func main() { user := User{Name: "Alexey", Age: 30, Email: "alex@example.com"} jsonData, err := json.Marshal(user) if err != nil { fmt.Println("Serialization error:", err) return } fmt.Println(string(jsonData)) // Output: {"name":"Alexey","age":30,"email":"alex@example.com"}}Note the tags json:"field_name". They allow you to control the key names in JSON, as well as add options such as omitempty (skip the field if it is empty).
Deserialization (Unmarshal)
To parse JSON into a Go structure, the function json.Unmarshal() is used. It takes a byte slice and a pointer to the structure into which the data should be written.
func main() { jsonString := `{"name":"Maria","age":25,"email":"maria@example.com"}` var user User err := json.Unmarshal([]byte(jsonString), &user) if err != nil { fmt.Println("Parsing error:", err) return } fmt.Printf("Name: %s, Age: %d, Email: %s", user.Name, user.Age, user.Email)}Important: the structure must be predefined, and its fields must correspond to the JSON keys (taking tags into account).
Working with Arbitrary JSON Objects
Sometimes the JSON structure is unknown in advance or is too complex. In such cases, you can use map[string]interface{} or interface{}.
Using map[string]interface{}
func main() { jsonString := `{"name":"Ivan","age":35,"address":{"city":"Moscow","street":"Lenina"}}` var result map[string]interface{} err := json.Unmarshal([]byte(jsonString), &result) if err != nil { fmt.Println("Error:", err) return } fmt.Println(result["name"]) // Ivan fmt.Println(result["age"]) // 35 (type float64) // For nested objects, type assertion is needed address := result["address"].(map[string]interface{}) fmt.Println(address["city"]) // Moscow}The disadvantage of this approach is the need for manual type assertion (.(map[string]interface{})), which can cause a panic if types do not match.
Using json.RawMessage
If you need to defer parsing of part of the JSON, use json.RawMessage. This allows you to first parse the main structure and then process the nested data separately.
type Response struct { Status string `json:"status"` Data json.RawMessage `json:"data"`}
func main() { jsonString := `{"status":"ok","data":{"user":"Peter","score":100}}` var resp Response json.Unmarshal([]byte(jsonString), &resp)
// Now Data can be parsed separately var userData map[string]interface{} json.Unm