Pengembangan Alat Baris Perintah: Membandingkan Go dan Rust

Artikel ini membahas eksperimen saya dalam menulis alat baris perintah kecil menggunakan dua bahasa yang saya tidak memiliki banyak pengalaman pemrograman. Ini tentang Go dan Rust. Jika Anda ingin melihat kode dan independen membandingkan satu versi dari program saya dengan yang lain, maka di sini adalah repositori dari versi Go proyek, dan di sini adalah repositori dari versi tertulisnya di Rust.











Ulasan Proyek



Saya memiliki proyek rumah bernama Hashtrack. Ini adalah situs kecil, aplikasi tumpukan penuh yang saya tulis untuk wawancara teknis. Sangat mudah untuk bekerja dengannya:



  1. Pengguna diotentikasi (mengingat bahwa dia telah membuat akun untuk dirinya sendiri).
  2. Dia memperkenalkan hashtag yang ingin dia tonton muncul di Twitter.
  3. Dia menunggu tweet yang ditemukan dengan hashtag yang ditentukan untuk muncul di layar.


Anda dapat mencoba Hashtrack di sini .



Setelah menyelesaikan wawancara, saya terus mengerjakan proyek tersebut karena minat olahraga dan menyadari bahwa ini bisa menjadi platform yang hebat di mana saya dapat menguji pengetahuan dan keterampilan saya di bidang pengembangan alat baris perintah. Saya sudah memiliki server, jadi saya hanya perlu memilih bahasa yang akan saya gunakan untuk mengimplementasikan sejumlah kecil kemampuan dalam API proyek saya.



Kemampuan alat baris perintah



Berikut adalah deskripsi fitur utama, khususnya - perintah yang ingin saya terapkan di alat baris perintah saya.



  • hashtrack login - masuk ke sistem, yaitu - membuat token sesi dan menyimpannya di sistem file lokal, di file konfigurasi.
  • hashtrack logout — , — , .
  • hashtrack track <hashtag> [...] — .
  • hashtrack untrack <hashtag> [...] — .
  • hashtrack tracks — , .
  • hashtrack list — 50 .
  • hashtrack watch — .
  • hashtrack status — , .
  • --endpoint, .
  • --config, .
  • endpoint.


Berikut beberapa hal penting yang perlu diingat tentang alat saya sebelum mulai mengerjakannya:



  • Ini harus menggunakan API proyek yang menggunakan GraphQL, HTTP dan WebSocket.
  • Ini harus menggunakan sistem file untuk menyimpan file konfigurasi.
  • Ini harus dapat mengurai argumen posisi dan tanda baris perintah.


Mengapa saya memutuskan untuk menggunakan Go dan Rust?



Ada banyak bahasa di mana Anda dapat menulis alat baris perintah.



Dalam kasus ini, saya ingin memilih bahasa yang belum pernah saya alami, atau bahasa yang hanya sedikit saya alami. Selain itu, saya ingin menemukan sesuatu yang dapat dengan mudah dikompilasi ke kode mesin, karena ini adalah nilai tambah tambahan untuk alat baris perintah.



Bahasa pertama, yang jelas bagi saya, muncul di benak saya Go. Ini mungkin karena banyak alat baris perintah yang saya gunakan ditulis dalam Go. Tetapi saya juga memiliki sedikit pengalaman dalam pemrograman Rust, dan menurut saya bahasa ini juga akan cocok untuk proyek saya.



Berpikir tentang Go dan Rust, saya pikir Anda dapat memilih kedua bahasa tersebut. Karena tujuan utama saya adalah belajar mandiri, langkah seperti itu akan memberi saya peluang bagus untuk mengimplementasikan proyek dua kali dan secara mandiri mencari tahu keuntungan dan kerugian dari masing-masing bahasa.



Di sini saya ingin menyebutkan bahasa Crystal dan Nim . Mereka terlihat menjanjikan. Saya menantikan kesempatan untuk mengujinya di proyek saya berikutnya.



Lingkungan lokal



Sebelum menggunakan seperangkat alat baru, saya selalu tertarik dengan kegunaannya. Yakni, apakah saya harus menggunakan semacam pengelola paket untuk menginstal program secara global pada sistem. Atau, yang menurut saya solusi yang jauh lebih nyaman, apakah mungkin untuk menginstal semuanya berdasarkan akun pengguna. Kita berbicara tentang pengelola versi, mereka menyederhanakan hidup kita, berfokus pada penginstalan program pada pengguna, dan bukan pada sistem secara keseluruhan. Di lingkungan Node.js, NVM melakukan ini dengan sangat baik .



