unisbadri.com » Python Java Golang Typescript Kotlin Ruby Rust Dart PHP

Struct #

Go tidak punya class. Bukan karena lupa, tapi karena keputusan desain yang sangat disengaja. Class dalam OOP membawa serta inheritance — dan inheritance membawa kompleksitas yang sering melebihi manfaatnya: fragile base class problem, diamond inheritance, coupling yang erat antara parent dan child. Go memilih jalan yang berbeda: komposisi. Alih-alih mewarisi perilaku dari class lain, kamu membangun tipe baru dengan menggabungkan tipe-tipe yang sudah ada. struct adalah fondasi dari semua ini — dan ketika dikombinasikan dengan method dan interface, ia mampu mengekspresikan semua pola OOP yang dibutuhkan tanpa kompleksitas yang menyertainya.

Mendefinisikan Struct #

Struct adalah kumpulan field yang dikelompokkan menjadi satu tipe. Definisikan dengan keyword type dan struct:

type Person struct {
    // Exported fields — huruf besar, bisa diakses dari package lain
    Name    string
    Age     int
    Email   string

    // Unexported fields — huruf kecil, hanya dalam package ini
    password string
    loginAt  time.Time
}

// Struct bisa bersarang
type Address struct {
    Street  string
    City    string
    Province string
    ZipCode string
}

type Employee struct {
    Name       string
    Department string
    Salary     float64
    Address    Address  // struct sebagai field (bukan embedded — punya nama)
    JoinDate   time.Time
}

Pemisahan exported dan unexported field bukan sekadar access control — ini adalah cara mendefinisikan API publik struct. Exported field adalah bagian yang kamu janjikan kepada pengguna package. Unexported field adalah implementation detail yang bebas kamu ubah kapan saja.


Cara Inisialisasi Struct #

Ada beberapa cara membuat instance struct, masing-masing dengan kelebihan tersendiri.

Named Fields — Cara yang Direkomendasikan #

p := Person{
    Name:  "Budi Santoso",
    Age:   28,
    Email: "[email protected]",
}

Gunakan named fields hampir selalu. Alasannya: jika struct ditambah field baru di kemudian hari, kode ini tetap valid dan compiler tidak akan error.

Positional — Hindari untuk Struct dengan Lebih dari 2 Field #

// ANTI-PATTERN: rapuh terhadap perubahan struct
p := Person{"Budi", 28, "[email protected]", "", time.Time{}}

// Jika urutan field diubah atau field baru ditambah di tengah,
// semua inisialisasi positional akan memberikan nilai yang salah
// tanpa ada compile error!

Zero Value — Semua Field Default #

var p Person
// p.Name    = ""
// p.Age     = 0
// p.Email   = ""
// p.password = ""
// p.loginAt  = time.Time{} (zero time)

Zero value sangat berguna ketika struct dirancang dengan baik — zero value-nya sudah merupakan state yang valid.

Address Literal — Langsung Pointer #

// Menghasilkan *Person, bukan Person
p := &Person{
    Name:  "Budi",
    Age:   28,
    Email: "[email protected]",
}
// p adalah *Person
fmt.Println(p.Name)  // Go auto-dereference: tidak perlu (*p).Name

new() — Pointer ke Zero Value #

p := new(Person)   // setara dengan &Person{}
p.Name = "Budi"    // isi field satu per satu
p.Age  = 28

Dalam praktiknya, &Person{...} lebih umum dari new(Person) karena bisa langsung diisi nilainya.


Struct adalah Value Type #

Ini perbedaan penting dari bahasa OOP lain: struct di Go adalah value type. Ketika kamu assign struct ke variabel lain atau pass ke fungsi, Go membuat salinan lengkap dari semua field-nya:

type Point struct {
    X, Y int
}

p1 := Point{X: 1, Y: 2}
p2 := p1         // salinan lengkap — p2 adalah COPY dari p1
p2.X = 99

fmt.Println(p1)  // {1 2} — tidak berubah!
fmt.Println(p2)  // {99 2}

Implikasinya untuk fungsi:

// ANTI-PATTERN: memodifikasi struct dalam fungsi tidak berpengaruh ke aslinya
func setName(p Person, name string) {
    p.Name = name  // hanya memodifikasi salinan lokal
}

