BAB 7: Tipe Data — Sistem Tipe Statis Go

Pahami sistem tipe data Go yang static — dari integer, float, boolean, string, hingga composite types seperti array, slice, map, dan struct yang jadi fondasi program Go.

Di bab sebelumnya, kamu belajar menyimpan data ke dalam variabel. Tapi ada pertanyaan yang belum dijawab: kenapa 10 dan 10.5 diperlakukan berbeda? Kenapa kamu tidak bisa langsung menjumlahkan angka dan teks? Jawabannya ada di tipe data.

Go adalah bahasa dengan static type system — setiap variabel harus punya tipe yang ditentukan saat kompilasi, bukan saat program berjalan. Ini terasa lebih ketat dibanding Python atau JavaScript, tapi justru itulah yang membuat program Go lebih mudah diprediksi: compiler akan menangkap kesalahan tipe sebelum program sempat dijalankan.

Angka Bulat: Integer

Pilihan tipe paling sering kamu temui adalah int. Ini adalah tipe bilangan bulat default di Go, ukurannya mengikuti platform — 32-bit di sistem 32-bit, 64-bit di sistem 64-bit.

// main.go
package main

import "fmt"

func main() {
    umur := 25
    jumlahSiswa := 42
    suhuKulkas := -18

    fmt.Println(umur, jumlahSiswa, suhuKulkas)
}

Go juga menyediakan varian integer dengan ukuran eksplisit ketika kamu perlu kontrol yang lebih ketat atas memori atau range nilai:

TipeRangeKapan dipakai
int8-128 sampai 127Data yang nilainya kecil, hemat memori
int16-32.768 sampai 32.767Jarang dipakai, protokol lama
int32-2 miliar sampai 2 miliarUnicode code point (rune)
int64Sangat besarTimestamp, ID besar
uint80 sampai 255Data byte, pixel warna
uint0 sampai batas platformPanjang dan indeks

Dalam praktik sehari-hari, int sudah cukup untuk hampir semua kebutuhan. Gunakan varian ukuran spesifik hanya ketika kamu punya alasan yang jelas — misalnya bekerja dengan data biner, protokol jaringan, atau memori terbatas.

byte adalah alias untuk uint8, dan rune adalah alias untuk int32. Kamu akan sering melihat keduanya saat bekerja dengan string dan karakter.

Angka Desimal: Float

Untuk angka dengan titik desimal, Go menyediakan dua pilihan: float32 dan float64. Perbedaannya ada di presisi — float64 menyimpan angka dengan akurasi yang jauh lebih tinggi karena menggunakan 64 bit.

// main.go
package main

import "fmt"

func main() {
    tinggi := 175.5         // float64 (default)
    var berat float32 = 68.3

    pi := 3.141592653589793 // float64, presisi penuh

    fmt.Printf("Tinggi: %.1f cm\n", tinggi)
    fmt.Printf("Berat: %.1f kg\n", berat)
    fmt.Printf("Pi: %.10f\n", pi)
}

Saat kamu menulis angka desimal tanpa deklarasi tipe eksplisit, Go selalu menggunakannya sebagai float64. Ini pilihan yang aman karena float64 lebih akurat. Gunakan float32 hanya kalau kamu benar-benar perlu menghemat memori dalam jumlah besar, misalnya array ratusan ribu elemen.

Boolean: Benar atau Salah

Tipe bool hanya punya dua nilai: true dan false. Sederhana, tapi perannya krusial — hampir semua logika kondisional bergantung pada boolean.

// main.go
package main

import "fmt"

func main() {
    sudahLogin := true
    aksesDitolak := false

    // Operator logika: &&, ||, !
    bisaAkses := sudahLogin && !aksesDitolak
    fmt.Println("Bisa akses:", bisaAkses) // true

    suhu := 35.0
    puasaAktif := true
    perluMinum := suhu > 33 && !puasaAktif
    fmt.Println("Perlu minum:", perluMinum) // false
}

Zero value untuk bool adalah false — variabel bool yang dideklarasikan tanpa nilai awal otomatis bernilai false.

String: Teks

String di Go adalah urutan byte yang immutable — sekali dibuat, isinya tidak bisa diubah secara langsung. Untuk membuat string biasa, gunakan tanda kutip ganda. Untuk string yang mengandung baris baru atau karakter khusus tanpa escape, gunakan backtick.

