Array dalam Bahasa Go

1.1 Definisi dan Deklarasi Array

Array adalah urutan elemen berukuran tetap dengan tipe yang sama. Dalam bahasa Go, panjang array dianggap sebagai bagian dari tipe array. Hal ini berarti bahwa array dengan panjang yang berbeda dianggap sebagai tipe yang berbeda.

Sintaks dasar untuk mendeklarasikan sebuah array adalah sebagai berikut:

var arr [n]T

Di sini, var adalah kata kunci untuk deklarasi variabel, arr adalah nama array, n mewakili panjang array, dan T mewakili tipe elemen dalam array.

Sebagai contoh, untuk mendeklarasikan sebuah array yang berisi 5 bilangan bulat:

var myArray [5]int

Pada contoh ini, myArray adalah array yang dapat berisi 5 bilangan bulat dengan tipe int.

1.2 Inisialisasi dan Penggunaan Array

Inisialisasi array dapat dilakukan langsung pada saat deklarasi atau dengan memberikan nilai menggunakan indeks. Terdapat beberapa metode untuk inisialisasi array:

Inisialisasi Langsung

var myArray = [5]int{10, 20, 30, 40, 50}

Juga memungkinkan untuk membiarkan kompiler untuk menginfer panjang array berdasarkan jumlah nilai yang diinisialisasi:

var myArray = [...]int{10, 20, 30, 40, 50}

Di sini, ... mengindikasikan bahwa panjang array dihitung oleh kompiler.

Inisialisasi Menggunakan Indeks

var myArray [5]int
myArray[0] = 10
myArray[1] = 20
// Elemen-elemen yang tersisa diinisialisasi menjadi 0, karena nilai nol dari int adalah 0

Penggunaan array juga sederhana, dan elemen-elemennya dapat diakses menggunakan indeks:

fmt.Println(myArray[2]) // Mengakses elemen ketiga

1.3 Traversal Array

Dua metode umum untuk traversal array adalah menggunakan loop for tradisional dan menggunakan range.

Traversal Menggunakan Loop for

for i := 0; i < len(myArray); i++ {
    fmt.Println(myArray[i])
}

Traversal Menggunakan range

for index, value := range myArray {
    fmt.Printf("Indeks: %d, Nilai: %d\n", index, value)
}

Kelebihan menggunakan range adalah bahwa ia mengembalikan dua nilai: posisi indeks saat ini dan nilai pada posisi tersebut.

1.4 Karakteristik dan Batasan Array

Dalam bahasa Go, array adalah tipe nilai, yang berarti bahwa saat sebuah array dilewatkan sebagai parameter ke sebuah fungsi, salinan dari array tersebut dilewatkan. Oleh karena itu, jika modifikasi terhadap array asli diperlukan dalam sebuah fungsi, biasanya digunakan slice atau pointer ke array.

2 Slice dalam Bahasa Go

2.1 Konsep Slice

Dalam bahasa Go, slice adalah sebuah abstraksi atas array. Ukuran array dalam Go tidak dapat diubah, yang membatasi penggunaannya dalam beberapa skenario. Slice dalam Go dirancang untuk lebih fleksibel, menyediakan antarmuka yang nyaman, fleksibel, dan kuat untuk melakukan serialisasi struktur data. Slice sendiri tidak menyimpan data; mereka hanyalah referensi ke array yang mendasarinya. Sifat dinamisnya secara umum ditandai oleh beberapa poin berikut:

  • Ukuran Dinamis: Berbeda dengan array, panjang dari sebuah slice bersifat dinamis, memungkinkannya untuk tumbuh atau menyusut secara otomatis sesuai kebutuhan.
  • Fleksibilitas: Elemen-elemen dapat dengan mudah ditambahkan ke sebuah slice dengan menggunakan fungsi bawaan append.
  • Tipe Referensi: Slice mengakses elemen-elemen dalam array yang mendasarinya secara referensi, tanpa membuat salinan data.

2.2 Deklarasi dan Inisialisasi Slice

Sintaks untuk mendeklarasikan sebuah slice mirip dengan mendeklarasikan sebuah array, tetapi tidak perlu menentukan jumlah elemen saat mendeklarasikan. Sebagai contoh, cara untuk mendeklarasikan sebuah slice dari bilangan bulat adalah sebagai berikut:

var slice []int

Anda dapat menginisialisasi sebuah slice menggunakan literal slice:

slice := []int{1, 2, 3}

Variabel slice di atas akan diinisialisasi sebagai slice yang berisi tiga bilangan bulat.

Anda juga dapat menginisialisasi sebuah slice menggunakan fungsi make, yang memungkinkan Anda untuk menentukan panjang dan kapasitas dari slice tersebut:

slice := make([]int, 5)  // Membuat sebuah slice dari bilangan bulat dengan panjang dan kapasitas 5

Jika diperlukan kapasitas yang lebih besar, Anda dapat melewatkan kapasitas sebagai parameter ketiga ke fungsi make:

slice := make([]int, 5, 10)  // Membuat sebuah slice dari bilangan bulat dengan panjang 5 dan kapasitas 10

