JSON in Go lesen und schreiben mit encoding/json
1. Überblick
JSON ist das häufigste Datenaustauschformat zwischen Services. Go unterstützt JSON nativ in der Standardbibliothek über das Paket encoding/json, ohne dass Sie zusätzliche Abhängigkeiten installieren müssen.
Dieser Artikel hilft Ihnen, Structs zu JSON zu konvertieren und umgekehrt, Feldnamen mit Struct-Tags zu kontrollieren, dynamische JSON zu verarbeiten und große Datenmengen zu streamen. Sie werden auch lernen, wie Sie benutzerdefinierte Typen schreiben und testen. Am Ende sind häufige Fehler aufgelistet, um Probleme in der Produktionsumgebung zu vermeiden.
Die Beispiele verwenden Go 1.24 oder höher.
2. JSON mit json.Marshal schreiben
Wir starten mit einer Bestellung mit Kundeninformationen:
package main
import (
"encoding/json"
"fmt"
"log"
)
type Customer struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
}
type Order struct {
ID int `json:"id"`
Customer Customer `json:"customer"`
Items []string `json:"items"`
Total float64 `json:"total"`
Note string `json:"-"`
createdBy string
}
func main() {
order := Order{
ID: 1001,
Customer: Customer{Name: "Lan"},
Items: []string{"Cà phê", "Bánh mì"},
Total: 45000,
Note: "giao trước 9h",
createdBy: "admin",
}
data, err := json.Marshal(order)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
}
Ergebnis:
{"id":1001,"customer":{"name":"Lan"},"items":["Cà phê","Bánh mì"],"total":45000}
Es gibt einige Punkte, die Sie im obigen Code beachten sollten:
json.Marshalgibt[]byteunderrorzurück. Sie müssen immererrorprüfen.- Das Tag
json:"id"ändert den Feldnamen zuidim JSON. Ohne Tag verwendet Go den ursprünglichen Feldnamen wieID. Emailist leer, daher wird es durchomitemptyweggelassen.Notehat das Tagjson:"-", daher wird es immer weggelassen.createdBybeginnt mit einem Kleinbuchstaben, also ist es unexported.encoding/jsonkann dieses Feld nicht sehen.- Vietnamesische Zeichen werden in UTF-8 beibehalten, nicht escaped.
Wenn Sie lesbares JSON benötigen, z.B. zum Protokollieren oder Schreiben von Konfigurationsdateien, verwenden Sie json.MarshalIndent:
data, _ := json.MarshalIndent(order.Customer, "", " ")
fmt.Println(string(data))
{
"name": "Lan"
}
3. Struct-Tags: detaillierte Kontrolle der Ausgabe
Das json-Tag hat die Form json:"name,option,...". Häufig verwendete Optionen:
| Tag | Bedeutung |
|---|---|
json:"name" |
Feldname ändern |
json:",omitempty" |
Feld weglassen, wenn es der Zero-Wert ist: "", 0, false, nil, leeres Slice oder Map |
json:",omitzero" |
Feld weglassen, wenn es der Zero-Wert ist, einschließlich Structs wie time.Time (Go 1.24+) |
json:",string" |
Zahlen oder Booleans als Strings schreiben |
json:"-" |
Immer weglassen |
omitempty hat keine Wirkung bei Structs. Das folgende Beispiel zeigt den Unterschied:
type Shipment struct {
OrderID int64 `json:"order_id,string"`
ShippedAt time.Time `json:"shipped_at,omitempty"`
PaidAt time.Time `json:"paid_at,omitzero"`
}
data, _ := json.Marshal(Shipment{OrderID: 1001})
fmt.Println(string(data))
{"order_id":"1001","shipped_at":"0001-01-01T00:00:00Z"}
ShippedAt erscheint immer noch, da omitempty den leeren time.Time-Struct nicht als “empty” behandelt. PaidAt verwendet omitzero und wird daher wie erwartet weggelassen. OrderID wird als String "1001" geschrieben, dank der string-Option. Dies ist nützlich, wenn der Client JavaScript ist und 64-Bit-Ganzzahlen nicht korrekt lesen kann.
4. JSON mit json.Unmarshal lesen
In der umgekehrten Richtung geben Sie einen Zeiger auf die Zielvariable an:
input := []byte(`{
"id": 1002,
"Customer": {"name": "Minh", "email": "minh@example.com"},
"items": ["Trà đào"],
"total": 20000,
"coupon": "SALE10"
}`)
var order Order
if err := json.Unmarshal(input, &order); err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", order)
{ID:1002 Customer:{Name:Minh Email:minh@example.com} Items:[Trà đào] Total:20000 Note: createdBy:}
Sie werden zwei Standardverhalten bemerken:
- Schlüssel werden case-insensitive verglichen.
"Customer"stimmt immer noch mit dem Tagcustomerüberein. - Unbekannte Schlüssel wie
"coupon"werden stillschweigend ignoriert. Felder, die nicht im JSON vorhanden sind, behalten ihren aktuellen Wert.
Hinweis: Wenn Sie einen Wert statt eines Zeigers übergeben, z.B.
json.Unmarshal(input, order), erhalten Sie den Fehlerjson: Unmarshal(non-pointer main.Order). Daten werden überhaupt nicht geschrieben.
4.1. Unbekannte Felder ablehnen
Wenn eine API Anfragen von Clients empfängt, ist es normalerweise ein Fehler, unbekannte Schlüssel zu ignorieren, da dies Tippfehler verschleiern kann. Sie können den strikten Modus über json.Decoder aktivieren:
dec := json.NewDecoder(bytes.NewReader(input))
dec.DisallowUnknownFields()
var order Order
err := dec.Decode(&order)
fmt.Println(err)
json: unknown field "coupon"
DisallowUnknownFields ist nur auf Decoder verfügbar, nicht mit json.Unmarshal.
5. Dynamisches JSON mit map[string]any
Wenn Sie die Struktur nicht im Voraus kennen, z.B. bei Webhooks von Drittanbietern, können Sie in map[string]any dekodieren:
var payload map[string]any
_ = json.Unmarshal([]byte(`{"event":"order.paid","order_id":9007199254740993}`), &payload)
fmt.Printf("%T %v\n", payload["event"], payload["event"])
fmt.Printf("%T %.0f\n", payload["order_id"], payload["order_id"])
string order.paid
float64 9007199254740992
Alle Zahlen werden zu float64 dekodiert. Daher verliert order_id an Genauigkeit: Der ursprüngliche Wert endet mit 3, aber das Ergebnis endet mit 2. Dies ist ein sehr schwer zu erkennender Fehler.
Die Lösung ist, UseNumber zu rufen, um Zahlen in ihrer ursprünglichen String-Form als json.Number zu behalten:
dec := json.NewDecoder(strings.NewReader(`{"order_id":9007199254740993}`))
dec.UseNumber()
var payload map[string]any
_ = dec.Decode(&payload)
id, _ := payload["order_id"].(json.Number).Int64()
fmt.Println(id)
9007199254740993
Wenn nur ein Teil der Payload dynamisch ist, können Sie json.RawMessage verwenden, um das Dekodieren dieses Teils zu verzögern:
type Event struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
Nachdem Sie Type gelesen haben, rufen Sie json.Unmarshal(e.Data, &order) mit dem passenden Struct auf. Dieser Ansatz ist viel typsicherer als die Suche nach einzelnen Schlüsseln in der Map.
6. Datenstreaming mit Encoder und Decoder
Marshal und Unmarshal arbeiten mit []byte, daher müssen alle Daten im Speicher vorhanden sein. json.Encoder und json.Decoder arbeiten direkt mit io.Writer und io.Reader. Sie eignen sich für HTTP-Body, Dateien und NDJSON-Daten (jede Zeile ist ein JSON-Objekt).
func main() {
input := strings.NewReader(`{"id":1,"total":10000}
{"id":2,"total":25000}
{"id":3,"total":5000}`)
dec := json.NewDecoder(input)
enc := json.NewEncoder(os.Stdout)
enc.SetEscapeHTML(false)
sum := 0.0
for {
var o Order
err := dec.Decode(&o)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
log.Fatal(err)
}
sum += o.Total
}
_ = enc.Encode(map[string]float64{"sum": sum})
}
{"sum":40000}
Die Schleife ruft Decode auf, bis sie io.EOF erreicht, und liest jedes Mal genau ein Objekt. Encode fügt nach jedem Wert automatisch ein Newline hinzu. SetEscapeHTML(false) verhindert, dass Go <, >, & zu \u003c, \u003e, \u0026 konvertiert. Standardmäßig escaped Go diese Zeichen für Sicherheit beim Einbetten von JSON in HTML.
In einem HTTP-Handler verwenden Sie genau dieses Muster:
func createOrder(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // maximal 1 MB
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var o Order
if err := dec.Decode(&o); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(o)
}
http.MaxBytesReader begrenzt die Körpergröße. Dadurch kann der Client nicht eine riesige Payload senden, die den Speicher des Servers erschöpft.
7. Benutzerdefinierte Typen
Angenommen, der Bestellstatus wird im Code als int gespeichert, muss aber im JSON als String angezeigt werden. Sie implementieren encoding.TextMarshaler und encoding.TextUnmarshaler:
type Status int
const (
StatusPending Status = iota
StatusPaid
StatusShipped
)
var statusNames = map[Status]string{
StatusPending: "pending",
StatusPaid: "paid",
StatusShipped: "shipped",
}
func (s Status) MarshalText() ([]byte, error) {
name, ok := statusNames[s]
if !ok {
return nil, fmt.Errorf("status ungültig: %d", s)
}
return []byte(name), nil
}
func (s *Status) UnmarshalText(b []byte) error {
for k, v := range statusNames {
if v == string(b) {
*s = k
return nil
}
}
return fmt.Errorf("status ungültig: %q", b)
}
Bei Verwendung in einem Struct:
type OrderStatus struct {
ID int `json:"id"`
Status Status `json:"status"`
}
data, _ := json.Marshal(OrderStatus{ID: 1001, Status: StatusPaid})
fmt.Println(string(data))
{"id":1001,"status":"paid"}
MarshalText verwendet einen Value-Receiver und funktioniert daher mit Status und *Status. UnmarshalText erfordert einen Pointer-Receiver, da es den Wert ändern muss. Die Verwendung von TextMarshaler statt json.Marshaler hat einen zusätzlichen Vorteil: Der Status-Typ kann als Schlüssel einer Map verwendet werden, z.B. map[Status]int.
Wenn Sie die vollständige Kontrolle über das Objekt benötigen, schreiben Sie MarshalJSON. Seien Sie vorsichtig vor Endlosrekursion: Wenn Sie json.Marshal(o) mit demselben Typ aufrufen, wird die Funktion sich selbst unendlich aufrufen. Die Lösung besteht darin, einen Alias-Typ zu verwenden:
func (o Order) MarshalJSON() ([]byte, error) {
type alias Order // Alias hat nicht die MarshalJSON-Methode
return json.Marshal(struct {
alias
ItemCount int `json:"item_count"`
}{alias(o), len(o.Items)})
}
alias hat die gleichen Felder wie Order, aber ohne die Methode, daher verwendet json.Marshal das Standardverhalten. Das eingebettete Feld wird “flachgelegt” zum äußeren Objekt, zusammen mit item_count.
8. Tests schreiben
Tests für den Hin- und Rückweg (Round-Trip), d.h. Schreiben und erneutes Lesen, helfen Ihnen, Fehler zu fangen, wenn jemand Tags oder benutzerdefinierte Logik ändert. Datei status_test.go:
package main
import (
"bytes"
"encoding/json"
"testing"
)
func TestStatusRoundTrip(t *testing.T) {
tests := []struct {
status Status
want string
}{
{StatusPending, `{"id":1,"status":"pending"}`},
{StatusPaid, `{"id":1,"status":"paid"}`},
{StatusShipped, `{"id":1,"status":"shipped"}`},
}
for _, tt := range tests {
t.Run(tt.want, func(t *testing.T) {
data, err := json.Marshal(OrderStatus{ID: 1, Status: tt.status})
if err != nil {
t.Fatalf("marshal-Fehler: %v", err)
}
if string(data) != tt.want {
t.Errorf("erhalten %s, erwartet %s", data, tt.want)
}
var back OrderStatus
if err := json.Unmarshal(data, &back); err != nil {
t.Fatalf("unmarshal-Fehler: %v", err)
}
if back.Status != tt.status {
t.Errorf("erhalten %v, erwartet %v", back.Status, tt.status)
}
})
}
}
func TestRejectUnknownField(t *testing.T) {
dec := json.NewDecoder(bytes.NewBufferString(`{"id":1,"coupon":"X"}`))
dec.DisallowUnknownFields()
var o OrderStatus
if err := dec.Decode(&o); err == nil {
t.Fatal("erwarten unknown-field-Fehler, erhalten nil")
}
}
go test ./...
ok example.com/shop 0.003s
Table-Driven Tests sind die übliche Schreibweise in Go. Jeder Fall wird in t.Run separat ausgeführt, daher sehen Sie sofort, welcher Fall fehlschlägt.
9. Häufige Fehler
- Feld erscheint nicht im JSON. Fast immer ist der Feldname klein geschrieben (unexported). Ändern Sie ihn zu großgeschrieben und verwenden Sie ein Tag zum Setzen des JSON-Namens.
nullstatt[]. Einnil-Slice wird zunullgeschrieben. Wenn der Client ein leeres Array benötigt, initialisieren SieItems: []string{}.- Zahlenpräzision geht verloren. Passiert bei Dekodierung zu
any. Verwenden Sie Structs mit spezifischen Typen (int64), oderUseNumber, oder Tag,string. - Typfehler werden verschluckt. Wenn
Unmarshalauf*json.UnmarshalTypeErrortrifft, füllt es immer noch andere Felder aus. Verwenden Sie den Struct nicht, wennerr != nil. []bytewird zu Base64. Dies ist beabsichtigt. Wenn Sie normalen Text möchten, verwenden Siestring.time.Timefolgt RFC 3339. Für ein anderes Format wiedd/MM/yyyymüssen Sie einen benutzerdefinierten Typ schreiben.json.Marshalinnerhalb vonMarshalJSONmit demselben Typ aufrufen. Dies führt zu einem Stack Overflow. Verwenden Sie einen Alias-Typ wie in Abschnitt 7.
Hinweis:
encoding/jsonverwendet Reflection und ist daher nicht die schnellste Wahl. Bei den meisten APIs ist seine Leistung völlig ausreichend. Nur wenn Profiling zeigt, dass JSON ein Engpass ist, sollten Sie codegenerierende Bibliotheken in Betracht ziehen. Go 1.25 hat auch ein Experimentencoding/json/v2, aktiviert durchGOEXPERIMENT=jsonv2. Diese Version ist noch nicht stabil, daher nicht für Production geeignet.
10. Fazit
Sie haben die wichtigsten Operationen mit encoding/json kennengelernt:
- Verwenden Sie
MarshalundUnmarshalfür kleine Datenmengen. - Verwenden Sie Struct-Tags (
omitempty,omitzero,string,-), um die Ausgabe zu kontrollieren. - Verwenden Sie
DecodermitDisallowUnknownFieldsundUseNumber, um Daten sicherer zu lesen. - Verwenden Sie
EncoderundDecoderzum Streamen von Daten. - Verwenden Sie
TextMarshaleroderMarshalJSONfür benutzerdefinierte Typen.
Die allgemeine Regel ist: bevorzugen Sie Structs mit expliziten Typen statt map[string]any, und prüfen Sie immer error. Sie sollten auch Round-Trip-Tests für benutzerdefinierte Typen schreiben. Diese drei Gewohnheiten helfen Ihnen, die meisten JSON-Fehler in Go-Services zu vermeiden.