func main() {
    p := Person{Name: "Budi"}
    setName(p, "Sari")
    fmt.Println(p.Name)  // "Budi" — tidak berubah!
}

// Solusi 1: return struct baru (idiomatic untuk perubahan kecil)
func withName(p Person, name string) Person {
    p.Name = name  // modifikasi salinan
    return p       // kembalikan salinan yang sudah dimodifikasi
}

// Solusi 2: terima pointer (idiomatic untuk struct besar atau banyak modifikasi)
func setNamePtr(p *Person, name string) {
    p.Name = name  // modifikasi struct asli
}

Method — Value Receiver vs Pointer Receiver #

Method di Go dikaitkan dengan tipe melalui receiver — argumen ekstra sebelum nama method.

Value Receiver — Bekerja pada Salinan #

type Rectangle struct {
    Width, Height float64
}

// Value receiver (r Rectangle) — r adalah salinan
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

func (r Rectangle) Scale(factor float64) Rectangle {
    // Kembalikan struct baru — tidak memodifikasi aslinya
    return Rectangle{
        Width:  r.Width * factor,
        Height: r.Height * factor,
    }
}

Pointer Receiver — Bekerja pada Aslinya #

// Pointer receiver (*Rectangle) — memodifikasi struct asli
func (r *Rectangle) ScaleInPlace(factor float64) {
    r.Width  *= factor
    r.Height *= factor
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}

    // Value receiver — tidak mengubah rect
    bigger := rect.Scale(2)
    fmt.Println(rect)   // {10 5} — tidak berubah
    fmt.Println(bigger) // {20 10}

    // Pointer receiver — mengubah rect
    rect.ScaleInPlace(2)
    fmt.Println(rect)   // {20 10} — berubah!
}

Panduan Memilih Receiver #

GUNAKAN VALUE RECEIVER jika:
  ✓ Method hanya membaca data (getter, kalkulasi)
  ✓ Struct kecil dan murah di-copy (Point, Color, Size)
  ✓ Tipe yang dirancang immutable (seperti time.Time)
  ✓ Method mengembalikan nilai baru bukan memodifikasi

GUNAKAN POINTER RECEIVER jika:
  ✓ Method memodifikasi struct
  ✓ Struct besar (banyak field, mahal di-copy)
  ✓ Struct mengandung sync.Mutex atau field yang tidak boleh di-copy
  ✓ Konsistensi — jika ada satu method pakai pointer, semua pakai pointer

ATURAN KONSISTENSI (paling penting):
  Jika ada satu method menggunakan pointer receiver, SEMUA method
  pada tipe itu sebaiknya menggunakan pointer receiver.
  Jangan campur-aduk kecuali ada alasan yang sangat kuat.

Jangan campur value receiver dan pointer receiver dalam satu tipe. Ini menyebabkan kebingungan tentang method mana yang “aman” untuk dipanggil pada value vs pointer, dan bisa menyebabkan bug halus terkait interface satisfaction.

// ANTI-PATTERN: campur receiver
type Counter struct{ count int }
func (c Counter)  Value() int   { return c.count }    // value receiver
func (c *Counter) Increment()   { c.count++ }         // pointer receiver
func (c *Counter) Reset()       { c.count = 0 }       // pointer receiver

// BENAR: konsisten dengan pointer receiver
func (c *Counter) Value() int   { return c.count }
func (c *Counter) Increment()   { c.count++ }
func (c *Counter) Reset()       { c.count = 0 }

Embedding — Komposisi, Bukan Inheritance #

Embedding memungkinkan satu struct “menyertakan” struct lain — semua field dan method dari struct yang di-embed bisa diakses langsung, seolah-olah milik struct pembungkus. Ini adalah cara Go mengekspresikan “is-a” relationship tanpa inheritance:

type Animal struct {
    Name string
    Age  int
}

func (a *Animal) Breathe() {
    fmt.Printf("%s sedang bernapas\n", a.Name)
}

func (a *Animal) Describe() string {
    return fmt.Sprintf("%s (umur %d tahun)", a.Name, a.Age)
}