2.3 Hubungan Antara Slice dan Array

Slice dapat dibuat dengan menentukan segmen dari sebuah array, membentuk referensi ke segmen tersebut. Sebagai contoh, dengan menggunakan array berikut:

array := [5]int{10, 20, 30, 40, 50}

Kita dapat membuat sebuah slice sebagai berikut:

slice := array[1:4]

Slice slice ini akan merujuk pada elemen-elemen dalam array array mulai dari indeks 1 hingga indeks 3 (inklusif pada indeks 1, namun eksklusif pada indeks 4).

Penting untuk dicatat bahwa slice tidak benar-benar menyalin nilai dari array; ia hanya menunjuk pada segmen kontinu dari array asli. Oleh karena itu, modifikasi pada slice juga akan mempengaruhi array yang mendasarinya, dan sebaliknya. Memahami hubungan referensi ini merupakan hal penting untuk menggunakan slice dengan efektif.

2.4 Operasi Dasar pada Slice

2.4.1 Pengindeksan

Slice mengakses elemennya menggunakan indeks, mirip dengan array, dengan indeks dimulai dari 0. Sebagai contoh:

slice := []int{10, 20, 30, 40}
// Mengakses elemen pertama dan ketiga
fmt.Println(slice[0], slice[2])

2.4.2 Panjang dan Kapasitas

Slice memiliki dua properti: panjang (len) dan kapasitas (cap). Panjang adalah jumlah elemen dalam slice, dan kapasitas adalah jumlah elemen dari elemen pertama slice hingga akhir dari array yang mendasarinya.

slice := []int{10, 20, 30, 40}
// Mencetak panjang dan kapasitas dari slice
fmt.Println(len(slice), cap(slice))

2.4.3 Penambahan Elemen

Fungsi append digunakan untuk menambahkan elemen ke dalam slice. Ketika kapasitas slice tidak mencukupi untuk menampung elemen-elemen baru, fungsi append secara otomatis memperluas kapasitas slice.

slice := []int{10, 20, 30}
// Menambahkan satu elemen
slice = append(slice, 40)
// Menambahkan beberapa elemen
slice = append(slice, 50, 60)
fmt.Println(slice)

Penting untuk dicatat bahwa ketika menggunakan append untuk menambahkan elemen, ini dapat mengembalikan sebuah slice baru. Jika kapasitas dari array yang mendasarinya tidak mencukupi, operasi append akan membuat slice merujuk pada array baru yang lebih besar.

2.5 Perluasan dan Penyalinan Slice

Fungsi copy dapat digunakan untuk menyalin elemen-elemen dari sebuah slice ke slice lainnya. Slice yang dituju harus sudah mengalokasikan cukup ruang untuk menampung elemen-elemen yang disalin, dan operasi ini tidak akan mengubah kapasitas dari slice yang dituju.

2.5.1 Menggunakan Fungsi copy

Berikut adalah contoh penggunaan copy:

src := []int{1, 2, 3}
dst := make([]int, 3)
// Menyalin elemen ke dalam slice yang dituju
disalin := copy(dst, src)
fmt.Println(dst, disalin)

Fungsi copy mengembalikan jumlah elemen yang disalin, dan tidak akan melebihi panjang dari slice yang dituju atau panjang dari slice sumber, mana yang lebih kecil.

2.5.2 Pertimbangan

Ketika menggunakan fungsi copy, jika elemen-elemen baru ditambahkan untuk disalin namun slice yang dituju tidak memiliki cukup ruang, hanya elemen-elemen yang dapat diakomodasi oleh slice yang dituju akan disalin.

2.6 Slice Multi-dimensi

Slice multi-dimensi adalah slice yang berisi beberapa slice. Ini mirip dengan array multi-dimensi, namun karena panjang variabel dari slice, slice multi-dimensi lebih fleksibel.

2.6.1 Membuat Slice Multi-dimensi

Membuat slice dua dimensi (slice dari slice):

twoD := make([][]int, 3)
for i := 0; i < 3; i++ {
    twoD[i] = make([]int, 3)
    for j := 0; j < 3; j++ {
        twoD[i][j] = i + j
    }
}
fmt.Println("Slice dua dimensi: ", twoD)

2.6.2 Menggunakan Slice Multi-dimensi

Menggunakan slice multi-dimensi mirip dengan menggunakan slice satu dimensi, diakses dengan indeks:

// Mengakses elemen dari slice dua dimensi
nilai := twoD[1][2]
fmt.Println(nilai)

3 Perbandingan Aplikasi Array dan Slice

3.1 Perbandingan Skenario Penggunaan

Array dan slice dalam Go keduanya digunakan untuk menyimpan koleksi data dengan tipe yang sama, namun mereka memiliki perbedaan yang jelas dalam skenario penggunaan.

Array:

  • Panjang array telah ditetapkan pada saat deklarasi, membuatnya cocok untuk menyimpan jumlah elemen yang sudah diketahui dan tetap.
  • Ketika diperlukan wadah dengan ukuran tetap, seperti merepresentasikan matriks berukuran tetap, array adalah pilihan terbaik.
  • Array dapat dialokasikan di stack, memberikan kinerja yang lebih baik ketika ukuran array tidak terlalu besar.

