Nested Struct vs Embedded Struct di Go: Marshaling dan Unmarshaling - Part 2

Annisa NadiaNov 19, 2024 · 10 min read · ___ views
Nested Struct vs Embedded Struct di Go: Marshaling dan Unmarshaling - Part 2

Pada artikel sebelumnya, kita telah membahas perbedaan utama antara nested struct dengan embedded struct. Sekarang kita akan coba lihat perbedaan hasil dari proses marshal dan unmarshal struct yang di dalamnya memiliki nested struct dan embedded struct. Kita juga akan melihat beberapa keunikan yang mungkin terjadi jika struct tersebut di-marshal atau di-unmarshal.

Marshaling

Marshaling adalah proses mengubah struktur data Go menjadi serangkaian byte, seperti JSON. Kita bisa mengubah tipe struct menjadi JSON. Berikut ini perbedaan hasil dari proses marshal pada struct yang memiliki nested struct dan struct yang memiliki embedded struct.

package main

import (
	"encoding/json"
	"fmt"
)

type Address struct {
	Street  string
	City    string
	Country string
}

type Person struct {
	Name    string
	Age     int
	Address Address // Nested struct
}

func main() {
	person := Person{
		Name:    "John",
		Age:     46,
		Address: Address{
			Street:  "Durian Street",
			City:    "Bandung",
			Country: "Indonesia",
		},
	}

	jsonBytes, err := json.Marshal(person)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(jsonBytes)) // Output: {"Name":"John","Age":46,"Address":{"Street":"Durian Street","City":"Bandung","Country":"Indonesia"}}
}

Pada contoh di atas, kita bisa lihat ada tipe Address dan tipe Person. Tipe Address dijadikan sebagai nested struct dalam tipe Person. Jika objek dengan tipe Person di-marshal, maka hasilnya jadi seperti di bawah ini.

{
  "Name": "John",
  "Age": 46,
  "Address": {
    "Street": "Durian Street",
    "City": "Bandung",
    "Country": "Indonesia"
  }
}

Sekarang kita akan coba marshal struct yang memiliki embedded struct di dalamnya.

package main

import (
	"encoding/json"
	"fmt"
)

type Address struct {
	Street  string
	City    string
	Country string
}

type Person struct {
	Name    string
	Age     int
	Address // Embedded struct
}

func main() {
	person := Person{
		Name:    "John",
		Age:     46,
		Address: Address{
			Street:  "Durian Street",
			City:    "Bandung",
			Country: "Indonesia",
		},
	}

	jsonBytes, err := json.Marshal(person)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(jsonBytes)) // Output: {"Name":"John","Age":46,"Street":"Durian Street","City":"Bandung","Country":"Indonesia"}
}

Pada contoh di atas, kita bisa lihat ada tipe Address dan tipe Person, sama seperti sebelumnya. Tapi kali ini tipe Address dijadikan sebagai embedded struct dalam tipe Person. Jika objek dengan tipe Person di-marshal, maka hasilnya jadi seperti di bawah ini.

{
  "Name": "John",
  "Age": 46,
  "Street": "Durian Street",
  "City": "Bandung",
  "Country": "Indonesia"
}

Perhatikan bahwa jika tipe Address di-embed, maka semua field dari tipe Address berada langsung di dalam objek bertipe Person.

Gimana Kalau Ada Nama Field yang Sama di Tingkatan yang Beda?

Hasil marshal suatu struct bisa jadi tidak sesuai dengan apa yang kita kira kalau di dalam struct tersebut terdapat field dengan nama yang sama. Pada contoh di bawah, ada tipe Address dan tipe Person. Tipe Address di-embed ke dalam tipe Person. Perhatikan bahwa tipe Address dan tipe Person sama-sama memiliki field Country.

package main

import (
	"encoding/json"
	"fmt"
)

type Address struct {
	Street  string
	City    string
	Country string
}

type Person struct {
	Name    string
	Age     int
	Country string
	Address // Embedded struct
}

func main() {
	person := Person{
		Name:    "John",
		Age:     46,
		Country: "Australia",
		Address: Address{
			Street:  "Durian Street",
			City:    "Bandung",
			Country: "Indonesia",
		},
	}

	jsonBytes, err := json.Marshal(person)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(jsonBytes)) // Output: {"Name":"John","Age":46,"Country":"Australia","Street":"Durian Street","City":"Bandung"}
}

Jika objek bertipe Person di-marshal, maka bisa dilihat bahwa value Country diambil dari field Country yang ada di tingkatan/ lapisan lebih tinggi. Field Country terlihat memiliki value "Australia", bukan "Indonesia". Seperti yang sudah kita ketahui pada artikel sebelumnya, hal itu terjadi karena menambahkan field Country di tingkatan/ lapisan yang lebih tinggi, akan menyembunyikan field Country yang ada di tingkatan/ lapisan struct yang lebih dalam.

{
  "Name": "John",
  "Age": 46,
  "Country": "Australia",
  "Street": "Durian Street",
  "City": "Bandung"
}

