1 Dasar-dasar Struct

Dalam bahasa Go, struct adalah tipe data komposit yang digunakan untuk menggabungkan tipe data yang berbeda atau identik menjadi satu entitas tunggal. Struct memiliki posisi penting dalam Go karena mereka berfungsi sebagai aspek fundamental dari pemrograman berorientasi objek, meskipun dengan perbedaan sedikit dari bahasa pemrograman berorientasi objek tradisional.

Kebutuhan akan struct muncul dari aspek-aspek berikut:

  • Mengorganisir variabel-variabel dengan keterkaitan yang kuat untuk meningkatkan keterjagaan kode.
  • Memberikan cara untuk mensimulasikan "kelas", memfasilitasi fitur enkapsulasi dan agregasi.
  • Saat berinteraksi dengan struktur data seperti JSON, catatan database, dll., struct menawarkan alat pemetaan yang nyaman.

Mengorganisir data dengan struct memungkinkan representasi yang lebih jelas dari model objek dunia nyata seperti pengguna, pesanan, dll.

2 Mendefinisikan Struct

Syntax untuk mendefinisikan struct adalah sebagai berikut:

type NamaStruct struct {
    Field1 TipeField1
    Field2 TipeField2
    // ... variabel anggota lainnya
}
  • Kata kunci type memperkenalkan definisi struct.
  • NamaStruct adalah nama tipe struct, mengikuti konvensi penamaan Go, biasanya kapital untuk menunjukkan keboleh dipakaiannya.
  • Kata kunci struct menandakan bahwa ini adalah tipe struct.
  • Dalam kurung kurawal {}, variabel anggota (field) dari struct didefinisikan, dengan masing-masing diikuti oleh tipe nya.

Tipe dari anggota struct bisa berupa tipe apa saja, termasuk tipe dasar (seperti int, string, dll.) dan tipe kompleks (seperti array, slice, struct lain, dll.).

Sebagai contoh, mendefinisikan struct yang mewakili seorang individu:

type Person struct {
    Name   string
    Age    int
    Emails []string // bisa mencakup tipe kompleks, seperti slice
}

Dalam kode di atas, struct Person memiliki tiga variabel anggota: Name bertipe string, Age bertipe integer, dan Emails bertipe slice string, menunjukkan bahwa seseorang mungkin memiliki beberapa alamat email.

3 Membuat dan Menginisialisasi Struct

3.1 Membuat Instansi Struct

Terdapat dua cara untuk membuat instansi struct: deklarasi langsung atau menggunakan kata kunci new.

Deklarasi langsung:

var p Person

Kode di atas membuat sebuah instansi p dengan tipe Person, di mana setiap variabel anggota dari struct tersebut adalah nilai nol dari tipe nya masing-masing.

Menggunakan kata kunci new:

p := new(Person)

Membuat struct menggunakan kata kunci new menghasilkan pointer ke struct tersebut. Variabel p pada titik ini memiliki tipe *Person, menunjuk ke variabel yang baru dialokasikan dengan tipe Person di mana variabel anggotanya telah diinisialisasi dengan nilai nol.

3.2 Menginisialisasi Instansi Struct

Instansi struct dapat diinisialisasi sekaligus saat mereka dibuat, menggunakan dua metode: dengan nama-nama field atau tanpa nama-nama field.

Inisialisasi dengan Nama-Nama Field:

p := Person{
    Name:   "Alice",
    Age:    30,
    Emails: []string{"[email protected]", "[email protected]"},
}

Saat menginisialisasi dengan bentuk assignment field, urutan inisialisasi tidak perlu sama dengan urutan pendeklarasian struct, dan setiap field yang belum diinisialisasi akan tetap mempertahankan nilai-nolnya.

Inisialisasi tanpa Nama-Nama Field:

p := Person{"Bob", 25, []string{"[email protected]"}}

Saat menginisialisasi tanpa nama-nama field, pastikan nilai awal dari setiap variabel anggota berada dalam urutan yang sama seperti saat struct didefinisikan, dan tidak ada field yang dapat dihilangkan.