Slice:

  • Sebuah slice adalah penjabaran dari array dinamis, dengan panjang yang dapat berubah, cocok untuk menyimpan jumlah yang tidak diketahui atau koleksi elemen yang dapat berubah secara dinamis.
  • Ketika diperlukan array dinamis yang dapat tumbuh atau menyusut sesuai kebutuhan, seperti untuk menyimpan input pengguna yang tidak pasti, slice adalah pilihan yang lebih cocok.
  • Tata letak memori dari sebuah slice memungkinkan untuk referensi yang mudah dari sebagian atau seluruh array, umumnya digunakan untuk pemrosesan substring, membagi isi file, dan skenario lainnya.

Secara keseluruhan, array cocok untuk skenario dengan kebutuhan ukuran tetap, mencerminkan fitur manajemen memori statis Go, sementara slice lebih fleksibel, berfungsi sebagai perluasan abstrak dari array, nyaman untuk menangani koleksi dinamis.

3.2 Pertimbangan Kinerja

Ketika kita perlu memilih antara menggunakan array atau slice, kinerja adalah faktor penting yang perlu dipertimbangkan.

Array:

  • Kecepatan akses cepat, karena memiliki memori yang kontinu dan indeks yang tetap.
  • Alokasi memori di stack (jika ukuran array diketahui dan tidak terlalu besar), tanpa melibatkan overhed memori heap tambahan.
  • Tidak ada memori tambahan untuk menyimpan panjang dan kapasitas, yang dapat bermanfaat untuk program yang sensitif terhadap memori.

Slice:

  • Pertumbuhan atau penyusutan dinamis dapat menyebabkan overhed kinerja: pertumbuhan dapat mengakibatkan alokasi memori baru dan menyalin elemen lama, sementara penyusutan dapat memerlukan penyesuaian pointer.
  • Operasi slice sendiri cepat, namun penambahan atau penghapusan elemen yang sering dapat menyebabkan fragmentasi memori.
  • Meskipun akses slice menimbulkan overhead tidak langsung yang kecil, umumnya tidak memiliki dampak signifikan pada kinerja kecuali dalam kode yang sangat sensitif terhadap kinerja.

Oleh karena itu, jika kinerja adalah pertimbangan utama dan ukuran data diketahui sebelumnya, maka menggunakan array lebih cocok. Namun, jika fleksibilitas dan kenyamanan diperlukan, maka disarankan untuk menggunakan slice, terutama untuk menangani kumpulan data yang besar.

4 Masalah Umum dan Solusinya

Dalam proses menggunakan array dan slice dalam bahasa Go, para pengembang dapat menghadapi masalah umum berikut.

Masalah 1: Array di Luar Batas

  • Array di luar batas merujuk pada mengakses indeks yang melampaui panjang array. Hal ini akan mengakibatkan kesalahan saat runtime.
  • Solusi: Selalu periksa apakah nilai indeks berada dalam rentang valid dari array sebelum mengakses elemen array. Ini dapat dicapai dengan membandingkan indeks dan panjang array.
var arr [5]int
index := 10 // Anggap indeks di luar jangkauan
if index < len(arr) {
    fmt.Println(arr[index])
} else {
    fmt.Println("Indeks di luar jangkauan array.")
}

Masalah 2: Memory Leak dalam Slice

  • Slice mungkin secara tidak sengaja menyimpan referensi ke bagian atau seluruh array asli, bahkan jika hanya sebagian kecil yang diperlukan. Hal ini dapat menyebabkan kebocoran memori jika array asli cukup besar.
  • Solusi: Jika diperlukan slice sementara, pertimbangkan untuk membuat slice baru dengan menyalin bagian yang diperlukan.
original := make([]int, 1000000)
smallSlice := make([]int, 10)
copy(smallSlice, original[:10]) // Hanya menyalin bagian yang diperlukan
// Dengan cara ini, smallSlice tidak merujuk ke bagian lain dari original, membantu GC untuk mendapatkan kembali memori yang tidak perlu

Masalah 3: Kesalahan Data yang Disebabkan oleh Penggunaan Ulang Slice

  • Karena slice berbagi referensi ke array yang sama, memungkinkan untuk melihat dampak modifikasi data pada slice yang berbeda, yang dapat menyebabkan kesalahan yang tidak terduga.
  • Solusi: Untuk menghindari situasi ini, lebih baik membuat salinan slice baru.
sliceA := []int{1, 2, 3, 4, 5}
sliceB := make([]int, len(sliceA))
copy(sliceB, sliceA)
sliceB[0] = 100
fmt.Println(sliceA[0]) // Output: 1
fmt.Println(sliceB[0]) // Output: 100

Di atas hanyalah beberapa masalah umum dan solusi yang mungkin muncul saat menggunakan array dan slice dalam bahasa Go. Ada banyak detail lain yang perlu diperhatikan dalam pengembangan yang sebenarnya, namun mengikuti prinsip dasar ini dapat membantu menghindari banyak kesalahan umum.