Go dla początkujących - Część 10: Obsługa JSON

2/8/2025 Kurs Go

Mateusz Kędziora

image

JSON (JavaScript Object Notation) to popularny format wymiany danych, szeroko stosowany w aplikacjach internetowych. Jest lekki, łatwy do odczytania zarówno dla ludzi, jak i maszyn, co czyni go idealnym wyborem do przesyłania danych między serwerem a klientem.

W języku Go, pakiet encoding/json dostarcza narzędzi do pracy z JSON. W tym artykule dowiesz się, jak używać tego pakietu do kodowania (serializacji) i dekodowania (deserializacji) danych JSON, a także jak mapować JSON na struktury Go i odwrotnie. Zrozumiesz, jak używać tagów structów, aby kontrolować proces kodowania i dekodowania.

Czym jest JSON?

JSON to format tekstowy, który reprezentuje dane w postaci obiektów i tablic. Obiekt JSON to zbiór par klucz-wartość, gdzie klucze są ciągami znaków, a wartości mogą być:

  • Prostymi typami danych: ciągi znaków, liczby, wartości logiczne (true/false), null.
  • Innymi obiektami JSON.
  • Tablicami JSON.

Tablica JSON to uporządkowana lista wartości, które mogą być dowolnego typu JSON.

Przykład JSON:

{
  "imie": "Jan",
  "nazwisko": "Kowalski",
  "wiek": 30,
  "adres": {
    "ulica": "Kwiatowa 1",
    "miasto": "Warszawa"
  },
  "umiejetnosci": ["Go", "Python", "JavaScript"],
  "czyAktywny": true,
  "ulubionaLiczba": null
}

Kodowanie (Serializacja) JSON

Kodowanie JSON polega na przekształceniu danych Go na format JSON. Używamy funkcji json.Marshal() z pakietu encoding/json do tego celu.

Przykład:

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

// Definiujemy strukturę Go, która będzie reprezentować dane.
type Osoba struct {
	Imie       string   `json:"imie"`      // Tag `json:"imie"` mapuje pole `Imie` na klucz "imie" w JSON.
	Nazwisko   string   `json:"nazwisko"`  // Tag `json:"nazwisko"` mapuje pole `Nazwisko` na klucz "nazwisko" w JSON.
	Wiek       int      `json:"wiek"`      // Tag `json:"wiek"` mapuje pole `Wiek` na klucz "wiek" w JSON.
	Umiejetnosci []string `json:"umiejetnosci,omitempty"` // Tag `json:"umiejetnosci,omitempty"` mapuje pole `Umiejetnosci` na klucz "umiejetnosci" w JSON i pomija, jeśli jest puste.
}

func main() {
	// Tworzymy instancję struktury Osoba.
	osoba := Osoba{
		Imie:       "Jan",
		Nazwisko:   "Kowalski",
		Wiek:       30,
		Umiejetnosci: []string{"Go", "Python"},
	}

	// Kodujemy strukturę Osoba do JSON.
	jsonBytes, err := json.Marshal(osoba) // Funkcja json.Marshal() przyjmuje jako argument interfejs `interface{}` (dowolny typ danych) i zwraca tablicę bajtów reprezentującą JSON oraz błąd.
	if err != nil {
		log.Fatalf("Błąd kodowania JSON: %v", err) // Obsługa błędu. Jeżeli wystąpił błąd podczas kodowania, program zostanie zatrzymany i zostanie wyświetlony komunikat o błędzie.
	}

	// Konwertujemy tablicę bajtów na string i wyświetlamy JSON.
	jsonString := string(jsonBytes) // Konwersja tablicy bajtów na string.
	fmt.Println(jsonString)           // Wyświetlenie JSON w konsoli.
}

