Stas Afanasyev. Juno. Pipa berdasarkan pada io.Reader / io.Writer. Bagian 1

Dalam laporan ini, kita akan berbicara tentang konsep io.Reader / io.Writer, mengapa mereka diperlukan, bagaimana menerapkannya dengan benar dan perangkap apa yang ada dalam hal ini, serta tentang membangun jaringan pipa berdasarkan implementasi io.Reader / io.Writer standar dan kebiasaan .



Stanislav Afanasyev (selanjutnya - SA): - Selamat siang! Nama saya Stas. Saya datang dari Minsk, dari perusahaan Juno. Terima kasih telah datang pada hari hujan ini, setelah menemukan kekuatan untuk meninggalkan rumah.

Hari ini saya ingin berbicara dengan Anda tentang topik seperti membangun saluran pipa Go berdasarkan antarmuka io.Reader / io.Writer. Apa yang akan saya bicarakan hari ini adalah, secara umum, konsep antarmuka io.Reader / io.Writer, mengapa mereka diperlukan, cara menggunakannya dengan benar, dan yang paling penting, cara menerapkannya dengan benar.

Kami juga akan berbicara tentang membangun saluran pipa berdasarkan berbagai implementasi dari antarmuka ini. Kami akan berbicara tentang metode yang ada, membahas pro dan kontra mereka. Saya akan menyebutkan berbagai jebakan (ini akan berlimpah).

Sebelum kita mulai, kita harus menjawab pertanyaan, mengapa antarmuka ini diperlukan? Angkat tanganmu, yang bekerja dengan Go dengan erat (setiap hari, setiap hari) ...



Hebat! Kami masih memiliki komunitas Go. Saya pikir banyak dari Anda telah bekerja dengan antarmuka ini, paling tidak pernah mendengarnya. Anda mungkin bahkan tidak tahu tentang mereka, tetapi Anda tentu harus mendengar sesuatu tentang mereka.

Pertama-tama, antarmuka ini adalah abstraksi operasi input-output dalam semua manifestasinya. Kedua, ini adalah API yang sangat nyaman yang memungkinkan Anda membangun saluran pipa, seperti konstruktor dari kubus, tanpa benar-benar memikirkan detail internal implementasi. Setidaknya itu awalnya dimaksudkan.

io.Reader


Ini adalah antarmuka yang sangat sederhana. Ini hanya terdiri dari satu metode - metode Baca. Secara konseptual, implementasi antarmuka io.Reader dapat menjadi koneksi jaringan - misalnya, di mana belum ada data, tetapi mereka dapat muncul di sana:



Ini dapat menjadi buffer di memori di mana data sudah ada dan dapat dibaca seluruhnya. Ini juga bisa menjadi deskriptor file - kita dapat membaca file ini dalam beberapa bagian jika sangat besar.

Implementasi konseptual dari antarmuka io.Reader adalah akses ke beberapa data. Semua kasus yang saya tulis didukung oleh metode Baca. Ini hanya memiliki satu argumen - ini adalah slice byte.
Satu hal yang ingin disampaikan di sini. Mereka yang datang ke Go baru-baru ini atau berasal dari beberapa teknologi lain, di mana tidak ada API serupa (saya salah satunya), tanda tangan ini agak membingungkan. Metode Baca tampaknya entah bagaimana membaca irisan ini. Faktanya, yang terjadi adalah sebaliknya: implementasi antarmuka Reader membaca data di dalam dan mengisi irisan ini dengan data yang dimiliki implementasi ini.

Jumlah maksimum data yang dapat dibaca berdasarkan permintaan dengan metode Baca sama dengan panjang irisan ini. Implementasi reguler mengembalikan data sebanyak yang dapat dikembalikan pada saat permintaan, atau jumlah maksimum yang cocok dengan irisan ini. Ini menunjukkan bahwa Reader dapat dibaca dalam beberapa bagian: setidaknya dengan byte, setidaknya sepuluh - sesuka Anda. Dan klien yang memanggil Pembaca, sesuai dengan nilai-nilai pengembalian dari metode Baca, berpikir untuk hidup terus.

