Panduan Lengkap Structs di Golang: Dari Pemula hingga Mahir

23 min read
Panduan Lengkap Structs di Golang: Dari Pemula hingga Mahir

Jika Anda baru belajar Golang, salah satu konsep yang paling penting untuk dikuasai adalah Struct. Bayangkan struct seperti sebuah “kotak” yang bisa menampung berbagai macam data yang saling berhubungan. Misalnya, data tentang seseorang bisa mencakup nama, umur, dan email - semua ini bisa dikemas dalam satu struct.

Dalam artikel ini, kita akan belajar structs dari nol hingga mahir dengan contoh-contoh praktis yang mudah dipahami.

Apa itu Struct? (Penjelasan untuk Pemula)

Struct adalah cara di Golang untuk mengelompokkan beberapa data yang berbeda tipe menjadi satu kesatuan. Bayangkan struct seperti formulir yang memiliki beberapa kolom isian:

  • Nama: (string)
  • Umur: (integer)
  • Email: (string)

Dalam bahasa pemrograman lain seperti Java atau C++, ini mirip dengan class, tapi di Go lebih sederhana dan mudah digunakan.

Sintaks Dasar Struct

// Template dasar membuat struct
type NamaStruct struct {
    field1 tipeData1  // kolom pertama dengan tipe data tertentu
    field2 tipeData2  // kolom kedua dengan tipe data tertentu
    // dan seterusnya...
}

Penjelasan:

  • type = kata kunci untuk membuat tipe data baru
  • NamaStruct = nama struct yang kita buat (gunakan PascalCase)
  • struct = kata kunci yang menandakan ini adalah struct
  • field1, field2 = nama-nama kolom dalam struct
  • tipeData1, tipeData2 = tipe data untuk setiap kolom (string, int, bool, dll)

Langkah 1: Membuat Struct Pertama Anda

Mari kita mulai dengan contoh sederhana. Kita akan membuat struct untuk menyimpan data seseorang:

package main

import "fmt"

// Membuat struct Person untuk menyimpan data seseorang
type Person struct {
    Name    string  // Nama orang (tipe string)
    Age     int     // Umur orang (tipe integer)
    Email   string  // Email orang (tipe string)
    IsAdmin bool    // Apakah dia admin atau bukan (tipe boolean)
}

func main() {
    // CARA 1: Membuat struct dengan zero values (nilai kosong)
    // Zero value = nilai default: string="", int=0, bool=false
    var person1 Person
    fmt.Printf("Person kosong: %+v\n", person1)
    // Output: Person kosong: {Name: Age:0 Email: IsAdmin:false}
    
    // CARA 2: Membuat struct dengan nilai, menyebutkan nama field
    // Cara ini paling direkomendasikan karena jelas dan aman
    person2 := Person{
        Name:    "Alice",
        Age:     25,
        Email:   "[email protected]",
        IsAdmin: false,
    }
    fmt.Printf("Person dengan nama field: %+v\n", person2)
    
    // CARA 3: Membuat struct tanpa menyebutkan nama field
    // HATI-HATI: Urutan nilai harus sama persis dengan urutan field di struct
    person3 := Person{"Bob", 30, "[email protected]", true}
    fmt.Printf("Person tanpa nama field: %+v\n", person3)
    
    // CARA 4: Membuat struct dengan sebagian nilai saja
    // Field yang tidak disebutkan akan menggunakan zero value
    person4 := Person{
        Name: "Charlie",
        Age:  35,
        // Email akan kosong (""), IsAdmin akan false
    }
    fmt.Printf("Person sebagian: %+v\n", person4)
}

Tips untuk Pemula:

  • Gunakan Cara 2 (dengan nama field) karena paling aman dan mudah dibaca
  • Format %+v menampilkan nama field beserta nilainya
  • Zero value adalah nilai default yang diberikan Go jika kita tidak mengisi field

Langkah 2: Mengakses dan Mengubah Data dalam Struct

Setelah membuat struct, kita perlu tahu cara mengakses dan mengubah datanya:

package main

import "fmt"

// Struct untuk menyimpan data mobil
type Car struct {
    Brand string    // Merek mobil
    Model string    // Model mobil  
    Year  int       // Tahun produksi
    Price float64   // Harga mobil
}

func main() {
    // Membuat struct mobil
    car := Car{
        Brand: "Toyota",
        Model: "Camry",
        Year:  2023,
        Price: 35000.00,
    }
    
    // MENGAKSES DATA: gunakan titik (.) untuk mengakses field
    fmt.Printf("Mobil: %s %s tahun %d\n", car.Brand, car.Model, car.Year)
    fmt.Printf("Harga: $%.2f\n", car.Price)
    
    // MENGUBAH DATA: langsung assign nilai baru ke field
    fmt.Println("\n=== Mengubah data mobil ===")
    car.Price = 32000.00  // Ubah harga
    car.Year = 2024       // Ubah tahun
    
    fmt.Printf("Data mobil setelah diubah: %+v\n", car)
    
    // MENGGUNAKAN POINTER untuk efisiensi memori
    // Saat struct besar, lebih baik gunakan pointer agar tidak copy seluruh data
    carPtr := &car  // & = ambil alamat memori dari car
    carPtr.Brand = "Honda"  // Ubah melalui pointer
    
    // Perubahan melalui pointer akan mengubah data asli
    fmt.Printf("Setelah diubah via pointer: %+v\n", car)
}