// main.go
package main

import "fmt"

func main() {
    nama := "Gopher"
    kota := "Bandung"

    // Concatenation
    sapaan := "Halo, " + nama + "!"
    fmt.Println(sapaan)

    // Panjang string (dalam byte)
    fmt.Println("Panjang nama:", len(nama)) // 6

    // Raw string literal — tidak perlu escape
    query := `SELECT *
FROM users
WHERE kota = 'Bandung'`
    fmt.Println(query)

    _ = kota
}

Ada hal penting yang perlu kamu tahu soal len() dan indeks string di Go. Keduanya bekerja pada level byte, bukan karakter.

// main.go
package main

import "fmt"

func main() {
    teks := "Halo"
    fmt.Println(len(teks))   // 4 — 4 byte, 4 karakter ASCII

    // Mengakses byte (bukan karakter)
    fmt.Println(teks[0])     // 72 — nilai byte dari 'H'
    fmt.Printf("%c\n", teks[0]) // H — tampilkan sebagai karakter
}

String dengan karakter non-ASCII (termasuk huruf beraksara atau emoji) bisa punya lebih banyak byte dari jumlah karakternya. Untuk iterasi per karakter, gunakan for range yang otomatis bekerja per rune, bukan per byte.

Array: Koleksi Berukuran Tetap

Array adalah koleksi elemen dengan tipe yang sama dan ukuran yang tetap. “Tetap” di sini berarti ukurannya ditentukan saat kompilasi dan tidak bisa berubah.

// main.go
package main

import "fmt"

func main() {
    var nilai [5]int              // [0 0 0 0 0] — zero value
    buah := [3]string{"apel", "jeruk", "mangga"}
    angka := [...]int{10, 20, 30, 40} // ukuran otomatis dari isinya

    nilai[0] = 85
    nilai[1] = 92

    fmt.Println("Nilai:", nilai)
    fmt.Println("Buah:", buah)
    fmt.Println("Jumlah angka:", len(angka)) // 4
}

Ukuran array adalah bagian dari tipenya — [3]string dan [5]string adalah tipe yang berbeda dan tidak bisa saling diassign. Ini membuat array kurang fleksibel untuk kebutuhan umum. Dalam praktik, kamu akan lebih sering menggunakan slice.

Slice: Array yang Fleksibel

Slice adalah abstraksi di atas array — ia menyimpan referensi ke array di bawahnya, bukan datanya secara langsung. Yang membuatnya istimewa: ukurannya bisa berubah dinamis menggunakan append.

// main.go
package main

import "fmt"

func main() {
    // Membuat slice
    var kota []string                        // nil slice, kosong
    provinsi := []string{"Jawa Barat", "Jawa Tengah"}
    angka := make([]int, 3)                  // [0, 0, 0]

    // Menambah elemen
    kota = append(kota, "Bandung")
    kota = append(kota, "Surabaya", "Medan")

    fmt.Println("Kota:", kota)       // [Bandung Surabaya Medan]
    fmt.Println("Provinsi:", provinsi)
    fmt.Println("Angka:", angka)     // [0 0 0]
}

len dan cap

Setiap slice punya dua properti penting: length (jumlah elemen yang ada) dan capacity (jumlah elemen yang bisa ditampung sebelum array di bawahnya perlu dialokasikan ulang).

// main.go
package main

import "fmt"

func main() {
    s := make([]int, 3, 5) // length 3, capacity 5

    fmt.Printf("len=%d, cap=%d, isi=%v\n", len(s), cap(s), s)

    s = append(s, 10)
    fmt.Printf("len=%d, cap=%d, isi=%v\n", len(s), cap(s), s)

    s = append(s, 20)
    fmt.Printf("len=%d, cap=%d, isi=%v\n", len(s), cap(s), s)

    // Ini akan trigger alokasi baru karena kapasitas penuh
    s = append(s, 30)
    fmt.Printf("len=%d, cap=%d, isi=%v\n", len(s), cap(s), s)
}

Slicing

Kamu bisa memotong slice menggunakan sintaks [awal:akhir]. Elemen yang diambil dimulai dari indeks awal sampai akhir-1.

