- Go adalah pilihan untuk mengurangi kompleksitas berlebihan dalam pengembangan backend, dengan keunggulan utama kompilasi cepat, deploy satu binary, dan pengelolaan dependensi yang stabil
- Alih-alih abstraksi rumit seperti decorator, metaclass, macro, trait, dan monad, Go memilih desain bahasa yang sederhana dengan fokus pada struct, fungsi, interface, goroutine, dan channel
- Hanya dengan standard library dan tool bawaan seperti
embed, html/template, net/http, database/sql, encoding/json, go test, dan pprof, kita bisa menangani web app, database, testing, benchmark, hingga profiling
- Goroutine adalah unit eksekusi stackful dengan biaya sekitar 2KB, dan lewat channel,
sync.Mutex, race detector, serta context.Context, penanganan concurrency dan propagasi pembatalan bisa dilakukan dengan sederhana
- Alur
go mod init, go build, scp, systemctl restart mendorong deploy sederhana berbasis satu binary Go dan Postgres, alih-alih node_modules, konfigurasi Docker·Kubernetes yang rumit, atau microservices yang berlebihan
Alasan memilih Go
- Go adalah pilihan untuk mengurangi kompleksitas berlebihan dalam pengembangan backend, dengan keunggulan utama kompilasi cepat, deploy satu binary, dan pengelolaan dependensi yang stabil
- Seperti HTML yang tetap menjadi alternatif terhadap kompleksitas berlebihan di frontend, Go juga telah hadir lebih dari 10 tahun sebagai pilihan untuk menyederhanakan backend
- Untuk aplikasi CRUD sederhana atau aplikasi dengan sekitar 40 request per detik, terlalu berlebihan jika harus melibatkan banyak paket Node, tool build TypeScript, Kubernetes, tim platform Rails, sampai rewrite ke Rust
- Arah Go lebih menekankan kode yang mudah dibaca, hasil yang bisa langsung dideploy, dan beban operasional yang kecil daripada “abstraksi pintar”
Desain bahasa yang sengaja dibuat membosankan
- Go terasa membosankan karena memang dirancang seperti itu; ia tidak menyediakan abstraksi rumit seperti decorator, metaclass, macro, trait, atau monad
- Komponen intinya dibatasi pada struct, fungsi, interface, goroutine, dan channel
- Tujuannya adalah kesederhanaan, sampai-sampai spesifikasinya bisa dibaca dalam waktu singkat dan pada hari yang sama orang sudah bisa produktif menulis kode
- Kebosanan ini justru menjadi keunggulan dalam codebase tim
- Junior yang baru masuk bulan lalu pun bisa membaca kode yang ditulis principal dua tahun lalu
- Karena
gofmt memaksa satu format, perdebatan soal style kode jadi berkurang
- Bahasa ini sendiri membuat abstraksi yang terlalu rumit sulit disisipkan ke dalam codebase
Standard library berperan sebagai framework
- Go dapat digunakan untuk membuat web app hanya dengan standard library, tanpa framework web terpisah
- Dengan
embed, html/template, dan net/http, kita bisa membuat aplikasi yang menyertakan template HTML ke dalam binary dan merendernya lewat HTTP handler
package main
import (
"embed"
"html/template"
"net/http"
)
//go:embed templates/*.html
var files embed.FS
var tmpl = template.Must(template.ParseFS(files, "templates/*.html"))
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmpl.ExecuteTemplate(w, "index.html", map[string]string{
"Name": "asshole",
})
})
http.ListenAndServe(":8080", nil)
}
- Contoh ini adalah web app yang benar-benar berjalan, dan template HTML-nya ikut dikompilasi ke dalam binary
- Tanpa
webpack, Vite, development server, atau node_modules raksasa, kita cukup menjalankan go build lalu deploy satu file
- Hanya dengan standard library dan tool dasar, sebagian besar pekerjaan backend bisa ditangani
- Database:
database/sql
- JSON:
encoding/json
- Memanggil service lain: client
net/http
- Eksekusi konkuren: keyword
go
- Testing:
go test
- Benchmark:
go test -bench
- Profiling:
pprof
Susunan standard library yang dalam
-
io.Reader dan io.Writer
io.Reader dan io.Writer masing-masing hanya interface dengan satu method, tetapi menjadi fondasi penting di seluruh ekosistem Go
- Menghubungkan response body HTTP ke gzip writer lalu meneruskannya lagi ke file di disk bisa dilakukan dengan sedikit kode
- Karena banyak package utama berbagi dua interface ini, pola yang sama bisa dipakai berulang kali di berbagai tempat
-
context.Context
context.Context adalah cara standar untuk propagasi pembatalan
- Jika pengguna menutup tab browser, request context akan dibatalkan, dan sesudah itu query database serta pemanggilan HTTP turunan juga bisa ikut dibatalkan
- Untuk menghindari kebocoran goroutine atau query zombie yang menghabiskan connection pool, context harus diteruskan sebagai argumen pertama dan dihormati
-
Package encoding
encoding/json, encoding/xml, encoding/csv, dan encoding/binary semuanya termasuk dalam standard library
- Karena pola penggunaannya mirip, seperti struct tag dan decoding berbasis pointer, mempelajari satu package memudahkan penggunaan package lain
Model concurrency yang mengurangi penderitaan
- Goroutine bukan OS thread itu sendiri, melainkan unit eksekusi stackful yang dimultipleks runtime di atas OS thread
- Biaya memulai goroutine sekitar 2KB, sehingga membuat 100 ribu goroutine pun memungkinkan bahkan di laptop
- Channel berfungsi sebagai pipa bertipe di antara goroutine; saat satu sisi mengirim dan sisi lain menerima, runtime yang menangani sinkronisasinya
- Saat membutuhkan shared state, kita bisa memakai
sync.Mutex, dan race detector akan membantu menemukan data race
- Bahkan parallel HTTP fetcher bisa ditulis tanpa library tambahan, framework, atau ritual
async/await
results := make(chan string, len(urls))
for _, url := range urls {
go func(u string) {
resp, _ := http.Get(u)
results <- resp.Status
}(url)
}
for range urls {
fmt.Println(<-results)
}
Contoh route CRUD nyata
- Route bergaya CRUD yang membaca post dari Postgres lalu merender HTML juga bisa disusun sesederhana hingga muat dalam satu layar
//go:embed templates/*.html
var tmplFS embed.FS
var tmpl = template.Must(template.ParseFS(tmplFS, "templates/*.html"))
type Post struct {
ID int
Title string
Body string
}
func postsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rows, err := db.QueryContext(r.Context(),
"SELECT id, title, body FROM posts ORDER BY id DESC LIMIT 50")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var posts []Post
for rows.Next() {
var p Post
if err := rows.Scan(&p.ID, &p.Title, &p.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
posts = append(posts, p)
}
tmpl.ExecuteTemplate(w, "posts.html", posts)
}
}
- Contoh ini memperlihatkan database, template, dan HTTP handler di satu tempat
- Karena
r.Context() diteruskan ke query SQL, saat koneksi ditutup query juga bisa dibatalkan
- Tanpa ORM, container DI, service layer, atau direktori
controllers/ penuh abstract base class, perilakunya bisa dipahami dengan membaca dari atas ke bawah
Pengelolaan dependensi yang tidak merusak akhir pekan
- Saat memulai modul dengan
go mod init, dependensi akan dicatat di go.mod dan go.sum
go.sum pada dasarnya adalah catatan kriptografis dari item yang benar-benar diunduh, sehingga kita bisa memeriksa bila dependensi yang masuk berbeda dari yang diharapkan
- Tidak ada kompleksitas seperti direktori
node_modules, lockfile drift antara environment development dan CI, peer dependencies, optional dependencies, devDependencies, atau peerDependenciesMeta
- Jika butuh build offline,
go mod vendor akan mengunduh dependensi ke direktori vendor/, dan toolchain akan menggunakannya secara otomatis
- Seluruh project beserta dependensinya bisa dimasukkan ke satu tarball, yang menguntungkan dari sisi operasional dan review keamanan
Tool yang datang bersama compiler
- Tool dasar Go disediakan tanpa plugin pihak ketiga atau file konfigurasi tambahan
gofmt menstandarkan format kode dan mengurangi perdebatan formatting serta membesarnya diff akibat perubahan spasi
go vet digunakan untuk menangkap kesalahan yang jelas
go test menjalankan testing
go test -race menjalankan testing dengan race detector untuk menemukan data race
go test -bench menjalankan benchmark
go test -cover memeriksa coverage testing
go tool pprof memungkinkan kita mendapatkan flame graph penggunaan CPU dan memori melalui HTTP endpoint dari service production yang sedang berjalan
Deploy selesai dengan perintah salin
- Alur inti deploy Go adalah membangun binary, menyalinnya ke server, lalu menjalankannya
GOOS=linux GOARCH=amd64 go build -o myapp ./cmd/myapp
scp myapp user@server:/usr/local/bin/
ssh user@server 'systemctl restart myapp'
- Alur ini memungkinkan deploy tanpa Dockerfile, multi-stage build, notifikasi CVE base image, manifest Kubernetes, Helm chart, ArgoCD, service mesh, atau sidecar
- Dengan binary static-linked sekitar 12MB dan file unit systemd sepanjang 20 baris, deploy production sudah bisa dilakukan
- Jika Docker memang diperlukan, cukup masukkan binary Go ke image
FROM scratch
Perbandingan dengan framework
- Framework seperti Rails, Django, Express, dan Next.js masing-masing membawa beban seperti prosedur deploy sendiri, ORM, admin, middleware, warning npm, dan perubahan konvensi routing
- Binary Go dikompilasi lalu dijalankan, dan keunggulannya adalah stabilitas sehingga masih mungkin tetap berjalan 5 tahun kemudian
- Di tengah kenyataan bahwa framework bisa lebih cepat ditinggalkan atau maintainernya mengalami burnout, model eksekusi Go yang sederhana jadi makin menonjol
Satu binary Go lebih baik daripada microservices
- Microservices seharusnya bukan pilihan default; lebih baik menulis monolith terlebih dahulu
- Susunan yang direkomendasikan adalah satu binary Go, satu Postgres, dan satu Redis hanya jika benar-benar perlu
- HTML dan JSON API bisa disajikan dari port yang sama, dan seluruh aplikasi dapat berjalan di satu VPS
- Karena biaya goroutine rendah dan penanganan concurrency kuat, Go dapat diskalakan hingga 10 ribu request per detik tanpa kesulitan berarti
- Jika nantinya benar-benar perlu dipisah, package dalam monolith Go bisa dipindahkan ke repository terpisah
- Karena interface sudah ada, bahasa ini secara alami mendorong struktur yang mempertimbangkan pemisahan
Generic dan penanganan error
if err != nil bukan bug, melainkan fitur
- Ini memaksa kita memutuskan sendiri apa yang harus dilakukan di setiap titik kegagalan, tanpa menyembunyikan error
- Tumpukan
try/catch tidak menghilangkan error, hanya bisa menyembunyikannya sampai gangguan production benar-benar terjadi
- Generic diperkenalkan pada Go 1.18, dan cukup dipakai saat memang diperlukan
Kesimpulan
- Framework, microservices, rewrite ke Rust, atau meta-framework JavaScript baru tidak selalu diperlukan
- Jalankan
go mod init, tulis main.go, embed template, lalu kompilasi dan deploy—alur sederhana itulah yang direkomendasikan
- Pilihan yang membosankan adalah pilihan yang benar, dan Go adalah pilihan itu
1 komentar
Pendapat di Lobste.rs
Bukan mau menyalahkan penyampai pesannya, tapi gaya tulisan blog seperti ini melelahkan dan kekanak-kanakan. Mungkin awalnya lucu, tapi makin sering diulang, rasa sebalnya naik secara eksponensial
Meski begitu, Go tetap bagus. Belakangan ini aku pindah dari proyek TypeScript ke proyek Go, dan kesehatan mental serta semangat kerjaku membaik dengan cepat
Aku menerima bahwa
if err != nilbukan bug melainkan fitur, tapi tetap menganggapnya sebagai cacat terbesar Go. Kalau ada sum type, ini bisa dibuat jauh lebih ergonomis tanpa bergantung pada type assertion saat runtimeKalau mau menulis seperti ini, setidaknya hinaannya harus sedikit lebih cerdas
Dari komentar lain, sepertinya ini pendapat yang tidak populer, dan aku tidak ingin terdengar kasar, tapi aku benar-benar benci Go
Go adalah bahasa dengan sintaks yang lumayan di atas runtime yang efisien untuk konkurensi, lalu ekosistemnya didorong dengan kekuatan Google. Selain itu menurutku mengerikan
Masalah terbesarnya adalah bahasa ini tampak seperti sengaja dirancang untuk mengabaikan puluhan tahun riset desain bahasa pemrograman, atau bahkan praktik nyata di lapangan. Generics baru muncul setelah puluhan tahun
Bukan berarti kita harus selalu memakai dependent types, tapi tetap ada batasnya. Di Go hampir tidak ada fitur untuk pemodelan data, pemodelan invariant, atau penataan kode yang seharusnya dimiliki bahasa modern. Rust memang punya kurva belajar yang lebih curam, tapi dalam hal ini jauh lebih baik, dan sebenarnya tidak perlu sistem tipe serumit Rust untuk hasil yang cukup bagus. Kalau yang dikhawatirkan adalah waktu kompilasi, sistem tipe yang sound, cepat, dan ekspresif tetap bisa dibuat hanya dengan fitur-fitur sederhana namun berguna
Dan
if err != nilmenurutku adalah cara terburuk untuk memenuhi kode dengan kebisingan penanganan error. Aku tidak mengerti kenapa kubu Go begitu alergi pada sum type. Dalam hal ini bahkan exception di Java lebih baik. Kenyataannya, karena bahasanya tidak punya fitur yang lebih baik untuk menangani error, orang-orang jadi mengira tambalan terburuk yang mungkin itu adalah fiturSeandainya tulisan aslinya tidak sok pintar sejak awal, aku juga tidak akan menulis komentar seperti ini. “Pakai saja X” adalah ucapan bodoh. Pakailah alat yang cocok dengan use case, nyaman dipakai, dan produktif. Kalau itu Go, pakai Go. Kalau bukan, pilih yang lain
Ini terutama membantu di organisasi seperti Google, yang punya ribuan developer dan masa tinggal di tim atau perusahaan tertentu bisa singkat
Dalam konteks seperti ini, terutama untuk developer yang belum matang, ketiadaan sistem tipe tingkat lanjut justru sampai batas tertentu menjadi kelebihan. Mereka hampir tidak perlu memikirkan tipe di luar konsep sangat dasar seperti tipe primitif atau struct. Bahasa ini hampir tidak memberi alat untuk memodelkan data, tapi sebagai gantinya memungkinkan banyak kode ditulis tanpa banyak berpikir
Menurutku ini tidak bagus untuk ketepatan di level bahasa. Tapi di organisasi besar, beban itu lebih banyak ditopang oleh infrastruktur sekitar seperti analisis monorepo, CI/CD, canary testing, dan alat observabilitas. Infrastruktur itu menopang jauh lebih banyak dibanding di organisasi kecil
Aku juga agak menyukai Go karena beban kognitifnya rendah. Aku hanya sesekali menulis kode untuk proyek tertentu, dan saat ini tidak terlibat mendalam setiap hari di proyek jangka panjang. Bisa masuk ke codebase yang tidak kulihat selama sebulan dan mengerjakan sesuatu dalam waktu kurang dari satu jam adalah kelebihan besar. Tapi kalau aku menjadi developer penuh waktu untuk proyek yang kompleks, kurasa aku akan kurang menyukainya
Menurutku developer Go fokus merapikan fundamental, karena bahasa-bahasa sebelumnya dan komunitas riset teori bahasa pemrograman selama ini mengabaikan hal-hal mendasar itu. Orang-orang terobsesi pada sistem tipe yang paling komprehensif, padahal semakin kompleks dan ekspresif sistem tipenya, semakin kecil imbal hasilnya. Dan sekeras apa pun kita menggarap sistem tipe, itu tidak bisa menutupi package management yang buruk, build tool yang memaksa tim mempelajari DSL baru, sistem dokumentasi yang tidak otomatis membuat info tipe atau tautan dokumentasi paket pihak ketiga, standard library yang lemah, masalah performa serius, tidak adanya strategi kompilasi statis, waktu build yang menyakitkan, kurva belajar yang curam, sistem tipe yang menghukum, sintaks yang sulit dibaca, atau integrasi editor yang buruk
Mengatakan Go sama sekali tidak punya fitur pemodelan data jelas salah. Di bahasa apa pun data dan invariant bisa dimodelkan, dan Go juga menyediakan sistem tipe yang cukup untuk menegakkan model itu
Rust itu hebat, dan pilihan bagus bila kecepatan iterasi tidak penting, atau bila deploy ke bare metal, atau bila tuntutan correctness dan performa sangat tinggi. Tapi sebagai pilihan default untuk pengembangan aplikasi umum, terutama dalam tim, itu bukan pilihan yang bagus. Memang kita banyak mengetik
if err != nil, tapi menurutku tidak ada orang yang bottleneck-nya ada pada jumlah penekanan tombol per detikPernyataan bahwa
if err != nilbukan bug melainkan fitur, dan membuatmu melihat semua titik yang berpotensi bermasalah, itu salahPada praktiknya ini tidak dipaksakan. Mengabaikan error justru lebih mudah kalau kamu tidak memeriksanya sendiri
Untuk cara menangani atau meneruskan error, Rust tetap jadi contoh yang menonjol
Untungnya, semua proyek Go yang kukerjakan beberapa tahun terakhir memakai golangci-lint di atas pemeriksaan statis bawaan Go yang lemah. Jujur saja, ini seharusnya wajib di semua proyek Go
Aku benar-benar benci tren gaya penulisan seperti ini, tapi aku setuju dengan inti yang mau disampaikan tulisan itu
Ungkapan “tidak ada
node_modulesseukuran Volkswagen” memang benar, tapi yang ada hanyalah cache paket global di~/go, bukannode_moduleslokal per proyekwc -l go.sumduluBegitu membuka halaman dan melihat “Hey, dipshit.”, aku langsung menutupnya
Ini punya masalah yang sama seperti kebanyakan tulisan yang mengagung-agungkan bahasa pemrograman. Fokusnya bukan pada betapa hebat bahasa saat ini, melainkan pada betapa buruk bahasa yang sebelumnya dipakai
Penulisnya tampaknya sangat menderita dengan Ruby dan TypeScript, mungkin juga Python, lalu Go menyelesaikan masalah itu baginya. Tapi aku tidak memakai Ruby atau TypeScript, jadi tulisannya tidak terlalu mengena bagiku
Rasanya seperti sudah puluhan kali membaca variasi seperti ini selama bertahun-tahun. Karena punya static typing tidak seperti Python dan JavaScript, pakailah Haskell. Karena bisa dideploy sebagai satu biner tidak seperti Perl dan Erlang, pakailah Rust. Karena punya konkurensi dan channel yang layak tidak seperti Ruby dan Tcl, pakailah Elixir
Aku senang penulisnya menemukan bahasa yang cocok untuk dirinya, tapi aku tidak akan mengikuti sarannya
Nilai nol di Go selalu terasa seperti kekurangan bagiku. Menurutku lebih baik memaksa pengguna menyatakan nilai default secara eksplisit. Selain itu, untuk bahasa yang bukan OCaml, Go lumayan bagus
bool-nya harus bernilaitruesaat tidak diisiPengalaman deploy dan kompilasinya luar biasa, tapi aku benar-benar tidak suka menulis bahasanya sendiri. Setiap kali memakainya, pengalamannya buruk. Apakah ada bahasa lain yang tidak seketat Go tapi tetap punya pengalaman deploy yang bagus?
Apa ada sesuatu yang kulewatkan dari Go?
Aku baru-baru ini mencoba men-deploy aplikasi Rails kecil, dan butuh terlalu banyak konfigurasi, jadi aku jelas jadi lebih bisa menghargai kelebihan Go
x86_64-unknown-linux-musl. Dengan begitu hasilnya adalah biner statis yang bisa langsung dijalankan di semua mesin Linux 64-bit. Lalu aku memindahkannya pakaiscpdan menjalankannyaMasih ada masalah harus menetapkan port dan memulainya secara manual, tapi aku berencana menyelesaikannya dengan sedikit sihir systemd
Dengan bundler itu, kami bisa membuat satu executable yang bisa dijatuhkan ke mesin Linux distro lain, dan bahkan jika Qt belum terpasang, pengguna cukup menjalankan executable-nya dan seluruh GUI akan berfungsi
Tapi ada catatan bahwa driver OpenGL bisa menimbulkan masalah. Tetap mungkin dilakukan, hanya saja lebih rumit daripada sekadar “salin lalu jalankan”
Masalah terbesar adalah orang mengklaim Go dirancang untuk konkurensi, tapi bahasa ini justru punya pointer mentah bawaan yang mudah sekali dipakai untuk berbagi state tanpa sengaja
Kebosanan itu sendiri tidak masalah, tapi menurutku Go justru sangat gagal menjadi bahasa yang benar-benar membosankan
Katanya “tidak ada decorator”, tapi ada struct tags dan reflection. Sulit memahami bagaimana keduanya berinteraksi sampai kamu benar-benar menjalankannya
Interface struktural dan reflection adalah sumber menakutkan dari perubahan perilaku yang muncul dari tempat yang jauh. Cukup tambahkan satu method yang salah ke struct, dan perilaku library bisa berubah total
Dari sudut pandang dokumentasi juga aneh. Mengapa orang tidak ingin secara jelas menunjukkan interface apa yang memang dimaksudkan untuk dipenuhi oleh suatu tipe?
Aku juga tidak mengerti kenapa goroutine tidak disebut thread saja
Dan kenapa channel harus menjadi fitur bahasa? Menurutku karena mereka terlambat 10 tahun untuk mengakui bahwa generics berguna untuk lebih dari sekitar tiga jenis tipe
Menurutku channel menjadi bagian dari runtime agar scheduler goroutine mengetahui channel itu, sehingga lebih mudah membangunkan goroutine penerima ketika channel tidak lagi kosong. Mungkin cara ini memang lebih mudah