4 Go Kritik Bahasa

Diterbitkan: 2022-03-11

Go (alias Golang) adalah salah satu bahasa yang paling diminati orang. Pada April 2018, bahasa ini menempati posisi ke-19 dalam indeks TIOBE. Semakin banyak orang yang beralih dari PHP, Node.js, dan bahasa lain ke Go dan menggunakannya dalam produksi. Banyak software keren (seperti Kubernetes, Docker, dan Heroku CLI) ditulis menggunakan Go.

Jadi, apa kunci sukses Go? Ada banyak hal di dalam bahasa yang membuatnya sangat keren. Namun salah satu hal utama yang membuat Go begitu populer adalah kesederhanaannya, seperti yang ditunjukkan oleh salah satu penciptanya, Rob Pike.

Kesederhanaan itu keren: Anda tidak perlu mempelajari banyak kata kunci. Itu membuat belajar bahasa sangat mudah dan cepat. Namun, di sisi lain, terkadang pengembang kekurangan beberapa fitur yang mereka miliki dalam bahasa lain dan, oleh karena itu, mereka perlu menyelesaikan kode atau menulis lebih banyak kode dalam jangka panjang. Sayangnya, Go tidak memiliki banyak fitur berdasarkan desain, dan terkadang sangat mengganggu.

Golang dimaksudkan untuk membuat pengembangan lebih cepat, tetapi dalam banyak situasi, Anda menulis lebih banyak kode daripada yang Anda tulis menggunakan bahasa pemrograman lain. Saya akan menjelaskan beberapa kasus seperti itu dalam kritik bahasa Go saya di bawah ini.

Kritik Bahasa 4 Go

1. Kurangnya Fungsi yang Berlebihan dan Nilai Default untuk Argumen

