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

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:
- Definicja struktury
Osoba
: Definiujemy strukturęOsoba
z polamiImie
,Nazwisko
,Wiek
,Umiejetnosci
. - 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, tagjson:"imie"
mapuje poleImie
na klucz"imie"
w JSON. Opcjaomitempty
(np.json:"umiejetnosci,omitempty"
) powoduje, że pole jest pomijane w JSON, jeśli jest puste (np. pusta tablica, pusty string, zero). json.Marshal()
: Funkcjajson.Marshal()
konwertuje strukturę Go na tablicę bajtów reprezentującą JSON.- Obsługa błędów: Ważne jest, aby zawsze sprawdzać, czy funkcja
json.Marshal()
zwróciła błąd. - 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:
- Definicja struktury
Osoba
: Definiujemy strukturęOsoba
(taka sama jak w przykładzie kodowania). Struktura musi pasować do struktury JSON, którą chcemy zdekodować. - String z JSON: Definiujemy string zawierający dane JSON.
json.Unmarshal()
: Funkcjajson.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, abyjson.Unmarshal()
mogła zmodyfikować jej wartość.- Obsługa błędów: Ważne jest, aby zawsze sprawdzać, czy funkcja
json.Unmarshal()
zwróciła błąd. - 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 poleID
w strukturzeProdukt
ma być mapowane na klucz"id"
w JSON.json:"opis,omitempty"
: Opcjaomitempty
powoduje, że poleOpis
jest pomijane w JSON, jeśli jest puste (pusty string).json:"dostepny,omitempty"
: Opcjaomitempty
powoduje, że poleDostepny
jest pomijane w JSON, jeśli jest false.json:"kategoria,default=inne"
: To przykład niestandardowej opcji. Pakietencoding/json
nie obsługuje domyślnych wartości w ten sposób natywnie. Aby to zaimplementować, musiałbyś napisać własną implementację interfejsujson.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 poleNiedostę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:
- Implementacja
UnmarshalJSON
: Definiujemy metodęUnmarshalJSON
dla typuProdukt
. Ta metoda jest wywoływana zamiast standardowego procesu dekodowania JSON. - Typ Alias: Używamy typu alias
Alias
aby uniknąć rekurencyjnego wywoływaniaUnmarshalJSON
. Dekodujemy JSON doaux
(typu*Alias
), który ma już ustawioną wartość domyślną dla polaKategoria
. - Kopiowanie wartości: Kopiujemy wartości z
aux
dop
(typu*Produkt
). - 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 waux
) 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 strukturzeOsoba
: StrukturaOsoba
zawiera poleAdres
typuAdres
. - 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
typumap[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 sprawdzamyok
, które jest drugim zwracanym argumentem z asercji typu, aby uniknąć paniki.
Praca domowa
- Stwórz strukturę Go reprezentującą książkę (tytuł, autor, rok wydania, ISBN, lista gatunków).
- Wygeneruj JSON z instancji tej struktury.
- Zapisz ten JSON do pliku.
- Odczytaj JSON z pliku.
- Zdekoduj JSON z pliku do instancji struktury książki.
- Wyświetl dane z zdekodowanej struktury.
- Zaimplementuj
json.Unmarshaler
dla swojej strukturyKsiąż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
- Oficjalna dokumentacja pakietu
encoding/json
: https://pkg.go.dev/encoding/json - A Tour of Go: https://go.dev/tour/welcome/1
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
Flutter DevTools: Debugowanie UI
Debugowanie układu UI we Flutterze z Flutter DevTools. Praktyczne wskazówki i techniki dla developerów.
Mateusz Kędziora
Flutter Animacje: Kompleksowy poradnik
Opanuj animacje Fluttera! Przykłady kodu, porady i wskazówki dotyczące angażujących interfejsów użytkownika. Podnieś swoje umiejętności Fluttera!
Mateusz Kędziora
Impeller Flutter: Nowa era mobilnej grafiki
Odkryj Impeller, nowy silnik renderujący Fluttera! Zwiększ wydajność, popraw grafikę i wykorzystaj nowoczesne API.
Mateusz Kędziora