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 ReceiverSintaksBisa Modifikasi?Kapan Dipakai
Value receiver(p Pengguna)TidakMethod hanya membaca data
Pointer receiver(p *Pengguna)YaMethod 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:

  • Nama muncul sebagai "nama" (huruf kecil) karena tag json:"nama" menentukan nama key di JSON.
  • Usia tidak muncul karena nilainya 0 dan tag omitempty memerintahkan JSON encoder untuk melewati field dengan zero value.
  • Password sama sekali tidak muncul karena tag json:"-" 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.

Referensi

  1. 1Structs — A Tour of Go
  2. 2Struct types — The Go Programming Language Specification
  3. 3JSON and Go — The Go Blog