Selain itu, struct dapat diinisialisasi dengan field-field tertentu, dan setiap field yang tidak spesifik akan mengambil nilai-nol:

p := Person{Name: "Charlie"}

Dalam contoh ini, hanya field Name yang diinisialisasi, sementara Age dan Emails akan keduanya mengambil nilai-nol masing-masing.

4 Mengakses Anggota Struct

Mengakses variabel anggota dari sebuah struct dalam Go sangat mudah, dicapai dengan menggunakan operator titik (.). Jika Anda memiliki variabel struct, Anda dapat membaca atau memodifikasi nilainya dengan cara ini.

5 Komposisi dan Penanaman Struct

Struct tidak hanya dapat ada secara independen tetapi juga dapat disusun dan ditanam bersama untuk membuat struktur data yang lebih kompleks.

5.1 Struct Tanpa Nama

Struct tanpa nama tidak secara eksplisit mendeklarasikan tipe baru, tetapi langsung menggunakan definisi struct. Ini berguna ketika Anda perlu membuat struct sekali dan menggunakannya secara sederhana, menghindari penciptaan tipe yang tidak perlu.

Contoh:

package main

import "fmt"

func main() {
    // Mendefinisikan dan menginisialisasi struct tanpa nama
    person := struct {
        Name string
        Age  int
    }{
        Name: "Eve",
        Age:  40,
    }

    // Mengakses member dari struct tanpa nama
    fmt.Println("Nama:", person.Name)
    fmt.Println("Usia:", person.Age)
}

Dalam contoh ini, daripada membuat tipe baru, kita langsung mendefinisikan struct dan membuat contohnya. Contoh ini menunjukkan cara menginisialisasi struct tanpa nama dan mengakses membernya.

5.2 Penanaman Struct

Penanaman struct melibatkan menanamkan satu struct sebagai anggota dari struct lain. Hal ini memungkinkan kita membangun model data yang lebih kompleks.

Contoh:

package main

import "fmt"

// Mendefinisikan struct Address
type Address struct {
    City    string
    Country string
}

// Menanamkan struct Address dalam struct Person
type Person struct {
    Name    string
    Age     int
    Address Address
}

func main() {
    // Menginisialisasi instance Person
    p := Person{
        Name: "Charlie",
        Age:  28,
        Address: Address{
            City:    "New York",
            Country: "Amerika Serikat",
        },
    }

    // Mengakses member dari struct yang ditanamkan
    fmt.Println("Nama:", p.Name)
    fmt.Println("Usia:", p.Age)
    // Mengakses member dari struct Address
    fmt.Println("Kota:", p.Address.City)
    fmt.Println("Negara:", p.Address.Country)
}

Dalam contoh ini, kita mendefinisikan struct Address dan menanamkannya sebagai anggota dalam struct Person. Ketika membuat contoh Person, kita juga membuat contoh Address secara bersamaan. Kita dapat mengakses member dari struct yang ditanamkan menggunakan notasi titik.

6 Metode Struct

Fitur pemrograman berbasis objek (OOP) dapat diimplementasikan melalui metode struct.

6.1 Konsep Dasar Metode

Dalam bahasa Go, meskipun tidak ada konsep kelas dan objek tradisional, fitur OOP yang serupa dapat dicapai dengan mengaitkan metode ke struct. Metode struct adalah jenis fungsi khusus yang terkait dengan tipe struct tertentu (atau pointer ke struct), memungkinkan tipe tersebut memiliki seperangkat metode sendiri.

// Mendefinisikan struct sederhana
type Rectangle struct {
    panjang, lebar float64
}

// Mendefinisikan metode untuk struct Rectangle untuk menghitung luas persegi
func (r Rectangle) Area() float64 {
    return r.panjang * r.lebar
}

Pada kode di atas, metode Area terkait dengan struct Rectangle. Dalam definisi metode, (r Rectangle) adalah penerima, yang menentukan bahwa metode ini terkait dengan tipe Rectangle. Penerima muncul sebelum nama metode.

