Lompat ke konten
Beranda ยป Structs dan Methods: OOP ala Go – Pendekatan Go terhadap Pemrograman Berorientasi Objek

Structs dan Methods: OOP ala Go – Pendekatan Go terhadap Pemrograman Berorientasi Objek

Struct in Go

Go (Golang) memiliki pendekatan unik terhadap pemrograman berorientasi objek (OOP) yang berbeda dari bahasa seperti Java, C++, atau Python. Meskipun Go tidak memiliki konsep class tradisional, bahasa ini menyediakan cara yang elegan dan powerful untuk menerapkan prinsip-prinsip OOP melalui structs dan methods.

Mengapa Go Tidak Memiliki Class?

Sebelum memahami structs dan methods di Go, penting untuk memahami filosofi desain Go. Tim pengembang Go di Google sengaja menghindari kompleksitas yang sering muncul dalam bahasa OOP tradisional seperti inheritance hierarchy yang rumit, diamond problem, dan overhead yang tidak perlu.

Go mengadopsi prinsip “composition over inheritance” yang membuat kode lebih mudah dipahami, di-maintain, dan di-test.

Apa itu Struct di Go?

Struct di Go adalah tipe data yang memungkinkan kita mengelompokkan data-data terkait menjadi satu kesatuan. Struct mirip dengan class di bahasa lain, tetapi lebih sederhana dan fokus pada data structure.

Sintaks Dasar Struct

type NamaStruct struct {
    field1 tipeData1
    field2 tipeData2
    // ... field lainnya
}

Contoh Implementasi Struct

package main

import "fmt"

// Definisi struct Person
type Person struct {
    Name     string
    Age      int
    Email    string
    IsActive bool
}

func main() {
    // Cara 1: Membuat instance dengan field names
    person1 := Person{
        Name:     "John Doe",
        Age:      30,
        Email:    "john@example.com",
        IsActive: true,
    }

    // Cara 2: Membuat instance tanpa field names (harus urut)
    person2 := Person{"Jane Smith", 25, "jane@example.com", true}

    // Cara 3: Membuat instance kosong dan mengisi field
    var person3 Person
    person3.Name = "Bob Johnson"
    person3.Age = 35

    fmt.Println(person1)
    fmt.Println(person2)
    fmt.Println(person3)
}

Anonymous Struct

Go juga mendukung anonymous struct yang berguna untuk data sementara:

func main() {
    // Anonymous struct
    user := struct {
        ID   int
        Name string
    }{
        ID:   1,
        Name: "Alice",
    }

    fmt.Printf("User: %+v\n", user)
}

Methods di Go: Memberikan Behavior pada Struct

Methods di Go adalah fungsi yang memiliki receiver. Receiver menentukan tipe data mana yang bisa “memiliki” method tersebut. Inilah cara Go mengimplementasikan behavior dalam OOP.

Sintaks Method

func (receiver ReceiverType) methodName(parameters) returnType {
    // implementasi method
}

Contoh Methods pada Struct

package main

import (
    "fmt"
    "strings"
)

type Person struct {
    FirstName string
    LastName  string
    Age       int
    Email     string
}

// Method dengan value receiver
func (p Person) GetFullName() string {
    return fmt.Sprintf("%s %s", p.FirstName, p.LastName)
}

// Method dengan value receiver
func (p Person) IsAdult() bool {
    return p.Age >= 18
}

// Method dengan value receiver
func (p Person) GetEmailDomain() string {
    parts := strings.Split(p.Email, "@")
    if len(parts) == 2 {
        return parts[1]
    }
    return ""
}

// Method dengan pointer receiver
func (p *Person) UpdateEmail(newEmail string) {
    p.Email = newEmail
}

// Method dengan pointer receiver
func (p *Person) HaveBirthday() {
    p.Age++
}

func main() {
    person := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       17,
        Email:     "john@gmail.com",
    }

    // Menggunakan methods
    fmt.Println("Full Name:", person.GetFullName())
    fmt.Println("Is Adult:", person.IsAdult())
    fmt.Println("Email Domain:", person.GetEmailDomain())

    // Method yang mengubah data (pointer receiver)
    person.HaveBirthday()
    fmt.Println("Age after birthday:", person.Age)

    person.UpdateEmail("john.doe@company.com")
    fmt.Println("New email:", person.Email)
}

Value Receiver vs Pointer Receiver

Salah satu konsep penting dalam Go methods adalah perbedaan antara value receiver dan pointer receiver.

Value Receiver

func (p Person) GetInfo() string {
    return fmt.Sprintf("%s is %d years old", p.GetFullName(), p.Age)
}

Karakteristik Value Receiver:

  • Menerima copy dari struct
  • Tidak bisa mengubah data asli
  • Lebih aman dari race condition
  • Cocok untuk methods yang hanya membaca data

Pointer Receiver

func (p *Person) SetAge(age int) {
    p.Age = age
}