Wyjaśnienie:

  1. Definicja struktury Osoba: Definiujemy strukturę Osoba z polami Imie, Nazwisko, Wiek, Umiejetnosci.
  2. Tagi structów: Używamy tagów structów (json:"...") aby określić, jak pola struktury mają być mapowane na klucze w JSON. Na przykład, tag json:"imie" mapuje pole Imie na klucz "imie" w JSON. Opcja omitempty (np. json:"umiejetnosci,omitempty") powoduje, że pole jest pomijane w JSON, jeśli jest puste (np. pusta tablica, pusty string, zero).
  3. json.Marshal(): Funkcja json.Marshal() konwertuje strukturę Go na tablicę bajtów reprezentującą JSON.
  4. Obsługa błędów: Ważne jest, aby zawsze sprawdzać, czy funkcja json.Marshal() zwróciła błąd.
  5. Wyświetlanie JSON: Konwertujemy tablicę bajtów na string i wyświetlamy w konsoli.

Wynik:

{"imie":"Jan","nazwisko":"Kowalski","wiek":30,"umiejetnosci":["Go","Python"]}

Dekodowanie (Deserializacja) JSON

Dekodowanie JSON polega na przekształceniu danych JSON na dane Go. Używamy funkcji json.Unmarshal() z pakietu encoding/json do tego celu.

Przykład:

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

// Definicja struktury Osoba (taka sama jak w przykładzie kodowania).
type Osoba struct {
	Imie       string   `json:"imie"`
	Nazwisko   string   `json:"nazwisko"`
	Wiek       int      `json:"wiek"`
	Umiejetnosci []string `json:"umiejetnosci,omitempty"`
}

func main() {
	// Definiujemy string z JSON.
	jsonString := `{"imie":"Jan","nazwisko":"Kowalski","wiek":30,"umiejetnosci":["Go","Python"]}`

	// Konwertujemy string na tablicę bajtów.
	jsonBytes := []byte(jsonString)

	// Tworzymy zmienną, do której zdekodujemy JSON.
	var osoba Osoba // Definiujemy zmienną typu Osoba, do której zostanie zdekodowany JSON.

	// Dekodujemy JSON do struktury Osoba.
	err := json.Unmarshal(jsonBytes, &osoba) // Funkcja json.Unmarshal() przyjmuje tablicę bajtów z JSON oraz wskaźnik na zmienną, do której ma zdekodować JSON.
	if err != nil {
		log.Fatalf("Błąd dekodowania JSON: %v", err) // Obsługa błędu.
	}

	// Wyświetlamy dane z zdekodowanej struktury.
	fmt.Printf("Imię: %s\n", osoba.Imie)     // Dostęp do pól struktury.
	fmt.Printf("Nazwisko: %s\n", osoba.Nazwisko) // Dostęp do pól struktury.
	fmt.Printf("Wiek: %d\n", osoba.Wiek)       // Dostęp do pól struktury.
	fmt.Printf("Umiejętności: %v\n", osoba.Umiejetnosci) // Dostęp do pól struktury (tablica stringów).
}

Wyjaśnienie:

  1. Definicja struktury Osoba: Definiujemy strukturę Osoba (taka sama jak w przykładzie kodowania). Struktura musi pasować do struktury JSON, którą chcemy zdekodować.
  2. String z JSON: Definiujemy string zawierający dane JSON.
  3. json.Unmarshal(): Funkcja json.Unmarshal() dekoduje JSON z tablicy bajtów do struktury Go. Przyjmuje dwa argumenty: tablicę bajtów z JSON i wskaźnik do zmiennej, do której ma zostać zdekodowany JSON. Ważne: Musimy przekazać wskaźnik do zmiennej, aby json.Unmarshal() mogła zmodyfikować jej wartość.
  4. Obsługa błędów: Ważne jest, aby zawsze sprawdzać, czy funkcja json.Unmarshal() zwróciła błąd.
  5. Dostęp do danych: Po zdekodowaniu JSON możemy uzyskać dostęp do danych zawartych w strukturze Osoba.

Wynik:

Imię: Jan
Nazwisko: Kowalski
Wiek: 30
Umiejętności: [Go Python]

Tagi structów - Zaawansowana konfiguracja

