Struct #
Go tidak punya class. Bukan karena lupa, tapi karena keputusan desain yang sangat disengaja. Class dalam OOP membawa serta inheritance — dan inheritance membawa kompleksitas yang sering melebihi manfaatnya: fragile base class problem, diamond inheritance, coupling yang erat antara parent dan child. Go memilih jalan yang berbeda: komposisi. Alih-alih mewarisi perilaku dari class lain, kamu membangun tipe baru dengan menggabungkan tipe-tipe yang sudah ada. struct adalah fondasi dari semua ini — dan ketika dikombinasikan dengan method dan interface, ia mampu mengekspresikan semua pola OOP yang dibutuhkan tanpa kompleksitas yang menyertainya.
Mendefinisikan Struct #
Struct adalah kumpulan field yang dikelompokkan menjadi satu tipe. Definisikan dengan keyword type dan struct:
type Person struct {
// Exported fields — huruf besar, bisa diakses dari package lain
Name string
Age int
Email string
// Unexported fields — huruf kecil, hanya dalam package ini
password string
loginAt time.Time
}
// Struct bisa bersarang
type Address struct {
Street string
City string
Province string
ZipCode string
}
type Employee struct {
Name string
Department string
Salary float64
Address Address // struct sebagai field (bukan embedded — punya nama)
JoinDate time.Time
}
Pemisahan exported dan unexported field bukan sekadar access control — ini adalah cara mendefinisikan API publik struct. Exported field adalah bagian yang kamu janjikan kepada pengguna package. Unexported field adalah implementation detail yang bebas kamu ubah kapan saja.
Cara Inisialisasi Struct #
Ada beberapa cara membuat instance struct, masing-masing dengan kelebihan tersendiri.
Named Fields — Cara yang Direkomendasikan #
p := Person{
Name: "Budi Santoso",
Age: 28,
Email: "[email protected]",
}
Gunakan named fields hampir selalu. Alasannya: jika struct ditambah field baru di kemudian hari, kode ini tetap valid dan compiler tidak akan error.
Positional — Hindari untuk Struct dengan Lebih dari 2 Field #
// ANTI-PATTERN: rapuh terhadap perubahan struct
p := Person{"Budi", 28, "[email protected]", "", time.Time{}}
// Jika urutan field diubah atau field baru ditambah di tengah,
// semua inisialisasi positional akan memberikan nilai yang salah
// tanpa ada compile error!
Zero Value — Semua Field Default #
var p Person
// p.Name = ""
// p.Age = 0
// p.Email = ""
// p.password = ""
// p.loginAt = time.Time{} (zero time)
Zero value sangat berguna ketika struct dirancang dengan baik — zero value-nya sudah merupakan state yang valid.
Address Literal — Langsung Pointer #
// Menghasilkan *Person, bukan Person
p := &Person{
Name: "Budi",
Age: 28,
Email: "[email protected]",
}
// p adalah *Person
fmt.Println(p.Name) // Go auto-dereference: tidak perlu (*p).Name
new() — Pointer ke Zero Value
#
p := new(Person) // setara dengan &Person{}
p.Name = "Budi" // isi field satu per satu
p.Age = 28
Dalam praktiknya, &Person{...} lebih umum dari new(Person) karena bisa langsung diisi nilainya.
Struct adalah Value Type #
Ini perbedaan penting dari bahasa OOP lain: struct di Go adalah value type. Ketika kamu assign struct ke variabel lain atau pass ke fungsi, Go membuat salinan lengkap dari semua field-nya:
type Point struct {
X, Y int
}
p1 := Point{X: 1, Y: 2}
p2 := p1 // salinan lengkap — p2 adalah COPY dari p1
p2.X = 99
fmt.Println(p1) // {1 2} — tidak berubah!
fmt.Println(p2) // {99 2}
Implikasinya untuk fungsi:
// ANTI-PATTERN: memodifikasi struct dalam fungsi tidak berpengaruh ke aslinya
func setName(p Person, name string) {
p.Name = name // hanya memodifikasi salinan lokal
}
func main() {
p := Person{Name: "Budi"}
setName(p, "Sari")
fmt.Println(p.Name) // "Budi" — tidak berubah!
}
// Solusi 1: return struct baru (idiomatic untuk perubahan kecil)
func withName(p Person, name string) Person {
p.Name = name // modifikasi salinan
return p // kembalikan salinan yang sudah dimodifikasi
}
// Solusi 2: terima pointer (idiomatic untuk struct besar atau banyak modifikasi)
func setNamePtr(p *Person, name string) {
p.Name = name // modifikasi struct asli
}
Method — Value Receiver vs Pointer Receiver #
Method di Go dikaitkan dengan tipe melalui receiver — argumen ekstra sebelum nama method.
Value Receiver — Bekerja pada Salinan #
type Rectangle struct {
Width, Height float64
}
// Value receiver (r Rectangle) — r adalah salinan
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
func (r Rectangle) Scale(factor float64) Rectangle {
// Kembalikan struct baru — tidak memodifikasi aslinya
return Rectangle{
Width: r.Width * factor,
Height: r.Height * factor,
}
}
Pointer Receiver — Bekerja pada Aslinya #
// Pointer receiver (*Rectangle) — memodifikasi struct asli
func (r *Rectangle) ScaleInPlace(factor float64) {
r.Width *= factor
r.Height *= factor
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
// Value receiver — tidak mengubah rect
bigger := rect.Scale(2)
fmt.Println(rect) // {10 5} — tidak berubah
fmt.Println(bigger) // {20 10}
// Pointer receiver — mengubah rect
rect.ScaleInPlace(2)
fmt.Println(rect) // {20 10} — berubah!
}
Panduan Memilih Receiver #
GUNAKAN VALUE RECEIVER jika:
✓ Method hanya membaca data (getter, kalkulasi)
✓ Struct kecil dan murah di-copy (Point, Color, Size)
✓ Tipe yang dirancang immutable (seperti time.Time)
✓ Method mengembalikan nilai baru bukan memodifikasi
GUNAKAN POINTER RECEIVER jika:
✓ Method memodifikasi struct
✓ Struct besar (banyak field, mahal di-copy)
✓ Struct mengandung sync.Mutex atau field yang tidak boleh di-copy
✓ Konsistensi — jika ada satu method pakai pointer, semua pakai pointer
ATURAN KONSISTENSI (paling penting):
Jika ada satu method menggunakan pointer receiver, SEMUA method
pada tipe itu sebaiknya menggunakan pointer receiver.
Jangan campur-aduk kecuali ada alasan yang sangat kuat.
Jangan campur value receiver dan pointer receiver dalam satu tipe. Ini menyebabkan kebingungan tentang method mana yang “aman” untuk dipanggil pada value vs pointer, dan bisa menyebabkan bug halus terkait interface satisfaction.
// ANTI-PATTERN: campur receiver type Counter struct{ count int } func (c Counter) Value() int { return c.count } // value receiver func (c *Counter) Increment() { c.count++ } // pointer receiver func (c *Counter) Reset() { c.count = 0 } // pointer receiver // BENAR: konsisten dengan pointer receiver func (c *Counter) Value() int { return c.count } func (c *Counter) Increment() { c.count++ } func (c *Counter) Reset() { c.count = 0 }
Embedding — Komposisi, Bukan Inheritance #
Embedding memungkinkan satu struct “menyertakan” struct lain — semua field dan method dari struct yang di-embed bisa diakses langsung, seolah-olah milik struct pembungkus. Ini adalah cara Go mengekspresikan “is-a” relationship tanpa inheritance:
type Animal struct {
Name string
Age int
}
func (a *Animal) Breathe() {
fmt.Printf("%s sedang bernapas\n", a.Name)
}
func (a *Animal) Describe() string {
return fmt.Sprintf("%s (umur %d tahun)", a.Name, a.Age)
}
// Dog "meng-embed" Animal — bukan "extends" Animal
type Dog struct {
Animal // embedded tanpa nama field
Breed string
Trained bool
}
func (d *Dog) Bark() {
fmt.Printf("%s menggonggong!\n", d.Name) // akses d.Animal.Name langsung
}
func main() {
d := Dog{
Animal: Animal{Name: "Buddy", Age: 3},
Breed: "Labrador",
Trained: true,
}
// Akses field Animal langsung (promoted field)
fmt.Println(d.Name) // "Buddy" — setara d.Animal.Name
fmt.Println(d.Age) // 3
// Akses method Animal langsung (promoted method)
d.Breathe() // "Buddy sedang bernapas"
fmt.Println(d.Describe()) // "Buddy (umur 3 tahun)"
// Method Dog sendiri
d.Bark() // "Buddy menggonggong!"
// Akses eksplisit jika perlu
fmt.Println(d.Animal.Name) // sama dengan d.Name
}
Override Method dari Embedded Struct #
Struct yang me-embed bisa mendefinisikan method dengan nama yang sama untuk “menimpa” method dari embedded struct:
type Base struct {
ID int
}
func (b Base) Describe() string {
return fmt.Sprintf("Base ID: %d", b.ID)
}
type Extended struct {
Base
Name string
}
// Override Describe — Extended punya implementasi sendiri
func (e Extended) Describe() string {
return fmt.Sprintf("%s (ID: %d)", e.Name, e.ID)
}
func main() {
e := Extended{Base: Base{ID: 42}, Name: "Server A"}
fmt.Println(e.Describe()) // "Server A (ID: 42)" — versi Extended
fmt.Println(e.Base.Describe()) // "Base ID: 42" — akses eksplisit versi Base
}
Embedding Multiple Struct #
type Logger struct{}
func (l Logger) Log(msg string) { fmt.Println("[LOG]", msg) }
type Metrics struct{}
func (m Metrics) Record(key string, val float64) {
fmt.Printf("[METRIC] %s = %.2f\n", key, val)
}
// Service memiliki kemampuan logging dan metrics
type Service struct {
Logger
Metrics
Name string
}
func (s *Service) Process(data string) {
s.Log("memproses: " + data)
// lakukan sesuatu...
s.Record("processing_time", 0.025)
}
Struct Tags #
Struct tags adalah metadata yang ditambahkan ke field — string literal yang muncul setelah tipe field. Paling umum digunakan untuk JSON serialization, database mapping, dan validasi:
import (
"encoding/json"
"time"
)
type User struct {
ID int `json:"id" db:"id"`
Username string `json:"username" db:"username"`
Email string `json:"email" db:"email"`
Password string `json:"-" db:"password_hash"`
// json:"-" → abaikan field ini saat marshal/unmarshal JSON
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at,omitempty" db:"updated_at"`
// omitempty → abaikan jika nilai zero value
IsAdmin bool `json:"is_admin" db:"is_admin"`
Score float64 `json:"score,omitempty" db:"score"`
}
JSON Serialization dengan Tags #
func main() {
user := User{
ID: 1,
Username: "budi99",
Email: "[email protected]",
Password: "hashedpassword123",
IsAdmin: false,
}
// Struct → JSON
data, err := json.Marshal(user)
if err != nil {
panic(err)
}
fmt.Println(string(data))
// Output: {"id":1,"username":"budi99","email":"[email protected]",
// "created_at":"0001-01-01T00:00:00Z","is_admin":false}
// Password tidak muncul (json:"-")
// UpdatedAt tidak muncul (omitempty + zero value)
// Score tidak muncul (omitempty + zero value 0.0)
// JSON → Struct
jsonStr := `{"id":2,"username":"sari","email":"[email protected]","is_admin":true}`
var user2 User
if err := json.Unmarshal([]byte(jsonStr), &user2); err != nil {
panic(err)
}
fmt.Printf("User: %s, Admin: %v\n", user2.Username, user2.IsAdmin)
}
Tag untuk Validasi #
// Dengan library go-playground/validator
type CreateUserRequest struct {
Username string `json:"username" validate:"required,min=3,max=50,alphanum"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8,max=128"`
Age int `json:"age" validate:"min=0,max=150"`
}
Anonymous Struct #
Anonymous struct adalah struct tanpa nama tipe — dideklarasikan dan dipakai langsung. Berguna untuk data sementara yang tidak perlu tipe reusable:
// Config inline — tidak perlu definisi tipe terpisah
config := struct {
Host string
Port int
Debug bool
Timeout time.Duration
}{
Host: "localhost",
Port: 5432,
Debug: true,
Timeout: 30 * time.Second,
}
fmt.Printf("Connect to %s:%d\n", config.Host, config.Port)
// Table-driven tests — pola yang sangat umum di Go
tests := []struct {
name string
input string
expected int
wantErr bool
}{
{name: "valid number", input: "42", expected: 42, wantErr: false},
{name: "negative", input: "-1", expected: -1, wantErr: false},
{name: "invalid string", input: "abc", expected: 0, wantErr: true},
{name: "empty string", input: "", expected: 0, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := strconv.Atoi(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("wantErr %v, got err %v", tt.wantErr, err)
}
if got != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, got)
}
})
}
Struct Comparability #
Struct bisa dibandingkan dengan == dan != hanya jika semua field-nya comparable. Field bertipe slice, map, atau function membuat struct tidak comparable:
type Point struct {
X, Y int
}
p1 := Point{1, 2}
p2 := Point{1, 2}
p3 := Point{3, 4}
fmt.Println(p1 == p2) // true — semua field sama
fmt.Println(p1 == p3) // false — field berbeda
fmt.Println(p1 != p3) // true
// Struct dengan field non-comparable tidak bisa dibandingkan
type Container struct {
Items []int // slice tidak comparable
}
c1 := Container{Items: []int{1, 2, 3}}
c2 := Container{Items: []int{1, 2, 3}}
// fmt.Println(c1 == c2) // ← compile error: struct containing []int cannot be compared
// Untuk struct non-comparable, gunakan reflect.DeepEqual
import "reflect"
fmt.Println(reflect.DeepEqual(c1, c2)) // true
Constructor Pattern #
Go tidak punya constructor bawaan. Konvensi yang sangat umum adalah membuat fungsi NewXxx() yang mengembalikan instance (biasanya pointer) yang sudah tervalidasi dan terinisialisasi dengan benar:
type Server struct {
host string
port int
timeout time.Duration
maxConn int
logger *Logger
}
// Constructor sederhana
func NewServer(host string, port int) *Server {
return &Server{
host: host,
port: port,
timeout: 30 * time.Second, // default yang masuk akal
maxConn: 100,
logger: defaultLogger,
}
}
// Functional options pattern — untuk banyak opsi opsional
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) { s.timeout = d }
}
func WithMaxConn(n int) Option {
return func(s *Server) { s.maxConn = n }
}
func WithLogger(l *Logger) Option {
return func(s *Server) { s.logger = l }
}
func NewServerWithOptions(host string, port int, opts ...Option) *Server {
s := &Server{
host: host,
port: port,
timeout: 30 * time.Second,
maxConn: 100,
}
for _, opt := range opts {
opt(s)
}
return s
}
// Penggunaan — sangat expressive
server := NewServerWithOptions(
"localhost", 8080,
WithTimeout(60 * time.Second),
WithMaxConn(500),
WithLogger(customLogger),
)
Contoh Program Lengkap #
Program berikut membangun sistem manajemen perpustakaan yang menggunakan berbagai konsep struct:
package main
import (
"fmt"
"strings"
"time"
)
// ── Tipe Dasar ─────────────────────────────────────────────
type BookID int
type MemberID int
type Author struct {
Name string
Nationality string
}
func (a Author) String() string {
return fmt.Sprintf("%s (%s)", a.Name, a.Nationality)
}
type Book struct {
ID BookID
Title string
Author Author
ISBN string
Year int
Available bool
Tags []string
}
func NewBook(id BookID, title string, author Author, isbn string, year int) *Book {
return &Book{
ID: id,
Title: title,
Author: author,
ISBN: isbn,
Year: year,
Available: true,
}
}
func (b *Book) Checkout() error {
if !b.Available {
return fmt.Errorf("buku %q sedang dipinjam", b.Title)
}
b.Available = false
return nil
}
func (b *Book) Return() {
b.Available = true
}
func (b Book) String() string {
status := "tersedia"
if !b.Available {
status = "dipinjam"
}
return fmt.Sprintf("[%d] %q oleh %s (%d) — %s",
b.ID, b.Title, b.Author.Name, b.Year, status)
}
// ── Member dengan Embedding ─────────────────────────────────
type Person struct {
Name string
Email string
Phone string
}
func (p Person) ContactInfo() string {
return fmt.Sprintf("%s <%s>", p.Name, p.Email)
}
type Member struct {
Person // embedding — Member "adalah" Person
ID MemberID
JoinDate time.Time
BorrowedBooks []*Book
}
func NewMember(id MemberID, name, email, phone string) *Member {
return &Member{
Person: Person{Name: name, Email: email, Phone: phone},
ID: id,
JoinDate: time.Now(),
}
}
// Override ContactInfo dengan informasi tambahan
func (m Member) ContactInfo() string {
return fmt.Sprintf("%s <%s> (ID: %d)", m.Name, m.Email, m.ID)
}
func (m *Member) Borrow(book *Book) error {
if len(m.BorrowedBooks) >= 3 {
return fmt.Errorf("%s sudah meminjam 3 buku (batas maksimum)", m.Name)
}
if err := book.Checkout(); err != nil {
return err
}
m.BorrowedBooks = append(m.BorrowedBooks, book)
fmt.Printf("✓ %s meminjam %q\n", m.Name, book.Title)
return nil
}
func (m *Member) ReturnBook(bookID BookID) error {
for i, b := range m.BorrowedBooks {
if b.ID == bookID {
b.Return()
m.BorrowedBooks = append(m.BorrowedBooks[:i], m.BorrowedBooks[i+1:]...)
fmt.Printf("✓ %s mengembalikan %q\n", m.Name, b.Title)
return nil
}
}
return fmt.Errorf("buku ID %d tidak ditemukan dalam pinjaman %s", bookID, m.Name)
}
func (m Member) Status() string {
if len(m.BorrowedBooks) == 0 {
return fmt.Sprintf("%s — tidak meminjam buku", m.Name)
}
titles := make([]string, len(m.BorrowedBooks))
for i, b := range m.BorrowedBooks {
titles[i] = fmt.Sprintf("%q", b.Title)
}
return fmt.Sprintf("%s — meminjam: %s", m.Name, strings.Join(titles, ", "))
}
// ── Library ─────────────────────────────────────────────────
type Library struct {
Name string
Books map[BookID]*Book
Members map[MemberID]*Member
}
func NewLibrary(name string) *Library {
return &Library{
Name: name,
Books: make(map[BookID]*Book),
Members: make(map[MemberID]*Member),
}
}
func (l *Library) AddBook(book *Book) {
l.Books[book.ID] = book
}
func (l *Library) RegisterMember(member *Member) {
l.Members[member.ID] = member
}
func (l *Library) Report() {
available := 0
for _, b := range l.Books {
if b.Available {
available++
}
}
fmt.Printf("\n=== Laporan %s ===\n", l.Name)
fmt.Printf("Total buku : %d (%d tersedia, %d dipinjam)\n",
len(l.Books), available, len(l.Books)-available)
fmt.Printf("Total anggota: %d\n\n", len(l.Members))
fmt.Println("Koleksi Buku:")
for _, b := range l.Books {
fmt.Printf(" %s\n", b)
}
fmt.Println("\nStatus Anggota:")
for _, m := range l.Members {
fmt.Printf(" %s\n", m.Status())
}
}
func main() {
lib := NewLibrary("Perpustakaan Go")
// Tambah buku menggunakan constructor
books := []*Book{
NewBook(1, "The Go Programming Language",
Author{"Alan Donovan", "Amerika"}, "978-0134190440", 2015),
NewBook(2, "Go in Action",
Author{"William Kennedy", "Amerika"}, "978-1617291784", 2015),
NewBook(3, "Concurrency in Go",
Author{"Katherine Cox-Buday", "Amerika"}, "978-1491941195", 2017),
NewBook(4, "Clean Code",
Author{"Robert Martin", "Amerika"}, "978-0132350884", 2008),
}
for _, b := range books {
lib.AddBook(b)
}
// Daftarkan anggota
members := []*Member{
NewMember(1, "Budi Santoso", "[email protected]", "081234567890"),
NewMember(2, "Sari Dewi", "[email protected]", "082345678901"),
}
for _, m := range members {
lib.RegisterMember(m)
}
// Simulasi peminjaman
fmt.Println("=== Aktivitas Peminjaman ===")
budi := members[0]
sari := members[1]
// Budi meminjam dua buku
if err := budi.Borrow(books[0]); err != nil {
fmt.Println("Error:", err)
}
if err := budi.Borrow(books[2]); err != nil {
fmt.Println("Error:", err)
}
// Sari mencoba meminjam buku yang sudah dipinjam Budi
if err := sari.Borrow(books[0]); err != nil {
fmt.Println("✗ Error:", err)
}
// Sari meminjam buku lain
if err := sari.Borrow(books[1]); err != nil {
fmt.Println("Error:", err)
}
// Budi mengembalikan satu buku
if err := budi.ReturnBook(1); err != nil {
fmt.Println("Error:", err)
}
// Sekarang Sari bisa meminjam buku yang tadi tidak tersedia
if err := sari.Borrow(books[0]); err != nil {
fmt.Println("Error:", err)
}
// ContactInfo menggunakan embedded Person.ContactInfo dan override
fmt.Printf("\nInfo Kontak Budi (Person): %s\n", budi.Person.ContactInfo())
fmt.Printf("Info Kontak Budi (Member): %s\n", budi.ContactInfo())
// Laporan akhir
lib.Report()
}
Ringkasan #
- Go tidak punya class — struct + method + interface menggantikan class OOP dengan cara yang lebih eksplisit dan lebih mudah di-compose.
- Gunakan named fields saat inisialisasi — lebih aman dan tahan terhadap perubahan struct.
- Struct adalah value type — assignment dan pass ke fungsi membuat salinan; gunakan pointer untuk struct besar atau ketika perlu memodifikasi aslinya.
- Value receiver untuk method yang membaca; pointer receiver untuk method yang memodifikasi atau struct besar. Pilih satu dan konsisten untuk seluruh tipe.
- Embedding adalah komposisi, bukan inheritance — promoted fields dan methods membuat kode lebih ekspresif tanpa coupling yang erat.
- Struct tags (
json:"name",db:"column") adalah metadata untuk serialization dan mapping — wajib dipakai untuk API dan database.json:"-"mengecualikan field dari JSON;omitemptymengabaikan zero value saat marshal.- Anonymous struct berguna untuk konfigurasi inline dan table-driven tests.
- Constructor pattern
NewXxx()untuk memastikan struct selalu dibuat dalam state yang valid.- Functional options pattern untuk constructor dengan banyak opsi opsional.