Gimana Kalau Ada Nama Field yang Sama di Tingkatan yang Sama?

Seperti pada contoh sebelumnya, kali ini kita juga akan melakukan marshal suatu struct yang di dalamnya terdapat embedded struct. Tapi kali ini nama field yang sama berada pada tingkatan yang sama.

Pada contoh di bawah ada tipe Address, tipe Birthplace, dan tipe Person. Tipe Address dan tipe Birthplace di-embed ke dalam tipe Person. Perhatikan bahwa tipe Address dan tipe Birthplace sama-sama memiliki field City. Lalu jika objek bertipe Person di-marshal, field City mana yang akan diambil?

package main

import (
	"encoding/json"
	"fmt"
)

type Address struct {
	Street string
	City   string
}

type Birthplace struct {
	City    string
	Country string
}

type Person struct {
	Name       string
	Age        int
	Address    // Embedded struct
	Birthplace // Embedded struct
}

func main() {
	person := Person{
		Name: "John",
		Age:  46,
		Address: Address{
			Street: "Durian Street",
			City:   "Bandung",
		},
		Birthplace: Birthplace{
			City:    "Jakarta",
			Country: "Indonesia",
		},
	}

	jsonBytes, err := json.Marshal(person)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(jsonBytes)) // Output: {"Name":"John","Age":46,"Street":"Durian Street","Country":"Indonesia"}
}

Jika objek bertipe Person di-marshal, kita bisa lihat field City tidak muncul. Berdasarkan yang tertulis pada dokumentasi, kalau ada field yang namanya sama di tingkatan/ lapisan yang sama, maka field tersebut diabaikan dan tidak memunculkan error.

{
  "Name": "John",
  "Age": 46,
  "Street": "Durian Street",
  "Country": "Indonesia"
}

Unmarshaling

Unmarshaling adalah proses mengubah serangkaian byte (contoh format: JSON), menjadi struktur data Go. Dua contoh di bawah menunjukkan cara unmarshal struct yang di dalamnya ada nested struct dan struct yang di dalamnya ada embedded struct.

package main

import (
	"encoding/json"
	"fmt"
)

type Address struct {
	Street  string
	City    string
	Country string
}

type Person struct {
	Name    string
	Age     int
	Address Address // Nested struct
}

func main() {
	jsonBytes := []byte(`{"Name":"John","Age":46,"Address":{"Street":"Durian Street","City":"Bandung","Country":"Indonesia"}}`)
	var person Person

	err := json.Unmarshal(jsonBytes, &person)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("%+v\n", person) // Output: {Name:John Age:46 Address:{Street:Durian Street City:Bandung Country:Indonesia}}
}

Untuk bisa memetakan JSON ke struct yang di dalamnya ada nested struct, maka JSON harus memiliki field yang sesuai dengan field dari struct tempat menyimpan hasil unmarshal. String JSON pada contoh di atas memiliki field Name, Age, dan Address. Lalu value dari field Address adalah objek yang memiliki field Street, City, dan Country. Itu sesuai dengan tempat untuk menyimpan hasil unmarshal, yaitu di variabel person yang bertipe Person.

Sekarang, perhatikan contoh selanjutnya.

package main

import (
	"encoding/json"
	"fmt"
)

type Address struct {
	Street  string
	City    string
	Country string
}

type Person struct {
	Name    string
	Age     int
	Address // Embedded struct
}

func main() {
	jsonBytes := []byte(`{"Name":"John","Age":46,"Street":"Durian Street","City":"Bandung","Country":"Indonesia"}`)
	var person Person

	err := json.Unmarshal(jsonBytes, &person)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("%+v\n", person) // Output: {Name:John Age:46 Address:{Street:Durian Street City:Bandung Country:Indonesia}}
}

Berbeda dari sebelumnya, string JSON kali ini tidak memiliki field Address. Field Street, City, dan Country bukan berada pada objek yang menjadi value dari field Address.

Hasil unmarshal string JSON tersebut dapat disimpan pada variabel person bertipe Person. Tipe Person adalah struct yang memiliki Address sebagai embedded struct di dalamnya. Karena tipe Address di-embed langsung ke dalam tipe Person, maka semua field dalam tipe Address diberikan kepada tipe Person. Ini membuat tipe Person dianggap memiliki field Street, City, dan Country langsung di dalamnya.

Gimana Kalau Ada Nama Field yang Sama di Tingkatan yang Beda?

Pada contoh di bawah, ada tipe Address dan tipe Person. Tipe Address di-embed ke dalam tipe Person. Perhatikan bahwa tipe Address dan tipe Person sama-sama memiliki field Country. Kita akan melakukan unmarshal terhadap JSON yang memiliki field Name, Age, Street, City, dan Country.

package main

import (
	"encoding/json"
	"fmt"
)

type Address struct {
	Street  string
	City    string
	Country string
}

type Person struct {
	Name    string
	Age     int
	Country string
	Address // Embedded struct
}

func main() {
	jsonBytes := []byte(`{"Name":"John","Age":46,"Country":"Australia","Street":"Durian Street","City":"Bandung"}`)
	var person Person

	err := json.Unmarshal(jsonBytes, &person)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("%+v\n", person) // Output: {Name:John Age:46 Country:Australia Address:{Street:Durian Street City:Bandung Country:}}
}

Perhatikan hasil print dari variabel person. Field Country dari struct terluar terisi dengan value "Australia", tapi field Country dari struct yang di-embed value-nya kosong.

Sama seperti pada contoh marshaling, hal itu terjadi karena field Country di tingkatan/ lapisan yang lebih tinggi akan menyembunyikan field Country yang ada di tingkatan/ lapisan struct yang lebih dalam.

Gimana Kalau Ada Nama Field yang Sama di Tingkatan yang Sama?

Kita akan melakukan unmarshal suatu struct yang di dalamnya terdapat embedded struct. Tapi kali ini nama field yang sama berada pada tingkatan yang sama.

Pada contoh di bawah ada tipe Address, tipe Birthplace, dan tipe Person. Tipe Address dan tipe Birthplace di-embed ke dalam tipe Person. Perhatikan bahwa tipe Address dan tipe Birthplace sama-sama memiliki field City.

Kita akan melakukan unmarshal terhadap JSON yang memiliki field Name, Age, Street, City, dan Country. Kira-kira value field City di JSON akan disimpan di field City tipe Address atau tipe Birthplace, ya?

package main

import (
	"encoding/json"
	"fmt"
)

type Address struct {
	Street string
	City   string
}

type Birthplace struct {
	City    string
	Country string
}

type Person struct {
	Name       string
	Age        int
	Address    // Embedded struct
	Birthplace // Embedded struct
}

func main() {
	jsonBytes := []byte(`{"Name":"John","Age":46,"Street":"Durian Street","City":"Bandung","Country":"Indonesia"}`)
	var person Person

	err := json.Unmarshal(jsonBytes, &person)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("%+v\n", person) // Output: {Name:John Age:46 Address:{Street:Durian Street City:} Birthplace:{City: Country:Indonesia}}
}

Jika kita lihat hasil print variabel person, ternyata field City dari tipe Address dan field City dari tipe Birthplace sama-sama kosong, tidak ada yang terisi value "Bandung".

Ini sama seperti pada contoh marshaling yang sebelumnya. Berdasarkan yang tertulis pada dokumentasi, kalau ada field yang namanya sama di tingkatan/ lapisan yang sama, maka field tersebut diabaikan dan tidak memunculkan error.

Kita bisa mengubah kodenya menjadi seperti ini agar value "Bandung" dapat tersimpan pada field City dari embedded struct.

package main

import (
	"encoding/json"
	"fmt"
)

type Address struct {
	Street string
	City   string
}

type Birthplace struct {
	City    string
	Country string
}

type PersonBase struct {
	Name string
	Age  int
}

type Person struct {
	PersonBase // Embedded struct
	Address    // Embedded struct
	Birthplace // Embedded struct
}

func (p *Person) UnmarshalJSON(jsonBytes []byte) error {
	var personBase PersonBase
	err := json.Unmarshal(jsonBytes, &personBase)
	if err != nil {
		return err
	}

	p.PersonBase = personBase

	var address Address
	err = json.Unmarshal(jsonBytes, &address)
	if err != nil {
		return err
	}

	p.Address = address

	var birthplace Birthplace
	err = json.Unmarshal(jsonBytes, &birthplace)
	if err != nil {
		return err
	}

	p.Birthplace = birthplace

	return nil
}

func main() {
	jsonBytes := []byte(`{"Name":"John","Age":46,"Street":"Durian Street","City":"Bandung","Country":"Indonesia"}`)
	var person Person

	err := json.Unmarshal(jsonBytes, &person)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("%+v\n", person) // Output: {PersonBase:{Name:John Age:46} Address:{Street:Durian Street City:Bandung} Birthplace:{City:Bandung Country:Indonesia}}
}

Pada kode di atas, kita bisa lihat field Name dan field Age dipindahkan ke tipe PersonBase. Lalu tipe PersonBase di-embed ke dalam tipe Person. Sekarang tipe Person memiliki 3 embedded struct, yaitu PersonBase, Address, dan Birthplace.

Selanjutnya, kita buat tipe Person mengimplementasikan interface Unmarshaler. Suatu tipe dianggap mengimplementasikan interface Unmarshaler kalau tipe itu memiliki method UnmarshalJSON().

Karena tipe Person sekarang memiliki method UnmarshalJSON(), method tersebut akan dieksekusi ketika melakukan unmarshal suatu JSON. Cara yang dilakukan untuk dapat mengambil value field City pada JSON adalah dengan melakukan unmarshal satu per satu untuk setiap embedded struct di dalam tipe Person. Pada hasil print variabel person terlihat field City pada tipe Address dan field City pada tipe Birthplace memiliki value "Bandung".

Last updated: Nov 23, 2024Edit on GitHub

Get in Touch

© 2021 - 2024 Annisa Nadia Neyla