Tagi structów pozwalają na dokładniejsze kontrolowanie procesu kodowania i dekodowania JSON. Pozwalają na:

  • Zmianę nazwy klucza w JSON.
  • Pomijanie pól w JSON.
  • Określanie domyślnych wartości dla pól.

Przykłady użycia tagów structów:

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type Produkt struct {
	ID          int     `json:"id"`                 // Nazwa klucza w JSON to "id".
	Nazwa       string  `json:"nazwa"`              // Nazwa klucza w JSON to "nazwa".
	Cena        float64 `json:"cena"`               // Nazwa klucza w JSON to "cena".
	Opis        string  `json:"opis,omitempty"`      // Pole pomijane, jeśli jest puste.
	Dostepny    bool    `json:"dostepny,omitempty"` // Pole pomijane, jeśli jest false.
	Kategoria   string  `json:"kategoria,default=inne"` // Domyślna wartość "inne", jeśli pole nie występuje w JSON. (niestandardowe, wymaga dodatkowej logiki)
	Niedostępne bool    `json:"-"`                     // Pole jest ignorowane podczas kodowania i dekodowania.
}

func main() {
	// Kodowanie
	produkt := Produkt{
		ID:    123,
		Nazwa: "Laptop",
		Cena:  2500.00,
	}

	jsonBytes, err := json.Marshal(produkt)
	if err != nil {
		log.Fatalf("Błąd kodowania JSON: %v", err)
	}
	fmt.Println("Kodowanie:", string(jsonBytes))

	// Dekodowanie
	jsonString := `{"id":456,"nazwa":"Telefon","cena":1200.00,"opis":"Nowoczesny smartfon"}`
	jsonBytes = []byte(jsonString)

	var produkt2 Produkt
	err = json.Unmarshal(jsonBytes, &produkt2)
	if err != nil {
		log.Fatalf("Błąd dekodowania JSON: %v", err)
	}
	fmt.Println("Dekodowanie:", produkt2)
}

Wyjaśnienie:

  • json:"id": Określa, że pole ID w strukturze Produkt ma być mapowane na klucz "id" w JSON.
  • json:"opis,omitempty": Opcja omitempty powoduje, że pole Opis jest pomijane w JSON, jeśli jest puste (pusty string).
  • json:"dostepny,omitempty": Opcja omitempty powoduje, że pole Dostepny jest pomijane w JSON, jeśli jest false.
  • json:"kategoria,default=inne": To przykład niestandardowej opcji. Pakiet encoding/json nie obsługuje domyślnych wartości w ten sposób natywnie. Aby to zaimplementować, musiałbyś napisać własną implementację interfejsu json.Unmarshaler, która sprawdziłaby, czy pole jest obecne w JSON, a jeśli nie, ustawiłaby je na wartość domyślną. Poniżej pokażę przykład jak to zrobić.
  • json:"-": Powoduje, że pole Niedostępne jest całkowicie ignorowane podczas kodowania i dekodowania JSON.

Przykład implementacji json.Unmarshaler z wartością domyślną:

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type Produkt struct {
	ID        int     `json:"id"`
	Nazwa     string  `json:"nazwa"`
	Cena      float64 `json:"cena"`
	Kategoria string  `json:"kategoria,default=inne"`
}

// Implementacja interfejsu json.Unmarshaler.
func (p *Produkt) UnmarshalJSON(data []byte) error {
	// Definiujemy typ pomocniczy, aby uniknąć rekursji podczas Unmarshal.
	type Alias Produkt
	aux := &Alias{
		Kategoria: "inne", // Ustawiamy domyślną wartość.
	}

	if err := json.Unmarshal(data, aux); err != nil {
		return err
	}

	// Kopiujemy wartości z aux do p.
	*p = Produkt(*aux)

	// Sprawdzamy, czy pole "kategoria" istnieje w JSON. Jeśli nie, to pozostaje wartość domyślna.
	var tempMap map[string]interface{}
	if err := json.Unmarshal(data, &tempMap); err != nil {
		return err
	}

	if _, ok := tempMap["kategoria"]; ok {
		// Pole istnieje, więc wartość z JSON została już przypisana.
	} else {
		// Pole nie istnieje, więc wartość domyślna została przypisana w aux.
		// Można opcjonalnie dodać logowanie lub inne działania.
		fmt.Println("Pole 'kategoria' nie występuje w JSON, użyto wartości domyślnej: 'inne'")
	}

	return nil
}

