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 i MarshalIndent: 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 i Decoder: 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.