Ketika program Go dijalankan, main() bukanlah kode pertama yang tereksekusi. Sebelum baris pertama main(), runtime Go menyiapkan setiap package yang diimpor: menginisialisasi variabel level-package, lalu menjalankan fungsi init() bila ada. Urutan ini biasanya tak terasa — sampai suatu hari variabel Anda ternyata nil padahal sudah diisi, atau driver database tiba-tiba terpasang tanpa pernah Anda panggil. Pada chapter ini kita akan membedah urutan inisialisasi secara nyata, membuktikannya dengan output terurut, lalu memahami pola blank import yang menjadi fondasi driver dan plugin di ekosistem Go.

Urutan Inisialisasi Satu Package #

Spesifikasi Go menetapkan urutan dua langkah untuk setiap package. Pertama, seluruh variabel level-package diinisialisasi sesuai urutan dependensinya: variabel yang bergantung pada variabel lain menunggu dependensinya siap lebih dulu. Kedua, setelah semua variabel terisi, setiap fungsi init() dijalankan. Baru setelah semua package selesai, eksekusi berpindah ke main().

Dua aturan tambahan turut berlaku. Package yang diimpor selalu selesai lebih dulu dari pemanggilnya — sebab pemanggil mungkin memakai nilai hasil inisialisasi import tersebut. Dan satu package hanya diinisialisasi sekali sekalipun diimpor oleh banyak package, sehingga inisialisasi tidak perlu dijaga dari pemanggilan ganda.

Demo: Membuktikan Urutan dengan Output #

Bundle chapter ini berisi module contoh.com/boot dengan empat package. Package siap mencatat setiap tahap inisialisasinya ke layar:

go
package siap

import "fmt"

var konfigurasi = muatKonfigurasi()

func init() {
	fmt.Println("siap  : init() dijalankan setelah variabel")
}

func muatKonfigurasi() string {
	fmt.Println("siap  : variabel level-package diinisialisasi")
	return "siap"
}

// Status melaporkan isi konfigurasi yang sudah dimuat.
func Status() string {
	return "konfigurasi=" + konfigurasi
}

Sementara src/main.go mengimpor siap dan memancarkan jejak yang sama dari sisi pemanggil:

go
package main

import (
	"fmt"

	_ "contoh.com/boot/plugin"
	"contoh.com/boot/registry"
	"contoh.com/boot/siap"
)

var aplikasi = versiAplikasi()

func versiAplikasi() string {
	fmt.Println("main  : variabel level-package diinisialisasi")
	return "1.0.0"
}

func init() {
	fmt.Println("main  : init() dijalankan setelah variabel")
}

func main() {
	fmt.Println("main  : func main() mulai, versi", aplikasi)
	fmt.Println("siap  :", siap.Status())
	fmt.Println("driver:", registry.Daftar())
}

Jalankan dari dalam direktori src/:

bash
cd src && go run .

Output yang Dihasilkan:

text
siap  : variabel level-package diinisialisasi
siap  : init() dijalankan setelah variabel
main  : variabel level-package diinisialisasi
main  : init() dijalankan setelah variabel
main  : func main() mulai, versi 1.0.0
siap  : konfigurasi=siap
driver: mysql, postgres

Mari kita telaah urutan pada output di atas:

  1. Baris 1-2 (siap: ...): package siap yang diimpor selesai lebih dulu — variabelnya (konfigurasi melalui pemanggilan muatKonfigurasi) terisi sebelum init()-nya berjalan.
  2. Baris 3-4 (main: ...): setelah semua import siap, giliran package main: variabel aplikasi diisi lewat versiAplikasi(), baru kemudian init() pemanggil dijalankan.
  3. Baris 5 (func main() mulai): titik masuk program baru tereksekusi setelah seluruh mekanisme inisialisasi selesai — nilai 1.0.0 dan konfigurasi=siap sudah tersedia.
  4. Baris 7 (driver: mysql, postgres): bukti bahwa plugin (yang diimpor anonim) sudah melakukan registrasinya jauh sebelum main() berjalan.