func main() {
	jsonString := `{"id":456,"nazwa":"Telefon","cena":1200.00}` // Brak pola "kategoria".
	jsonBytes := []byte(jsonString)

	var produkt Produkt
	err := json.Unmarshal(jsonBytes, &produkt)
	if err != nil {
		log.Fatalf("Błąd dekodowania JSON: %v", err)
	}
	fmt.Println("Dekodowanie:", produkt)

	jsonString2 := `{"id":789,"nazwa":"Tablet","cena":800.00, "kategoria": "elektronika"}` // Jest pole "kategoria".
	jsonBytes2 := []byte(jsonString2)

	var produkt2 Produkt
	err = json.Unmarshal(jsonBytes2, &produkt2)
	if err != nil {
		log.Fatalf("Błąd dekodowania JSON: %v", err)
	}
	fmt.Println("Dekodowanie:", produkt2)
}

Wyjaśnienie:

  1. Implementacja UnmarshalJSON: Definiujemy metodę UnmarshalJSON dla typu Produkt. Ta metoda jest wywoływana zamiast standardowego procesu dekodowania JSON.
  2. Typ Alias: Używamy typu alias Alias aby uniknąć rekurencyjnego wywoływania UnmarshalJSON. Dekodujemy JSON do aux (typu *Alias), który ma już ustawioną wartość domyślną dla pola Kategoria.
  3. Kopiowanie wartości: Kopiujemy wartości z aux do p (typu *Produkt).
  4. Sprawdzanie obecności pola: Sprawdzamy, czy pole “kategoria” istnieje w JSON, dekodując JSON do mapy map[string]interface{}. Jeśli pole nie istnieje, oznacza to, że wartość domyślna (ustawiona w aux) powinna zostać użyta.

Wynik (pierwszy przykład kodowania):

Kodowanie: {"id":123,"nazwa":"Laptop","cena":2500}
Dekodowanie: {456 Telefon 1200.000000 Nowoczesny smartfon false }

Wynik (przykład z UnmarshalJSON):

Pole 'kategoria' nie występuje w JSON, użyto wartości domyślnej: 'inne'
Dekodowanie: {456 Telefon 1200 inne}
Dekodowanie: {789 Tablet 800 elektronika}

Praca zagnieżdżonymi strukturami

JSON często zawiera zagnieżdżone obiekty. W Go możemy to reprezentować za pomocą zagnieżdżonych struktur.

Przykład:

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type Adres struct {
	Ulica  string `json:"ulica"`
	Miasto string `json:"miasto"`
	KodPocztowy string `json:"kod_pocztowy"`
}

type Osoba struct {
	Imie     string `json:"imie"`
	Nazwisko string `json:"nazwisko"`
	Wiek     int    `json:"wiek"`
	Adres    Adres  `json:"adres"` // Zagnieżdżona struktura Adres
}

func main() {
	jsonString := `{"imie":"Jan","nazwisko":"Kowalski","wiek":30,"adres":{"ulica":"Kwiatowa 1","miasto":"Warszawa", "kod_pocztowy": "00-001"}}`
	jsonBytes := []byte(jsonString)

	var osoba Osoba
	err := json.Unmarshal(jsonBytes, &osoba)
	if err != nil {
		log.Fatalf("Błąd dekodowania JSON: %v", err)
	}

	fmt.Printf("Imię: %s\n", osoba.Imie)
	fmt.Printf("Miasto: %s\n", osoba.Adres.Miasto) // Dostęp do pola zagnieżdżonej struktury
}