Penjelasan untuk Pemula:

  1. Operator Titik (.): Digunakan untuk mengakses field dalam struct

    namaStruct.namaField  // Baca nilai field
    namaStruct.namaField = nilaiBaru  // Ubah nilai field
    
  2. Pointer:

    • &var = ambil alamat memori dari variabel
    • Gunakan pointer saat struct besar untuk menghemat memori
    • Go otomatis “dereference” pointer, jadi ptr.field sama dengan (*ptr).field
  3. Kapan Gunakan Pointer?

    • Struct kecil (< 100 bytes): boleh copy langsung
    • Struct besar: gunakan pointer untuk efisiensi

Langkah 3: Struct Bersarang (Nested Structs)

Saat aplikasi berkembang, kita sering butuh struktur data yang lebih kompleks. Struct bisa berisi struct lain di dalamnya, ini disebut nested struct.

Analogi: Bayangkan formulir pendaftaran yang memiliki bagian “Data Pribadi” dan “Alamat”. Alamat sendiri memiliki beberapa kolom seperti jalan, kota, dll.

package main

import "fmt"

// Struct untuk alamat (akan digunakan di dalam struct lain)
type Address struct {
    Street  string  // Nama jalan
    City    string  // Nama kota
    State   string  // Nama provinsi/negara bagian
    ZipCode string  // Kode pos
}

// Struct untuk orang yang berisi struct Address di dalamnya
type Person struct {
    Name    string  // Nama orang
    Age     int     // Umur orang
    Email   string  // Email orang
    Address Address // Field bertipe struct Address (nested struct)
}

func main() {
    // Membuat person dengan alamat lengkap
    person := Person{
        Name:  "John Doe",
        Age:   28,
        Email: "[email protected]",
        
        // Mengisi nested struct Address
        Address: Address{
            Street:  "Jl. Sudirman No. 123",
            City:    "Jakarta",
            State:   "DKI Jakarta", 
            ZipCode: "10220",
        },
    }
    
    // Mengakses data utama
    fmt.Printf("Nama: %s\n", person.Name)
    fmt.Printf("Umur: %d tahun\n", person.Age)
    fmt.Printf("Email: %s\n", person.Email)
    
    // Mengakses data nested struct dengan notation titik berganda
    fmt.Printf("Alamat lengkap: %s, %s, %s %s\n", 
        person.Address.Street,   // person -> Address -> Street
        person.Address.City,     // person -> Address -> City
        person.Address.State,    // person -> Address -> State
        person.Address.ZipCode)  // person -> Address -> ZipCode
    
    // Mengubah data nested struct
    person.Address.City = "Bandung"
    person.Address.State = "Jawa Barat"
    
    fmt.Printf("Alamat setelah pindah: %s, %s\n", 
        person.Address.City, person.Address.State)
}

Tips untuk Pemula:

  1. Kapan Gunakan Nested Struct?

    • Saat ada data yang logically grouped (contoh: alamat, kontak, dll)
    • Untuk membuat kode lebih terorganisir dan mudah dibaca
  2. Cara Akses Nested Struct:

    struct1.struct2.field  // Gunakan titik beruntun
    
  3. Alternatif Pembuatan:

    // Bisa juga buat Address terpisah dulu
    alamat := Address{
        Street: "Jl. Merdeka",
        City:   "Surabaya",
        // dll...
    }
    
    person := Person{
        Name:    "Jane",
        Address: alamat,  // Gunakan variabel alamat
    }
    

Langkah 4: Struct Embedding (Konsep Advanced untuk Pemula)

Go memiliki fitur unik yang disebut struct embedding. Ini seperti “mewarisi” properti dari struct lain tanpa harus menulis ulang kodenya.

Analogi: Bayangkan Anda punya template “Hewan” dengan properti dasar seperti nama dan spesies. Kemudian Anda bisa membuat template “Anjing” dan “Kucing” yang otomatis punya semua properti “Hewan” plus properti tambahan yang spesifik.

package main

import "fmt"

// Struct dasar untuk semua hewan
type Animal struct {
    Name    string  // Nama hewan
    Species string  // Jenis spesies (mamalia, reptil, dll)
}

// Method (fungsi) yang bisa dipanggil oleh Animal
func (a Animal) Speak() string {
    return fmt.Sprintf("%s si %s mengeluarkan suara", a.Name, a.Species)
}

// Struct Dog yang "embed" (mewarisi) Animal
type Dog struct {
    Animal // Anonymous field - ini yang disebut embedding
    Breed  string  // Field tambahan khusus untuk anjing
}

// Method khusus untuk Dog
func (d Dog) Bark() string {
    return fmt.Sprintf("%s menggonggong: Guk guk!", d.Name)
}

// Struct Cat yang juga embed Animal  
type Cat struct {
    Animal // Embedding struct Animal
    Color  string  // Field tambahan khusus untuk kucing
}