Fungsi init(): Banyak Boleh, Jangan Berlebihan #

Go mengizinkan lebih dari satu init() dalam satu package, bahkan dalam satu berkas — masing-masing akan dijalankan berurutan sesuai posisi deklarasi. Urutan antar berkas mengikuti urutan berkas yang disajikan build system; compiler go menyajikannya alfabetis, tetapi spesifikasi tidak menjamin urutan tersebut, sehingga dua init() yang saling bergantung lintas berkas adalah desain yang rapuh. Fungsi init() juga tidak memiliki parameter, tidak mengembalikan nilai, dan tidak bisa dipanggil manual — mencobanya menghasilkan galat nyata:

go
func main() {
	init()
}
text
./manual-init.go:6:2: undefined: init

Blank Import: Import demi Side Effect #

Perhatikan baris _ "contoh.com/boot/plugin" pada main.go di atas. Garis bawah mengubah import menjadi blank import: package diinisialisasi penuh (variabel dan init() tetap berjalan), tetapi namanya tidak terikat ke identifier mana pun, sehingga kita tidak bisa memanggil anggotanya. Pola ini tepat ketika nilai sebuah package terletak pada side effect inisialisasinya:

go
package plugin

import "contoh.com/boot/registry"

func init() {
	// Side effect: mendaftarkan driver begitu package diimpor.
	registry.Pasang("mysql", "8.0")
	registry.Pasang("postgres", "17")
}

Pola inilah yang dipakai driver database di dunia nyata: _ "github.com/jackc/pgx/v5/stdlib" mendaftarkan driver ke database/sql tanpa kita memanggil satu fungsi pun, lalu sql.Open("pgx", ...) menemukan driver tersebut di registry internal. Side effect plugin pada demo kita (mendaftarkan mysql dan postgres) memakai mekanisme yang sama.

Inisialisasi memberi package kita “denyut nadi” awal, tetapi belum bicara soal siapa yang boleh mengimpor. Go menyediakan direktori internal/ yang pelanggarannya ditolak langsung oleh compiler — aturan pohon direktorinya kita bedah pada chapter Package internal .

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

src/main.go

Unduh berkas
go
package main

import (
	"fmt"

	_ "contoh.com/boot/plugin"
	"contoh.com/boot/registry"
	"contoh.com/boot/siap"
)

var aplikasi = versiAplikasi()

func versiAplikasi() string {
	fmt.Println("main  : variabel level-package diinisialisasi")
	return "1.0.0"
}

func init() {
	fmt.Println("main  : init() dijalankan setelah variabel")
}

func main() {
	fmt.Println("main  : func main() mulai, versi", aplikasi)
	fmt.Println("siap  :", siap.Status())
	fmt.Println("driver:", registry.Daftar())
}

src/plugin/plugin.go

Unduh berkas
go
package plugin

import "contoh.com/boot/registry"

func init() {
	// Side effect: mendaftarkan driver begitu package diimpor.
	registry.Pasang("mysql", "8.0")
	registry.Pasang("postgres", "17")
}

src/registry/registry.go

Unduh berkas
go
package registry

import (
	"maps"
	"slices"
	"strings"
)

// Driver menyimpan daftar driver yang terpasang di registry.
var Driver = map[string]string{}

// Pasang mendaftarkan driver baru ke registry.
func Pasang(nama, versi string) {
	Driver[nama] = versi
}

// Daftar mengembalikan nama driver terurut agar output deterministik.
func Daftar() string {
	return strings.Join(slices.Sorted(maps.Keys(Driver)), ", ")
}

src/siap/siap.go

Unduh berkas
go
package siap

import "fmt"

var konfigurasi = muatKonfigurasi()

func init() {
	fmt.Println("siap  : init() dijalankan setelah variabel")
}

func muatKonfigurasi() string {
	fmt.Println("siap  : variabel level-package diinisialisasi")
	return "siap"
}

// Status melaporkan isi konfigurasi yang sudah dimuat.
func Status() string {
	return "konfigurasi=" + konfigurasi
}