Wyjaśnienie:

  • Zagnieżdżona struktura Adres: Definiujemy strukturę Adres reprezentującą adres.
  • Pole Adres w strukturze Osoba: Struktura Osoba zawiera pole Adres typu Adres.
  • Dostęp do pól zagnieżdżonej struktury: Aby uzyskać dostęp do pól zagnieżdżonej struktury, używamy notacji kropkowej (np. osoba.Adres.Miasto).

Praca z interfejsami (dynamiczny JSON)

Czasami nie znamy struktury JSON z góry. W takich przypadkach możemy użyć interfejsu interface{} (pusty interfejs), który może przechowywać dowolny typ danych.

Przykład:

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

func main() {
	jsonString := `{"imie":"Jan","wiek":30,"umiejetnosci":["Go","Python"], "czyAktywny": true}`
	jsonBytes := []byte(jsonString)

	var data map[string]interface{} // Mapa stringów na interfejsy.

	err := json.Unmarshal(jsonBytes, &data)
	if err != nil {
		log.Fatalf("Błąd dekodowania JSON: %v", err)
	}

	for key, value := range data {
		fmt.Printf("Klucz: %s, Wartość: %v, Typ: %T\n", key, value, value) // Wyświetlamy klucz, wartość i typ wartości.
	}

	// Dostęp do konkretnych wartości (trzeba użyć asercji typu).
	imie, ok := data["imie"].(string)
	if !ok {
		log.Fatal("Błąd: 'imie' nie jest stringiem")
	}
	fmt.Println("Imię:", imie)

	// Dostęp do tablicy (trzeba użyć asercji typu).
	umiejetnosci, ok := data["umiejetnosci"].([]interface{})
	if !ok {
		log.Fatal("Błąd: 'umiejetnosci' nie jest tablicą")
	}

	fmt.Println("Umiejętności:")
	for _, umiejetnosc := range umiejetnosci {
		fmt.Println("-", umiejetnosc)
	}
}

Wyjaśnienie:

  • map[string]interface{}: Deklarujemy zmienną data typu map[string]interface{}. Oznacza to, że kluczem w mapie jest string, a wartością może być dowolny typ danych.
  • Asercja typu: Ponieważ interface{} może przechowywać dowolny typ danych, musimy użyć asercji typu, aby uzyskać dostęp do konkretnej wartości. Na przykład, data["imie"].(string) sprawdza, czy wartość pod kluczem "imie" jest stringiem, i jeśli tak, zwraca ją. Jeśli wartość nie jest stringiem, operacja .(string) spowoduje panikę. Dlatego zawsze sprawdzamy ok, które jest drugim zwracanym argumentem z asercji typu, aby uniknąć paniki.

Praca domowa

  1. Stwórz strukturę Go reprezentującą książkę (tytuł, autor, rok wydania, ISBN, lista gatunków).
  2. Wygeneruj JSON z instancji tej struktury.
  3. Zapisz ten JSON do pliku.
  4. Odczytaj JSON z pliku.
  5. Zdekoduj JSON z pliku do instancji struktury książki.
  6. Wyświetl dane z zdekodowanej struktury.
  7. Zaimplementuj json.Unmarshaler dla swojej struktury Książka, dodając domyślną kategorię “Literatura Faktu”, jeżeli pole “gatunki” jest puste.

Podsumowanie

W tym artykule omówiliśmy, jak pracować z JSON w Go przy użyciu pakietu encoding/json. Dowiedzieliśmy się, jak kodować i dekodować JSON, jak używać tagów structów, jak pracować z zagnieżdżonymi strukturami i interfejsami. Praca z JSON jest kluczowa w wielu aplikacjach, a znajomość pakietu encoding/json jest niezbędna dla każdego programisty Go.

Przydatne zasoby

Zachęcam do dalszego eksperymentowania z pakietem encoding/json i eksplorowania innych funkcji języka Go. Pamiętaj, że praktyka czyni mistrza! Sprawdź również inne posty na tym blogu, aby pogłębić swoją wiedzę na temat Go. Powodzenia!

Polecane artykuły