// Dog "meng-embed" Animal — bukan "extends" Animal
type Dog struct {
    Animal          // embedded tanpa nama field
    Breed  string
    Trained bool
}

func (d *Dog) Bark() {
    fmt.Printf("%s menggonggong!\n", d.Name)  // akses d.Animal.Name langsung
}

func main() {
    d := Dog{
        Animal:  Animal{Name: "Buddy", Age: 3},
        Breed:   "Labrador",
        Trained: true,
    }

    // Akses field Animal langsung (promoted field)
    fmt.Println(d.Name)     // "Buddy" — setara d.Animal.Name
    fmt.Println(d.Age)      // 3

    // Akses method Animal langsung (promoted method)
    d.Breathe()             // "Buddy sedang bernapas"
    fmt.Println(d.Describe()) // "Buddy (umur 3 tahun)"

    // Method Dog sendiri
    d.Bark()                // "Buddy menggonggong!"

    // Akses eksplisit jika perlu
    fmt.Println(d.Animal.Name)  // sama dengan d.Name
}

Override Method dari Embedded Struct #

Struct yang me-embed bisa mendefinisikan method dengan nama yang sama untuk “menimpa” method dari embedded struct:

type Base struct {
    ID int
}

func (b Base) Describe() string {
    return fmt.Sprintf("Base ID: %d", b.ID)
}

type Extended struct {
    Base
    Name string
}

// Override Describe — Extended punya implementasi sendiri
func (e Extended) Describe() string {
    return fmt.Sprintf("%s (ID: %d)", e.Name, e.ID)
}

func main() {
    e := Extended{Base: Base{ID: 42}, Name: "Server A"}
    fmt.Println(e.Describe())       // "Server A (ID: 42)" — versi Extended
    fmt.Println(e.Base.Describe())  // "Base ID: 42" — akses eksplisit versi Base
}

Embedding Multiple Struct #

type Logger struct{}
func (l Logger) Log(msg string) { fmt.Println("[LOG]", msg) }

type Metrics struct{}
func (m Metrics) Record(key string, val float64) {
    fmt.Printf("[METRIC] %s = %.2f\n", key, val)
}

// Service memiliki kemampuan logging dan metrics
type Service struct {
    Logger
    Metrics
    Name string
}

func (s *Service) Process(data string) {
    s.Log("memproses: " + data)
    // lakukan sesuatu...
    s.Record("processing_time", 0.025)
}

Struct Tags #

Struct tags adalah metadata yang ditambahkan ke field — string literal yang muncul setelah tipe field. Paling umum digunakan untuk JSON serialization, database mapping, dan validasi:

import (
    "encoding/json"
    "time"
)

type User struct {
    ID        int       `json:"id"                    db:"id"`
    Username  string    `json:"username"               db:"username"`
    Email     string    `json:"email"                  db:"email"`
    Password  string    `json:"-"                      db:"password_hash"`
    // json:"-" → abaikan field ini saat marshal/unmarshal JSON
    CreatedAt time.Time `json:"created_at"             db:"created_at"`
    UpdatedAt time.Time `json:"updated_at,omitempty"   db:"updated_at"`
    // omitempty → abaikan jika nilai zero value
    IsAdmin   bool      `json:"is_admin"               db:"is_admin"`
    Score     float64   `json:"score,omitempty"        db:"score"`
}

JSON Serialization dengan Tags #

func main() {
    user := User{
        ID:       1,
        Username: "budi99",
        Email:    "[email protected]",
        Password: "hashedpassword123",
        IsAdmin:  false,
    }

    // Struct → JSON
    data, err := json.Marshal(user)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(data))
    // Output: {"id":1,"username":"budi99","email":"[email protected]",
    //          "created_at":"0001-01-01T00:00:00Z","is_admin":false}
    // Password tidak muncul (json:"-")
    // UpdatedAt tidak muncul (omitempty + zero value)
    // Score tidak muncul (omitempty + zero value 0.0)

    // JSON → Struct
    jsonStr := `{"id":2,"username":"sari","email":"[email protected]","is_admin":true}`
    var user2 User
    if err := json.Unmarshal([]byte(jsonStr), &user2); err != nil {
        panic(err)
    }
    fmt.Printf("User: %s, Admin: %v\n", user2.Username, user2.IsAdmin)
}