Saat bekerja dengan Go, Anda dapat menggunakan GVM untuk tujuan yang sama . Proyek ini bertanggung jawab atas penginstalan perangkat lunak lokal dan kontrol versi. Menginstalnya sangat sederhana:



gvm install go1.14 -B
gvm use go1.14


Saat menyiapkan lingkungan pengembangan di Go, Anda perlu menyadari keberadaan dua variabel lingkungan - GOROOTdan GOPATH. Anda dapat membaca lebih lanjut tentang mereka di sini .



Masalah pertama yang saya hadapi saat menggunakan Go adalah sebagai berikut. Ketika saya mencoba memahami cara kerja sistem resolusi modul dan cara penerapannya GOPATH, cukup sulit bagi saya untuk menyiapkan struktur proyek dengan lingkungan pengembangan lokal yang fungsional.



Saya akhirnya hanya menggunakan direktori proyek GOPATH=$(pwd). Kelebihan utama dari ini adalah saya memiliki sistem untuk bekerja dengan dependensi yang saya miliki, dibatasi oleh kerangka kerja proyek terpisah, seperti node_modules. Sistem ini telah bekerja dengan baik.



Setelah saya selesai mengerjakan alat saya, saya menemukan bahwa ada proyek virtualgo yang akan membantu saya memecahkan masalah saya GOPATH.



Rust memiliki penginstal Rustup resmi yang menginstal toolkit yang diperlukan untuk menggunakan Rust. Karat dapat dipasang dengan satu perintah. Selain itu, ketika digunakan rustup, kita memiliki akses ke komponen tambahan seperti rls Server dan rustfmt kode formatter . Banyak proyek memerlukan pembangunan kotak peralatan Rust setiap malam. Berkat aplikasinya rustup, saya tidak punya masalah untuk beralih antar versi.



Dukungan editor



Saya menggunakan VS Code dan dapat menemukan ekstensi yang menargetkan Go dan Rust. Kedua bahasa didukung dengan sempurna di editor.



Untuk men-debug kode Rust, mengikuti tutorial ini , saya perlu menginstal ekstensi CodeLLDB .



Manajemen paket



Ekosistem Go tidak memiliki pengelola paket atau bahkan registri resmi. Di sini sistem resolusi modul didasarkan pada modul impor dari URL eksternal.



Rust menggunakan manajer paket Cargo untuk mengelola dependensi, yang mengunduh paket dari crates.io dari registri paket Rust resmi. Dalam paket ekosistem krat bisa dokumentasi diposting di docs.rs .



Perpustakaan



Tujuan pertama saya dalam menjelajahi bahasa baru adalah untuk mencari tahu betapa sulitnya menerapkan komunikasi HTTP sederhana dengan server GraphQL menggunakan permintaan dan mutasi.



Berbicara tentang Go, saya berhasil menemukan beberapa library seperti machinebox / graphql dan shurcooL / graphql . Yang kedua menggunakan struktur untuk data marshaling dan unmarshaling. Karena itu, saya memilih dia.



Saya membuat garpu shurcooL / graphql karena saya perlu menyesuaikan tajuk pada klien Authorization. Perubahan disampaikan oleh PR ini .



Berikut adalah contoh pemanggilan mutasi GraphQL yang ditulis di Go:



type creationMutation struct {
    CreateSession struct {
        Token graphql.String
    } `graphql:"createSession(email: $email, password: $password)"`
}

type CreationPayload struct {
    Email    string
    Password string
}

func Create(client *graphql.Client, payload CreationPayload) (string, error) {
    var mutation creationMutation
    variables := map[string]interface{}{
        "email":    graphql.String(payload.Email),
        "password": graphql.String(payload.Password),
    }
    err := client.Mutate(context.Background(), &mutation, variables)

    return string(mutation.CreateSession.Token), err
}


Saat menggunakan Rust, saya perlu menggunakan dua perpustakaan untuk menjalankan kueri GraphQL. Intinya di sini adalah bahwa pustaka itu tidak graphql_clientbergantung pada protokol, yang ditujukan untuk menghasilkan kode untuk membuat serial dan deserialisasi data. Oleh karena itu, saya membutuhkan pustaka kedua ( reqwest), yang dengannya saya mengatur pekerjaan dengan permintaan HTTP.