// main.go
package main

import "fmt"

func main() {
    angka := []int{10, 20, 30, 40, 50}

    fmt.Println(angka[1:4])  // [20 30 40]
    fmt.Println(angka[:3])   // [10 20 30]
    fmt.Println(angka[2:])   // [30 40 50]
    fmt.Println(angka[:])    // [10 20 30 40 50] — semua elemen
}

Slice yang dibuat dengan slicing berbagi memori yang sama dengan slice asalnya. Mengubah elemen di slice hasil potongan akan mengubah slice asal juga. Gunakan copy() jika kamu butuh salinan yang independen.

Map: Data Berpasangan

Map adalah struktur data yang menyimpan pasangan key-value. Berbeda dari array atau slice yang menggunakan indeks numerik, map menggunakan key bertipe apapun yang comparable.

// main.go
package main

import "fmt"

func main() {
    // Membuat map
    nilaiSiswa := map[string]int{
        "Budi":  85,
        "Sari":  92,
        "Anton": 78,
    }

    // Menambah dan mengubah
    nilaiSiswa["Rina"] = 88
    nilaiSiswa["Budi"] = 90 // update nilai Budi

    // Mengakses
    fmt.Println("Nilai Sari:", nilaiSiswa["Sari"])

    // Menghapus
    delete(nilaiSiswa, "Anton")

    fmt.Println("Semua nilai:", nilaiSiswa)
}

Comma-ok idiom

Mengakses key yang tidak ada di map tidak menghasilkan error — Go mengembalikan zero value dari tipe value-nya. Untuk membedakan “key ada dengan nilai 0” dan “key tidak ada”, gunakan comma-ok idiom:

// main.go
package main

import "fmt"

func main() {
    stok := map[string]int{
        "apel":  10,
        "jeruk": 0, // ada, tapi stoknya memang 0
    }

    // Tanpa comma-ok — tidak bisa bedakan ada/tidak ada
    fmt.Println(stok["mangga"]) // 0, tapi mangga tidak ada

    // Dengan comma-ok
    jumlah, ada := stok["apel"]
    fmt.Printf("apel: %d, ada: %t\n", jumlah, ada) // 10, true

    jumlah, ada = stok["mangga"]
    fmt.Printf("mangga: %d, ada: %t\n", jumlah, ada) // 0, false
}

Struct: Data yang Dikelompokkan

Sejauh ini kamu menyimpan satu jenis data per variabel. Bagaimana kalau kamu perlu menyimpan data yang saling berkaitan — nama, umur, dan email seseorang? Di sinilah struct berperan.

// main.go
package main

import "fmt"

type Pengguna struct {
    Nama  string
    Umur  int
    Email string
}

func main() {
    // Struct literal
    p1 := Pengguna{
        Nama:  "Budi",
        Umur:  25,
        Email: "budi@mail.com",
    }

    // Mengakses field
    fmt.Println(p1.Nama)
    fmt.Printf("%s berumur %d tahun\n", p1.Nama, p1.Umur)

    // Mengubah field
    p1.Umur = 26
    fmt.Println("Umur sekarang:", p1.Umur)
}

Struct juga bisa bersarang — field dari sebuah struct bisa bertipe struct lain:

// main.go
package main

import "fmt"

type Alamat struct {
    Jalan string
    Kota  string
}

type Pengguna struct {
    Nama   string
    Umur   int
    Alamat Alamat
}

func main() {
    p := Pengguna{
        Nama: "Sari",
        Umur: 28,
        Alamat: Alamat{
            Jalan: "Jl. Merdeka 10",
            Kota:  "Bandung",
        },
    }

    fmt.Printf("%s tinggal di %s, %s\n", p.Nama, p.Alamat.Jalan, p.Alamat.Kota)
}

Struct akan dibahas lebih mendalam di bab tersendiri — termasuk method dan embedding. Untuk sekarang, cukup pahami struct sebagai cara mengelompokkan data yang saling berkaitan.

Pointer: Variabel yang Menyimpan Alamat

Setiap variabel di Go disimpan di lokasi tertentu di memori. Pointer adalah variabel yang menyimpan alamat memori dari variabel lain, bukan nilainya langsung.

Operator & mengambil alamat sebuah variabel. Operator * membaca nilai yang ada di alamat tersebut.

// main.go
package main

import "fmt"

func main() {
    nilai := 42
    ptr := &nilai // ptr menyimpan alamat variabel nilai

    fmt.Println("Nilai:", nilai)       // 42
    fmt.Println("Alamat:", ptr)        // alamat memori, misal 0xc000018060
    fmt.Println("Via pointer:", *ptr)  // 42

    // Mengubah nilai melalui pointer
    *ptr = 100
    fmt.Println("Nilai setelah diubah:", nilai) // 100
}

Pointer akan dibahas lebih lengkap di bab tersendiri, termasuk kenapa Go memilih pendekatan pointer yang eksplisit dibanding bahasa lain.

Zero Values

Setiap tipe di Go punya zero value yang aman — nilai default saat variabel dideklarasikan tanpa nilai awal. Ini adalah salah satu keputusan desain Go yang mencegah bug “uninitialized variable”.

TipeZero value
int, semua varian integer0
float32, float640.0
boolfalse
string""
arraysemua elemen zero value
slice, map, pointernil
structsemua field zero value
// main.go
package main

import "fmt"

func main() {
    var angka int
    var teks string
    var aktif bool
    var harga float64
    var daftar []string

    fmt.Printf("int: %d\n", angka)      // 0
    fmt.Printf("string: %q\n", teks)    // ""
    fmt.Printf("bool: %t\n", aktif)     // false
    fmt.Printf("float64: %f\n", harga)  // 0.000000
    fmt.Printf("slice nil: %t\n", daftar == nil) // true
}

Konversi Tipe

Go tidak melakukan konversi tipe secara implisit — kamu harus melakukannya secara eksplisit. Ini mencegah bug halus yang sering terjadi di bahasa lain ketika tipe dikonversi secara diam-diam.

// main.go
package main

import "fmt"

func main() {
    var umur int = 25
    var tinggi float64 = 175.5

    // Tidak bisa langsung dijumlahkan
    // total := umur + tinggi // compile error!

    // Harus konversi eksplisit
    total := float64(umur) + tinggi
    fmt.Printf("Total: %.1f\n", total) // 200.5

    // int ke string — perlu fmt.Sprintf, bukan string(angka)
    teks := fmt.Sprintf("%d", umur)
    fmt.Println("Teks:", teks) // "25"

    // string ke []byte dan sebaliknya
    kalimat := "Halo Go"
    bytes := []byte(kalimat)
    kembali := string(bytes)
    fmt.Println(bytes)   // [72 97 108 111 32 71 111]
    fmt.Println(kembali) // Halo Go
}

string(25) tidak menghasilkan "25". Ia menghasilkan karakter dengan Unicode code point 25, yang bukan karakter yang dapat dicetak. Gunakan fmt.Sprintf("%d", angka) atau package strconv untuk konversi angka ke string.

Latihan

  1. Buat program yang menyimpan data lima kota beserta jumlah penduduknya menggunakan map. Tampilkan kota dengan jumlah penduduk terbesar.

  2. Buat struct Produk dengan field Nama (string), Harga (float64), dan Stok (int). Buat slice yang berisi tiga produk berbeda, lalu hitung total nilai stok (harga dikali stok) untuk seluruh produk.

  3. Buat slice angka dari 1 sampai 10 menggunakan append dalam loop. Cetak elemen-elemen di posisi genap menggunakan slicing dan iterasi.

  4. Deklarasikan variabel suhu bertipe float64 dengan nilai 36.6. Konversikan ke int dan cetak selisih antara nilai asli dan hasil konversi untuk menunjukkan efek pemotongan desimal.

Kamu sudah mengenal semua tipe data dasar yang ada di Go. Tapi variabel dan tipe saja belum cukup untuk membuat program yang berguna — kamu perlu cara untuk menyimpan nilai yang tidak boleh berubah sama sekali. Di bab berikutnya, kita akan membahas konstanta, dan kenapa Go punya cara yang cukup unik untuk mendefinisikannya lewat iota.

Referensi

  1. 1Basic Types — A Tour of Go
  2. 2Types — The Go Programming Language Specification
  3. 3Go Slices: usage and internals — The Go Blog
  4. 4The Zero Value — Go Specification