Saya akan memposting contoh kode nyata di sini. Ketika saya sedang mengerjakan penjilidan Selenium Golang, saya perlu menulis fungsi yang memiliki tiga parameter. Dua di antaranya adalah opsional. Berikut tampilan setelah diimplementasikan:

 func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { // the actual implementation was here } func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error { return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) } func (wd *remoteWD) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) }

Saya harus mengimplementasikan tiga fungsi yang berbeda karena saya tidak bisa begitu saja membebani fungsi atau melewatkan nilai default—Go tidak menyediakannya secara desain. Bayangkan apa yang akan terjadi jika saya tidak sengaja menelepon orang yang salah? Berikut ini contohnya:

Saya akan mendapatkan banyak `undefined`

Saya harus mengakui bahwa terkadang fungsi yang berlebihan dapat menghasilkan kode yang berantakan. Di sisi lain, karena itu, programmer perlu menulis lebih banyak kode.

Bagaimana Itu Bisa Ditingkatkan?

Berikut adalah contoh yang sama (hampir sama) dalam JavaScript:

 function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) { // actual implementation here }

Seperti yang Anda lihat, itu terlihat jauh lebih jelas.

Saya juga menyukai pendekatan Elixir dalam hal itu. Berikut adalah tampilannya di Elixir (saya tahu bahwa saya dapat menggunakan nilai default, seperti pada contoh di atas—saya hanya menunjukkannya sebagai cara yang dapat dilakukan):

 defmodule Waiter do @default_interval 1 @default_timeout 10 def wait(condition, timeout, interval) do // implementation here end def wait(condition, timeout), do: wait(condition, timeout, @default_interval) def wait(condition), do: wait(condition, @default_timeout, @default_interval) end Waiter.wait("condition", 2, 20) Waiter.wait("condition", 2) Waiter.wait("condition")

2. Kurangnya Obat Generik

Ini bisa dibilang fitur yang paling banyak diminta oleh pengguna Go.

Bayangkan Anda ingin menulis fungsi peta, di mana Anda melewatkan larik bilangan bulat dan fungsi, yang akan diterapkan ke semua elemennya. Kedengarannya mudah, bukan?

Mari kita lakukan untuk bilangan bulat:

 package main import "fmt" func mapArray(arr []int, callback func (int) (int)) []int { newArray := make([]int, len(arr)) for index, value := range arr { newArray[index] = callback(value) } return newArray; } func main() { square := func(x int) int { return x * x } fmt.Println(mapArray([]int{1,2,3,4,5}, square)) // prints [1 4 9 16 25] }

Terlihat bagus, bukan?

Nah, bayangkan Anda juga perlu melakukannya untuk string. Anda harus menulis implementasi lain, yang persis sama kecuali untuk tanda tangannya. Fungsi ini akan membutuhkan nama yang berbeda, karena Golang tidak mendukung fungsi overloading. Hasilnya, Anda akan memiliki banyak fungsi serupa dengan nama berbeda, dan akan terlihat seperti ini:

 func mapArrayOfInts(arr []int, callback func (int) (int)) []int { // implementation } func mapArrayOfFloats(arr []float64, callback func (float64) (float64)) []float64 { // implementation } func mapArrayOfStrings(arr []string, callback func (string) (string)) []string { // implementation }

Itu jelas bertentangan dengan prinsip KERING (Jangan Ulangi Diri Sendiri), yang menyatakan bahwa Anda perlu menulis kode salin/tempel sesedikit mungkin dan alih-alih memindahkannya ke fungsi dan menggunakannya kembali.

Kurangnya obat generik berarti ratusan fungsi varian

Pendekatan lain adalah menggunakan implementasi tunggal dengan interface{} sebagai parameter, tetapi ini bisa mengakibatkan kesalahan waktu proses karena pemeriksaan tipe waktu proses lebih rawan kesalahan. Dan juga akan lebih lambat, jadi tidak ada cara sederhana untuk mengimplementasikan fungsi-fungsi ini sebagai satu kesatuan.

Bagaimana Itu Bisa Ditingkatkan?

Ada banyak bahasa bagus yang menyertakan dukungan generik. Misalnya, berikut adalah kode yang sama di Rust (Saya telah menggunakan vec alih-alih array untuk membuatnya lebih sederhana):

 fn map<T>(vec:Vec<T>, callback:fn(T) -> T) -> Vec<T> { let mut new_vec = vec![]; for value in vec { new_vec.push(callback(value)); } return new_vec; } fn square (val:i32) -> i32 { return val * val; } fn underscorify(val:String) -> String { return format!("_{}_", val); } fn main() { let int_vec = vec![1, 2, 3, 4, 5]; println!("{:?}", map::<i32>(int_vec, square)); // prints [1, 4, 9, 16, 25] let string_vec = vec![ "hello".to_string(), "this".to_string(), "is".to_string(), "a".to_string(), "vec".to_string() ]; println!("{:?}", map::<String>(string_vec, underscorify)); // prints ["_hello_", "_this_", "_is_", "_a_", "_vec_"] }

Perhatikan bahwa ada satu implementasi fungsi map , dan itu dapat digunakan untuk semua jenis yang Anda butuhkan, bahkan yang khusus.

3. Manajemen Ketergantungan

Siapa pun yang memiliki pengalaman dalam Go dapat mengatakan bahwa manajemen ketergantungan sangat sulit. Alat Go memungkinkan pengguna untuk menginstal perpustakaan yang berbeda dengan menjalankan go get <library repo> . Masalahnya di sini adalah manajemen versi. Jika pengelola perpustakaan membuat beberapa perubahan yang tidak kompatibel ke belakang dan mengunggahnya ke GitHub, siapa pun yang mencoba menggunakan program Anda setelah itu akan mendapatkan kesalahan, karena go get tidak melakukan apa pun selain git clone repositori Anda ke dalam folder perpustakaan. Juga jika perpustakaan tidak diinstal, program tidak akan dikompilasi karena itu.

Anda dapat melakukan sedikit lebih baik dengan menggunakan Dep untuk mengelola dependensi (https://github.com/golang/dep), tetapi masalahnya di sini adalah Anda menyimpan semua dependensi di repositori Anda (yang tidak baik, karena repositori Anda akan berisi tidak hanya kode Anda tetapi ribuan dan ribuan baris kode ketergantungan), atau hanya menyimpan daftar paket (tetapi sekali lagi, jika pengelola ketergantungan membuat perubahan yang tidak kompatibel ke belakang, semuanya akan macet).

Bagaimana Itu Bisa Ditingkatkan?

Saya pikir contoh sempurna di sini adalah Node.js (dan JavaScript secara umum, saya kira) dan NPM. NPM adalah repositori paket. Ini menyimpan berbagai versi paket, jadi jika Anda memerlukan versi paket tertentu, tidak masalah—Anda bisa mendapatkannya dari sana. Juga, salah satu hal dalam aplikasi Node.js/JavaScript adalah file package.json . Di sini, semua dependensi dan versinya terdaftar, sehingga Anda dapat menginstal semuanya (dan mendapatkan versi yang pasti berfungsi dengan kode Anda) dengan npm install .

Juga, contoh bagus dari manajemen paket adalah RubyGems/Bundler (untuk paket Ruby) dan Crates.io/Cargo (untuk perpustakaan Rust).

4. Penanganan Kesalahan

Penanganan kesalahan di Go sangat sederhana. Di Go, pada dasarnya Anda dapat mengembalikan beberapa nilai dari fungsi, dan fungsi dapat mengembalikan kesalahan. Sesuatu seperti ini:

 err, value := someFunction(); if err != nil { // handle it somehow }

Sekarang bayangkan Anda perlu menulis fungsi yang melakukan tiga tindakan yang mengembalikan kesalahan. Ini akan terlihat seperti ini:

 func doSomething() (err, int) { err, value1 := someFunction(); if err != nil { return err, nil } err, value2 := someFunction2(value1); if err != nil { return err, nil } err, value3 := someFunction3(value2); if err != nil { return err, nil } return value3; }

Ada banyak kode berulang di sini, yang tidak bagus. Dan dengan fungsi besar, itu bisa lebih buruk! Anda mungkin memerlukan kunci pada keyboard Anda untuk ini:

gambar lucu dari kode penanganan kesalahan pada keyboard

Bagaimana Itu Bisa Ditingkatkan?

Saya suka pendekatan JavaScript untuk itu. Fungsi tersebut dapat menimbulkan kesalahan, dan Anda dapat menangkapnya. Pertimbangkan contoh:

 function doStuff() { const value1 = someFunction(); const value2 = someFunction2(value1); const value3 = someFunction3(value2); return value3; } try { const value = doStuff(); // do something with it } catch (err) { // handle the error }

Ini jauh lebih jelas dan tidak mengandung kode berulang untuk penanganan kesalahan.

Hal-Hal Baik di Go

Meskipun Go memiliki banyak kekurangan dalam desain, Go memiliki beberapa fitur yang sangat keren juga.

1. Goroutine

Pemrograman Async dibuat sangat sederhana di Go. Meskipun pemrograman multithreading biasanya sulit dalam bahasa lain, memunculkan utas baru dan menjalankan fungsi di dalamnya sehingga tidak akan memblokir utas saat ini sangat sederhana:

 func doSomeCalculations() { // do some CPU intensive/long running tasks } func main() { go doSomeCalculations(); // This will run in another thread; }

2. Alat yang Dibundel dengan Go

Sementara dalam bahasa pemrograman lain Anda perlu menginstal pustaka/alat yang berbeda untuk tugas yang berbeda (seperti pengujian, pemformatan kode statis, dll.), ada banyak alat keren yang sudah disertakan dalam Go secara default, seperti:

  • gofmt - Alat untuk analisis kode statis. Dibandingkan dengan JavaScript, di mana Anda perlu menginstal ketergantungan tambahan, seperti eslint atau jshint , ini disertakan secara default. Dan program bahkan tidak akan dikompilasi jika Anda tidak menulis kode Go-style (tidak menggunakan variabel yang dideklarasikan, mengimpor paket yang tidak digunakan, dll.).
  • go test - Kerangka pengujian. Sekali lagi, dibandingkan dengan JavaScript, Anda perlu menginstal dependensi tambahan untuk pengujian (Jest, Mocha, AVA, dll.). Di sini, itu disertakan secara default. Dan itu memungkinkan Anda untuk melakukan banyak hal keren secara default, seperti pembandingan, mengonversi kode dalam dokumentasi ke tes, dll.
  • godoc - Alat dokumentasi. Sangat menyenangkan untuk memasukkannya ke dalam alat default.
  • Kompiler itu sendiri. Ini sangat cepat, dibandingkan dengan bahasa kompilasi lainnya!

3. Tunda

Saya pikir ini adalah salah satu fitur terbaik dalam bahasa ini. Bayangkan Anda perlu menulis fungsi yang membuka tiga file. Dan jika ada yang gagal, Anda harus menutup file yang sudah dibuka. Jika ada banyak konstruksi seperti itu, itu akan terlihat berantakan. Pertimbangkan contoh kode semu ini:

 function openManyFiles() { let file1, file2, file3; try { file1 = open('path-to-file1'); } catch (err) { return; } try { file2 = open('path-to-file2'); } catch (err) { // we need to close first file, remember? close(file1); return; } try { file3 = open('path-to-file3'); } catch (err) { // and now we need to close both first and second file close(file1); close(file2); return; } // do some stuff with files // closing files after successfully processing them close(file1); close(file2); close(file3); return; }

Terlihat rumit. Di situlah defer Go terjadi:

 package main import ( "fmt" ) func openFiles() { // Pretending we're opening files fmt.Printf("Opening file 1\n"); defer fmt.Printf("Closing file 1\n"); fmt.Printf("Opening file 2\n"); defer fmt.Printf("Closing file 2\n"); fmt.Printf("Opening file 3\n"); // Pretend we've got an error on file opening // In real products, an error will be returned here. return; } func main() { openFiles() /* Prints: Opening file 1 Opening file 2 Opening file 3 Closing file 2 Closing file 1 */ }

Seperti yang Anda lihat, jika kita mendapatkan kesalahan saat membuka file nomor tiga, file lain akan ditutup secara otomatis, karena pernyataan defer dieksekusi sebelum kembali dalam urutan terbalik. Juga, bagus untuk membuka dan menutup file di tempat yang sama alih-alih bagian yang berbeda dari suatu fungsi.

Kesimpulan

Saya tidak menyebutkan semua hal baik dan buruk di Go, hanya yang saya anggap terbaik dan terburuk.

Go benar-benar salah satu bahasa pemrograman yang menarik yang digunakan saat ini, dan benar-benar memiliki potensi. Ini memberi kami alat dan fitur yang sangat keren. Namun, ada banyak hal yang bisa diperbaiki di sana.

Jika kami, sebagai pengembang Go, akan menerapkan perubahan ini, itu akan sangat bermanfaat bagi komunitas kami, karena itu akan membuat pemrograman dengan Go jauh lebih menyenangkan.

Sementara itu, jika Anda mencoba meningkatkan pengujian Anda dengan Go, cobalah Menguji Aplikasi Go Anda: Memulai dengan Cara yang Benar oleh sesama Toptaler Gabriel Aszalos.

Terkait: Logika yang Terstruktur dengan Baik: Tutorial OOP Golang