#[derive(GraphQLQuery)]
#[graphql(
    schema_path = "graphql/schema.graphql",
    query_path = "graphql/createSession.graphql"
)]
struct CreateSession;

pub struct Session {
    pub token: String,
}

pub type Creation = create_session::Variables;

pub async fn create(context: &Context, creation: Creation) -> Result<Session, api::Error> {
    let res = api::build_base_request(context)
        .json(&CreateSession::build_query(creation))
        .send()
        .await?
        .json::<Response<create_session::ResponseData>>()
        .await?;
    match res.data {
        Some(data) => Ok(Session {
            token: data.create_session.token,
        }),
        _ => Err(api::Error(api::get_error_message(res).to_string())),
    }
}


Tidak ada pustaka Go dan Rust yang mendukung GraphQL melalui protokol WebSocket.



Faktanya, pustaka graphql_clientmendukung langganan, tetapi karena ini adalah protokol independen, saya harus menerapkan mekanisme interaksi WebSocket dengan GraphQL sendiri.



Untuk menggunakan WebSocket dalam versi Go aplikasi, pustaka harus dimodifikasi. Karena saya sudah menggunakan garpu perpustakaan, saya tidak ingin melakukan ini. Sebaliknya, saya menggunakan cara yang disederhanakan untuk "menonton" tweet baru. Yakni, untuk menerima tweet, saya mengirimkan request ke API setiap 5 detik. Saya tidak bangga saya melakukan itu .



Saat menulis program di Go, Anda dapat menggunakan kata kuncigountuk menjalankan streaming ringan yang disebut goroutine. Rust menggunakan utas sistem operasi, ini dilakukan dengan memanggil Thread::spawn. Saluran digunakan untuk mentransfer data antar aliran dan di sana-sini.



Pemrosesan kesalahan



Go memperlakukan kesalahan dengan cara yang sama seperti nilai lainnya. Cara biasa untuk menangani kesalahan di Go adalah dengan memeriksa kesalahan:



func (config *Config) Save() error {
    contents, err := json.MarshalIndent(config, "", "    ")
    if err != nil {
        return err
    }

    err = ioutil.WriteFile(config.path, contents, 0o644)
    if err != nil {
        return err
    }

    return nil
}


Rust memiliki pencacahan Result<T, E>yang mencakup nilai-nilai yang menunjukkan keberhasilan atau kegagalan. Ini, masing-masing, Ok(T)dan Err(E). Ada pencacahan lain di sini, Option<T>yang mencakup nilai Some(T)dan None. Jika Anda familiar dengan Haskell, maka Anda bisa mengenali monad Eitherdan artinya Maybe.



Ada juga "sintaksis gula" yang berkaitan dengan propagasi error (operator ?), yang memutuskan nilai struktur Resultbaik Optiondan secara otomatis kembali Err(...)atau Nonejika terjadi kesalahan.



pub fn save(&mut self) -> io::Result<()> {
    let json = serde_json::to_string(&self.contents)?;
    let mut file = File::create(&self.path)?;
    file.write_all(json.as_bytes())
}


Kode ini setara dengan kode berikut:



pub fn save(&mut self) -> io::Result<()> {
    let json = match serde_json::to_string(&self.contents) {
        Ok(json) => json,
        Err(e) => return Err(e.into())
    };
    let mut file = match File::create(&self.path) {
        Ok(file) => file,
        Err(e) => return Err(e.into())
    };
    file.write_all(json.as_bytes())
}


Jadi, Rust memiliki yang berikut:



  • Struktur monadik ( Optiondan Result).
  • Dukungan operator ?.
  • Sifat yang Fromdigunakan untuk secara otomatis mengubah kesalahan saat mereka menyebar.


Kombinasi dari tiga fitur di atas memberi kita sistem penanganan kesalahan yang menurut saya terbaik yang pernah saya lihat. Ini sederhana dan efisien, dan kode yang ditulis menggunakannya mudah dipelihara.



Kompilasi waktu



Go adalah bahasa yang dibuat dengan gagasan bahwa kode yang tertulis di dalamnya akan dikompilasi secepat mungkin. Mari kita periksa pertanyaan ini:



> time go get hashtrack #  
go get hashtrack  1,39s user 0,41s system 43% cpu 4,122 total