Karakteristik Pointer Receiver:

  • Menerima pointer ke struct asli
  • Bisa mengubah data asli
  • Lebih efisien untuk struct besar
  • Cocok untuk methods yang memodifikasi data

Guidelines Memilih Receiver Type

  1. Gunakan pointer receiver jika:
    • Method perlu memodifikasi receiver
    • Struct berukuran besar (efisiensi memori)
    • Consistency (jika ada satu method pointer receiver, sebaiknya semua menggunakan pointer)
  2. Gunakan value receiver jika:
    • Method hanya membaca data
    • Struct berukuran kecil
    • Immutability diinginkan

Embedded Structs: Composition dalam Go

Go mendukung composition melalui embedded structs, yang mirip dengan inheritance tetapi lebih fleksibel.

package main

import "fmt"

// Base struct
type Address struct {
    Street  string
    City    string
    Country string
}

// Method untuk Address
func (a Address) GetFullAddress() string {
    return fmt.Sprintf("%s, %s, %s", a.Street, a.City, a.Country)
}

// Struct yang meng-embed Address
type Employee struct {
    Name     string
    Position string
    Salary   float64
    Address  // Embedded struct
}

// Method untuk Employee
func (e Employee) GetDetails() string {
    return fmt.Sprintf("%s works as %s, lives at %s", 
        e.Name, e.Position, e.GetFullAddress())
}

func main() {
    emp := Employee{
        Name:     "Alice Johnson",
        Position: "Software Engineer",
        Salary:   75000,
        Address: Address{
            Street:  "123 Tech Street",
            City:    "San Francisco",
            Country: "USA",
        },
    }

    // Akses field embedded struct
    fmt.Println("Employee Name:", emp.Name)
    fmt.Println("Employee City:", emp.City) // Langsung akses field Address

    // Akses method embedded struct
    fmt.Println("Full Address:", emp.GetFullAddress())
    fmt.Println("Employee Details:", emp.GetDetails())
}

Struct Tags: Metadata untuk Struct Fields

Go menyediakan struct tags yang berguna untuk metadata, terutama untuk serialization/deserialization.

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email"`
    Password string `json:"-"` // Tidak akan di-serialize
    IsActive bool   `json:"is_active,omitempty"`
}

func main() {
    user := User{
        ID:       1,
        Name:     "John Doe",
        Email:    "john@example.com",
        Password: "secret123",
        IsActive: true,
    }

    // Convert to JSON
    jsonData, err := json.Marshal(user)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    fmt.Println("JSON:", string(jsonData))

    // Convert from JSON
    jsonStr := `{"id":2,"name":"Jane Smith","email":"jane@example.com"}`
    var newUser User
    err = json.Unmarshal([]byte(jsonStr), &newUser)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    fmt.Printf("Parsed User: %+v\n", newUser)
}

Contoh Praktis: Implementasi Bank Account

Mari kita lihat contoh praktis yang menggabungkan semua konsep yang telah dipelajari:

package main

import (
    "errors"
    "fmt"
    "time"
)

// Transaction struct untuk mencatat transaksi
type Transaction struct {
    ID          string
    Type        string // "deposit" atau "withdrawal"
    Amount      float64
    Timestamp   time.Time
    Description string
}

// BankAccount struct
type BankAccount struct {
    AccountNumber string
    HolderName    string
    Balance       float64
    IsActive      bool
    Transactions  []Transaction
}

// Constructor function untuk BankAccount
func NewBankAccount(accountNumber, holderName string) *BankAccount {
    return &BankAccount{
        AccountNumber: accountNumber,
        HolderName:    holderName,
        Balance:       0.0,
        IsActive:      true,
        Transactions:  make([]Transaction, 0),
    }
}

// Method untuk mendapatkan informasi akun
func (ba *BankAccount) GetAccountInfo() string {
    status := "Active"
    if !ba.IsActive {
        status = "Inactive"
    }
    return fmt.Sprintf("Account: %s, Holder: %s, Balance: %.2f, Status: %s",
        ba.AccountNumber, ba.HolderName, ba.Balance, status)
}

// Method untuk deposit
func (ba *BankAccount) Deposit(amount float64, description string) error {
    if !ba.IsActive {
        return errors.New("account is not active")
    }
    if amount <= 0 {
        return errors.New("deposit amount must be positive")
    }

    ba.Balance += amount
    
    // Tambahkan transaksi
    transaction := Transaction{
        ID:          fmt.Sprintf("TXN_%d", time.Now().Unix()),
        Type:        "deposit",
        Amount:      amount,
        Timestamp:   time.Now(),
        Description: description,
    }
    ba.Transactions = append(ba.Transactions, transaction)

    return nil
}

