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
- Gunakan pointer receiver jika:
- Method perlu memodifikasi receiver
- Struct berukuran besar (efisiensi memori)
- Consistency (jika ada satu method pointer receiver, sebaiknya semua menggunakan pointer)
- 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:
- Structs adalah cara Go mengorganisir data terkait
- Methods memberikan behavior pada structs melalui receivers
- Composition over inheritance membuat kode lebih maintainable
- Pointer vs value receivers memiliki use case yang berbeda
- Embedded structs memungkinkan code reuse yang efektif
- 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.