> time go build -o hashtrack hashtrack #  
go build -o hashtrack hashtrack  0,80s user 0,12s system 152% cpu 0,603 total

> time go build -o hashtrack hashtrack #  
go build -o hashtrack hashtrack  0,19s user 0,07s system 400% cpu 0,065 total

> time go build -o hashtrack hashtrack #      
go build -o hashtrack hashtrack  0,94s user 0,13s system 169% cpu 0,629 total


Impresif. Sekarang mari kita lihat apa yang akan ditunjukkan Rust kepada kita:



> time cargo build
   Compiling libc v0.2.67
   Compiling cfg-if v0.1.10
   Compiling autocfg v1.0.0
   ...
   ...
   ...
   Compiling hashtrack v0.1.0 (/home/paulo/code/cuchi/hashtrack/cli-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 1m 44s
cargo build  363,80s user 17,05s system 365% cpu 1:44,09 total


Semua dependensi dikompilasi di sini, yaitu 214 modul. Saat Anda memulai ulang kompilasi, semuanya sudah siap, jadi tugas ini dilakukan hampir secara instan:



> time cargo build #  
    Finished dev [unoptimized + debuginfo] target(s) in 0.08s
cargo build  0,07s user 0,03s system 104% cpu 0,094 total

> time cargo build #      
   Compiling hashtrack v0.1.0 (/home/paulo/code/cuchi/hashtrack/cli-rust)
    Finished dev [unoptimized + debuginfo] target(s) in 3.15s
cargo build  3,01s user 0,52s system 111% cpu 3,162 total


Seperti yang Anda lihat, Rust menggunakan model kompilasi inkremental. Kompilasi ulang sebagian dari pohon ketergantungan dilakukan, dimulai dengan modul yang dimodifikasi dan diakhiri dengan modul yang bergantung padanya.



Build rilis proyek membutuhkan lebih banyak waktu, yang diharapkan, karena compiler mengoptimalkan kode dalam kasus ini:



> time cargo build --release
   Compiling libc v0.2.67
   Compiling cfg-if v0.1.10
   Compiling autocfg v1.0.0
   ...
   ...
   ...
   Compiling hashtrack v0.1.0 (/home/paulo/code/cuchi/hashtrack/cli-rust)
    Finished release [optimized] target(s) in 2m 42s
cargo build --release  1067,72s user 16,95s system 667% cpu 2:42,45 total


Integrasi berkelanjutan



Fitur proyek kompilasi yang ditulis dalam Go dan Rust, yang kami identifikasi di atas, muncul, yang diharapkan, dalam sistem integrasi berkelanjutan.





Lakukan pemrosesan proyek





Memproses proyek Rust



Konsumsi memori



Untuk menganalisis konsumsi memori dari berbagai versi alat baris perintah saya, saya menggunakan perintah berikut:



/usr/bin/time -v ./hashtrack list


Perintah ini time -vmenampilkan banyak informasi menarik, tetapi saya tertarik dengan indikator proses Maximum resident set size, yang merupakan jumlah puncak memori fisik yang dialokasikan ke program selama pelaksanaannya.



Berikut kode yang saya gunakan untuk mengumpulkan data konsumsi memori untuk berbagai versi program:



for n in {1..5}; do
    /usr/bin/time -v ./hashtrack list > /dev/null 2>> time.log
done
grep 'Maximum resident set size' time.log


Berikut hasil untuk versi Go:



Maximum resident set size (kbytes): 13632
Maximum resident set size (kbytes): 14016
Maximum resident set size (kbytes): 14244
Maximum resident set size (kbytes): 13648
Maximum resident set size (kbytes): 14500


Berikut adalah konsumsi memori dari program versi Rust:



Maximum resident set size (kbytes): 9840
Maximum resident set size (kbytes): 10068
Maximum resident set size (kbytes): 9972
Maximum resident set size (kbytes): 10032
Maximum resident set size (kbytes): 10072


Memori ini dialokasikan selama tugas berikut:



  • Interpretasi argumen sistem.
  • Memuat dan mengurai file konfigurasi dari sistem file.
  • Mengakses GraphQL melalui HTTP menggunakan TLS.
  • Mengurai respons JSON.
  • Menulis data yang diformat ke stdout.


Go dan Rust memiliki cara berbeda untuk mengelola memori.



Go memiliki pengumpul sampah yang digunakan untuk mendeteksi memori yang tidak digunakan dan mengambilnya kembali. Hasilnya, programmer tidak terganggu oleh tugas-tugas ini. Karena pengumpul sampah didasarkan pada algoritma heuristik, menggunakannya selalu berarti membuat kompromi. Biasanya antara kinerja dan jumlah memori yang digunakan oleh aplikasi.



Model manajemen memori Rust memiliki konsep seperti kepemilikan, peminjaman, seumur hidup. Hal ini tidak hanya berkontribusi pada penanganan memori yang aman, tetapi juga memastikan kontrol penuh atas memori yang dialokasikan di heap tanpa memerlukan pengelolaan memori manual atau pengumpulan sampah.



Sebagai perbandingan, mari kita lihat program lain yang memecahkan masalah yang serupa dengan saya.



Perintah Ukuran set penduduk maksimum (kbytes)
heroku apps 56436
gh pr list 26456
git ls-remote (dengan akses SSH) 6448
git ls-remote (dengan akses HTTP) 23488


Alasan mengapa saya memilih Go



Saya akan memilih Pergi untuk beberapa proyek karena alasan berikut:



  • Jika saya membutuhkan bahasa yang akan mudah dipelajari oleh anggota tim saya.
  • Jika saya ingin menulis kode sederhana dengan mengorbankan fleksibilitas bahasa yang kurang.
  • Jika saya mengembangkan perangkat lunak hanya untuk Linux, atau jika Linux adalah sistem operasi yang paling menarik bagi saya.
  • Jika waktu kompilasi proyek itu penting.
  • Jika saya membutuhkan mekanisme yang matang untuk eksekusi kode asinkron.


Alasan mengapa saya memilih Rust



Berikut adalah alasan yang mungkin membuat saya memilih Rust untuk sebuah proyek:



  • Jika saya membutuhkan sistem penanganan kesalahan tingkat lanjut.
  • Jika saya ingin menulis dalam bahasa multi-paradigma yang memungkinkan saya menulis kode yang lebih ekspresif daripada yang bisa saya buat dengan bahasa lain.
  • Jika proyek saya memiliki persyaratan keamanan yang sangat tinggi.
  • Jika kinerja tinggi sangat penting untuk proyek tersebut.
  • Jika proyek menargetkan beberapa sistem operasi dan saya ingin benar-benar memiliki basis kode multi-platform.


Ucapan umum



Go dan Rust memiliki beberapa keanehan yang masih menghantui saya. Ini adalah sebagai berikut:



  • Go sangat berfokus pada kesederhanaan sehingga terkadang pengejaran ini memiliki efek sebaliknya (misalnya, seperti dalam kasus dengan GOROOTdan GOPATH).
  • Saya masih belum begitu memahami konsep "seumur hidup" di Rust. Bahkan upaya untuk bekerja dengan mekanisme bahasa yang sesuai membuat saya kehilangan keseimbangan.


Ya, saya ingin menunjukkan bahwa di versi Go yang lebih baru, bekerja dengan GOPATHtidak lagi menyebabkan masalah, jadi saya harus mentransfer proyek saya ke versi Go yang lebih baru.



Bisa saya katakan bahwa Go dan Rust adalah bahasa yang sangat menarik untuk dipelajari. Saya menemukan mereka sebagai tambahan yang bagus untuk kemampuan dunia pemrograman C / C ++. Mereka memungkinkan Anda membuat aplikasi untuk berbagai tujuan. Misalnya, layanan web dan bahkan, berkat WebAssembly, aplikasi web sisi klien .



Hasil



Go dan Rust adalah alat hebat yang cocok untuk mengembangkan alat baris perintah. Tapi, tentu saja, pencipta mereka dipandu oleh prioritas yang berbeda. Satu bahasa bertujuan untuk membuat pengembangan perangkat lunak sederhana dan dapat diakses, sehingga kode yang ditulis dalam bahasa tersebut dapat dipelihara. Prioritas bahasa lain adalah rasionalitas, keamanan, dan kinerja.



Jika Anda ingin membaca lebih lanjut tentang perbandingan Go dan Rust, lihat artikel ini . Antara lain, ini menimbulkan masalah yang serius dengan kompatibilitas program multi-platform.



Bahasa apa yang akan Anda gunakan untuk mengembangkan alat baris perintah?






All Articles