Cara Kerja Go

Kompilasi cepat, biner statis tunggal, dan mental model penjadwal goroutine.

Daftar isi

Kode sumber Go adalah teks yang ditulis manusia, sedangkan yang dijalankan mesin adalah biner hasil kompilasinya. Topik ini menjelaskan perjalanan di antara keduanya, mulai dari proses kompilasi yang cepat, wujud biner statis yang berdiri sendiri, sampai mental model tentang cara runtime Go menjalankan banyak goroutine di atas sedikit thread sistem operasi. Setelah selesai membaca, proses go build dan ukuran biner hasilnya dapat dijelaskan dengan bukti nyata, bukan sekadar teori.

Kompilasi ke kode mesin#

Go termasuk bahasa yang dikompilasi (compiled language), yaitu bahasa yang kode sumbernya diterjemahkan lebih dahulu menjadi kode mesin sebelum dijalankan. Penerjemahnya disebut kompiler (compiler), yang membaca seluruh berkas sumber, memeriksa kesalahan tipe, lalu menghasilkan berkas biner untuk pasangan sistem operasi dan arsitektur prosesor tertentu. Perilaku ini berbeda dari bahasa yang ditafsirkan (interpreted), yang kode sumbernya dibaca baris demi baris oleh penafsir pada saat program berjalan. Pada Go, satu perintah go build menyelesaikan seluruh proses penerjemahan itu.

Kompiler Go bekerja dalam dua tahap besar. Tahap pertama memeriksa kesesuaian sintaks dan tipe, sehingga kesalahan seperti pemanggilan fungsi dengan argumen yang salah tertangkap sebelum program berjalan. Tahap kedua menerjemahkan program yang lolos pemeriksaan menjadi kode mesin, misalnya untuk pasangan linux dan amd64. Karena penerjemahannya menyasar platform tertentu, biner yang dibangun di Linux tidak langsung dapat dijalankan di Windows. Alur singkatnya tampak pada diagram berikut.

Alur dari kode sumber ke biner statiskode sumberkonversi-suhu.gokompilasigo buildbiner statiskonversi-suhukode mesin + runtime Go+ pustaka yang dipakai
Alur dari kode sumber ke biner statis

Kompilasi yang cepat#

Kecepatan kompilasi adalah tujuan rancangan yang menyertai Go sejak awal, bukan kebetulan. Pengalaman di Google menunjukkan bahwa menunggu build berpuluh menit mengganggu alur kerja pengembang, sehingga Go dirancang agar prosesnya berlangsung singkat bahkan pada proyek berukuran menengah. Beberapa keputusan bahasa melayani tujuan itu. Sintaksnya kecil dan tegas, dan impor paket ditulis secara eksplisit. Aturan bahasa dijalankan keras, misalnya impor atau variabel yang tidak terpakai dianggap error. Karena itu, kompiler tidak perlu menebak maksud penulisnya.

Kecepatan tersebut juga datang dari alatnya, bukan hanya dari bahasanya. Perintah go menyimpan hasil kompilasi setiap paket dalam cache build dan memakainya kembali apabila paket beserta dependensinya tidak berubah. Pada proyek yang terdiri atas banyak paket, hanya bagian yang terpengaruh perubahan yang dikompilasi ulang. Karena itu, waktu tunggu tetap pendek meskipun proyek bertumbuh besar.

Biner statis tunggal#

Hasil go build adalah biner statis (static binary), yaitu berkas eksekusi tunggal yang memuat semua yang dibutuhkan untuk berjalan. Isinya mencakup kode mesin program, kode pustaka yang digunakan, serta runtime Go, yakni komponen bawaan yang mengelola memori dan penjadwalan eksekusi. Program itu tidak membutuhkan mesin virtual, penafsir, maupun pustaka tambahan yang harus dipasang lebih dahulu. Menyalin biner tersebut ke mesin lain dengan sistem operasi dan arsitektur yang sama sudah cukup untuk menjalankannya.

Bukti perilaku ini paling mudah diamati sendiri. Program berikut mengonversi deret suhu Celcius ke Fahrenheit. Program itu disimpan sebagai konversi-suhu.go dan dijalankan dengan go run.

go konversi-suhu.go
package main

import "fmt"

func main() {
	for c := 0; c <= 40; c += 10 {
		f := c*9/5 + 32
		fmt.Printf("%3d C = %3d F\n", c, f)
	}
}
text Keluaran
  0 C =  32 F
 10 C =  50 F
 20 C =  68 F
 30 C =  86 F
 40 C = 104 F

Perintah go run mengompilasi program ke folder sementara lalu langsung menjalankannya, sehingga binernya tidak tertinggal untuk diperiksa. Agar hasil kompilasi dapat diamati, program yang sama dibangun dengan go build ke folder eksperimen seperti pada langkah berikut.

bash
# menyiapkan folder eksperimen
rm -rf /tmp/gobiner && mkdir -p /tmp/gobiner
# menyalin kode sumber ke folder eksperimen
cp konversi-suhu.go /tmp/gobiner/main.go
# pindah ke folder eksperimen
cd /tmp/gobiner
# mengompilasi program menjadi biner bernama konversi-suhu
go build -o konversi-suhu main.go
# menjalankan biner hasil build
./konversi-suhu
# menampilkan ukuran biner dalam bentuk ringkas
du -h konversi-suhu | awk '{print $1, $2}'
text Keluaran
  0 C =  32 F
 10 C =  50 F
 20 C =  68 F
 30 C =  86 F
 40 C = 104 F