6.2 Penerima Nilai dan Penerima Pointer

Metode dapat dikategorikan sebagai penerima nilai dan penerima pointer berdasarkan jenis receiver-nya. Penerima nilai menggunakan salinan dari struk untuk memanggil metode, sedangkan penerima pointer menggunakan pointer ke struk dan dapat memodifikasi struk asli.

// Mendefinisikan sebuah metode dengan penerima nilai
func (r Rectangle) Keliling() float64 {
    return 2 * (r.panjang + r.lebar)
}

// Mendefinisikan sebuah metode dengan penerima pointer, yang dapat memodifikasi struk
func (r *Rectangle) AturPanjang(panjangBaru float64) {
    r.panjang = panjangBaru // dapat memodifikasi nilai asli dari struk
}

Pada contoh di atas, Keliling adalah metode penerima nilai, memanggilnya tidak akan mengubah nilai dari Rectangle. Namun, AturPanjang adalah metode penerima pointer, dan memanggil metode ini akan memengaruhi instansi Rectangle asli.

6.3 Penggilan Metode

Anda dapat memanggil metode dari sebuah struk menggunakan variabel struk dan pointer-nya.

func main() {
    rect := Rectangle{panjang: 10, lebar: 5}

    // Memanggil metode dengan penerima nilai
    fmt.Println("Luas:", rect.Luas())

    // Memanggil metode dengan penerima nilai
    fmt.Println("Keliling:", rect.Keliling())

    // Memanggil metode dengan penerima pointer
    rect.AturPanjang(20)

    // Memanggil metode dengan penerima nilai lagi, perhatikan bahwa panjang telah dimodifikasi
    fmt.Println("Setelah dimodifikasi, Luas:", rect.Luas())
}

Saat Anda memanggil sebuah metode menggunakan sebuah pointer, Go secara otomatis menangani konversi antara nilai dan pointer, tanpa memperhatikan apakah metode Anda didefinisikan dengan penerima nilai atau penerima pointer.

6.4 Pemilihan Jenis Penerima

Ketika mendefinisikan metode, Anda harus memutuskan apakah akan menggunakan penerima nilai atau penerima pointer berdasarkan situasi. Berikut adalah beberapa pedoman umum:

  • Jika metode perlu memodifikasi isi struktur, gunakan penerima pointer.
  • Jika strukturnya besar dan biaya penyalinannya tinggi, gunakan penerima pointer.
  • Jika Anda ingin metode tersebut memodifikasi nilai yang ditunjuk oleh receiver, gunakan penerima pointer.
  • Secara efisiensi, bahkan jika Anda tidak memodifikasi isi struktur, wajar untuk menggunakan penerima pointer untuk struktur yang besar.
  • Untuk struktur yang kecil, atau saat hanya membaca data tanpa perlu modifikasi, penerima nilai seringkali lebih sederhana dan efisien.

Melalui metode struk, kita dapat mensimulasikan beberapa fitur pemrograman berorientasi objek dalam Go, seperti enkapsulasi dan metode. Pendekatan ini dalam Go menyederhanakan konsep objek sementara memberikan kemampuan yang cukup untuk mengatur dan mengelola fungsi terkait.

7 Struk dan Serialisasi JSON

Dalam Go, seringkali diperlukan untuk melakukan serialisasi struk menjadi format JSON untuk transmisi jaringan atau sebagai file konfigurasi. Demikian pula, kita juga perlu mampu melakukan deserialisasi JSON menjadi instansi struk. Paket encoding/json dalam Go menyediakan fungsionalitas ini.

Berikut adalah contoh bagaimana mengonversi antara sebuah struk dan JSON:

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

// Mendefinisikan struktur Person, dan menggunakan tag json untuk mendefinisikan pemetaan antara bidang struk dan nama bidang JSON
type Person struct {
	Name   string   `json:"name"`
	Age    int      `json:"age"`
	Emails []string `json:"emails,omitempty"`
}

