Variabel #
Variabel di Go terlihat sederhana di permukaan — ada var dan ada :=. Tapi di balik kesederhanaan itu ada keputusan desain yang punya implikasi nyata: kapan kamu harus pakai var, kapan kamu seharusnya pakai :=, bagaimana zero value membedakan Go dari bahasa lain, dan bagaimana variable shadowing bisa menjadi sumber bug yang sangat halus. Memahami variabel di Go secara mendalam berarti memahami cara compiler Go berpikir — dan itu membuat kamu lebih jarang melawan compiler, lebih sering bekerja bersamanya.
Zero Value — Fondasi Keamanan Go #
Sebelum bicara cara mendeklarasikan variabel, ada satu konsep yang membedakan Go dari hampir semua bahasa lain: zero value. Di C, variabel yang baru dideklarasikan punya nilai tak terduga dari memori — ini adalah salah satu sumber bug terbesar dalam sejarah pemrograman. Di Go, setiap variabel yang dideklarasikan tanpa nilai eksplisit mendapat nilai awal yang well-defined dan konsisten.
package main
import "fmt"
func main() {
var i int // → 0
var i8 int8 // → 0
var i64 int64 // → 0
var u uint // → 0
var f32 float32 // → 0.0
var f64 float64 // → 0.0
var b bool // → false
var s string // → "" (string kosong, bukan null, bukan undefined)
var p *int // → nil (pointer ke nol)
var sl []int // → nil (nil slice, bukan empty slice)
var m map[string]int // → nil (nil map)
var fn func() // → nil (nil function)
var iface interface{} // → nil
fmt.Println(i, i8, i64, u, f32, f64) // 0 0 0 0 0 0
fmt.Println(b) // false
fmt.Println(s) // (string kosong)
fmt.Println(p) // <nil>
fmt.Println(sl) // []
fmt.Println(m) // map[]
fmt.Println(fn) // <nil>
}
Implikasi Praktis Zero Value #
Zero value memungkinkan kamu menulis kode yang lebih bersih karena tidak butuh inisialisasi defensif:
// Di C/Java — perlu inisialisasi eksplisit untuk nilai "aman"
int counter = 0;
String message = "";
List<String> items = new ArrayList<>();
// Di Go — zero value sudah memberikan nilai yang berguna
var counter int // sudah 0, langsung bisa counter++
var message string // sudah "", langsung bisa message += "text"
var items []string // nil slice, langsung bisa append(items, "item")
Zero value juga berlaku untuk struct — semua field mendapat zero value tipe mereka masing-masing:
type Config struct {
Host string
Port int
Debug bool
Timeout time.Duration
}
var cfg Config
// cfg.Host = ""
// cfg.Port = 0
// cfg.Debug = false
// cfg.Timeout = 0 (zero duration)
// Ini valid dan useful — Config dengan semua default
Empat Cara Deklarasi Variabel #
Go menyediakan empat cara mendeklarasikan variabel, masing-masing punya konteks penggunaan yang tepat. Memilih yang tepat bukan sekadar selera — ini komunikasi niat kepada developer yang membaca kode.
Cara 1: Deklarasi Penuh dengan var
#
Format paling eksplisit — menyebutkan nama, tipe, dan nilai secara eksplisit:
var nama string = "Budi Santoso"
var umur int = 28
var gaji float64 = 8500000.0
var aktif bool = true
Kapan menggunakannya: ketika kamu ingin sangat eksplisit tentang tipe, biasanya dalam kode yang harus sangat jelas untuk pembaca baru.
Cara 2: var dengan Zero Value (Tanpa Inisialisasi)
#
Deklarasi tanpa nilai — variabel mendapat zero value tipenya:
var total float64 // akan diakumulasi nanti
var errorCount int // dihitung selama proses
var lastError error // akan diisi jika ada error
var resultBuffer bytes.Buffer // buffer kosong siap dipakai
Kapan menggunakannya: ketika kamu tahu akan mengisi nilai nanti, atau ketika zero value itu sendiri adalah nilai yang kamu inginkan. Ini juga cara yang sangat jelas untuk mengkomunikasikan “variabel ini sengaja dimulai dari kosong.”
Cara 3: var dengan Type Inference
#
Go menyimpulkan tipe dari nilai yang diberikan:
var nama = "Budi" // tipe: string (disimpulkan)
var umur = 28 // tipe: int (disimpulkan)
var pi = 3.14159 // tipe: float64 (disimpulkan)
var aktif = true // tipe: bool (disimpulkan)
var limit = int64(1000) // tipe: int64 (explicit via conversion)
Cara 4: Short Variable Declaration :=
#
Cara paling ringkas dan paling sering digunakan di dalam fungsi:
func processOrder(orderID int) error {
order := getOrder(orderID) // tipe disimpulkan dari return type
total := calculateTotal(order) // langsung declare dan assign
err := saveOrder(order) // err adalah error
if err != nil {
return fmt.Errorf("processOrder: %w", err)
}
fmt.Printf("Order %d diproses, total: %.2f\n", orderID, total)
return nil
}
:= tidak bisa digunakan di level package (di luar fungsi) — ini adalah batasan yang disengaja karena assignment di level package bisa punya efek samping yang sulit diprediksi.
var vs := — Panduan Lengkap Kapan Menggunakan Mana #
Ini pertanyaan yang paling sering ditanyakan developer Go pemula. Jawabannya bukan “selalu pakai :=” atau “selalu pakai var” — tergantung konteks.
Gunakan var Untuk:
#
1. Deklarasi di level package:
package main
// Hanya var yang bisa digunakan di sini
var (
db *sql.DB
redisClient *redis.Client
appConfig Config
)
// func main() { ... } — := hanya valid di dalam fungsi
2. Ketika zero value adalah nilai yang diinginkan, dan kamu ingin mengkomunikasikannya secara eksplisit:
func processData(items []Item) ([]Result, error) {
var results []Result // sengaja mulai kosong
var lastErr error // akan diisi jika ada error
for _, item := range items {
result, err := processItem(item)
if err != nil {
lastErr = err
continue
}
results = append(results, result)
}
return results, lastErr
}
3. Ketika tipe perlu dinyatakan eksplisit karena tidak sesuai default inference:
// float literal defaultnya float64 — jika butuh float32:
var temperature float32 = 36.6
// int literal defaultnya int — jika butuh int64:
var fileSize int64 = 10 * 1024 * 1024 * 1024 // 10 GB
// interface — tidak bisa pakai := untuk nil interface yang typed
var r io.Reader // r adalah io.Reader dengan nilai nil
r = os.Stdin // assign nanti
4. Deklarasi bersamaan banyak variabel yang saling berkaitan:
var (
serverHost = "localhost"
serverPort = 8080
maxConns = 100
readTimeout = 30 * time.Second
writeTimeout = 30 * time.Second
)
Gunakan := Untuk:
#
1. Hampir semua variabel lokal dalam fungsi:
func getUserProfile(userID int) (*Profile, error) {
user, err := db.GetUser(userID)
if err != nil {
return nil, fmt.Errorf("getUserProfile: %w", err)
}
posts, err := db.GetUserPosts(userID)
if err != nil {
return nil, fmt.Errorf("getUserProfile: %w", err)
}
profile := buildProfile(user, posts)
return profile, nil
}
2. Hasil function call yang langsung digunakan:
data, err := json.Marshal(payload)
resp, err := http.Get(url)
rows, err := db.Query(query, args...)
3. Short-lived variables dalam scope kecil:
for i := 0; i < len(items); i++ {
item := items[i]
result := process(item)
fmt.Println(result)
}
Scope dan Block Scope #
Scope menentukan di mana sebuah variabel bisa diakses. Go menggunakan lexical block scope — variabel hanya hidup dalam blok {} tempat ia dideklarasikan.
package main
import "fmt"
// Package scope — bisa diakses dari semua fungsi dalam file yang sama package
var appName = "MyApp"
var appVersion = "1.0.0"
func contohScope() {
// Function scope — hanya dalam fungsi ini
message := "Selamat datang"
if len(message) > 0 {
// Block scope dalam if — hanya dalam blok ini
detail := "pesan aktif"
fmt.Println(message, detail) // ✓ keduanya bisa diakses
}
// fmt.Println(detail) // ✗ compile error: undefined: detail
fmt.Println(message) // ✓ masih dalam scope fungsi
for i := 0; i < 3; i++ {
// Loop scope — i hanya hidup dalam blok for ini
squared := i * i
fmt.Println(i, squared)
}
// fmt.Println(i) // ✗ compile error: undefined: i
// fmt.Println(squared) // ✗ compile error: undefined: squared
fmt.Println(appName) // ✓ package scope selalu bisa diakses
}
Package Scope vs Exported #
Penting dipahami: package scope berbeda dari “exported”. Variabel package-level yang dimulai huruf kecil bisa diakses oleh semua file dalam package yang sama, tapi tidak dari package lain:
// file: config/config.go
package config
var defaultPort = 8080 // package scope, unexported
var MaxRetry = 3 // package scope, exported
// file: config/loader.go
package config
func Load() Config {
// defaultPort bisa diakses di sini — sama package
return Config{Port: defaultPort, MaxRetry: MaxRetry}
}
// file: main.go
package main
import "myapp/config"
func main() {
fmt.Println(config.MaxRetry) // ✓ exported
// fmt.Println(config.defaultPort) // ✗ compile error: unexported
}
Variable Shadowing — Sumber Bug yang Halus #
Shadowing terjadi ketika variabel di scope dalam punya nama yang sama dengan variabel di scope luar. Compiler Go mengizinkan ini, tapi bisa menjadi sumber bug yang sangat sulit dideteksi karena tidak ada error atau warning.
func main() {
x := 10
fmt.Println("outer x:", x) // 10
{
x := 20 // variabel BARU bernama x, bukan assign ke x di atas
fmt.Println("inner x:", x) // 20
}
fmt.Println("outer x lagi:", x) // 10 — tidak berubah!
}
Shadowing Error — Bug Klasik Go #
Bug paling umum dengan shadowing adalah pada variabel err:
// ANTI-PATTERN: err di-shadow secara tidak sengaja
func processWithBug() error {
result, err := langkahPertama()
if err != nil {
return err
}
if result > 0 {
// := di sini membuat err BARU dalam scope blok if ini
data, err := langkahKedua(result)
if err != nil {
return err
}
fmt.Println(data)
}
// err di sini adalah err dari langkahPertama (scope luar)
// error dari langkahKedua bisa terlewat jika ada path lain!
return nil
}
// BENAR: deklarasi err di luar, assign saja di dalam
func processCorrect() error {
result, err := langkahPertama()
if err != nil {
return err
}
if result > 0 {
var data SomeType
// Gunakan = bukan := untuk err yang sudah ada
data, err = langkahKedua(result)
if err != nil {
return err
}
fmt.Println(data)
}
return nil
}
Perhatikan perbedaan:=dan=dalam blok bersarang. Jika semua variabel di sisi kiri adalah baru, kamu harus pakai:=. Tapi jika salah satu sudah ada di scope luar dan kamu ingin me-assign ke variabel yang sama, pastikan variabel baru lainnya dideklarasikan terpisah — atau refactor agar tidak perlu nested assignment.
if dengan Initializer Scope #
Satu kasus shadowing yang sangat umum dan justru berguna adalah dengan initializer di if:
// err dalam if-initializer ter-scope ke blok if
if err := doSomething(); err != nil {
fmt.Println("error:", err)
return err
}
// err tidak bisa diakses di sini — ini BAGUS, bukan bug
Pola ini sengaja membatasi scope err agar tidak “bocor” ke kode di bawah. Ini adalah idiom Go yang sangat umum dan direkomendasikan.
Deklarasi Bersamaan dan Multiple Assignment #
Go memungkinkan deklarasi atau assignment beberapa variabel sekaligus dalam satu baris:
// Deklarasi bersamaan
var a, b, c int // semua int, semua zero value
var x, y = 10, 20 // type inference
p, q := "hello", "world" // short declaration
// Assignment bersamaan
a, b, c = 1, 2, 3
// Swap tanpa variabel temporary — sangat elegan
a, b = b, a
fmt.Println(a, b) // nilai a dan b sudah tertukar
// Multiple return values
func getNameAndAge() (string, int) {
return "Budi", 28
}
nama, umur := getNameAndAge() // dua variabel dari satu fungsi
Deklarasi Bersamaan dengan Blok var #
Untuk variabel package-level atau ketika ingin mengelompokkan variabel yang berkaitan:
var (
// Database config
dbHost = "localhost"
dbPort = 5432
dbName = "myapp"
dbUser = "postgres"
dbPassword = ""
// Server config
serverPort = 8080
serverHost = "0.0.0.0"
readTimeout = 30 * time.Second
writeTimeout = 30 * time.Second
)
Blank Identifier _
#
Blank identifier _ adalah cara Go untuk “membuang” nilai yang tidak dibutuhkan. Ini wajib digunakan karena Go tidak mengizinkan variabel yang dideklarasikan tapi tidak dipakai.
// Fungsi mengembalikan dua nilai — kita hanya butuh yang kedua
func getDimensions() (width, height int) {
return 1920, 1080
}
_, height := getDimensions()
fmt.Println("Height:", height) // hanya pakai height
// Dalam for-range — buang index jika tidak dibutuhkan
names := []string{"Alice", "Bob", "Charlie"}
for _, name := range names {
fmt.Println(name) // hanya butuh value, bukan index
}
// Dalam for-range — buang value jika hanya butuh index
for i := range names {
fmt.Printf("index %d\n", i)
}
// Mengabaikan error — BERBAHAYA, gunakan dengan sangat hati-hati
data, _ := os.ReadFile("config.json") // jika file tidak ada, data adalah nil!
Jangan abaikan error dengan_sembarangan.data, _ := os.ReadFile("file.txt")adalah kode yang kompilasi dan berjalan, tapi jika file tidak ada,dataakannildan operasi selanjutnya padadataakan panic. Selalu tangani error kecuali kamu benar-benar yakin bahwa error tidak mungkin terjadi — dan dokumentasikan alasannya dalam komentar.
Variabel dan Type Assertion #
Dalam Go yang idiomatik, kamu sering menemui pola deklarasi variabel yang dikombinasikan dengan type assertion atau type switch:
// Type assertion dengan two-value form
var i interface{} = "Hello, Go!"
if s, ok := i.(string); ok {
// s di-scope ke blok if ini
fmt.Println("String length:", len(s))
}
// Pola yang sering muncul: assert ke error type tertentu
if pathErr, ok := err.(*os.PathError); ok {
fmt.Println("Path error pada:", pathErr.Path)
}
Contoh Program Lengkap #
Berikut program yang menggabungkan semua konsep variabel yang sudah dibahas dalam skenario nyata — menghitung statistik dari data penjualan:
package main
import (
"fmt"
"math"
)
// Package-level constants dan variables
var (
storeName = "Toko Go"
currency = "IDR"
)
type Sale struct {
Product string
Quantity int
Price float64
}
func calculateStats(sales []Sale) (total, average, min, max float64) {
if len(sales) == 0 {
return 0, 0, 0, 0
}
// Inisialisasi dengan zero value yang berguna
min = math.MaxFloat64
var count int
for _, sale := range sales {
revenue := float64(sale.Quantity) * sale.Price
total += revenue // akumulasi
count++
if revenue < min {
min = revenue
}
if revenue > max {
max = revenue
}
}
average = total / float64(count)
return // named return — mengembalikan total, average, min, max
}
func formatCurrency(amount float64) string {
return fmt.Sprintf("%s %.2f", currency, amount)
}
func main() {
// Inisialisasi data dengan composite literal
sales := []Sale{
{Product: "Laptop", Quantity: 2, Price: 15_000_000},
{Product: "Mouse", Quantity: 10, Price: 250_000},
{Product: "Keyboard", Quantity: 5, Price: 500_000},
{Product: "Monitor", Quantity: 3, Price: 4_000_000},
{Product: "Headset", Quantity: 7, Price: 750_000},
}
// Multiple return values
total, average, min, max := calculateStats(sales)
// Variabel lokal untuk presentasi
separator := "=========================="
fmt.Printf("\n%s\n", storeName)
fmt.Println(separator)
// Iterasi dengan for-range, blank identifier untuk index
for _, sale := range sales {
revenue := float64(sale.Quantity) * sale.Price
fmt.Printf("%-12s x%d = %s\n",
sale.Product,
sale.Quantity,
formatCurrency(revenue),
)
}
fmt.Println(separator)
fmt.Printf("Total: %s\n", formatCurrency(total))
fmt.Printf("Rata-rata: %s\n", formatCurrency(average))
fmt.Printf("Transaksi Min: %s\n", formatCurrency(min))
fmt.Printf("Transaksi Max: %s\n", formatCurrency(max))
fmt.Printf("Jumlah transaksi: %d\n", len(sales))
}
Ringkasan #
- Zero value menjamin semua variabel terinisialisasi:
0,false,"",nil— tidak ada nilai random dari memori seperti di C.- Empat cara deklarasi:
var name Type = value,var name Type,var name = value,name := value— masing-masing punya konteks penggunaan tepat.varwajib untuk package-level variables;:=lebih idiomatik untuk variabel lokal dalam fungsi.- Gunakan
varketika ingin zero value eksplisit, tipe berbeda dari default inference, atau mengelompokkan variabel berkaitan.- Scope adalah block-based — variabel hanya hidup dalam
{}tempat dideklarasikan; scope bersarang memungkinkan shadowing.- Variable shadowing tidak menyebabkan error tapi bisa menjadi sumber bug halus — paling sering pada variabel
err.- Blank identifier
_untuk membuang nilai yang tidak dipakai — wajib karena Go tidak mengizinkan variabel tidak terpakai.- Jangan abaikan error dengan
_sembarangan — selalu pertimbangkan konsekuensi jika operasi gagal.- Swap nilai elegan tanpa temp variable:
a, b = b, a.