Metode Baca mengembalikan dua nilai:

  • jumlah byte dikurangi;
  • kesalahan jika itu terjadi.

Nilai-nilai ini memengaruhi perilaku klien selanjutnya. Ada gif pada slide yang menunjukkan, menampilkan proses ini, yang baru saja saya jelaskan:





Io.Reader - Bagaimana caranya?


Tepatnya ada dua cara agar data Anda memenuhi antarmuka Reader.



Yang pertama adalah yang paling sederhana. Jika Anda memiliki beberapa jenis byte slice, dan Anda ingin membuatnya memuaskan antarmuka Reader, Anda dapat mengambil implementasi beberapa perpustakaan standar yang sudah memenuhi antarmuka ini. Misalnya, Pembaca dari paket byte. Pada slide di atas, Anda dapat melihat tanda tangan tentang bagaimana Pembaca ini dibuat.

Ada cara yang lebih rumit - untuk mengimplementasikan antarmuka Reader sendiri. Ada sekitar 30 baris dalam dokumentasi dengan aturan rumit, batasan yang harus diikuti. Sebelum kita berbicara tentang semuanya, menjadi menarik bagi saya: “Dan dalam hal apa implementasi standar yang tidak memadai (perpustakaan standar)? Kapan saat ketika kita perlu mengimplementasikan antarmuka Reader sendiri? "

Untuk menjawab pertanyaan ini, saya mengambil ribuan repositori paling populer di Github (dengan jumlah bintang), menambahkannya dan menemukan semua implementasi antarmuka Reader di sana. Pada slide, saya memiliki beberapa statistik (dikategorikan) ketika orang mengimplementasikan antarmuka ini.

  • Kategori yang paling populer adalah koneksi. Ini adalah implementasi dari protokol dan pembungkus berpemilik untuk tipe yang ada. Jadi, Brad Fitzpatrick memiliki proyek Camlistore - ada contoh dalam bentuk statTrackingConn, yang, secara umum, adalah Wrapper biasa pada tipe con dari paket net (menambahkan metrik ke tipe ini).
  • Kategori paling populer kedua adalah buffer khusus. Di sini saya menyukai satu-satunya contoh: dataBuffer dari paket x / net. Keunikannya adalah bahwa ia menyimpan data yang dipotong menjadi potongan-potongan, dan ketika mengurangi itu melewati potongan-potongan ini. Jika data di chunk sudah selesai, ia akan pindah ke chunk berikutnya. Pada saat yang sama, ia memperhitungkan panjangnya, tempat ia dapat mengisi irisan yang dikirimkan.
  • Kategori lain adalah semua jenis progress-bar, menghitung jumlah byte yang dikurangi dengan pengiriman metrik ...

Berdasarkan data ini, kita dapat mengatakan bahwa kebutuhan untuk mengimplementasikan antarmuka io.Reader cukup sering terjadi. Mari kita mulai berbicara tentang aturan yang ada di dokumentasi.

Aturan Dokumentasi


Seperti yang saya katakan, daftar aturan, dan secara umum dokumentasinya cukup besar, masif. 30 baris sudah cukup untuk antarmuka yang hanya terdiri dari tiga baris.

Aturan pertama dan terpenting menyangkut jumlah byte yang dikembalikan. Itu harus benar-benar lebih besar dari atau sama dengan nol dan kurang dari atau sama dengan panjang irisan yang dikirim. Mengapa ini penting?



Karena ini adalah kontrak yang cukup ketat, klien dapat mempercayai jumlah yang berasal dari implementasi. Ada Wrappers di perpustakaan standar (misalnya, bytes.Buffer dan bufio). Ada momen seperti itu di perpustakaan standar: beberapa implementasi percaya dibungkus Pembaca, beberapa tidak percaya (kita akan membicarakan ini nanti).