// Method untuk withdrawal
func (ba *BankAccount) Withdraw(amount float64, description string) error {
    if !ba.IsActive {
        return errors.New("account is not active")
    }
    if amount <= 0 {
        return errors.New("withdrawal amount must be positive")
    }
    if amount > ba.Balance {
        return errors.New("insufficient balance")
    }

    ba.Balance -= amount
    
    // Tambahkan transaksi
    transaction := Transaction{
        ID:          fmt.Sprintf("TXN_%d", time.Now().Unix()),
        Type:        "withdrawal",
        Amount:      amount,
        Timestamp:   time.Now(),
        Description: description,
    }
    ba.Transactions = append(ba.Transactions, transaction)

    return nil
}

// Method untuk mendapatkan riwayat transaksi
func (ba *BankAccount) GetTransactionHistory() []Transaction {
    return ba.Transactions
}

// Method untuk menonaktifkan akun
func (ba *BankAccount) DeactivateAccount() {
    ba.IsActive = false
}

func main() {
    // Buat akun baru
    account := NewBankAccount("ACC001", "John Doe")
    
    fmt.Println("=== Initial Account Info ===")
    fmt.Println(account.GetAccountInfo())

    // Lakukan deposit
    err := account.Deposit(1000, "Initial deposit")
    if err != nil {
        fmt.Println("Deposit error:", err)
    } else {
        fmt.Println("Deposit successful")
    }

    // Lakukan withdrawal
    err = account.Withdraw(250, "ATM withdrawal")
    if err != nil {
        fmt.Println("Withdrawal error:", err)
    } else {
        fmt.Println("Withdrawal successful")
    }

    fmt.Println("\n=== Updated Account Info ===")
    fmt.Println(account.GetAccountInfo())

    // Tampilkan riwayat transaksi
    fmt.Println("\n=== Transaction History ===")
    for _, txn := range account.GetTransactionHistory() {
        fmt.Printf("%s: %s %.2f - %s (%s)\n",
            txn.ID, txn.Type, txn.Amount, txn.Description,
            txn.Timestamp.Format("2006-01-02 15:04:05"))
    }
}

Best Practices untuk Structs dan Methods di Go

1. Naming Conventions

// Good: PascalCase untuk exported structs
type UserAccount struct {
    ID   int
    Name string
}

// Good: camelCase untuk unexported structs
type internalConfig struct {
    apiKey string
    debug  bool
}

2. Constructor Functions

// Gunakan constructor functions untuk validasi
func NewUser(name, email string) (*User, error) {
    if name == "" {
        return nil, errors.New("name cannot be empty")
    }
    if !isValidEmail(email) {
        return nil, errors.New("invalid email format")
    }
    
    return &User{
        Name:      name,
        Email:     email,
        CreatedAt: time.Now(),
    }, nil
}

3. Method Naming

// Good: Gunakan verb untuk methods yang melakukan action
func (u *User) UpdateEmail(email string) error { ... }
func (u *User) Validate() error { ... }

// Good: Gunakan adjective/noun untuk methods yang mengembalikan informasi
func (u User) IsActive() bool { ... }
func (u User) GetFullName() string { ... }

4. Konsistensi Receiver Type

// Good: Konsisten menggunakan pointer receiver
func (u *User) SetName(name string) { ... }
func (u *User) SetEmail(email string) { ... }
func (u *User) GetName() string { ... } // Tetap pointer untuk konsistensi

Performance Tips

1. Struct Size Optimization

// Kurang optimal (24 bytes dengan padding)
type BadStruct struct {
    A bool   // 1 byte + 7 padding
    B int64  // 8 bytes
    C bool   // 1 byte + 7 padding
}

// Lebih optimal (16 bytes)
type GoodStruct struct {
    B int64  // 8 bytes
    A bool   // 1 byte
    C bool   // 1 byte + 6 padding
}

2. Pointer vs Value

// Untuk struct kecil, value receiver lebih efisien
type SmallStruct struct {
    X, Y int
}

func (s SmallStruct) Distance() float64 { ... }

// Untuk struct besar, pointer receiver lebih efisien
type LargeStruct struct {
    Data [1000]int
}

func (s *LargeStruct) Process() { ... }

Kesimpulan

Structs dan methods di Go menyediakan cara yang elegant dan efisien untuk menerapkan prinsip-prinsip OOP. Meskipun berbeda dari pendekatan tradisional dengan class dan inheritance, composition-based approach Go memberikan fleksibilitas yang lebih besar dan menghindari complexity yang tidak perlu.

Key Points yang perlu diingat:

  1. Structs adalah cara Go mengorganisir data terkait
  2. Methods memberikan behavior pada structs melalui receivers
  3. Composition over inheritance membuat kode lebih maintainable
  4. Pointer vs value receivers memiliki use case yang berbeda
  5. Embedded structs memungkinkan code reuse yang efektif
  6. Struct tags berguna untuk metadata dan serialization

Dengan memahami konsep-konsep ini, Anda dapat mulai membangun aplikasi Go yang lebih terstruktur dan maintainable. Di artikel selanjutnya, kita akan membahas Interfaces di Go yang akan melengkapi pemahaman OOP di Go dengan konsep duck typing yang powerful.

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *