1. Przegląd biblioteki standardowej encoding/json
Język Go udostępnia potężną bibliotekę encoding/json
do obsługi formatu danych JSON. Przy użyciu tej biblioteki, łatwo możesz konwertować typy danych Go na format JSON (serializacja) lub konwertować dane JSON na typy danych Go (deserializacja). Biblioteka ta zapewnia wiele funkcji, takich jak kodowanie, dekodowanie, strumieniowe wejście/wyjście oraz obsługę niestandardowej logiki parsowania JSON.
Najważniejsze typy danych i funkcje w tej bibliotece to:
-
Marshal
iMarshalIndent
: używane do serializacji typów danych Go do ciągów JSON. -
Unmarshal
: używane do deserializacji ciągów JSON na typy danych Go. -
Encoder
iDecoder
: używane do strumieniowego wejścia/wyjścia danych JSON. -
Valid
: używane do sprawdzenia, czy dany ciąg jest w prawidłowym formacie JSON.
W kolejnych rozdziałach specjalnie dowiemy się, jak korzystać z tych funkcji i typów.
2. Serializacja struktur danych Go do JSON
2.1 Użycie json.Marshal
json.Marshal
to funkcja, która serializuje typy danych Go do ciągów JSON. Przyjmuje ona typy danych z języka Go jako wejście, konwertuje je na format JSON i zwraca tablicę bajtów wraz z ewentualnymi błędami.
Oto prosty przykład, który demonstruje, jak przekształcić strukturę danych Go w ciąg JSON:
package main
import (
"encoding/json"
"fmt"
"log"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
person := Person{"Alice", 30}
jsonData, err := json.Marshal(person)
if err != nil {
log.Fatalf("Serializacja do JSON nie powiodła się: %s", err)
}
fmt.Println(string(jsonData)) // Wynik: {"name":"Alice","age":30}
}
Oprócz struktur, funkcja json.Marshal
może również serializować inne typy danych, takie jak mapy i slice'y. Poniżej są przykłady użycia map[string]interface{}
i slice
:
// Konwersja mapy do JSON
myMap := map[string]interface{}{
"name": "Bob",
"age": 25,
}
jsonData, err := json.Marshal(myMap)
// ... obsługa błędów i wynik pominięte ...
// Konwersja slice'a do JSON
mySlice := []string{"Jabłko", "Banan", "Wiśnia"}
jsonData, err := json.Marshal(mySlice)
// ... obsługa błędów i wynik pominięte ...
2.2 Tagi Struktur
W języku Go tagi struktur są używane do dostarczania metadanych dla pól struktury, kontrolując zachowanie serializacji JSON. Najczęstsze przypadki użycia obejmują zmianę nazwy pola, ignorowanie pól i warunkową serializację.
Na przykład, tag json:"<nazwa>"
można użyć do określenia nazwy pola JSON:
type Animal struct {
SpeciesName string `json:"species"`
Description string `json:"desc,omitempty"`
Tag string `json:"-"` // Dodanie tagu "-" oznacza, że to pole nie będzie serializowane
}
W powyższym przykładzie tag json:"-"
przed polem Tag
mówi json.Marshal
, żeby zignorować to pole. Opcja omitempty
dla pola Description
oznacza, że jeśli pole jest puste (wartość zero, na przykład pusty ciąg znaków), nie będzie ono uwzględnione w wygenerowanym ciągu JSON.
Poniżej jest kompletny przykład użycia tagów struktur:
package main
import (
"encoding/json"
"fmt"
"log"
)
type Animal struct {
SpeciesName string `json:"species"`
Description string `json:"desc,omitempty"`
Tag string `json:"-"`
}
func main() {
animal := Animal{
SpeciesName: "Słoń Afrykański",
Description: "Duże ssak z trąbą i kłami.",
Tag: "zagrożony", // To pole nie będzie serializowane do JSON
}
jsonData, err := json.Marshal(animal)
if err != nil {
log.Fatalf("Serializacja do JSON nie powiodła się: %s", err)
}
fmt.Println(string(jsonData)) // Wynik: {"species":"Słoń Afrykański","desc":"Duże ssak z trąbą i kłami."}
}
W ten sposób można zapewnić klarowną strukturę danych, kontrolując jednocześnie reprezentację JSON i elastycznie obsługując różne potrzeby serializacji.
3. Deserializacja JSON do Struktury Danych Go
3.1 Korzystanie z json.Unmarshal
Funkcja json.Unmarshal
pozwala nam parsować ciągi JSON na struktury danych Go, takie jak struktury, mapy, itp. Aby skorzystać z json.Unmarshal
, musimy najpierw zdefiniować strukturę danych Go, która pasuje do danych JSON.
Załóżmy, że mamy następujące dane JSON:
{
"name": "Alice",
"age": 25,
"emails": ["[email protected]", "[email protected]"]
}
Aby sparsować te dane do struktury Go, musimy zdefiniować pasującą strukturę:
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Emails []string `json:"emails"`
}
Teraz możemy użyć json.Unmarshal
do deserializacji:
import (
"encoding/json"
"fmt"
)
func main() {
jsonData := `{
"name": "Alice",
"age": 25,
"emails": ["[email protected]", "[email protected]"]
}`
var user User
err := json.Unmarshal([]byte(jsonData), &user)
if err != nil {
fmt.Println("Błąd deserializacji JSON:", err)
return
}
fmt.Printf("Użytkownik: %+v\n", user)
}
W powyższym przykładzie użyliśmy tagów, takich jak json:"name"
, aby poinformować funkcję json.Unmarshal
o mapowaniu pól JSON na pola struktury.
3.2 Dynamiczne Parsowanie
Czasami struktura JSON, którą musimy sparsować, nie jest znana z góry, lub struktura danych JSON może zmieniać się dynamicznie. W takich przypadkach możemy użyć typów interface{}
lub json.RawMessage
do parsowania.
Używanie interface{}
pozwala nam na parsowanie bez znajomości struktury JSON:
func main() {
jsonData := `{
"name": "Alice",
"details": {
"age": 25,
"job": "Inżynier"
}
}`
var result map[string]interface{}
json.Unmarshal([]byte(jsonData), &result)
fmt.Println(result)
// Asertywność typu, zapewnienie dopasowania typu przed użyciem
name := result["name"].(string)
fmt.Println("Imię:", name)
details := result["details"].(map[string]interface{})
age := details["age"].(float64) // Uwaga: liczby w interface{} są traktowane jako float64
fmt.Println("Wiek:", age)
}
Użycie json.RawMessage
pozwala nam zachować oryginalny JSON podczas selektywnego parsowania jego części:
type UserDynamic struct {
Name string `json:"name"`
Details json.RawMessage `json:"details"`
}
func main() {
jsonData := `{
"name": "Alice",
"details": {
"age": 25,
"job": "Inżynier"
}
}`
var user UserDynamic
json.Unmarshal([]byte(jsonData), &user)
var details map[string]interface{}
json.Unmarshal(user.Details, &details)
fmt.Println("Imię:", user.Name)
fmt.Println("Wiek:", details["age"])
fmt.Println("Praca:", details["job"])
}
Ten sposób jest przydatny do obsługi struktur JSON, w których pewne pola mogą zawierać różne rodzaje danych i pozwala na elastyczne zarządzanie danymi.
4 Obsługa Zagnieżdżonych Struktur i Tablic
4.1 Zagnieżdżone Obiekty JSON
Zwykłe dane JSON często nie są płaskie, lecz zawierają zagnieżdżone struktury. W Go możemy radzić sobie z tą sytuacją poprzez definiowanie zagnieżdżonych struktur.
Załóżmy, że mamy następujący zagnieżdżony JSON:
{
"name": "Bob",
"contact": {
"email": "[email protected]",
"address": "123 Main St"
}
}
Możemy zdefiniować strukturę Go w następujący sposób:
type ContactInfo struct {
Email string `json:"email"`
Address string `json:"address"`
}
type UserWithContact struct {
Name string `json:"name"`
Contact ContactInfo `json:"contact"`
}
Operacja deserializacji jest podobna do operacji na strukturach niezagnieżdżonych:
func main() {
jsonData := `{
"name": "Bob",
"contact": {
"email": "[email protected]",
"address": "123 Main St"
}
}`
var user UserWithContact
err := json.Unmarshal([]byte(jsonData), &user)
if err != nil {
fmt.Println("Błąd deserializacji JSON:", err)
}
fmt.Printf("%+v\n", user)
}
4.2 Tablice JSON
W formacie JSON, tablice są powszechną strukturą danych. W języku Go odpowiadają one typowi slice.
Rozważmy następującą tablicę JSON:
[
{"name": "Dave", "age": 34},
{"name": "Eve", "age": 28}
]
W języku Go definiujemy odpowiadającą strukturę i slice w następujący sposób:
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
jsonData := `[
{"name": "Dave", "age": 34},
{"name": "Eve", "age": 28}
]`
var people []Person
json.Unmarshal([]byte(jsonData), &people)
for _, person := range people {
fmt.Printf("%+v\n", person)
}
}
W ten sposób możemy deserializować każdy element z tablicy JSON do slice' a struktur w Go w celu dalszego przetwarzania i dostępu.
5 Obsługa Błędów
Podczas pracy z danymi JSON, czy to podczas serializacji (konwertowanie strukturalnych danych na format JSON) czy deserializacji (konwertowanie JSON z powrotem na strukturalne dane), mogą wystąpić błędy. W następnym punkcie omówimy typowe błędy i sposób ich obsługi.
5.1 Obsługa Błędów Serializacji
Błędy serializacji zazwyczaj występują podczas konwertowania struktury lub innych typów danych na ciąg JSON. Na przykład, jeśli próbujemy zserializować strukturę zawierającą nielegalne pola (takie jak typ kanału lub funkcję, które nie mogą być reprezentowane w formacie JSON), json.Marshal
zwróci błąd.
import (
"encoding/json"
"fmt"
"log"
)
type User struct {
Name string
Age int
// Załóżmy, że tutaj znajduje się pole, które nie może zostać zserializowane
// Data chan struct{} // Kanały nie mogą być reprezentowane w formacie JSON
}
func main() {
u := User{
Name: "Alice",
Age: 30,
// Data: make(chan struct{}),
}
bytes, err := json.Marshal(u)
if err != nil {
log.Fatalf("Serializacja JSON nie powiodła się: %v", err)
}
fmt.Println(string(bytes))
}
W powyższym przykładzie celowo zakomentowaliśmy pole Data
. Jeśli usuniemy komentarz, serializacja zakończy się niepowodzeniem, a program zaloguje błąd i przerwie wykonanie. Obsługa takich błędów zazwyczaj polega na sprawdzaniu błędów oraz wprowadzaniu odpowiednich strategii obsługi błędów (takich jak logowanie błędów, zwracanie domyślnych danych, itp.).
5.2 Obsługa Błędów Deserializacji
Błędy deserializacji mogą wystąpić podczas procesu konwertowania ciągu JSON z powrotem na struct w języku Go lub inny typ danych. Na przykład, jeśli format ciągu JSON jest niepoprawny lub niekompatybilny z docelowym typem, json.Unmarshal
zwróci błąd.
import (
"encoding/json"
"fmt"
"log"
)
func main() {
var data = []byte(`{"name":"Alice","age":"unknown"}`) // "age" powinno być liczbą całkowitą, ale tutaj podano ciąg znaków
var u User
err := json.Unmarshal(data, &u)
if err != nil {
log.Fatalf("Deserializacja JSON nie powiodła się: %v", err)
}
fmt.Printf("%+v\n", u)
}
W tym przykładzie kodu celowo podaliśmy błędny typ danych dla pola age
(ciąg znaków zamiast oczekiwanej liczby całkowitej), co spowodowało błąd w json.Unmarshal
. Dlatego musimy odpowiednio obsłużyć tę sytuację. Powszechną praktyką jest logowanie komunikatu o błędzie oraz w zależności od sytuacji, ewentualne zwrócenie pustego obiektu, wartości domyślnej lub komunikatu o błędzie.
6 Zaawansowane Funkcje i Optymalizacja Wydajności
6.1 Niestandardowe Serializowanie i Deserializowanie
Domyślnie pakiet encoding/json
w Go serializuje i deserializuje JSON poprzez refleksję. Jednak możemy dostosować te procesy, implementując interfejsy json.Marshaler
i json.Unmarshaler
.
import (
"encoding/json"
"fmt"
)
type Color struct {
Red uint8
Green uint8
Blue uint8
}
func (c Color) MarshalJSON() ([]byte, error) {
hex := fmt.Sprintf("\"#%02x%02x%02x\"", c.Red, c.Green, c.Blue)
return []byte(hex), nil
}
func (c *Color) UnmarshalJSON(data []byte) error {
_, err := fmt.Sscanf(string(data), "\"#%02x%02x%02x\"", &c.Red, &c.Green, &c.Blue)
return err
}
func main() {
c := Color{Red: 255, Green: 99, Blue: 71}
jsonColor, _ := json.Marshal(c)
fmt.Println(string(jsonColor))
var newColor Color
json.Unmarshal(jsonColor, &newColor)
fmt.Println(newColor)
}
Tutaj zdefiniowaliśmy typ Color
i zaimplementowaliśmy metody MarshalJSON
oraz UnmarshalJSON
do konwersji kolorów RGB na ciągi heksadecymalne, a następnie z powrotem na kolory RGB.
6.2 Kodery i Dekodery
Przy pracy z dużymi danymi w formacie JSON, bezpośrednie użycie json.Marshal
i json.Unmarshal
może prowadzić do nadmiernego zużycia pamięci lub nieefektywnych operacji wejścia/wyjścia. Dlatego pakiet encoding/json
w Go udostępnia typy Encoder
i Decoder
, które mogą przetwarzać dane JSON w sposób strumieniowy.
6.2.1 Użycie json.Encoder
json.Encoder
może bezpośrednio zapisywać dane JSON do dowolnego obiektu implementującego interfejs io.Writer, co oznacza, że można kodować dane JSON bezpośrednio do pliku, połączenia sieciowego, itp.
import (
"encoding/json"
"os"
)
func main() {
users := []User{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
}
file, _ := os.Create("users.json")
defer file.Close()
encoder := json.NewEncoder(file)
if err := encoder.Encode(users); err != nil {
log.Fatalf("Błąd kodowania: %v", err)
}
}
6.2.2 Użycie json.Decoder
json.Decoder
może bezpośrednio odczytywać dane JSON z dowolnego obiektu implementującego interfejs io.Reader, wyszukując i analizując obiekty i tablice JSON.
import (
"encoding/json"
"os"
)
func main() {
file, _ := os.Open("users.json")
defer file.Close()
var users []User
decoder := json.NewDecoder(file)
if err := decoder.Decode(&users); err != nil {
log.Fatalf("Błąd dekodowania: %v", err)
}
for _, u := range users {
fmt.Printf("%+v\n", u)
}
}
Poprzez przetwarzanie danych za pomocą kodowników i dekodowników, możesz przeprowadzać przetwarzanie JSON podczas odczytywania, ograniczając zużycie pamięci i poprawiając wydajność przetwarzania, co jest szczególnie przydatne przy obsłudze transferów sieciowych lub dużych plików.