Go Masih Tetap Tidak Bagus
(blog.habets.se)- Berbagai keputusan desain bahasa Go dibuat secara tidak perlu atau dengan mengabaikan pengalaman yang sudah ada
- Masalah pengelolaan cakupan variabel error membuat keterbacaan kode dan pelacakan bug menjadi lebih sulit
- Dalam berbagai aspek seperti dualisme nil, penggunaan memori, dan portabilitas kode, muncul desain yang tidak intuitif dan tidak sesuai dengan kenyataan
- Keterbatasan sintaks defer serta cara penanganan pengecualian di pustaka standar membuat jaminan exception safety sulit dicapai
- Masalah yang terakumulasi seperti pengelolaan memori dan penanganan UTF-8 yang buruk dalam jangka panjang berdampak negatif pada kualitas codebase Go
Kritik jangka panjang terhadap bahasa Go
- Seperti yang telah diungkapkan dalam posting sebelumnya (Why Go is not my favourite language, Go programs are not portable), saya telah menunjukkan berbagai masalah bahasa Go selama lebih dari 10 tahun
- Secara khusus, keputusan desain yang tidak perlu yang mengabaikan praktik baik yang sudah dikenal terasa makin disayangkan
Ketidakintuitifan cakupan variabel error
- Sintaks Go memperluas cakupan variabel error (
err) secara tidak perlu sehingga meningkatkan kemungkinan kesalahan- Dalam contoh kode, variabel
errtetap hidup di seluruh fungsi dan digunakan kembali, sehingga keterbacaan dan maintainability kode menurun - Bahkan developer berpengalaman mengalami salah paham dan membuang waktu saat melacak bug karena masalah cakupan seperti ini
- Cara untuk membatasi variabel secara lokal dengan tepat tidak diizinkan oleh sintaks
- Dalam contoh kode, variabel
Dua bentuk nil
- Di Go ada kebingungan karena
nilberperilaku berbeda pada tipe interface dan tipe pointer- Seperti pada contoh di bawah, meskipun
nildiberikan kes(pointer) dani(interface),s==idievaluasi berbeda, menunjukkan perilaku yang tidak konsisten - Ini adalah jenis masalah yang umumnya ingin dihindari dalam penanganan null, dan menunjukkan jejak desain yang dibuat tanpa pertimbangan memadai
- Seperti pada contoh di bawah, meskipun
Batasan portabilitas kode
- Penggunaan komentar untuk conditional compilation sangat tidak efisien dari sisi maintainability dan portabilitas
- Jika pernah benar-benar membuat perangkat lunak portabel, mudah dipahami bahwa cara seperti ini merepotkan dan rawan kesalahan
- Pengalaman historis yang telah terkumpul (portabilitas kode, contoh praktis di lapangan) diabaikan
- Untuk detail lebih lanjut, lihat Go programs are not portable
Ketidakjelasan kepemilikan append
- Hubungan kepemilikan antara fungsi
appenddan slice tidak jelas, sehingga perilaku kode sulit diprediksi- Melalui contoh, sulit mengetahui sebelumnya dampak sebenarnya pada data asli ketika sebuah slice di-append di fungsi
foo - Semakin banyak ‘quirk’ bahasa yang harus dihafal, sehingga memicu kesalahan
- Melalui contoh, sulit mengetahui sebelumnya dampak sebenarnya pada data asli ketika sebuah slice di-append di fungsi
Desain sintaks defer yang kurang matang
- Tidak mendukung pelepasan resource secara jelas seperti prinsip RAII (Resource Acquisition Is Initialization)
- Dibandingkan dengan sintaks manajemen resource terstruktur di Java dan Python, Go tidak memperjelas resource mana yang harus dilepas dengan
defer - Seperti dalam contoh pekerjaan file, bahkan masalah double-close harus ditangani sendiri, dan urutan serta cara pelepasan yang benar tidak jelas
- Dibandingkan dengan sintaks manajemen resource terstruktur di Java dan Python, Go tidak memperjelas resource mana yang harus dilepas dengan
Penanganan pengecualian di pustaka standar
- Go adalah struktur yang tidak mendukung exception secara eksplisit, tetapi situasi pengecualian seperti
panictetap bisa terjadi- Ada juga kasus ketika
panicdalam situasi tertentu tidak sepenuhnya menghentikan program dan malah terserap diam-diam - Ada pola di pustaka standar (
fmt.Print, server HTTP, dll.) yang mengabaikan pengecualian, sehingga jaminan exception safety yang sesungguhnya tidak mungkin dicapai - Pada akhirnya, menulis kode yang aman terhadap exception tetap wajib, tetapi exception tidak bisa digunakan secara langsung
- Ada juga kasus ketika
Penanganan UTF-8 dan string
- Meskipun tipe
stringdiisi data biner arbitrer, Go tetap berjalan tanpa validasi khusus- Nama file yang dibuat sebelum encoding UTF-8, misalnya, bisa diam-diam terlewat
- Data penting dapat hilang dalam proses seperti backup, dan ini merupakan pendekatan sederhana yang tidak mencerminkan kondisi dunia nyata
Batasan pengelolaan memori
- Sulit mengendalikan penggunaan RAM secara langsung, dan keandalan GC (garbage collection) juga memiliki batasan
- Penggunaan memori Go meningkat dan dalam jangka panjang terhubung ke masalah biaya serta performa
- Dalam lingkungan multi-instance dan container, masalah biaya serta skalabilitas benar-benar terjadi
Kesimpulan: ada jalan yang lebih baik
- Walaupun desain bahasa yang telah terbukti efektif sebenarnya sudah ada, Go justru mengabaikannya di banyak bagian
- Berbeda dari masalah pada rancangan awal Java, ketika Go dirilis sebenarnya sudah ada pendekatan yang lebih baik
Referensi
- Uber: Data race patterns in Go
- FasterThanLime: Lies we tell ourselves to keep using Golang
- FasterThanLime: I want off Mr Golang’s wild ride
1 komentar
Komentar Hacker News
Saya sudah memakai Go sejak era pre-1.0 di hampir semua pekerjaan full-time. Bahasa ini sederhana untuk dipelajari anggota tim dari dasar, dan umumnya berjalan stabil. Saat memperbarui ke versi Go terbaru, hampir tidak ada yang perlu dikhawatirkan, dan sebagian besar fitur berguna sudah tersedia bawaan. Kecepatan kompilasinya menarik. Pemrosesan paralel memang agak rumit, tetapi kalau diluangkan waktu, Go jadi enak untuk mengekspresikan aliran data. Sistem tipenya umumnya nyaman, meski kadang terasa bertele-tele. Secara keseluruhan, ini alat yang bisa diandalkan. Namun, saya juga setuju dengan berbagai kritik yang disebutkan di artikel. Jelas ada bagian di mana Go terlalu terikat pada prinsip-prinsip generasi lama sehingga melewatkan kenyamanan praktis. Tentu ini hanya kesan saya, dan mungkin kalau semua kekurangannya diperbaiki, hasilnya malah bisa lebih buruk dari sekarang. Saya juga ingin menyebut bahwa dalam beberapa tahun terakhir terasa ada suasana yang lebih terbuka untuk memperbaiki quirk. Dulu saya tak pernah membayangkan generics atau custom iterator akan ditambahkan. Kritik soal RAM dan portabilitas terasa agak seperti keluhan pribadi. Akan bagus kalau membaik, tetapi GC sangat jarang menimbulkan masalah serius di kebanyakan program, dan debugging juga tidak terlalu sulit. Selain itu, Go mendukung hampir semua platform penting. Meski begitu, saya tetap tidak nyaman dengan cara penanganan error dan
nil. Saya sering merindukan sintaks sepertiResult[Ok, Err],Optional[T]Menurut saya justru Go bukan keras kepala pada prinsip, melainkan terobsesi pada kemudahan praktis untuk cepat menyelesaikan masalah yang terlihat di depan mata. Bukan menganalisis masalah secara mendasar lalu menyelesaikannya dengan benar, tetapi terasa seperti menanggalkan semangat "Not Invented Here" dan merakit solusi seadanya saat itu juga. API filesystem Go adalah contoh yang representatif. Kalau butuh fungsi untuk membuka file, ya cukup bikin
func Open(name string) (*File, error)seperti itu lalu selesai. Tapi bagaimana kalau nama filenya bukan UTF-8? Karena masalah itu tidak muncul selama 5 tahun, ya diabaikan sajaSaya sering merasa prinsip desain Go terlalu berat ke tujuan “membuat compiler mudah dibuat dan kompilasi cepat”. Strukturnya lebih fokus pada compiler/kompilasinya sendiri ketimbang kenyamanan pengembang
Setelah 20 tahun, di pekerjaan baru saya akhirnya benar-benar memakai Go sebagai bahasa terkompilasi untuk pertama kalinya. Mungkin ini soal selera pribadi, tapi jujur saya sampai merasa tidak enak saat memakainya. Tidak ada nilai argumen default, saya tidak suka cara penanganan error, dan tidak ada stack trace yang layak di production. Sintaks berorientasi objek juga kelihatan jelek karena harus menempelkan referensi yang canggung ke tiap fungsi. Pointer juga terasa membebani. Akhirnya rasanya seperti kembali ke teknologi lama C/C++. Suasananya persis seperti pemrograman yang saya lakukan di kampus sekitar tahun 1999
Dari sisi pemrosesan paralel, menurut pengalaman saya Go adalah satu-satunya sistem bahasa yang secara alami menangani paralelisme di lingkungan CPU multi-core pada level bahasa itu sendiri. Berkat formula goroutine/channel bergaya CSP, logika paralel bisa diekspresikan dengan intuitif. Python bikin pusing dengan GIL dan library async yang rumit. C, C++, Java, dan lainnya memerlukan library tambahan di luar bahasa, jadi tidak mudah menalar paralelisme di level bahasa. Karena itu saya merasa go sangat cocok untuk server HTTP atau service. Dalam pengalaman saya, tidak ada alternatif yang sebanding
Dari sudut pandang pengembang, ergonomics—yakni dalam hal standarisasi dan konsistensi—terasa nyaris sempurna. Di berbagai codebase microservice pun tidak perlu khawatir gaya penulisannya berbeda-beda, dan tidak perlu debat soal formatting. Namun, saat memilih cara standar ala Go, rasanya Go terlalu ngotot pada gaya lama. Pengembang masa kini lebih mengharapkan metode fungsional seperti
map/filter, tetapi Go hanya memberi loop yang berisiko salah indeks. Sistem tipenya juga tidak sepintar TypeScript. Penanganan error juga tidak nyaman. Saya paham kekhawatiran bahwa menambahkan fitur-fitur itu bisa memperbanyak “cara pakai kreatif tapi buruk”, tetapi saya juga merasakan sulitnya meyakinkan generasi JS untuk memakai goSaya sudah lebih dari 5 tahun fokus pada proyek Golang besar, dan saat harus membuat komponen yang meminimalkan penggunaan memori, saya sering berhadapan dengan sisi Go yang rapuh. GC kadang tidak membersihkan cukup cepat atau fragmentasi heap menjadi parah (karena Go bukan compacting garbage collector). Akibatnya saya berusaha sebisa mungkin menghindari alokasi, tetapi itu mudah memicu bug. Debugging juga sangat sulit. Meski melihat heap profile, yang tampak hanya informasi objek yang masih hidup, bukan sampah yang menumpuk atau detail fragmentasi, jadi kita harus menebak-nebak. Misalnya, fungsi X terlihat hanya mengalokasikan 1KB di heap, tetapi kalau dipanggil terus di dalam loop, bisa menghasilkan sampah puluhan MB. Karena itu kami mengalokasikan buffer statis lebih dulu lalu mendaur ulangnya, tetapi masalah ownership jadi rumit dan muncul celah seperti
append. Bahkan kadang library standar pun harus kami implementasikan ulang sendiri. Saya tahu kasus kami tidak umum, tetapi tetap saja rasanya mengecewakan karena seperti sedang melawan bahasa itu sendiriDalam kasus seperti ini, justru memindahkan memori ke luar heap mungkin lebih tidak menyakitkan. Tentu tidak mudah karena ini bahasa GC, tetapi daripada memaksakan kode yang terlalu ala C++/Rust di Go, lebih baik bagian itu saja langsung diganti ke bahasa tersebut
Menurut saya, memilih go untuk situasi seperti ini memang masalah pada pemilihan bahasanya sendiri. C/C++/Rust/Zig lebih cocok
Ada kabar bahwa garbage collector baru "Green Tea" mungkin bisa membantu. Ini adalah algoritma parallel mark yang lebih baik dalam menangani objek yang berdekatan di memori, walau bukan GC yang berfokus penuh pada memori. Info terkait bisa dilihat di sini
Eksperimen arena sempat berjalan, tetapi sekarang sudah dihentikan. Meski begitu, tetap menarik untuk diperhatikan
Maaf kalau ini tidak terlalu membantu, tetapi melihat situasinya sekarang, saya rasa pilihan bahasanya benar-benar salah. Saya menduga mungkin kalian terpaksa memakai go karena kebijakan bahasa resmi di perusahaan. Perusahaan besar sering hanya menyetujui production untuk bahasa yang dipakai luas
Saya masih belum paham kenapa
deferdi Go hanya bekerja pada function scope dan tidak berlaku pada lexical scope. Saya tahu fakta ini juga karena pernah memproses file di dalam loop, lalu saat daftar file membesar,defertidak menutup handle sampai fungsi selesai dan akhirnya menyebabkan crash. Pengembang Go di sekitar saya menyarankan agar isi loop dibungkus dengan fungsi anonim. Selain beberapa hal kecil lainnya, Go terasa menyenangkan, sintaksnya efisien, dan juga mencegah budaya ‘pamer’ yang tidak perlu. Saya pernah me-rewrite proyek C# skala besar ke Go, dan meski fiturnya hanya sepersepuluh, jumlah kodenya justru lebih sedikit. Bahasa ini mendorong kita memakai default yang cepat alih-alih memaksa alokasi GC, dan fitur code generation bawaan untuk hal seperti serialization terasa praktis. Berbeda dengan sintaks C# yang ingin menggantikan segalanya lewat bahasa seperti ORM, di Go suasananya lebih seperti: SQL ya tetap ditulis sebagai SQL, gRPC ditangani lewat spesifikasi protobufKadang yang dibutuhkan adalah
deferpada lexical scope, kadang justru function scope. Misalnya, kalau di dalam loop Anda membuka banyak file dan ingin semuanya tetap terbuka sampai fungsi selesai, maka function scope memang dibutuhkan. Sekarang yang ada adalah function scope, tetapi saat lexical scope diperlukan, kita bisa membungkusnya denganfunc. Kalau yang didukung hanya lexical scope lalu kita butuh function scope, justru tidak jelas harus bagaimanaKeuntungannya adalah satu tingkat indentasi berkurang tanpa perlu fungsi pembungkus, perilakunya terkait dengan call stack atau stack unwinding, dan dari sudut pandang gaya C seperti
goto fail, ini terasa natural. Tentu saja, saat memakaideferdi dalam loop, memang agak tidak nyaman karena harus dibungkus fungsi terpisahSaya pernah memakai bahasa dengan
deferlevel blok dan level fungsi sekaligus, dan kadang memang berharap bisa memakaideferlevel fungsi juga di dalamifRasanya tidak ada alasan yang terlalu mendalam, dan saya juga bertanya-tanya apakah ini benar-benar penting
Di C# juga bisa bekerja dengan SQL atau spesifikasi protobuf. Bedanya hanya ada pilihan lain juga
Go memang punya banyak kekurangan, tetapi dalam kategori bahasa server-side, saya merasa tidak ada bahasa lain yang seimbang seperti ini. Ia lebih cepat daripada Node atau Python, dan menurut saya sistem tipenya juga lebih baik. Ambang masuknya lebih rendah daripada Rust, dan library standar serta tooling-nya juga hebat. Saya suka sintaksnya yang sederhana dan cara Go memaksa hanya ada satu cara. Penanganan error memang bermasalah, tetapi tetap lebih baik daripada
catchdi Node yang bisa berisi error apa saja. Saya penasaran apakah ada bahasa lain yang lebih baik dan memenuhi semua kriteria ini. Saya sendiri bukan fanatik Go; sepanjang karier saya justru lebih banyak memakai Node untuk backend, tetapi belakangan sedang bereksperimen dengan GoSebenarnya semua kelebihan ini bisa saja dikatakan juga untuk Java atau C#
Saya agak terganggu kalau 'Node' disebut sebagai bahasa pemrograman. Node adalah runtime JavaScript, dan sekarang banyak proyek yang berjalan di Node justru ditulis dalam TypeScript. Artinya, ketika mengatakan Node, bahasanya sendiri tidak jelas. Kalau acuannya TypeScript, saya justru merasa sistem tipe Go lebih tidak produktif. Klaim serupa juga bisa dibuat saat membandingkannya dengan Rust
Kebanyakan bahasa punya ketidaknyamanan masing-masing. Go unggul di performa, portabilitas, dan runtime/ekosistem. Di sisi lain, ada kekurangan seperti pointer
nil, zero value, tidak ada destructor, tidak ada macro, dan ketiadaan macro di Go malah mendorong penyalahgunaan code generation. Ada bahasa yang lebih baik juga (misalnya Rust), tetapi biasanya jadi jauh lebih kompleks daripada Go. Akar masalahnya adalah pembuat Go menempatkan kesederhanaan sebagai prioritas utamaKalau melihat perkembangan terbaru sistem tipe Python, saya rasa Python sudah jauh di depan Go. Dari sisi structural typing saja, Python lebih mengesankan
Saya rasa sistem tipe Go memang sangat kurang
Saya pernah memperluas static site generator yang dibuat dengan Go. Kodenya sangat jelas dan mudah dibaca, tetapi karena keterbatasan bahasanya, kemampuan ekspansinya rendah. Perubahan sederhana pun mengharuskan bongkar pasang sulit di banyak tempat. Sulit membuat berbagai tingkat enkapsulasi dan abstraksi, dan abstraksi dikorbankan demi “kesederhanaan”. Padahal abstraksi adalah cara paling penting untuk membuat kode yang mudah diperluas. Go memilih kesederhanaan alih-alih ekspansibilitas. Secara umum, program Go terasa berhenti pada “kesederhanaan yang tidak bisa diperluas”. Orang-orang bersikeras bahwa memang begitulah Go, tetapi menurut pengalaman saya itu tidak meyakinkan. Meski begitu, setidaknya ‘pengalaman pengembang’-nya tidak buruk
Percakapan tentang Go selalu terasa agak aneh. Kalau dikritik, biasanya suasananya jadi “ya memang bahasanya begitu” dan disuruh menerimanya saja. Kesederhanaan disebut sebagai kekuatan, tetapi saya meragukan apakah harus menulis loop sendiri untuk mengambil daftar key dari map benar-benar lebih sederhana
Saya ingin bertanya, apakah mudah melontarkan kritik seperti ini hanya setelah mencoba Go sebentar. Saya telah menangani banyak codebase Go besar (jutaan baris) sejak 2015 dan bekerja di banyak tim. Skalabilitas Go tidak secara khusus lebih buruk dibanding C, C#, atau Java. Go memang cenderung memilih kejelasan daripada ekspresivitas. Karena itu lapisan abstraksinya lebih sedikit, dan kita terbiasa menulis dengan cara yang lebih konkret dan eksplisit. Namun saya tidak melihat itu otomatis berarti tidak bisa diperluas. Desain modular yang mudah diperluas adalah wilayah yang dicapai lewat pembelajaran pengembang, bukan ditentukan oleh bahasanya. Kode yang Anda tangani mungkin saja dirancang dengan buruk; itu bukan batasan bahasa Go
Saya memakai Go selama beberapa tahun, dan meski hal-hal kecil bisa dibuat cepat, semakin besar skalanya, semakin banyak gangguan kecil yang menyiksa. Debugging terutama terasa seperti mimpi buruk: kalau ada X yang tidak dipakai (yang selalu terjadi saat mengomentari bagian tertentu saat debugging), maka kodenya bahkan tidak bisa dikompilasi. Banyaknya formalisasi yang tidak perlu, nama file khusus, dan nama field yang dicadangkan juga merepotkan.
panicyang tersembunyi di library standar dan salinan heap tak terduga juga lambat dan menjengkelkan. Bagian-bagian Go yang terasa ‘ajaib’ sebagian besar muncul sebagai efek samping dari memaksa penggunaan fitur lama yang sudah ada (nama file khusus, huruf besar-kecil, dan sebagainya). Kalau memang ingin menandai sesuatu sebagai public, mestinya bisa saja pakaipub, tetapi anehnya tetap keras kepala. Sekarang AI sudah jauh lebih baik, jadi saat ada masalah tipe atau borrow checker di Rust, saya bisa langsung bertanya ke AI dan menyelesaikannya cepat, sehingga terasa jauh lebih menyenangkan. Tidak perlu lagi membuang waktu seperti dulu dengan mengubek dokumentasi atau SOSaya belum benar-benar mendalami Rust belakangan ini, tetapi saat sempat mencobanya sebentar Desember lalu, saya kaget AI sangat bagus menangani Rust. Karena banyak detail sintaks dan informasi tipe yang eksplisit, AI malah lebih piawai daripada manusia dalam banyak kasus
Kalau mengeluhkan masalah compile error saat debugging di Go, kubu Go biasanya menegur dengan mengatakan “pakailah alat yang benar”. Prinsipnya diterapkan terlalu ekstrem sampai tidak nyaman
Saya pernah menyampaikan ketidaknyamanan debugging ini kepada salah satu pendiri Go, tetapi bahkan dia pun tidak memahami masalahnya. Saya kecewa karena terasa sangat amatir. Sebagai catatan, AI justru tidak terlalu bagus menangani Go. Meski bahasanya relatif sederhana, ChatGPT lebih baik mendukung Java, C#, dan Python
Secara pribadi saya tidak suka Go dan melihat banyak kekurangan fatal, tetapi jelas ada alasan kenapa popularitasnya tetap tinggi. Go relatif cepat, dan berkat goroutine, mudah dipakai untuk menulis layanan dengan konkurensi tinggi yang stabil dan andal tanpa harus berkutat dengan multithreading secara langsung. Saat Google merilis Go, hampir tidak ada bahasa statis terkompilasi yang populer dengan posisi serupa. Bahkan sekarang pun pesaing di posisi yang mirip praktis hanya Java (sekarang dengan dukungan virtual thread). Bahasa yang mendukung async/await memang menjanjikan hal serupa, tetapi pada praktiknya kompleksitasnya besar juga (menghindari blocking di task async, function coloring, dan sebagainya). Erlang pun kategorinya berbeda. Jadi meski kekurangannya banyak, popularitasnya tetap tinggi berkat goroutine dan nilai nama proyek Google
Secara bertahap JVM sedang memperkecil jarak dengan Go. Melalui proyek seperti virtual threads, zgc, lilliput, Leyden, dan Valhalla, semuanya makin membaik. Perubahan dari Java 8 sampai 25 sangat besar. Ke depan sepertinya akan makin nyaman
Eksplisitas dan kesederhanaan Go sangat cocok untuk pemrograman berbantuan LLM. Kode Go 1.x lama pun tetap berjalan baik di versi terbaru
Faktanya, di dalam Google sendiri, Java dengan virtual threads jauh lebih sering dipakai daripada Go
Saya penasaran menurut Anda bahasa “modern” apa yang paling cocok untuk proyek baru
Saya menyukai Go sejak sebelum rilis 1.0, tetapi saya tidak setuju dengan penilaian “masih belum berhasil dibuat dengan benar”. Tentu ada kekurangan dan keluhan, tetapi saya juga merasa bahwa ketika para pendiri meninggalkan proyek, akan sulit mempertahankan visi pusat dan ada risiko bahasanya memburuk. Fakta bahwa Go diposisikan hanya sebagai “bahasa server” pada akhirnya juga akan mendorong orang pindah ke Rust, Python, dan lainnya. Dulu Visual Basic juga sering diejek, tetapi pada akhirnya orang yang membutuhkannya tetap memakainya dengan baik
Kalau diteliti cermat, tulisan-tulisan kritik tentang kekurangan Go sering kali tidak membahas hal yang sungguh besar. Kebanyakan memang benar secara teknis, tetapi sepele. Sebaliknya, masalah desain bahasa yang benar-benar serius adalah zero value, tidak adanya constructor, buruknya penanganan null, mutability bawaan, sistem tipe yang tidak dirancang dengan mempertimbangkan generics,
intyang tidak mendukung presisi arbitrer, sertasliceyang ownership-nya ambigu (isu terkait 1, isu terkait 2). Tidak adanya sum type dan tidak adanya string interpolation juga merupakan kekuranganSaya mungkin bias sampai menulis buku tentang Go, tetapi sebagai orang yang sudah lebih dari 10 tahun memakai Go, awalnya saya benar-benar merasa segar saat mengenalnya. Boilerplate-nya lebih sedikit daripada Java, mudah dipelajari, dan performanya cukup bagus. Tidak ada bahasa terbaik untuk semuanya, dan setiap kebutuhan punya pilihan yang paling tepat, tetapi untuk pekerjaan backend yang umum, ini pilihan yang tidak akan saya sesali