Tag untuk Validasi #

// Dengan library go-playground/validator
type CreateUserRequest struct {
    Username string `json:"username" validate:"required,min=3,max=50,alphanum"`
    Email    string `json:"email"    validate:"required,email"`
    Password string `json:"password" validate:"required,min=8,max=128"`
    Age      int    `json:"age"      validate:"min=0,max=150"`
}

Anonymous Struct #

Anonymous struct adalah struct tanpa nama tipe — dideklarasikan dan dipakai langsung. Berguna untuk data sementara yang tidak perlu tipe reusable:

// Config inline — tidak perlu definisi tipe terpisah
config := struct {
    Host     string
    Port     int
    Debug    bool
    Timeout  time.Duration
}{
    Host:    "localhost",
    Port:    5432,
    Debug:   true,
    Timeout: 30 * time.Second,
}

fmt.Printf("Connect to %s:%d\n", config.Host, config.Port)

// Table-driven tests — pola yang sangat umum di Go
tests := []struct {
    name     string
    input    string
    expected int
    wantErr  bool
}{
    {name: "valid number",   input: "42",  expected: 42,  wantErr: false},
    {name: "negative",       input: "-1",  expected: -1,  wantErr: false},
    {name: "invalid string", input: "abc", expected: 0,   wantErr: true},
    {name: "empty string",   input: "",    expected: 0,   wantErr: true},
}

for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
        got, err := strconv.Atoi(tt.input)
        if (err != nil) != tt.wantErr {
            t.Errorf("wantErr %v, got err %v", tt.wantErr, err)
        }
        if got != tt.expected {
            t.Errorf("expected %d, got %d", tt.expected, got)
        }
    })
}

Struct Comparability #

Struct bisa dibandingkan dengan == dan != hanya jika semua field-nya comparable. Field bertipe slice, map, atau function membuat struct tidak comparable:

type Point struct {
    X, Y int
}

p1 := Point{1, 2}
p2 := Point{1, 2}
p3 := Point{3, 4}

fmt.Println(p1 == p2)  // true  — semua field sama
fmt.Println(p1 == p3)  // false — field berbeda
fmt.Println(p1 != p3)  // true

// Struct dengan field non-comparable tidak bisa dibandingkan
type Container struct {
    Items []int  // slice tidak comparable
}

c1 := Container{Items: []int{1, 2, 3}}
c2 := Container{Items: []int{1, 2, 3}}
// fmt.Println(c1 == c2)  // ← compile error: struct containing []int cannot be compared

// Untuk struct non-comparable, gunakan reflect.DeepEqual
import "reflect"
fmt.Println(reflect.DeepEqual(c1, c2))  // true

Constructor Pattern #

Go tidak punya constructor bawaan. Konvensi yang sangat umum adalah membuat fungsi NewXxx() yang mengembalikan instance (biasanya pointer) yang sudah tervalidasi dan terinisialisasi dengan benar:

type Server struct {
    host    string
    port    int
    timeout time.Duration
    maxConn int
    logger  *Logger
}

// Constructor sederhana
func NewServer(host string, port int) *Server {
    return &Server{
        host:    host,
        port:    port,
        timeout: 30 * time.Second,  // default yang masuk akal
        maxConn: 100,
        logger:  defaultLogger,
    }
}

// Functional options pattern — untuk banyak opsi opsional
type Option func(*Server)

func WithTimeout(d time.Duration) Option {
    return func(s *Server) { s.timeout = d }
}

func WithMaxConn(n int) Option {
    return func(s *Server) { s.maxConn = n }
}

func WithLogger(l *Logger) Option {
    return func(s *Server) { s.logger = l }
}