Bufio sama sekali tidak mempercayai apa pun - itu benar-benar memeriksa segalanya. Bytes.Buffer benar-benar mempercayai segala yang datang kepadanya. Sekarang saya akan menunjukkan apa yang terjadi sehubungan dengan ini ...

Kami sekarang akan mempertimbangkan tiga kemungkinan kasus - ini adalah tiga Pembaca yang diimplementasikan. Mereka cukup sintetis, berguna untuk pengertian. Kami akan membaca semua Pembaca ini menggunakan pembantu ReadAll. Tanda tangannya disajikan di bagian atas slide:



io.Reader # 1. Contoh 1


ReadAll adalah penolong yang membutuhkan semacam implementasi antarmuka Reader, membaca semuanya dan mengembalikan data yang dibaca, serta kesalahan.

Contoh pertama kami adalah Reader, yang akan selalu mengembalikan -1 dan nil sebagai kesalahan, mis. NegativeReader. Mari kita jalankan dan lihat apa yang terjadi:



Seperti yang Anda tahu, panik tanpa alasan adalah pertanda kebodohan. Tapi siapa yang dalam hal ini bodoh - saya atau byte.Buffer - tergantung pada sudut pandang. Mereka yang menulis paket ini dan yang mengikutinya memiliki sudut pandang yang berbeda.

Apa yang terjadi disini? Bytes.Buffer menerima sejumlah byte negatif, tidak memeriksa apakah itu negatif, dan mencoba memotong buffer internal di sepanjang batas atas, yang diterimanya - dan kami keluar dari batas slice.

Ada dua masalah dalam contoh ini. Yang pertama adalah bahwa tanda tangan tidak dilarang untuk mengembalikan angka negatif, dan dokumentasinya dilarang. Jika tanda tangan memiliki Uint, maka kami akan mendapatkan luapan klasik (ketika nomor yang ditandatangani ditafsirkan sebagai tidak ditandatangani). Dan ini adalah bug yang sangat rumit, yang pasti akan terjadi pada Jumat malam, ketika Anda sudah dirakit di rumah. Karenanya, panik dalam hal ini adalah opsi yang lebih disukai.

"Titik" kedua adalah bahwa jejak tumpukan tidak mengerti apa yang terjadi sama sekali. Jelas bahwa kita telah melampaui batas irisan - jadi apa? Ketika Anda memiliki pipa multilayer dan kesalahan seperti itu terjadi, tidak segera jelas apa yang terjadi. Jadi bufio dari perpustakaan standar juga "panik" dalam situasi ini, tetapi melakukannya dengan lebih indah. Dia segera menulis: “Saya mengurangi sejumlah byte negatif. Saya tidak akan melakukan hal lain - saya tidak tahu apa yang harus saya lakukan dengannya. "

Dan byte. Buffer panik sebaik mungkin. Saya memposting masalah ke Golang meminta saya untuk menambahkan kesalahan manusia. Hari ketiga, kami membahas prospek keputusan ini. Alasannya adalah ini: secara historis terjadi bahwa orang yang berbeda pada waktu yang berbeda membuat keputusan yang tidak terkoordinasi yang berbeda. Dan sekarang kami memiliki yang berikut: dalam satu kasus kami tidak percaya sama sekali implementasi (kami memeriksa semuanya), dan yang lain kami percaya sepenuhnya, kami tidak mendapatkan apa yang datang dari sana. Ini adalah masalah yang belum terselesaikan, dan kami akan berbicara lebih banyak tentang ini.

io.Reader # 1. Contoh 2


Situasi berikut: Pembaca kami akan selalu mengembalikan 0 dan nol sebagai hasilnya. Dari sudut pandang kontrak, semuanya legal di sini - tidak ada masalah. Satu-satunya peringatan: dokumentasi mengatakan bahwa implementasi tidak disarankan untuk mengembalikan nilai 0 dan nihil, di samping kasus ini, ketika panjang slice yang dikirim adalah nol.