// Method khusus untuk Cat
func (c Cat) Meow() string {
    return fmt.Sprintf("%s bersuara: Meong!", c.Name)
}

func main() {
    // Membuat anjing dengan embedded Animal
    dog := Dog{
        Animal: Animal{
            Name:    "Buddy",
            Species: "Mamalia",
        },
        Breed: "Golden Retriever",
    }
    
    // KEUNTUNGAN EMBEDDING: Bisa akses field Animal langsung tanpa menyebut "Animal"
    fmt.Printf("Nama anjing: %s\n", dog.Name)     // Akses langsung, tidak perlu dog.Animal.Name
    fmt.Printf("Spesies: %s\n", dog.Animal.Species) // Atau bisa juga akses eksplisit
    fmt.Printf("Ras: %s\n", dog.Breed)
    
    // Bisa panggil method dari Animal
    fmt.Println(dog.Speak()) // Method dari Animal
    fmt.Println(dog.Bark())  // Method dari Dog
    
    fmt.Println("\n=== Contoh Kucing ===")
    
    // Membuat kucing
    cat := Cat{
        Animal: Animal{Name: "Whiskers", Species: "Mamalia"},
        Color:  "Orange",
    }
    
    // Sama seperti dog, bisa akses field Animal langsung
    fmt.Printf("Nama kucing: %s\n", cat.Name)
    fmt.Printf("Warna: %s\n", cat.Color)
    
    fmt.Println(cat.Speak()) // Method dari Animal
    fmt.Println(cat.Meow())  // Method dari Cat
}

Penjelasan untuk Pemula:

  1. Anonymous Field: Animal di dalam Dog tidak punya nama field, ini disebut anonymous field
  2. Direct Access: Karena embedding, dog.Name sama dengan dog.Animal.Name
  3. Method Inheritance: Dog dan Cat otomatis bisa gunakan method Speak() dari Animal
  4. Extensibility: Setiap struct bisa punya method tambahan sendiri

Keuntungan Embedding:

  • Menghindari duplikasi kode
  • Membuat hierarki yang jelas
  • Mudah menambahkan fitur baru tanpa ubah kode lama

Kapan Gunakan Embedding?

  • Saat punya struct dengan properti/behavior yang mirip

  • Ingin membuat “keluarga” struct yang terkait fmt.Printf(“Dog species: %s\n”, dog.Animal.Species) // Explicit access fmt.Printf(“Dog breed: %s\n”, dog.Breed)

    // Dapat memanggil methods dari embedded struct fmt.Println(dog.Speak()) // Method dari Animal fmt.Println(dog.Bark()) // Method dari Dog

    // Cat example cat := Cat{ Animal: Animal{Name: “Whiskers”, Species: “Feline”}, Color: “Orange”, }

Langkah 5: Methods - Memberikan “Perilaku” pada Struct

Methods adalah fungsi yang “dimiliki” oleh struct. Bayangkan struct sebagai objek dan methods sebagai aksi yang bisa dilakukan objek tersebut.

Analogi: Jika struct Car adalah data mobil, maka method Start() adalah aksi “menyalakan mobil”, Stop() adalah “mematikan mobil”.

package main

import (
    "fmt"
    "math"
)

// Struct untuk persegi panjang
type Rectangle struct {
    Width  float64  // Lebar
    Height float64  // Tinggi
}

// METHOD 1: Method dengan VALUE RECEIVER (tidak mengubah data asli)
// Menghitung luas persegi panjang
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// METHOD 2: Method dengan VALUE RECEIVER  
// Menghitung keliling persegi panjang
func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// METHOD 3: Method dengan POINTER RECEIVER (bisa mengubah data asli)
// Memperbesar ukuran persegi panjang dengan faktor tertentu
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor   // Ubah lebar asli
    r.Height *= factor  // Ubah tinggi asli
}

// METHOD 4: Method yang mengembalikan struct baru (data asli tidak berubah)
// Membuat persegi panjang baru dengan ukuran yang diperbesar
func (r Rectangle) ScaleNew(factor float64) Rectangle {
    return Rectangle{
        Width:  r.Width * factor,
        Height: r.Height * factor,
    }
}

// Struct untuk lingkaran
type Circle struct {
    Radius float64  // Jari-jari
}

// Method untuk menghitung luas lingkaran
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

// Method untuk menghitung keliling lingkaran  
func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

func main() {
    // Membuat persegi panjang
    rect := Rectangle{Width: 10, Height: 5}
    
    fmt.Printf("Persegi panjang: %+v\n", rect)
    
    // Memanggil methods dengan VALUE RECEIVER
    fmt.Printf("Luas: %.2f\n", rect.Area())
    fmt.Printf("Keliling: %.2f\n", rect.Perimeter())
    
    // PENTING: Lihat perbedaan Scale vs ScaleNew
    fmt.Println("\n=== Menggunakan Scale (POINTER RECEIVER) ===")
    rect.Scale(2)  // Mengubah data asli
    fmt.Printf("Setelah Scale(2): %+v\n", rect)
    
    fmt.Println("\n=== Menggunakan ScaleNew (VALUE RECEIVER) ===")
    newRect := rect.ScaleNew(0.5)  // Membuat struct baru
    fmt.Printf("Rect baru hasil ScaleNew(0.5): %+v\n", newRect)
    fmt.Printf("Rect asli tetap: %+v\n", rect)  // Data asli tidak berubah
    
    // Contoh lingkaran
    fmt.Println("\n=== Contoh Lingkaran ===")
    circle := Circle{Radius: 7}
    fmt.Printf("Luas lingkaran: %.2f\n", circle.Area())
    fmt.Printf("Keliling lingkaran: %.2f\n", circle.Perimeter())
}