2.3M konversi-suhu

Keluaran biner identik dengan keluaran go run, sedangkan ukurannya memperlihatkan sisi lain dari biner statis. Kode sumbernya hanya berisi sepuluh baris, tetapi binernya mencapai sekitar 2,3 MB, karena runtime Go, pengumpul sampah (garbage collector), dan seluruh kode pustaka yang digunakan ikut terkompilasi ke dalam biner. Kebutuhan itu tidak perlu dipasang di mesin tujuan, dan langkah berikut membuktikannya dengan menyalin biner ke lokasi lain lalu menjalankannya tanpa kode sumber.

bash
# menyalin biner ke berkas baru di lokasi lain
cp /tmp/gobiner/konversi-suhu /tmp/konversi-salinan
# menjalankan salinan biner tersebut
/tmp/konversi-salinan
text Keluaran
  0 C =  32 F
 10 C =  50 F
 20 C =  68 F
 30 C =  86 F
 40 C = 104 F

Salinan tersebut berjalan sama persis tanpa kode sumber, tanpa alat Go, dan tanpa pustaka eksternal. Inilah alasan distribusi program Go sering sederhana, yaitu mengirim satu berkas ke mesin tujuan. Pola yang sama berlaku saat menyediakan perangkat CLI untuk pengguna berbagai sistem operasi, karena satu kode sumber dapat dibangun ulang untuk tiap platform tanpa perubahan.

Mental model penjadwal goroutine#

Runtime Go menyertakan penjadwal (scheduler), yaitu komponen yang membagi waktu eksekusi di antara banyak aktivitas sekaligus. Unit aktivitasnya disebut goroutine, yakni fungsi yang dijalankan secara bersamaan oleh runtime Go tanpa biaya yang berat. Goroutine dimulai dengan kata kunci go dan membutuhkan memori awal hanya beberapa kilobyte, sehingga ratusan ribu goroutine dapat hidup dalam satu program. Pembanding yang lazim adalah thread sistem operasi (thread), yaitu jalur eksekusi yang dikelola langsung oleh sistem operasi. Thread membutuhkan memori jauh lebih besar dan mahal untuk dibuat dalam jumlah besar.

Penjadwal bekerja dengan pola yang sering disebut M:N, yaitu banyak goroutine yang dijalankan bergantian di atas sedikit thread sistem operasi. Apabila satu goroutine menunggu sesuatu, misalnya respons jaringan, runtime tidak membiarkan threadnya menganggur, melainkan memindahkan goroutine lain ke thread yang sama. Mental model sederhananya seperti pada diagram berikut, yaitu antrean goroutine yang dibagikan penjadwal ke beberapa thread.

Pola M:N penjadwal goroutinegoroutine 1goroutine 2goroutine 3goroutine 4goroutine 5Penjadwalruntime GoThread OS 1beberapa goroutine bergantianThread OS 2beberapa goroutine bergantian
Pola M:N, yaitu banyak goroutine yang dibagikan ke sedikit thread OS

Model semacam ini menjelaskan mengapa konkurensi (concurrency) disebut bagian dari bahasa Go, bukan tambahan pustaka. Konkurensi adalah susunan program yang memuat beberapa aktivitas yang berlangsung dalam waktu saling tumpang tindih, sedangkan paralelisme adalah berjalannya banyak aktivitas pada saat yang persis sama di beberapa inti prosesor. Penjadwal mengurus pembagian keduanya secara otomatis, sehingga program tidak perlu mengelola thread sendiri untuk sebagian besar kebutuhan.

Kesalahan umum#

  • Menganggap go run bukan kompilasi. Perintah itu tetap mengompilasi program, tetapi ke folder sementara, lalu menghapusnya setelah program selesai, sehingga terasa seperti bahasa yang ditafsirkan. Apabila binernya ingin disimpan, gunakan go build.
  • Menjalankan biner Go di sistem operasi yang berbeda. Biner hasil go build menyasar satu pasangan sistem operasi dan arsitektur, misalnya linux dan amd64, sehingga menjalankannya di Windows menghasilkan error format. Perbaikannya adalah kompilasi ulang dengan GOOS dan GOARCH yang sesuai.
  • Menganggap ukuran biner sebagai tanda pemborosan. Biner berukuran megabyte memuat runtime, pengumpul sampah, dan pustaka yang digunakan. Tanpa biner statis, mesin tujuan harus memasang lingkungan eksekusi terpisah terlebih dahulu.

Ringkasan#

  • Go dikompilasi ke kode mesin, dan go build menghasilkan biner statis yang memuat kode program, pustaka yang digunakan, dan runtime Go sekaligus.
  • Kompilasi cepat diperoleh dari sintaks yang kecil, aturan yang keras, dan cache build yang menyimpan hasil paket yang tidak berubah.
  • Biner statis cukup disalin ke mesin lain dengan sistem operasi dan arsitektur yang sama untuk langsung dijalankan.
  • Penjadwal runtime Go membagikan banyak goroutine yang ringan ke sedikit thread sistem operasi dengan pola M:N.

Referensi#