Dalam kehidupan nyata, Pembaca seperti itu dapat menyebabkan banyak masalah. Jadi, kita kembali ke pertanyaan, haruskah kita percaya pada Reader? Sebagai contoh, sebuah cek dibangun ke dalam bufio: ia secara berurutan membaca Reader 100 kali - jika sepasang nilai dikembalikan 100 kali, ia hanya mengembalikan NoProgress.

Tidak ada yang seperti ini di byte. Buffer. Jika kita menjalankan contoh ini, kita hanya mendapatkan loop tanpa akhir (ReadAll menggunakan bytes.Buffer di bawah tenda, bukan Reader itu sendiri):



io.Reader # 1. Contoh 2


Satu lagi contoh. Ini juga cukup sintetik, tetapi berguna untuk memahami:



Di sini kita selalu mengembalikan 1 dan nihil. Tampaknya tidak ada masalah di sini juga - semuanya legal dari sudut pandang kontrak. Ada nuansa: jika saya menjalankan contoh ini di komputer saya, maka akan membeku setelah 30 detik ...

Ini disebabkan oleh fakta bahwa klien yang membaca Pembaca ini (yaitu, bytes.Buffer) tidak pernah mendapat tanda akhir data - bunyinya, kurangi ... Plus, dia mendapat satu byte yang dikurangi setiap waktu. Baginya, ini berarti bahwa pada titik tertentu, buffer yang direposisi berakhir, masih berjalan - situasinya berulang, dan itu berjalan hingga tak terbatas hingga meledak.

io.Reader # 2. Kesalahan kembali


Kami sampai pada aturan penting kedua untuk mengimplementasikan antarmuka Reader - ini adalah pengembalian kesalahan. Dokumentasi menyatakan tiga kesalahan yang harus dikembalikan oleh implementasi. Yang paling penting dari mereka adalah EOF.

EOF adalah tanda akhir dari data, yang implementasinya harus kembali setiap kali kehabisan data. Secara konseptual, ini, secara umum, bukan kesalahan, tetapi dibuat sebagai kesalahan.

Ada kesalahan lain yang disebut UnexpectedEOF. Jika tiba-tiba saat membaca Pustaka tidak dapat lagi membaca data, diperkirakan akan mengembalikan UnexpectedEOF. Namun pada kenyataannya, kesalahan ini hanya digunakan di satu tempat perpustakaan standar - dalam fungsi ReadAtLeast.



Kesalahan lain adalah NoProgress, yang sudah kita bicarakan. Dokumentasi mengatakan demikian: ini adalah tanda bahwa antarmuka diimplementasikan menyebalkan.

Saya. Pemimpin # 3


Dokumentasi menetapkan serangkaian kasus tentang cara mengembalikan kesalahan dengan benar. Di bawah ini Anda dapat melihat tiga kemungkinan kasus:



Kami dapat mengembalikan kesalahan baik dengan jumlah byte dikurangi, dan secara terpisah. Tetapi jika tiba-tiba data Anda habis di Pustaka Anda, dan Anda tidak dapat mengembalikan [tanda akhir] EOF sekarang (banyak implementasi dari pekerjaan perpustakaan standar seperti itu), maka diasumsikan bahwa Anda akan mengembalikan EOF ke panggilan berikutnya yang berurutan (yaitu, Anda harus melepaskan pelanggan).

Untuk klien, ini berarti tidak ada lagi data - jangan mendatangi saya lagi. Jika Anda mengembalikan nol, dan klien membutuhkan data, maka ia harus mendatangi Anda lagi.

io.Reader. Kesalahan


Secara umum, menurut Reader, ini adalah aturan penting utama. Masih ada satu set yang kecil, tetapi mereka tidak begitu penting dan tidak mengarah pada situasi seperti itu:



Sebelum kita membahas semua hal yang berkaitan dengan Reader, kita perlu menjawab pertanyaan: apakah ini penting, apakah kesalahan sering terjadi dalam implementasi custom? Untuk menjawab pertanyaan ini, saya beralih ke spool saya untuk 1000 repositori (dan di sana kami mendapat sekitar 550 implementasi kustom). Saya melihat seratus pertama dengan mata saya. Tentu saja, ini bukan super-analisis, tetapi apa itu ... Saya

mengidentifikasi dua kesalahan paling populer:
  • tidak pernah mengembalikan EOF;
  • terlalu banyak kepercayaan pada Pembaca terbungkus.

Sekali lagi, ini masalah dari sudut pandang saya. Dan dari mereka yang menonton paket io, ini bukan masalah. Kami akan membicarakan ini lagi.

Saya ingin kembali ke satu nuansa. Lihat:



Klien tidak boleh menafsirkan pasangan 0 dan nihil sebagai EOF. Ini salah! Bagi Pustaka, nilai ini hanyalah peluang untuk melepaskan klien. Jadi, dua kesalahan yang saya sebutkan tampaknya tidak signifikan, tetapi cukup untuk membayangkan bahwa Anda memiliki pipa multi-layer di prod dan “bagul” licik merayap di tengah, maka “ketukan bawah tanah” tidak akan memakan waktu lama - dijamin!

Menurut Reader, pada dasarnya semuanya. Ini adalah aturan implementasi dasar.

penulis


Di ujung lain dari jalur pipa, kami memiliki io.Writer, yang merupakan tempat kami biasanya menulis data. Antarmuka yang sangat mirip: itu juga terdiri dari satu metode (Tulis), tanda tangan mereka mirip. Dari sudut pandang semantik, antarmuka Writer lebih dapat dimengerti: Saya akan mengatakan bahwa ketika didengar, itu ditulis.



Metode Write mengambil byte slice dan menulisnya secara keseluruhan. Ia juga memiliki seperangkat aturan yang harus diikuti.

  1. Yang pertama adalah jumlah byte yang ditulis. Saya akan mengatakan bahwa itu tidak begitu ketat, karena saya tidak menemukan satu pun contoh ketika itu akan menyebabkan beberapa konsekuensi kritis - misalnya, panik. Ini tidak terlalu ketat karena ada aturan berikut ...
  2. Implementasi Writer diperlukan untuk mengembalikan kesalahan setiap kali jumlah data yang ditulis kurang dari apa yang dikirim. Artinya, perekaman parsial tidak didukung. Ini berarti bahwa tidak terlalu penting berapa banyak byte yang ditulis.
  3. Satu lagi aturan: Penulis tidak boleh memodifikasi slice yang dikirim, karena klien masih akan bekerja dengan slice ini.
  4. Penulis tidak boleh memegang slice ini (Reader memiliki aturan yang sama). Jika Anda memerlukan data dalam implementasi Anda untuk beberapa operasi, Anda hanya perlu menyalin slide ini, dan hanya itu.



Oleh Pembaca dan Penulis, itu saja.

Dendrogram


Khusus untuk laporan ini, saya membuat grafik implementasi dan mendesainnya dalam bentuk dendrogram. Mereka yang ingin sekarang dapat mengikuti kode QR ini:



Dendrogram ini memiliki semua implementasi dari semua antarmuka paket io. Dendrogram ini diperlukan hanya untuk memahami: apa dan dengan apa yang dapat Anda satukan dalam pipa, di mana dan apa yang dapat Anda baca, di mana Anda dapat menulis. Saya masih akan merujuknya dalam laporan saya, jadi silakan merujuk ke kode QR.

Jaringan pipa


Kami berbicara tentang apa itu Reader, io.Writer. Sekarang mari kita bicara tentang API yang ada di perpustakaan standar untuk membangun jaringan pipa. Mari kita mulai dengan dasar-dasarnya. Mungkin itu bahkan tidak akan menarik bagi siapa pun. Namun, ini sangat penting.

