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 baruNamaStruct
= nama struct yang kita buat (gunakan PascalCase)struct
= kata kunci yang menandakan ini adalah structfield1, field2
= nama-nama kolom dalam structtipeData1, 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:
-
Operator Titik (.): Digunakan untuk mengakses field dalam struct
namaStruct.namaField // Baca nilai field namaStruct.namaField = nilaiBaru // Ubah nilai field
-
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
-
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:
-
Kapan Gunakan Nested Struct?
- Saat ada data yang logically grouped (contoh: alamat, kontak, dll)
- Untuk membuat kode lebih terorganisir dan mudah dibaca
-
Cara Akses Nested Struct:
struct1.struct2.field // Gunakan titik beruntun
-
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:
- Anonymous Field:
Animal
di dalamDog
tidak punya nama field, ini disebut anonymous field - Direct Access: Karena embedding,
dog.Name
sama dengandog.Animal.Name
- Method Inheritance: Dog dan Cat otomatis bisa gunakan method
Speak()
dari Animal - 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 JSONjson:"-"
→ 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 → kolomfull_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:
- Nested Struct:
Student
berisi slice ofSubject
- Methods: Setiap struct punya method untuk operasi yang relevan
- Pointer Receivers: Untuk method yang mengubah data (seperti
AddSubject
) - Value Receivers: Untuk method yang hanya membaca data (seperti
DisplayInfo
) - Constructor Function:
NewStudentRegistry()
untuk membuat instance baru - Collection Management:
StudentRegistry
untuk mengelola banyak mahasiswa - 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:
- Readable: Kode mudah dibaca dan dipahami
- Flexible: Bisa set parameter dalam urutan apa saja
- Optional Parameters: Tidak perlu set semua parameter
- Method Chaining: Bisa chain method calls secara fluent
- 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
- Struct Definition: Cara membuat “template” data dengan
type StructName struct
- Field Access: Menggunakan titik (.) untuk mengakses dan mengubah data
- Initialization: Berbagai cara membuat instance struct (zero value, literal, partial)
- Nested Structs: Struct di dalam struct untuk data hierarkis
- Embedding: “Mewarisi” field dan method dari struct lain
- Methods: Memberikan “perilaku” pada struct dengan value/pointer receiver
- Struct Tags: Metadata untuk JSON, database, dll
- Comparison: Membandingkan struct dengan operator ==
- Real-world Example: Sistem manajemen mahasiswa yang kompleks
- 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
- Go Playground: https://play.golang.org/ - Untuk testing kode cepat
- JSON-to-Go: https://mholt.github.io/json-to-go/ - Convert JSON ke struct Go
- VS Code Extensions: Go extension untuk autocomplete dan debugging
📚 Langkah Selanjutnya
Setelah menguasai structs, lanjutkan belajar:
- Interfaces: Cara membuat “kontrak” yang bisa diimplementasi berbagai struct
- Error Handling: Cara menangani error dengan elegant di Go
- Concurrency: Goroutines dan channels untuk program concurrent
- Testing: Cara menulis unit test untuk struct dan method
- 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! 🚀