Penjelasan Penting untuk Pemula:

1. Sintaks Method

func (receiver tipe) NamaMethod() returnType {
    // kode method
}

2. Value Receiver vs Pointer Receiver

Value Receiver (r Rectangle):

  • Menerima copy dari struct
  • Tidak bisa mengubah data asli
  • Lebih aman, cocok untuk method yang hanya “membaca” data
  • Contoh: Area(), Perimeter()

Pointer Receiver (r *Rectangle):

  • Menerima pointer ke struct asli
  • Bisa mengubah data asli
  • Lebih efisien untuk struct besar
  • Contoh: Scale()

3. Kapan Gunakan Masing-masing?

  • Value Receiver: Untuk method yang hanya membaca/menghitung
  • Pointer Receiver: Untuk method yang mengubah data struct

Langkah 6: Struct Tags - Metadata untuk Struct

Struct tags adalah “label” atau metadata yang bisa kita tambahkan ke field struct. Tags sangat berguna saat bekerja dengan JSON, database, atau format data lainnya.

Analogi: Bayangkan struct tags seperti “stiker label” pada folder. Stiker memberitahu cara folder tersebut harus diperlakukan (contoh: “RAHASIA”, “URGENT”, dll).

package main

import (
    "encoding/json"
    "fmt"
)

// Struct User dengan berbagai tags
type User struct {
    ID       int    `json:"id" db:"user_id"`                    // Tag untuk JSON dan database
    Name     string `json:"name" db:"full_name"`                // Field akan jadi "name" di JSON
    Email    string `json:"email" db:"email_address"`           // Field akan jadi "email" di JSON  
    Password string `json:"-" db:"password_hash"`               // "-" berarti TIDAK masuk JSON (keamanan)
    IsActive bool   `json:"is_active,omitempty" db:"active"`    // "omitempty" = hilangkan jika kosong
}

func main() {
    // Membuat user
    user := User{
        ID:       1,
        Name:     "John Doe",
        Email:    "[email protected]", 
        Password: "secret123",        // Password ini tidak akan muncul di JSON
        IsActive: true,
    }
    
    fmt.Println("=== CONVERT STRUCT KE JSON ===")
    
    // Marshal = convert struct Go ke JSON string
    jsonData, err := json.Marshal(user)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    
    // Perhatikan: Password tidak muncul di JSON (karena tag "-")
    // Field name mengikuti tag JSON (bukan field name asli)
    fmt.Printf("JSON hasil: %s\n", string(jsonData))
    
    fmt.Println("\n=== CONVERT JSON KE STRUCT ===")
    
    // JSON string yang mau diconvert ke struct
    jsonStr := `{
        "id": 2,
        "name": "Jane Smith", 
        "email": "[email protected]",
        "is_active": false
    }`
    
    var newUser User
    
    // Unmarshal = convert JSON string ke struct Go
    err = json.Unmarshal([]byte(jsonStr), &newUser)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    
    // Password akan kosong karena tidak ada di JSON, IsActive akan false
    fmt.Printf("User hasil parsing: %+v\n", newUser)
    
    fmt.Println("\n=== DEMONSTRASI OMITEMPTY ===")
    
    // User dengan IsActive = false (default value)
    userWithFalse := User{
        ID:       3,
        Name:     "Bob Wilson",
        Email:    "[email protected]",
        IsActive: false,  // Nilai default untuk bool
    }
    
    jsonWithFalse, _ := json.Marshal(userWithFalse)
    fmt.Printf("Dengan IsActive false: %s\n", string(jsonWithFalse))
    // Perhatikan: "is_active" tetap muncul karena nilai false bukan "empty" untuk bool
    
    // User dengan field kosong
    emptyUser := User{
        ID:   4,
        Name: "Charlie Brown",
        // Email kosong, IsActive default false
    }
    
    jsonEmpty, _ := json.Marshal(emptyUser)
    fmt.Printf("Dengan field kosong: %s\n", string(jsonEmpty))
    // Email kosong ("") akan dihilangkan jika pakai omitempty
}

Penjelasan Tag untuk Pemula:

1. Sintaks Tag

type Struct struct {
    Field type `tag1:"value1" tag2:"value2"`
}

2. Tag JSON yang Umum

  • json:"nama_field" → Ganti nama field di JSON
  • json:"-" → Jangan masukkan field ini ke JSON (untuk data sensitif)
  • json:"nama,omitempty" → Hilangkan dari JSON jika nilainya “empty” (kosong)

3. Kapan Gunakan Struct Tags?

Untuk API/Web Development:

  • Saat membuat REST API yang return JSON
  • Perlu kontrol format JSON yang dikirim ke client
  • Menyembunyikan field sensitif (password, token)