Kami akan membaca data dari aliran input standar (dari Stdin):



Stdin diwakili dalam Go oleh variabel global tipe file dari paket os. Jika Anda melihat pada dendrogram, Anda akan melihat bahwa jenis file juga mengimplementasikan antarmuka Reader dan Writer.

Saat ini kami tertarik pada Pustaka. Kami akan membacakan Stdin menggunakan pembantu ReadAll yang sama yang telah kami gunakan.

Satu nuansa mengenai pembantu ini patut dicatat: ReadAll membaca Pembaca sampai akhir, tetapi menentukan akhir dengan EOF, dengan tanda akhir yang kita bicarakan.
Kami sekarang akan membatasi jumlah data yang kami baca dari Stdin. Untuk melakukan ini, ada implementasi LimitedReader di perpustakaan standar:



Saya ingin Anda memperhatikan bagaimana LimitedReader membatasi jumlah byte yang akan dibaca. Orang akan berpikir bahwa implementasi ini, Wrapper ini, mengurangi semua yang ada di Reader, yang dibungkusnya, dan kemudian memberikan sebanyak yang kita inginkan. Tapi semuanya bekerja sedikit berbeda ...

LimitedReader memotong irisan yang diberikan padanya sebagai argumen di sepanjang batas atas. Dan dia memberikan potongan yang dipotong ini ke Reader, yang membungkusnya. Ini adalah demonstrasi yang jelas tentang bagaimana panjang data yang dibaca diatur dalam implementasi antarmuka io.Reader.

Kesalahan saat mengembalikan file


Poin menarik lainnya: perhatikan bagaimana implementasi ini menghasilkan kesalahan EOF! Nilai-nilai yang dinamai digunakan di sini, dan mereka ditugaskan oleh nilai-nilai yang kami dapatkan dari Pembaca terbungkus.

Dan jika itu terjadi bahwa ada lebih banyak data dalam Pembaca terbungkus dari yang kita butuhkan, kami menetapkan nilai Pembaca terbungkus - misalnya, 10 byte dan nihil - karena masih ada data dalam Pembaca terbungkus. Tetapi variabel n, yang menurun (di garis kedua dari belakang), mengatakan bahwa kita telah mencapai "bawah" - akhir dari apa yang kita butuhkan.

Dalam iterasi berikutnya, klien harus datang lagi - dengan syarat pertama, ia akan menerima EOF. Ini adalah kasus yang saya sebutkan.

Akan dilanjutkan segera ...


Sedikit iklan :)


Terima kasih untuk tetap bersama kami. Apakah Anda suka artikel kami? Ingin melihat materi yang lebih menarik? Dukung kami dengan melakukan pemesanan atau merekomendasikan kepada teman Anda VPS berbasis cloud untuk pengembang mulai $ 4,99 , analog unik dari server entry-level yang diciptakan oleh kami untuk Anda: Seluruh kebenaran tentang VPS (KVM) E5-2697 v3 (6 Cores) 10GB DDR4 480GB SSD 1Gbps mulai dari $ 19 atau cara membagi server? (opsi tersedia dengan RAID1 dan RAID10, hingga 24 core dan hingga 40GB DDR4).

Dell R730xd 2 kali lebih murah di pusat data Equinix Tier IV di Amsterdam? Hanya kami yang memiliki 2 x Intel TetraDeca-Core Xeon 2x E5-2697v3 2.6GHz 14C 64GB DDR4 4x960GB SSD 1Gbps 100 TV dari $ 199 di Belanda!Dell R420 - 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB - mulai dari $ 99! Baca tentang Cara Membangun Infrastruktur Bldg. kelas c menggunakan server Dell R730xd E5-2650 v4 seharga 9.000 euro untuk satu sen?

All Articles