Struct tag adalah anotasi string di belakang deklarasi field yang dibaca package encoding saat runtime. Dengan tag, nama field JSON tidak lagi terikat nama field Go: NamaLengkap bisa tampil sebagai nama_lengkap, field kosong bisa dipangkas dari output, dan field rahasia bisa disembunyikan total. Pada chapter ini kita akan membedah anatomi struct tag, perilaku omitempty terhadap zero value, penulisan tag untuk beberapa format sekaligus, dan pembacaan tag lewat reflection.

Anatomi Struct Tag #

Struct tag ditulis sebagai string literal mentah di belakang tipe field:

go
Nama string `json:"nama" xml:"nama"`

String tag terdiri atas pasangan kunci:"nilai" yang dipisah spasi. Setiap package encoding membaca kunci miliknya sendiri: encoding/json membaca json, encoding/xml membaca xml. Nilai di balik kunci lalu dipecah lagi dengan koma menjadi nama dan opsi, misalnya nama,omitempty. Tabel berikut merangkum atribut tag JSON yang paling sering dipakai:

AtributEfek saat marshalEfek saat unmarshal
json:"nama"Field tampil dengan nama kecilPencocokan nama, tidak peka huruf besar-kecil
json:"nama,omitempty"Field zero tidak ditulisSama seperti nama
json:"-"Field tidak ditulisField tidak diisi
json:",omitempty"Nama field Go dipertahankanNama field Go
json:"nama,string"Nilai ditulis sebagai stringString diurai menjadi nilai

omitempty dan Zero Value #

omitempty memangkas field yang bernilai zero: string kosong, angka nol, false, slice dan map kosong, serta pointer nil. Perilaku ini membuat output JSON ramping, tetapi juga menghapus makna “benar-benar kosong” versus “tidak diisi”. Trik pointer mengembalikan perbedaan tersebut: pointer nil tetap dipangkas, sedangkan pointer ke nilai zero tetap ditulis. Program tag-demo.go berikut memperagakan seluruh perilaku tersebut:

go
package main

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

type Produk struct {
	ID       string   `json:"id" xml:"id,attr"`
	Nama     string   `json:"nama,omitempty" xml:"nama"`
	Harga    int      `json:"harga,omitempty" xml:"-"`
	Aktif    bool     `json:"aktif,omitempty" xml:"aktif"`
	Tag      []string `json:"tag,omitempty" xml:"tag"`
	Stok     *int     `json:"stok,omitempty" xml:"-"`
	Rahasia  string   `json:"-" xml:"-"`
	Kategori string   `json:"kategori,omitempty" xml:"kategori"`
}

func main() {
	// Semua field opsional bernilai zero: omitempty memangkasnya.
	p := Produk{ID: "P-01", Nama: "Buku Go", Harga: 95000, Aktif: true}
	stok := 0 // pointer ke nol tetap ikut output
	p.Stok = &stok
	data, err := json.Marshal(p)
	if err != nil {
		fmt.Println("marshal gagal:", err)
		return
	}
	fmt.Println(string(data))

	// Hanya ID terisi: field lain bernilai zero dan hilang.
	data2, _ := json.Marshal(Produk{ID: "P-02"})
	fmt.Println(string(data2))

	// Decode: pencocokan nama field tidak peka huruf besar-kecil,
	// field yang tidak dikenal (stok, berat, diskon) diabaikan.
	sumber := []byte(`{"ID":"P-03","NAMA":"Kopi","harga":15000,"stok":7,"berat":250,"diskon":true}`)
	var p3 Produk
	if err := json.Unmarshal(sumber, &p3); err != nil {
		fmt.Println("unmarshal gagal:", err)
		return
	}
	fmt.Printf("hasil decode: ID=%s Nama=%s Harga=%d", p3.ID, p3.Nama, p3.Harga)
	if p3.Stok != nil {
		fmt.Printf(" stok=%d", *p3.Stok)
	}
	fmt.Println()

	// reflect membaca struct tag saat runtime.
	t := reflect.TypeOf(p)
	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i)
		fmt.Printf("field %-8s json=%-18q xml=%q\n", f.Name, f.Tag.Get("json"), f.Tag.Get("xml"))
	}
}

Output yang Dihasilkan:

text
{"id":"P-01","nama":"Buku Go","harga":95000,"aktif":true,"stok":0}
{"id":"P-02"}
hasil decode: ID=P-03 Nama=Kopi Harga=15000 stok=7
field ID       json="id"               xml="id,attr"
field Nama     json="nama,omitempty"   xml="nama"
field Harga    json="harga,omitempty"  xml="-"
field Aktif    json="aktif,omitempty"  xml="aktif"
field Tag      json="tag,omitempty"    xml="tag"
field Stok     json="stok,omitempty"   xml="-"
field Rahasia  json="-"                xml="-"
field Kategori json="kategori,omitempty" xml="kategori"

Mari kita telaah baris demi baris program di atas:

  1. Baris 9-18 (type Produk): setiap field memuat tag json sekaligus xml — dua format dalam satu deklarasi. Field Rahasia bertag "-" pada kedua format sehingga tidak pernah keluar.
  2. Baris 22-24: struct dengan banyak field terisi, tetapi p.Stok diarahkan ke angka nol lewat pointer — "stok":0 tetap muncul pada output justru karena nilainya bukan pointer nil.
  3. Baris 25-30: json.Marshal menghasilkan JSON dengan nama field kecil sesuai tag; aktif muncul karena bernilai true, tag dan kategori hilang karena zero.
  4. Baris 33-34: struct kedua hanya mengisi ID, sehingga seluruh field bertag omitempty terpangkas dan output tinggal {"id":"P-02"}.
  5. Baris 38-48: decode memakai JSON dengan nama field campuran huruf besar dan kecil; pencocokan nama tidak peka huruf, dan field berat serta diskon yang tidak dikenal diabaikan tanpa galat.
  6. Baris 51-55: reflect.TypeOf mengambil tipe struct saat runtime; t.Field(i).Tag.Get("json") membaca nilai tag per field — mekanisme yang sama dipakai package encoding di balik layar.

Decode Tidak Peka Huruf Besar-Kecil #

Output kedua program membuktikan dua kelonggaran json.Unmarshal ketika memakai tag: nama field dicocokkan tanpa memperhatikan huruf besar-kecil, dan field JSON yang tidak dikenal diperbolehkan tanpa galat. Kelonggaran pertama menjaga kompatibilitas dengan sumber data yang berbeda gaya penamaan; kelonggaran kedua menjadi fondasi evolusi format yang dibahas pada chapter Format dan Compatibility .

Satu Struct, Banyak Format #

Deklarasi xml:"id,attr" pada struct di atas menunjukkan bahwa satu field bisa melayani format berbeda sekaligus. Keluarga tag populer lain misalnya yaml:"nama" untuk YAML dan db:"nama" untuk pemetaan kolom basis data. Package YAML dan pemetaan database tersebut datang dari pihak ketiga, tetapi mekanisme pembacaannya sama: string tag diurai dengan reflect.StructTag.Get sehingga setiap library cukup memilih kunci sendiri.

Membaca Tag dengan reflect #

Bagian akhir program memakai reflect.TypeOf(p) untuk menelusuri field struct dan mencetak tag masing-masing. f.Tag.Get("json") mengembalikan nilai setelah kunci json, atau string kosong bila kunci tidak ada. Kebutuhan praktisnya bukan sekadar mencetak: validator, ORM ringan, dan generator kode memakai mekanisme ini untuk menemukan field dan aturannya tanpa menulis kode per struct.

Referensi #

Tag mengatur bentuk data yang keluar masuk, tetapi tidak memeriksa isinya. Apakah field wajib terisi, rentang umur masuk akal, dan format email benar adalah pekerjaan lapisan validasi — materi chapter Validasi Data berikutnya.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

tag-demo.go

Unduh berkas
go
package main

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

type Produk struct {
	ID       string   `json:"id" xml:"id,attr"`
	Nama     string   `json:"nama,omitempty" xml:"nama"`
	Harga    int      `json:"harga,omitempty" xml:"-"`
	Aktif    bool     `json:"aktif,omitempty" xml:"aktif"`
	Tag      []string `json:"tag,omitempty" xml:"tag"`
	Stok     *int     `json:"stok,omitempty" xml:"-"`
	Rahasia  string   `json:"-" xml:"-"`
	Kategori string   `json:"kategori,omitempty" xml:"kategori"`
}

func main() {
	// Semua field opsional bernilai zero: omitempty memangkasnya.
	p := Produk{ID: "P-01", Nama: "Buku Go", Harga: 95000, Aktif: true}
	stok := 0 // pointer ke nol tetap ikut output
	p.Stok = &stok
	data, err := json.Marshal(p)
	if err != nil {
		fmt.Println("marshal gagal:", err)
		return
	}
	fmt.Println(string(data))

	// Hanya ID terisi: field lain bernilai zero dan hilang.
	data2, _ := json.Marshal(Produk{ID: "P-02"})
	fmt.Println(string(data2))

	// Decode: pencocokan nama field tidak peka huruf besar-kecil,
	// field yang tidak dikenal (stok, berat, diskon) diabaikan.
	sumber := []byte(`{"ID":"P-03","NAMA":"Kopi","harga":15000,"stok":7,"berat":250,"diskon":true}`)
	var p3 Produk
	if err := json.Unmarshal(sumber, &p3); err != nil {
		fmt.Println("unmarshal gagal:", err)
		return
	}
	fmt.Printf("hasil decode: ID=%s Nama=%s Harga=%d", p3.ID, p3.Nama, p3.Harga)
	if p3.Stok != nil {
		fmt.Printf(" stok=%d", *p3.Stok)
	}
	fmt.Println()

	// reflect membaca struct tag saat runtime.
	t := reflect.TypeOf(p)
	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i)
		fmt.Printf("field %-8s json=%-18q xml=%q\n", f.Name, f.Tag.Get("json"), f.Tag.Get("xml"))
	}
}