Untuk Database:

  • Mapping field struct ke kolom database dengan nama berbeda
  • Contoh: field Name di struct → kolom full_name di database

4. Contoh Dunia Nyata

type Product struct {
    ID          int     `json:"id" db:"product_id"`
    Name        string  `json:"name" db:"product_name"`
    Price       float64 `json:"price" db:"price"`
    InternalID  string  `json:"-" db:"internal_code"`      // Disembunyikan dari API
    Description string  `json:"description,omitempty"`     // Hilang jika kosong
## Langkah 7: Membandingkan Structs

Go memungkinkan kita membandingkan dua struct menggunakan operator `==` dan `!=`, tapi hanya jika semua field dalam struct bisa dibandingkan (comparable).

```go
package main

import "fmt"

// Struct Point dengan field yang bisa dibandingkan
type Point struct {
    X, Y int  // int bisa dibandingkan
}

// Struct Person dengan field yang bisa dibandingkan  
type Person struct {
    Name string  // string bisa dibandingkan
    Age  int     // int bisa dibandingkan
}

func main() {
    // Membandingkan Point
    point1 := Point{X: 1, Y: 2}
    point2 := Point{X: 1, Y: 2}  // Nilai sama dengan point1
    point3 := Point{X: 2, Y: 3}  // Nilai berbeda dengan point1
    
    fmt.Printf("point1 == point2: %t\n", point1 == point2) // true (nilai sama)
    fmt.Printf("point1 == point3: %t\n", point1 == point3) // false (nilai beda)
    
    // Membandingkan Person
    person1 := Person{Name: "Alice", Age: 25}
    person2 := Person{Name: "Alice", Age: 25}  // Sama dengan person1
    person3 := Person{Name: "Bob", Age: 30}    // Beda dengan person1
    
    fmt.Printf("person1 == person2: %t\n", person1 == person2) // true
    fmt.Printf("person1 == person3: %t\n", person1 == person3) // false
    
    // Contoh perbandingan yang lebih kompleks
    if point1 == point2 {
        fmt.Println("Point1 dan Point2 identik!")
    }
    
    if person1 != person3 {
        fmt.Println("Person1 dan Person3 berbeda!")
    }
}

Penting untuk Pemula:

Field yang BISA dibandingkan:

  • bool, int, float64, string
  • Array dengan element yang comparable
  • Struct dengan semua field comparable

Field yang TIDAK BISA dibandingkan:

  • slice, map, function
  • Struct yang mengandung field tidak comparable
// ❌ Struct ini TIDAK bisa dibandingkan
type BadStruct struct {
    Name    string
    Numbers []int    // slice tidak bisa dibandingkan
}

// ✅ Struct ini BISA dibandingkan  
type GoodStruct struct {
    Name string
    Age  int
}

Proyek Praktis: Sistem Manajemen Mahasiswa

Sekarang kita akan membangun aplikasi nyata menggunakan semua konsep struct yang sudah dipelajari. Kita akan membuat sistem untuk mengelola data mahasiswa dan nilai mereka.

package main

import (
    "fmt"
    "sort"
    "time"
)

// Struct untuk mata kuliah
type Subject struct {
    Name    string   // Nama mata kuliah (contoh: "Matematika")
    Credits int      // Jumlah SKS (contoh: 3)
    Grade   float64  // Nilai (contoh: 3.5)
}

// Struct untuk mahasiswa
type Student struct {
    ID          int         // ID unik mahasiswa
    Name        string      // Nama lengkap
    Email       string      // Email mahasiswa  
    DateOfBirth time.Time   // Tanggal lahir
    Subjects    []Subject   // Daftar mata kuliah yang diambil
    GPA         float64     // IPK (Indeks Prestasi Kumulatif)
}

// METHOD 1: Menambahkan mata kuliah baru ke mahasiswa
func (s *Student) AddSubject(subject Subject) {
    // Tambahkan mata kuliah ke slice
    s.Subjects = append(s.Subjects, subject)
    
    // Hitung ulang IPK setelah menambah mata kuliah
    s.calculateGPA()
}

// METHOD 2: Menghitung IPK (menggunakan pointer receiver karena mengubah data)
func (s *Student) calculateGPA() {
    // Jika belum ada mata kuliah, IPK = 0
    if len(s.Subjects) == 0 {
        s.GPA = 0
        return
    }
    
    var totalPoints, totalCredits float64
    
    // Hitung total poin dan total SKS
    for _, subject := range s.Subjects {
        totalPoints += subject.Grade * float64(subject.Credits)  // Nilai × SKS
        totalCredits += float64(subject.Credits)                 // Total SKS
    }
    
    // IPK = Total Poin ÷ Total SKS
    s.GPA = totalPoints / totalCredits
}

// METHOD 3: Menghitung umur mahasiswa  
func (s Student) GetAge() int {
    // Hitung selisih waktu dari tanggal lahir sampai sekarang
    duration := time.Since(s.DateOfBirth)
    
    // Convert ke tahun (365 hari = 1 tahun)
    years := int(duration.Hours() / 24 / 365)
    return years
}

// METHOD 4: Menampilkan info lengkap mahasiswa
func (s Student) DisplayInfo() {
    fmt.Printf("=== INFO MAHASISWA ===\n")
    fmt.Printf("ID: %d\n", s.ID)
    fmt.Printf("Nama: %s\n", s.Name)
    fmt.Printf("Email: %s\n", s.Email)
    fmt.Printf("Umur: %d tahun\n", s.GetAge())
    fmt.Printf("IPK: %.2f\n", s.GPA)
    
    // Tampilkan daftar mata kuliah jika ada
    if len(s.Subjects) > 0 {
        fmt.Println("Mata Kuliah:")
        for i, subject := range s.Subjects {
            fmt.Printf("  %d. %s (%d SKS): %.1f\n", 
                i+1, subject.Name, subject.Credits, subject.Grade)
        }
    } else {
        fmt.Println("Belum mengambil mata kuliah")
    }
    fmt.Println()
}

// Struct untuk mengelola koleksi mahasiswa
type StudentRegistry struct {
    Students []Student  // Daftar semua mahasiswa
    nextID   int        // ID untuk mahasiswa berikutnya
}

// CONSTRUCTOR: Function untuk membuat registry baru
func NewStudentRegistry() *StudentRegistry {
    return &StudentRegistry{
        Students: make([]Student, 0),  // Buat slice kosong
        nextID:   1,                   // Mulai dari ID 1
    }
}

// METHOD: Menambahkan mahasiswa baru
func (sr *StudentRegistry) AddStudent(name, email string, dob time.Time) *Student {
    // Buat mahasiswa baru dengan ID otomatis
    student := Student{
        ID:          sr.nextID,
        Name:        name,
        Email:       email,
        DateOfBirth: dob,
        Subjects:    make([]Subject, 0),  // Slice kosong untuk mata kuliah
        GPA:         0.0,                 // IPK awal 0
    }
    
    // Tambahkan ke registry
    sr.Students = append(sr.Students, student)
    sr.nextID++  // Increment ID untuk mahasiswa selanjutnya
    
    // Return pointer ke mahasiswa yang baru ditambahkan
    return &sr.Students[len(sr.Students)-1]
}

// METHOD: Mencari mahasiswa berdasarkan ID
func (sr *StudentRegistry) GetStudentByID(id int) *Student {
    for i := range sr.Students {
        if sr.Students[i].ID == id {
            return &sr.Students[i]  // Return pointer ke mahasiswa yang ditemukan
        }
    }
    return nil  // Return nil jika tidak ditemukan
}

// METHOD: Mendapatkan mahasiswa dengan IPK tertinggi
func (sr *StudentRegistry) GetTopStudents(n int) []Student {
    // Copy slice untuk sorting (agar data asli tidak berubah)
    students := make([]Student, len(sr.Students))
    copy(students, sr.Students)
    
    // Sort berdasarkan IPK dari tinggi ke rendah
    sort.Slice(students, func(i, j int) bool {
        return students[i].GPA > students[j].GPA
    })
    
    // Batasi jumlah hasil sesuai parameter n
    if n > len(students) {
        n = len(students)
    }
    
    return students[:n]
}

func main() {
    fmt.Println("=== SISTEM MANAJEMEN MAHASISWA ===\n")
    
    // Buat registry mahasiswa baru
    registry := NewStudentRegistry()
    
    // Tambahkan beberapa mahasiswa
    alice := registry.AddStudent(
        "Alice Johnson", 
        "[email protected]", 
        time.Date(2000, 5, 15, 0, 0, 0, 0, time.UTC),
    )
    
    bob := registry.AddStudent(
        "Bob Smith", 
        "[email protected]", 
        time.Date(1999, 8, 22, 0, 0, 0, 0, time.UTC),
    )
    
    charlie := registry.AddStudent(
        "Charlie Brown", 
        "[email protected]", 
        time.Date(2001, 2, 10, 0, 0, 0, 0, time.UTC),
    )
    
    // Tambahkan mata kuliah untuk Alice
    alice.AddSubject(Subject{"Matematika", 4, 3.7})
    alice.AddSubject(Subject{"Fisika", 3, 3.5})
    alice.AddSubject(Subject{"Kimia", 4, 3.8})
    
    // Tambahkan mata kuliah untuk Bob
    bob.AddSubject(Subject{"Ilmu Komputer", 4, 3.9})
    bob.AddSubject(Subject{"Matematika", 4, 3.6})
    bob.AddSubject(Subject{"Statistika", 3, 3.7})
    
    // Tambahkan mata kuliah untuk Charlie
    charlie.AddSubject(Subject{"Biologi", 4, 3.2})
    charlie.AddSubject(Subject{"Kimia", 4, 3.4})
    
    // Tampilkan semua mahasiswa
    fmt.Println("=== DAFTAR SEMUA MAHASISWA ===")
    for _, student := range registry.Students {
        student.DisplayInfo()
    }
    
    // Dapatkan 2 mahasiswa dengan IPK tertinggi
    fmt.Println("=== TOP 2 MAHASISWA BERDASARKAN IPK ===")
    topStudents := registry.GetTopStudents(2)
    for i, student := range topStudents {
        fmt.Printf("%d. %s (IPK: %.2f)\n", i+1, student.Name, student.GPA)
    }
    
    // Cari mahasiswa berdasarkan ID
    fmt.Println("\n=== PENCARIAN MAHASISWA BERDASARKAN ID ===")
    foundStudent := registry.GetStudentByID(2)  // Cari Bob (ID = 2)
    if foundStudent != nil {
        fmt.Printf("Mahasiswa ditemukan:\n")
        foundStudent.DisplayInfo()
    } else {
        fmt.Println("Mahasiswa tidak ditemukan")
    }
}

Penjelasan Konsep yang Digunakan:

  1. Nested Struct: Student berisi slice of Subject
  2. Methods: Setiap struct punya method untuk operasi yang relevan
  3. Pointer Receivers: Untuk method yang mengubah data (seperti AddSubject)
  4. Value Receivers: Untuk method yang hanya membaca data (seperti DisplayInfo)
  5. Constructor Function: NewStudentRegistry() untuk membuat instance baru
  6. Collection Management: StudentRegistry untuk mengelola banyak mahasiswa
  7. Data Processing: Sorting, searching, calculating berdasarkan data struct

Design Pattern dengan Structs: Builder Pattern (Advanced)

Setelah menguasai dasar-dasar struct, mari kita pelajari salah satu design pattern yang populer: Builder Pattern. Pattern ini sangat berguna saat kita perlu membuat object yang kompleks dengan banyak parameter optional.

Analogi: Bayangkan Anda memesan burger di restoran. Anda bisa pilih roti, daging, sayuran, saus satu per satu, lalu “build” burger lengkap di akhir.

package main

import "fmt"

// Struct untuk HTTP Request yang kompleks
type HTTPRequest struct {
    URL     string              // URL tujuan
    Method  string              // GET, POST, PUT, dll
    Headers map[string]string   // HTTP headers
    Body    string              // Request body
    Timeout int                 // Timeout dalam detik
}

// Struct Builder untuk membangun HTTPRequest step by step
type HTTPRequestBuilder struct {
    request HTTPRequest  // Request yang sedang dibangun
}

// Constructor untuk Builder (dengan nilai default)
func NewHTTPRequest() *HTTPRequestBuilder {
    return &HTTPRequestBuilder{
        request: HTTPRequest{
            Method:  "GET",                          // Default method
            Headers: make(map[string]string),        // Map kosong untuk headers
            Timeout: 30,                            // Default timeout 30 detik
        },
    }
}

// Method untuk set URL (return self agar bisa chain)
func (b *HTTPRequestBuilder) URL(url string) *HTTPRequestBuilder {
    b.request.URL = url
    return b  // Return self untuk method chaining
}

// Method untuk set HTTP Method
func (b *HTTPRequestBuilder) Method(method string) *HTTPRequestBuilder {
    b.request.Method = method
    return b
}

// Method untuk menambah header (bisa dipanggil berkali-kali)
func (b *HTTPRequestBuilder) Header(key, value string) *HTTPRequestBuilder {
    b.request.Headers[key] = value
    return b
}

// Method untuk set request body
func (b *HTTPRequestBuilder) Body(body string) *HTTPRequestBuilder {
    b.request.Body = body
    return b
}

// Method untuk set timeout
func (b *HTTPRequestBuilder) Timeout(timeout int) *HTTPRequestBuilder {
    b.request.Timeout = timeout
    return b
}

// Method untuk "membangun" HTTPRequest final
func (b *HTTPRequestBuilder) Build() HTTPRequest {
    return b.request
}

func main() {
    fmt.Println("=== BUILDER PATTERN DEMO ===\n")
    
    // CARA LAMA (tanpa builder): ribet dan prone error
    /*
    request := HTTPRequest{
        URL:     "https://api.example.com/users",
        Method:  "POST", 
        Headers: map[string]string{
            "Content-Type":  "application/json",
            "Authorization": "Bearer token123",
        },
        Body:    `{"name":"John","email":"[email protected]"}`,
        Timeout: 60,
    }
    */
    
    // CARA BARU (dengan builder): jelas dan mudah dibaca
    request := NewHTTPRequest().                                    // Buat builder baru
        URL("https://api.example.com/users").                      // Set URL
        Method("POST").                                             // Set method
        Header("Content-Type", "application/json").                // Tambah header 1
        Header("Authorization", "Bearer token123").                // Tambah header 2  
        Body(`{"name":"John","email":"[email protected]"}`).        // Set body
        Timeout(60).                                               // Set timeout
        Build()                                                    // Build request final
    
    fmt.Printf("Request yang dibangun:\n")
    fmt.Printf("URL: %s\n", request.URL)
    fmt.Printf("Method: %s\n", request.Method)
    fmt.Printf("Timeout: %d detik\n", request.Timeout)
    fmt.Printf("Headers:\n")
    for key, value := range request.Headers {
        fmt.Printf("  %s: %s\n", key, value)
    }
    fmt.Printf("Body: %s\n", request.Body)
    
    fmt.Println("\n=== CONTOH REQUEST SEDERHANA ===")
    
    // Contoh request GET sederhana (hanya URL)
    simpleRequest := NewHTTPRequest().
        URL("https://api.example.com/profile").
        Build()
    
    fmt.Printf("Simple GET request: %+v\n", simpleRequest)
}

Keuntungan Builder Pattern:

  1. Readable: Kode mudah dibaca dan dipahami
  2. Flexible: Bisa set parameter dalam urutan apa saja
  3. Optional Parameters: Tidak perlu set semua parameter
  4. Method Chaining: Bisa chain method calls secara fluent
  5. Default Values: Builder bisa provide nilai default yang masuk akal

Kapan Gunakan Builder Pattern?

  • Struct dengan banyak field (>5 field)
  • Banyak parameter yang optional
  • Perlu validasi kompleks sebelum create object
  • Ingin API yang user-friendly

Rangkuman dan Tips untuk Pemula

Selamat! Anda telah mempelajari konsep struct di Golang dari dasar hingga advanced. Mari kita rangkum apa yang telah dipelajari:

🎯 Konsep Utama yang Sudah Dipelajari

  1. Struct Definition: Cara membuat “template” data dengan type StructName struct
  2. Field Access: Menggunakan titik (.) untuk mengakses dan mengubah data
  3. Initialization: Berbagai cara membuat instance struct (zero value, literal, partial)
  4. Nested Structs: Struct di dalam struct untuk data hierarkis
  5. Embedding: “Mewarisi” field dan method dari struct lain
  6. Methods: Memberikan “perilaku” pada struct dengan value/pointer receiver
  7. Struct Tags: Metadata untuk JSON, database, dll
  8. Comparison: Membandingkan struct dengan operator ==
  9. Real-world Example: Sistem manajemen mahasiswa yang kompleks
  10. Design Patterns: Builder pattern untuk object construction yang fleksibel

🚀 Best Practices untuk Pemula

1. Naming Conventions

// ✅ BENAR: PascalCase untuk struct dan field yang public
type UserProfile struct {
    FirstName string
    LastName  string
    age       int    // lowercase untuk private field
}

// ❌ SALAH: camelCase atau snake_case
type userProfile struct {  // struct name should be PascalCase if public
    first_name string      // use camelCase, not snake_case
}

2. Kapan Gunakan Pointer Receiver

// ✅ Gunakan POINTER RECEIVER jika:
func (u *User) UpdateEmail(email string) {
    u.Email = email  // Mengubah data struct
}

func (u *User) Save() error {
    // Method yang mahal/kompleks
    return database.Save(u)
}

// ✅ Gunakan VALUE RECEIVER jika:
func (u User) GetFullName() string {
    return u.FirstName + " " + u.LastName  // Hanya membaca data
}

func (u User) IsAdult() bool {
    return u.Age >= 18  // Simple calculation, tidak mengubah data
}

3. Struct Tags yang Umum

type User struct {
    ID       int    `json:"id" db:"user_id" validate:"required"`
    Email    string `json:"email" db:"email" validate:"required,email"`
    Password string `json:"-" db:"password_hash"`  // Disembunyikan dari JSON
    IsActive bool   `json:"is_active,omitempty" db:"active"`
}

🛠️ Workflow Development dengan Structs

Step 1: Design Data Structure

// Mulai dengan struct sederhana
type Product struct {
    ID    int
    Name  string
    Price float64
}

Step 2: Add Methods

// Tambahkan method yang relevan
func (p Product) GetFormattedPrice() string {
    return fmt.Sprintf("Rp %.2f", p.Price)
}

func (p *Product) ApplyDiscount(percent float64) {
    p.Price = p.Price * (1 - percent/100)
}

Step 3: Add Complexity Gradually

// Tambahkan nested struct jika diperlukan
type Product struct {
    ID       int
    Name     string
    Price    float64
    Category Category  // Nested struct
    Tags     []string  // Slice untuk multiple values
}

type Category struct {
    ID   int
    Name string
}

🔧 Tools dan Resources untuk Belajar Lebih Lanjut

  1. Go Playground: https://play.golang.org/ - Untuk testing kode cepat
  2. JSON-to-Go: https://mholt.github.io/json-to-go/ - Convert JSON ke struct Go
  3. VS Code Extensions: Go extension untuk autocomplete dan debugging

📚 Langkah Selanjutnya

Setelah menguasai structs, lanjutkan belajar:

  1. Interfaces: Cara membuat “kontrak” yang bisa diimplementasi berbagai struct
  2. Error Handling: Cara menangani error dengan elegant di Go
  3. Concurrency: Goroutines dan channels untuk program concurrent
  4. Testing: Cara menulis unit test untuk struct dan method
  5. JSON & API: Membuat REST API menggunakan struct

🎉 Pesan Penutup

Structs adalah fondasi yang sangat penting dalam Go programming. Dengan memahami konsep-konsep di artikel ini, Anda sudah memiliki base yang kuat untuk:

  • Backend Development: API servers, microservices
  • Data Processing: ETL, data analysis tools
  • System Programming: CLI tools, system utilities
  • Web Development: Full-stack applications dengan Go

Ingat: Programming adalah skill yang dibangun dengan praktek. Cobalah buat project kecil menggunakan struct untuk mengaplikasikan apa yang sudah dipelajari!

Happy Coding! 🚀