func main() {
	// Membuat instansi baru dari Person
	p := Person{
		Name:   "John Doe",
		Age:    30,
		Emails: []string{"[email protected]", "[email protected]"},
	}

	// Serialisasi ke JSON
	dataJSON, err := json.Marshal(p)
	if err != nil {
		log.Fatalf("Pengelompokan JSON gagal: %s", err)
	}
	fmt.Printf("Format JSON: %s\n", dataJSON)

	// Deserialisasi menjadi sebuah struk
	var p2 Person
	if err := json.Unmarshal(dataJSON, &p2); err != nil {
		log.Fatalf("Pengelompokan JSON gagal: %s", err)
	}
	fmt.Printf("Struk Dipulihkan: %#v\n", p2)
}

Pada kode di atas, kita mendefinisikan struktur Person, termasuk sebuah bidang tipe slice dengan opsi "omitempty". Opsi ini menentukan bahwa jika bidangnya kosong atau tidak ada, itu tidak akan disertakan dalam JSON.

Kita menggunakan fungsi json.Marshal untuk melakukan serialisasi sebuah instansi struk menjadi JSON, dan fungsi json.Unmarshal untuk melakukan deserialisasi data JSON menjadi instansi struk.

8 Topik Lanjutan dalam Struk

8.1 Perbandingan Structs

Di Go, membandingkan dua instansi dari struct secara langsung diperbolehkan, namun perbandingan ini didasarkan pada nilai-nilai dari field-field di dalam struct. Jika semua nilai field sama, maka dua instansi dari struct dianggap sama. Perlu diketahui bahwa tidak semua tipe field dapat dibandingkan. Sebagai contoh, sebuah struct yang berisi slice tidak dapat dibandingkan secara langsung.

Berikut ini adalah contoh membandingkan structs:

package main

import "fmt"

type Point struct {
    X, Y int
}

func main() {
    p1 := Point{1, 2}
    p2 := Point{1, 2}
    p3 := Point{1, 3}

    fmt.Println("p1 == p2:", p1 == p2) // Output: p1 == p2: true
    fmt.Println("p1 == p3:", p1 == p3) // Output: p1 == p3: false
}

Dalam contoh ini, p1 dan p2 dianggap sama karena semua nilai field pada keduanya sama. Dan p3 tidak sama dengan p1 karena nilai dari Y berbeda.

8.2 Menyalin Structs

Di Go, instansi dari struct dapat disalin dengan menggunakan assignment. Apakah salinan ini merupakan deep copy atau shallow copy tergantung pada tipe-tipe field di dalam struct.

Jika struct hanya berisi tipe-tipe dasar (seperti int, string, dll.), maka salinannya akan menjadi deep copy. Jika struct berisi tipe-tipe referensi (seperti slice, map, dll.), maka salinannya akan menjadi shallow copy, dan instansi asli dan instansi yang disalin akan berbagi memori dari tipe-tipe referensi.

Berikut adalah contoh menyalin sebuah struct:

package main

import "fmt"

type Data struct {
    Numbers []int
}

func main() {
    // Inisialisasi sebuah instansi dari struct Data
    original := Data{Numbers: []int{1, 2, 3}}

    // Salin struct
    copied := original

    // Modifikasi elemen-elemen dari slice yang disalin
    copied.Numbers[0] = 100

    // Lihat elemen-elemen dari instansi original dan yang disalin
    fmt.Println("Original:", original.Numbers) // Output: Original: [100 2 3]
    fmt.Println("Copied:", copied.Numbers) // Output: Copied: [100 2 3]
}

Seperti yang ditunjukkan dalam contoh, instansi original dan copied berbagi slice yang sama, sehingga memodifikasi data slice di copied juga akan memengaruhi data slice di original.

Untuk menghindari masalah ini, Anda dapat mencapai deep copying yang sebenarnya dengan menyalin konten slice secara eksplisit ke slice baru:

newNumbers := make([]int, len(original.Numbers))
copy(newNumbers, original.Numbers)
copied := Data{Numbers: newNumbers}

Dengan cara ini, setiap modifikasi pada copied tidak akan memengaruhi original.