func NewServerWithOptions(host string, port int, opts ...Option) *Server {
    s := &Server{
        host:    host,
        port:    port,
        timeout: 30 * time.Second,
        maxConn: 100,
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// Penggunaan — sangat expressive
server := NewServerWithOptions(
    "localhost", 8080,
    WithTimeout(60 * time.Second),
    WithMaxConn(500),
    WithLogger(customLogger),
)

Contoh Program Lengkap #

Program berikut membangun sistem manajemen perpustakaan yang menggunakan berbagai konsep struct:

package main

import (
    "fmt"
    "strings"
    "time"
)

// ── Tipe Dasar ─────────────────────────────────────────────

type BookID int
type MemberID int

type Author struct {
    Name        string
    Nationality string
}

func (a Author) String() string {
    return fmt.Sprintf("%s (%s)", a.Name, a.Nationality)
}

type Book struct {
    ID        BookID
    Title     string
    Author    Author
    ISBN      string
    Year      int
    Available bool
    Tags      []string
}

func NewBook(id BookID, title string, author Author, isbn string, year int) *Book {
    return &Book{
        ID:        id,
        Title:     title,
        Author:    author,
        ISBN:      isbn,
        Year:      year,
        Available: true,
    }
}

func (b *Book) Checkout() error {
    if !b.Available {
        return fmt.Errorf("buku %q sedang dipinjam", b.Title)
    }
    b.Available = false
    return nil
}

func (b *Book) Return() {
    b.Available = true
}

func (b Book) String() string {
    status := "tersedia"
    if !b.Available {
        status = "dipinjam"
    }
    return fmt.Sprintf("[%d] %q oleh %s (%d) — %s",
        b.ID, b.Title, b.Author.Name, b.Year, status)
}

// ── Member dengan Embedding ─────────────────────────────────

type Person struct {
    Name  string
    Email string
    Phone string
}

func (p Person) ContactInfo() string {
    return fmt.Sprintf("%s <%s>", p.Name, p.Email)
}

type Member struct {
    Person              // embedding — Member "adalah" Person
    ID          MemberID
    JoinDate    time.Time
    BorrowedBooks []*Book
}

func NewMember(id MemberID, name, email, phone string) *Member {
    return &Member{
        Person:   Person{Name: name, Email: email, Phone: phone},
        ID:       id,
        JoinDate: time.Now(),
    }
}

// Override ContactInfo dengan informasi tambahan
func (m Member) ContactInfo() string {
    return fmt.Sprintf("%s <%s> (ID: %d)", m.Name, m.Email, m.ID)
}

func (m *Member) Borrow(book *Book) error {
    if len(m.BorrowedBooks) >= 3 {
        return fmt.Errorf("%s sudah meminjam 3 buku (batas maksimum)", m.Name)
    }
    if err := book.Checkout(); err != nil {
        return err
    }
    m.BorrowedBooks = append(m.BorrowedBooks, book)
    fmt.Printf("✓ %s meminjam %q\n", m.Name, book.Title)
    return nil
}

func (m *Member) ReturnBook(bookID BookID) error {
    for i, b := range m.BorrowedBooks {
        if b.ID == bookID {
            b.Return()
            m.BorrowedBooks = append(m.BorrowedBooks[:i], m.BorrowedBooks[i+1:]...)
            fmt.Printf("✓ %s mengembalikan %q\n", m.Name, b.Title)
            return nil
        }
    }
    return fmt.Errorf("buku ID %d tidak ditemukan dalam pinjaman %s", bookID, m.Name)
}

func (m Member) Status() string {
    if len(m.BorrowedBooks) == 0 {
        return fmt.Sprintf("%s — tidak meminjam buku", m.Name)
    }
    titles := make([]string, len(m.BorrowedBooks))
    for i, b := range m.BorrowedBooks {
        titles[i] = fmt.Sprintf("%q", b.Title)
    }
    return fmt.Sprintf("%s — meminjam: %s", m.Name, strings.Join(titles, ", "))
}

// ── Library ─────────────────────────────────────────────────

type Library struct {
    Name    string
    Books   map[BookID]*Book
    Members map[MemberID]*Member
}

func NewLibrary(name string) *Library {
    return &Library{
        Name:    name,
        Books:   make(map[BookID]*Book),
        Members: make(map[MemberID]*Member),
    }
}

func (l *Library) AddBook(book *Book) {
    l.Books[book.ID] = book
}

func (l *Library) RegisterMember(member *Member) {
    l.Members[member.ID] = member
}

func (l *Library) Report() {
    available := 0
    for _, b := range l.Books {
        if b.Available {
            available++
        }
    }

    fmt.Printf("\n=== Laporan %s ===\n", l.Name)
    fmt.Printf("Total buku   : %d (%d tersedia, %d dipinjam)\n",
        len(l.Books), available, len(l.Books)-available)
    fmt.Printf("Total anggota: %d\n\n", len(l.Members))

    fmt.Println("Koleksi Buku:")
    for _, b := range l.Books {
        fmt.Printf("  %s\n", b)
    }

    fmt.Println("\nStatus Anggota:")
    for _, m := range l.Members {
        fmt.Printf("  %s\n", m.Status())
    }
}

func main() {
    lib := NewLibrary("Perpustakaan Go")

    // Tambah buku menggunakan constructor
    books := []*Book{
        NewBook(1, "The Go Programming Language",
            Author{"Alan Donovan", "Amerika"}, "978-0134190440", 2015),
        NewBook(2, "Go in Action",
            Author{"William Kennedy", "Amerika"}, "978-1617291784", 2015),
        NewBook(3, "Concurrency in Go",
            Author{"Katherine Cox-Buday", "Amerika"}, "978-1491941195", 2017),
        NewBook(4, "Clean Code",
            Author{"Robert Martin", "Amerika"}, "978-0132350884", 2008),
    }
    for _, b := range books {
        lib.AddBook(b)
    }

    // Daftarkan anggota
    members := []*Member{
        NewMember(1, "Budi Santoso", "[email protected]", "081234567890"),
        NewMember(2, "Sari Dewi", "[email protected]", "082345678901"),
    }
    for _, m := range members {
        lib.RegisterMember(m)
    }

    // Simulasi peminjaman
    fmt.Println("=== Aktivitas Peminjaman ===")

    budi := members[0]
    sari := members[1]

    // Budi meminjam dua buku
    if err := budi.Borrow(books[0]); err != nil {
        fmt.Println("Error:", err)
    }
    if err := budi.Borrow(books[2]); err != nil {
        fmt.Println("Error:", err)
    }

    // Sari mencoba meminjam buku yang sudah dipinjam Budi
    if err := sari.Borrow(books[0]); err != nil {
        fmt.Println("✗ Error:", err)
    }

    // Sari meminjam buku lain
    if err := sari.Borrow(books[1]); err != nil {
        fmt.Println("Error:", err)
    }

    // Budi mengembalikan satu buku
    if err := budi.ReturnBook(1); err != nil {
        fmt.Println("Error:", err)
    }

    // Sekarang Sari bisa meminjam buku yang tadi tidak tersedia
    if err := sari.Borrow(books[0]); err != nil {
        fmt.Println("Error:", err)
    }

    // ContactInfo menggunakan embedded Person.ContactInfo dan override
    fmt.Printf("\nInfo Kontak Budi (Person): %s\n", budi.Person.ContactInfo())
    fmt.Printf("Info Kontak Budi (Member): %s\n", budi.ContactInfo())

    // Laporan akhir
    lib.Report()
}

Ringkasan #

  • Go tidak punya class — struct + method + interface menggantikan class OOP dengan cara yang lebih eksplisit dan lebih mudah di-compose.
  • Gunakan named fields saat inisialisasi — lebih aman dan tahan terhadap perubahan struct.
  • Struct adalah value type — assignment dan pass ke fungsi membuat salinan; gunakan pointer untuk struct besar atau ketika perlu memodifikasi aslinya.
  • Value receiver untuk method yang membaca; pointer receiver untuk method yang memodifikasi atau struct besar. Pilih satu dan konsisten untuk seluruh tipe.
  • Embedding adalah komposisi, bukan inheritance — promoted fields dan methods membuat kode lebih ekspresif tanpa coupling yang erat.
  • Struct tags (json:"name", db:"column") adalah metadata untuk serialization dan mapping — wajib dipakai untuk API dan database.
  • json:"-" mengecualikan field dari JSON; omitempty mengabaikan zero value saat marshal.
  • Anonymous struct berguna untuk konfigurasi inline dan table-driven tests.
  • Constructor pattern NewXxx() untuk memastikan struct selalu dibuat dalam state yang valid.
  • Functional options pattern untuk constructor dengan banyak opsi opsional.

← Sebelumnya: Fungsi   Berikutnya: Interface →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact