BAB 17: Struct — Membuat Tipe Data Sendiri
Pelajari cara mendefinisikan struct di Go, mengakses dan memodifikasi field, menggunakan pointer receiver, struct tag untuk JSON, dan komposisi dengan embedded struct.
Di bab sebelumnya kamu belajar bahwa pointer memungkinkan function memodifikasi data aslinya tanpa membuat salinan yang mahal. Di bagian akhir bab itu ada petunjuk: pointer sangat berguna saat bekerja dengan struct. Sekarang saatnya memahami apa itu struct — dan kenapa hampir semua program Go nyata bergantung padanya.
Sejauh ini, data yang kamu kelola adalah primitif tunggal: satu angka, satu string, satu boolean. Tapi data di dunia nyata jarang sesederhana itu. Seorang pengguna punya nama, email, dan usia sekaligus. Sebuah produk punya nama, harga, dan stok. Bagaimana menyimpan data yang saling berkaitan ini?
Apa Itu Struct?
Struct adalah tipe data komposit yang mengelompokkan beberapa field dengan nama dan tipe masing-masing. Kalau variabel biasa seperti kotak berisi satu item, struct adalah laci dengan beberapa sekat — setiap sekat punya label dan menyimpan satu jenis data.
Ini berbeda dari slice atau map yang menyimpan banyak nilai bertipe sama. Struct menyimpan nilai-nilai bertipe berbeda yang secara logika membentuk satu entitas.
Mendefinisikan dan Menggunakan Struct
Struct didefinisikan dengan kata kunci type dan struct. Setelah didefinisikan, kamu bisa membuat instance-nya seperti membuat variabel biasa.
// main.go
package main
import "fmt"
type Pengguna struct {
Nama string
Email string
Usia int
}
func main() {
p := Pengguna{
Nama: "Rina",
Email: "rina@contoh.com",
Usia: 28,
}
fmt.Println("Nama:", p.Nama)
fmt.Println("Email:", p.Email)
fmt.Println("Usia:", p.Usia)
}
Nama: Rina
Email: rina@contoh.com
Usia: 28
Field diakses dengan notasi titik: p.Nama, p.Email, p.Usia. Nama field dalam struct dimulai dengan huruf kapital — ini bukan kebetulan. Di Go, huruf kapital berarti exported, artinya field bisa diakses dari package lain. Field dengan huruf kecil bersifat private untuk package itu sendiri.
Struct Literal dan Zero Value
Ada dua cara membuat instance struct. Cara pertama adalah struct literal dengan named fields seperti di atas — ini yang paling sering dipakai karena eksplisit. Cara kedua menggunakan urutan posisi:
// main.go
package main
import "fmt"
type Pengguna struct {
Nama string
Email string
Usia int
}
func main() {
// cara 1: named fields (direkomendasikan)
p1 := Pengguna{Nama: "Rina", Email: "rina@contoh.com", Usia: 28}
// cara 2: urutan posisi — berbahaya jika field berubah
p2 := Pengguna{"Budi", "budi@contoh.com", 32}
// cara 3: tanpa inisialisasi — semua field mendapat zero value
var p3 Pengguna
fmt.Println(p1.Nama) // Rina
fmt.Println(p2.Nama) // Budi
fmt.Println(p3.Nama) // "" (string kosong)
fmt.Println(p3.Usia) // 0
}
Hindari struct literal tanpa named fields (cara 2) kecuali untuk struct yang sangat sederhana dan tidak akan berubah. Jika suatu saat kamu menambah atau mengubah urutan field, semua literal posisional akan salah — dan kompiler tidak akan memperingatkan kamu.
Memodifikasi Field Struct
Field struct bisa dimodifikasi langsung lewat variabelnya:
// main.go
package main
import "fmt"
type Pengguna struct {
Nama string
Email string
Usia int
}
func main() {
p := Pengguna{Nama: "Rina", Email: "rina@contoh.com", Usia: 28}
p.Usia = 29
p.Email = "rina.baru@contoh.com"
fmt.Printf("%s kini berusia %d tahun\n", p.Nama, p.Usia)
fmt.Println("Email baru:", p.Email)
}
Rina kini berusia 29 tahun
Email baru: rina.baru@contoh.com
Pointer ke Struct
Ingat masalah dari bab pointer: ketika struct dioper ke function, Go menyalin seluruh struct. Untuk struct kecil ini tidak masalah, tapi untuk struct besar dengan banyak field, ini boros. Solusinya adalah pointer ke struct.
// main.go
package main
import "fmt"
type Pengguna struct {
Nama string
Email string
Usia int
}
func ulangTahun(p *Pengguna) {
p.Usia++
}
func main() {
p := Pengguna{Nama: "Rina", Email: "rina@contoh.com", Usia: 28}
ulangTahun(&p)
fmt.Println("Usia setelah ulang tahun:", p.Usia) // 29
}
Usia setelah ulang tahun: 29
Di dalam function ulangTahun, akses field dilakukan dengan p.Usia — bukan (*p).Usia. Go secara otomatis melakukan dereferencing saat mengakses field lewat pointer struct. Ini membuat kode lebih bersih.
p.Usia++ saat p adalah pointer ke struct adalah shorthand dari (*p).Usia++. Go mengizinkan notasi titik langsung pada pointer struct, jadi kamu jarang perlu menulis bentuk panjangnya.
Method pada Struct
Struct di Go bisa punya method — function yang terikat pada tipe tertentu. Ini memungkinkan struct memiliki perilaku, bukan hanya data.
// main.go
package main
import "fmt"
type Pengguna struct {
Nama string
Email string
Usia int
}
// method dengan value receiver — tidak mengubah p asli
func (p Pengguna) Sapa() string {
return fmt.Sprintf("Halo, saya %s berusia %d tahun.", p.Nama, p.Usia)
}
// method dengan pointer receiver — bisa mengubah p asli
func (p *Pengguna) UlangTahun() {
p.Usia++
}
func main() {
p := Pengguna{Nama: "Rina", Email: "rina@contoh.com", Usia: 28}
fmt.Println(p.Sapa())
p.UlangTahun()
fmt.Println(p.Sapa())
}
Halo, saya Rina berusia 28 tahun.
Halo, saya Rina berusia 29 tahun.
Value Receiver vs Pointer Receiver
Ada dua jenis receiver, dan perbedaannya penting:
| Jenis Receiver | Sintaks | Bisa Modifikasi? | Kapan Dipakai |
|---|---|---|---|
| Value receiver | (p Pengguna) | Tidak | Method hanya membaca data |
| Pointer receiver | (p *Pengguna) | Ya | Method perlu mengubah field |
Secara umum: jika satu method pada tipe tertentu menggunakan pointer receiver, konsistenkan semua method-nya dengan pointer receiver. Ini menghindari kebingungan saat memanggil method.
Struct Tag untuk JSON
Salah satu fitur paling praktis dari struct adalah tag — metadata string yang ditempel di belakang deklarasi field. Tag paling sering digunakan bersama package encoding/json untuk mengontrol bagaimana struct dikonversi ke JSON dan sebaliknya.
// main.go
package main
import (
"encoding/json"
"fmt"
)
type Pengguna struct {
Nama string `json:"nama"`
Email string `json:"email"`
Usia int `json:"usia,omitempty"`
Password string `json:"-"`
}
func main() {
p := Pengguna{
Nama: "Rina",
Email: "rina@contoh.com",
Usia: 0,
Password: "rahasia123",
}
data, err := json.Marshal(p)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(string(data))
}
{"nama":"Rina","email":"rina@contoh.com"}
Beberapa hal yang terjadi di output di atas:
Namamuncul sebagai"nama"(huruf kecil) karena tagjson:"nama"menentukan nama key di JSON.Usiatidak muncul karena nilainya0dan tagomitemptymemerintahkan JSON encoder untuk melewati field dengan zero value.Passwordsama sekali tidak muncul karena tagjson:"-"menyembunyikan field ini dari output JSON — berguna untuk data sensitif.
Komposisi dengan Embedded Struct
Go tidak punya inheritance seperti bahasa OOP, tapi punya cara yang lebih fleksibel: embedded struct atau komposisi. Kamu bisa menyematkan satu struct ke dalam struct lain, dan semua field serta method struct yang disematkan otomatis terangkat ke struct luarnya.
// main.go
package main
import "fmt"
type Alamat struct {
Jalan string
Kota string
Kode string
}
func (a Alamat) Format() string {
return fmt.Sprintf("%s, %s %s", a.Jalan, a.Kota, a.Kode)
}
type Pengguna struct {
Nama string
Email string
Alamat // embedded struct — tanpa nama field eksplisit
}
func main() {
p := Pengguna{
Nama: "Rina",
Email: "rina@contoh.com",
Alamat: Alamat{
Jalan: "Jl. Mawar No. 10",
Kota: "Bandung",
Kode: "40112",
},
}
// akses field Alamat langsung dari Pengguna
fmt.Println(p.Kota)
// method Alamat juga terangkat ke Pengguna
fmt.Println(p.Format())
}
Bandung
Jl. Mawar No. 10, Bandung 40112
p.Kota bekerja karena Kota adalah field dari Alamat yang sudah di-embed. Kamu tidak perlu menulis p.Alamat.Kota, meskipun itu juga valid. Begitu pula p.Format() — method dari Alamat terangkat menjadi method Pengguna.
Latihan
Latihan 1 — Buku:
Definisikan struct Buku dengan field Judul string, Penulis string, Halaman int, dan Harga float64. Buat method Ringkasan() string yang mengembalikan informasi singkat buku dalam satu kalimat. Inisialisasi minimal tiga buku dan cetak ringkasannya.
Latihan 2 — Diskon:
Tambahkan method TerapkanDiskon(persen float64) pada struct Buku dari latihan pertama. Method ini harus menggunakan pointer receiver dan mengubah Harga langsung. Uji dengan beberapa skenario diskon berbeda.
Latihan 3 — JSON:
Ubah struct Buku agar field-nya bisa di-marshal ke JSON dengan nama key huruf kecil (judul, penulis, dll). Tambahkan field StokRahasia int yang tidak boleh muncul di output JSON. Uji dengan json.Marshal dan cetak hasilnya.
Struct adalah fondasi dari hampir semua kode Go yang lebih kompleks. Interface — topik yang akan kita jelajahi berikutnya — sangat bergantung pada struct dan method yang baru saja kamu pelajari. Dengan interface, kamu bisa menulis kode yang bekerja dengan berbagai tipe struct sekaligus, tanpa perlu tahu persis struct mana yang digunakan.