Pendahuluan

Untuk mengirim struktur data dalam jaringan atau menyimpannya ke dalam sebuah berkas, ia haruslah ditulis (encode) dan kemudian dibaca (decode) kembali. Ada banyak encoding yang tersedia, seperti: JSON, XML, protocol buffer dari Google, dan banyak lagi. Dan sekarang ada satu lagi, disediakan oleh paket Go gob.

Kenapa mendefinisikan encoding baru? Ia menambah kerja dan tampak mubazir. Kenapa tidak menggunakan format yang ada? Pertama, Go punya semuanya! Go memiliki paket-paket yang mendukung semua encoding yang disebutkan di atas (paket protocol buffer ada pada repositori terpisah namun yang paling sering diunduh). Dan untuk tujuan secara umumnya, termasuk berkomunikasi dengan perkakas dan sistem yang ditulis dengan bahasa yang berbeda, mereka adalah format yang sesuai.

Namun untuk lingkungan yang spesifik dengan Go, seperti berkomunikasi antara dua server yang dibuat dengan Go, ada sebuah kesempatan untuk membuat sesuatu yang lebih mudah digunakan dan mungkin lebih efisien.

Gob bekerja dalam bahasa Go yang mana encoding lain yang terdefinisi secara eksternal dan berdiri sendiri (tidak bergantung pada bahasa pemrograman) tidak mampu. Pada saat yang sama, ada beberapa pelajaran yang dapat diambil dari sistem (encoding) yang sudah ada.

Tujuan

Paket gob dirancang dengan sejumlah tujuan.

Pertama, dan yang paling kentara, ia haruslah mudah digunakan. Pertama, karena Go memiliki fitur refleksi, maka tidak perlu antar muka definisi bahasa yang terpisah atau "protocol compiler". Struktur data saja sudah cukup bagi paket gob untuk mengetahui bagaimana menulis dan membaca sebuah data. Di sisi lain, pendekatan ini berarti gob tidak akan pernah bisa bekerja dengan bahasa pemrograman lain, namun tidak apa-apa: gob adalah Go.

Efisiensi juga penting. Representasi teks, seperti pada XML dan JSON, terlalu lambat bila digunakan sebagai pusat komunikasi jaringan yang efisien. Sistem encoding dengan binary memang dibutuhkan.

Stream dari gob haruslah self-describing (berisi informasi yang menjelaskan dirinya sendiri). Artinya, stream gob yang dibaca dari awal, memiliki informasi yang cukup sehingga keseluruhan stream dapat dibaca oleh sebuah agen yang sebelumnya tidak mengetahui isinya. Dengan properti ini berarti kita bisa selalu membaca stream gob yang disimpan dalam berkas, bahkan setelah kita lupa representasi dari data di dalamnya.

Ada juga beberapa hal yang kita pelajari dari pengalaman kita dengan protocol buffer dari Google.

Kesalahan protocol buffer

Protocol buffer (PB) berpengaruh besar dalam merancang gob, namun ia memiliki tiga fitur yang secara sengaja dihindari dalam merancang gob. (lupakan dahulu bahwa properti dari PB itu sendiri tidaklah self-describing: jika kita tidak mengetahui definisi data yang digunakan untuk menulis sebuah PB, maka kita tidak akan bisa membacanya.)

Pertama, PB hanya bekerja pada tipe data yang kita sebut struct dalam Go. Kita tidak bisa menulis sebuah integer atau array pada level teratas, hanya sebuah struct dengan field-field di dalamnya. Hal ini tampak seperti batasan yang tidak berarti, setidaknya dalam Go. Jika kita hanya ingin mengirim sebuah array dari integer, kenapa harus disimpan dalam sebuah struct terlebih dahulu?

Selanjutnya, definisi dari sebuah PB bisa men spesifikasi bahwa field T.x dan T.y haruslah ada bila nilai bertipe T ditulis atau dibaca. Walaupun field-field yang dibutuhkan tersebut tampak seperti ide yang bagus, mereka cukup memakan biaya untuk diimplementasikan karena codec harus menjaga supaya struktur data terpisah saat ditulis dan dibaca, supaya dapat melaporkan kesalahan bila field-field yang dibutuhkan tersebut tidak ditemukan nilainya. Dan juga ada permasalahan pemeliharaan. Suatu waktu, kita bisa saja ingin mengubah definisi data, misalnya menghapus field yang dibutuhkan, namun hal ini menyebabkan klien-klien yang mengonsumsi data yang belum memperbarui definisi data menjadi crash. Maka dari itu lebih baik bila kita tidak memiliki field-field yang dibutuhkan tersebut pada saat encoding sama sekali. (PB juga memiliki field-field opsional. Bila kita tidak menentukan field-field yang dibutuhkan, maka semua field adalah opsional dalam PB. Ada beberapa hal lain yang perlu kita jelaskan lagi nantinya.)

Kesalahan ketiga dari PB yaitu nilai default (bawaan). Jika PB mengindahkan nilai dari sebuah field yang memiliki nilai bawaan pada saat ditulis, maka field pada struktur akan di set dengan nilai bawaannya. Ide ini bekerja baik bila kita memiliki method-method getter dan setter untuk mengontrol akses terhadap field, namun akan lebih sulit ditangani secara bersih saat penampung adalah struct polos biasa. Field yang dibutuhkan juga cukup rumit diimplementasikan: kapan nilai bawaan didefinisikan, tipe apa yang dimiliki (apakah teks berupa UTF-8? atau bytes yang tidak perlu diinterpretasi? berapa banyak bits dalam sebuah float?) dan bahkan dari kesederhanaan-nya, ada sejumlah komplikasi dalam rancangan dan implementasi PB. Kami memutuskan untuk tidak mengikutkan-nya pada gob dan kembali ke aturan biasa dari Go dengan aturan bawaan yang efektif: kecuali bila di set dengan nilai lain, ia akan berisi "nilai kosong" untuk tipe tersebut - dan ia tidak perlu dikirim.

Jadi gob akhirnya seperti generalisasi dan penyederhanaan dari protocol buffer. Bagaimana cara bekerjanya?

Nilai

Data gob yang ditulis bukanlah tipe seperti int8 atau uint16. Namun, secara analogi mirip dengan konstan dalam Go, nilai integer-nya yaitu bilangan tanpa ukuran dan abstrak, baik signed maupun unsigned. Saat kita menulis sebuah int8, nilainya dikirim berupa integer dengan panjang tertentu tanpa ukuran. Saat kita menulis sebuah int64, nilainya juga dikirim berupa integer dengan panjang tertentu dan tanpa ukuran. (Signed dan unsigned diperlakukan terpisah, namun perilaku tanpa-ukuran yang sama diterapkan pada nilai unsigned juga.) Jika keduanya bernilai 7, bits yang dikirim akan identik. Saat si penerima membaca nilai tersebut, ia menyimpannya ke dalam variabel, yang bisa bertipe integer apa pun. Maka encoder bisa mengirim nilai 7 yang datang dari int8, namun si penerima bisa saja menyimpannya dalam sebuah int64. Hal ini wajar: nilainya adalah integer dan selama dapat disimpan, semua akan bekerja dengan baik (Jika tidak dapat disimpan, akan mengeluarkan eror.) Pemisahan ukuran dari variabel memberikan fleksibilitas pada proses encoding: kita dapat memperbesar tipe dari variabel integer saat program berkembang, namun masih tetap dapat membaca data yang lama.

Fleksibilitas ini juga berlaku pada pointer. Sebelum dikirim, semua pointer akan diratakan. Nilai dari tipe int8, int8, *int8, int8, dan seterusnya, dikirim sebagai nilai integer, yang kemudian dapat disimpan dalam int berukuran apa pun, atau int, atau ***int, dan seterusnya. Sekali lagi, hal ini untuk fleksibilitas.

Fleksibilitas juga terjadi karena saat membaca sebuah struct, hanya field-field yang dikirim oleh encoder yang disimpan dalam tujuan. Misalnya diberikan nilai

type T struct{ X, Y, Z int } // Hanya field yang diekspor yang ditulis dan dibaca
var t = T{X: 7, Y: 0, Z: 8}

encoding dari t hanya mengirim 7 dan 8. Karena nilai Y adalah kosong, ia tidak dikirim; paket gob tidak perlu mengirim nilai yang kosong.

Si penerima bisa membaca nilai tersebut ke dalam struktur berikut:

type U struct{ X, Y *int8 } // Catatan: pointer ke int8
var u U

dan mendapatkan nilai u dengan hanya X yang di set (yang menunjuk alamat variabel int8 yang di set dengan 7); field Z diindahkan - mau disimpan di mana? Saat membaca struct, field dicocokkan berdasarkan nama dan tipe yang kompatibel, dan hanya field-field yang ada pada keduanya yang terpengaruh. Pendekatan sederhana ini mengatasi permasalahan "field opsional": saat tipe T berkembang dengan menambahkan field baru, penerima yang ketinggalan masih dapat berfungsi dengan bagian tipe yang dikenali. Oleh karena itu gob menyediakan solusi dari permasalahan dari field opsional - ekstensibilitas - tanpa adanya mekanisme atau notasi tambahan.

Dari perilaku tipe dan nilai integer di atas kita dapat membangun semua tipe lainnya: byte, string, array, slice, map, bahkan float. Nilai floating-point direpresentasikan dengan pola bit floating-point IEEE 754, disimpan sebagai integer, yang bekerja baik selama kita tahu tipenya. Nilai integer tersebut dikirim dalam urutan byte-terbalik karena nilai bersama dari bilangan floating-point, seperti integer yang kecil, memiliki banyak nilai nol pada akhirannya yang dapat diindahkan saat pengiriman.

Salah satu fitur yang bagus dari gob ialah Go membolehkan kita mendefinisikan encoding sendiri dengan memenuhi interface GobEncoder dan GobDecoder, dengan cara yang sama seperti Marshaler dan Unmarshaler pada paket JSON dan juga seperti pada interface Stringer dari paket fmt. Fasilitas ini memungkinkan merepresentasikan fitur khusus, membuat batasan-batasan, atau merahasiakan sesuatu saat data dikirim. Lihat dokumentasi dari paket gob untuk lebih rinci.

Bentuk tipe saat dikirim

Pertama kali kita mengirim sebuah tipe tertentu, paket gob mengikutkan deskripsi dari tipe tersebut dalam stream data. Yang terjadi adalah encoder menulis struktur internal, dalam format standar encoding gob, yang menjelaskan tipe dan memberinya sebuah angka unik. (Tipe-tipe dasar, berikut dengan deskripsi tipe dari struct, didefinisikan terlebih dahulu oleh perangkat lunak untuk bootstrapping.) Setelah tipe dideskripsikkan, ia bisa diacu dengan angka.

Maka saat kita mengirim tipe pertama kita T, encoder gob mengirim deskripsi dari T dan men-tag-nya dengan angka, katakanlah 127. Semua nilai, termasuk yang pertama, diberi prefiks dengan angka tersebut, sehingga stream dari nilai T berbentuk seperti berikut:

("define type id" 127, definisi dari tipe T)(127, nilai T)(127, nilai T), ...

Angka-angka tersebut membuat kita bisa mendeskripsikan tipe rekursif dan mengirim nilainya. Sehingga gob dapat menulis tipe seperti tree berikut:

type Node struct {
	Value       int
	Left, Right *Node
}

(Latihan bagi pembaca untuk mengetahui bagaimana aturan nilai kosong bawaan bekerja, walaupun gob tidak merepresentasikan pointer.)

Dengan informasi tipe, sebuah stream dari gob secara penuh self-describing kecuali untuk sekumpulan tipe bootstrap, yang mana telah terdefinisi pada saat awal.

Mengompilasi mesin

Pada saat pertama kali kita menulis sebuah nilai dari tipe tertentu, paket gob membuat sebuah interpretasi mesin khusus untuk tipe data tersebut. Ia menggunakan refleksi pada tipe untuk mengonstruksi mesin tersebut, namun setelah mesin tersebut telah dibuat ia tidak bergantung lagi pada refleksi. Mesin tersebut menggunakan package unsafe dan beberapa trik untuk mengonversi data menjadi byte dengan cepat. Ia bisa saja menggunakan refleksi dan mengindahkan unsafe, namun akan lebih lambat. (Pendekatan yang sama, yang juga cepat, digunakan oleh dukungan terhadap protocol buffer pada Go, yang rancangannya dipengaruhi oleh implementasi dari gob.) Nilai selanjutnya dari tipe yang sama menggunakan mesin yang telah dikompilasi, sehingga bisa langsung ditulis.

(Pembaruan: Pada Go 1.4, paket unsafe tidak lagi digunakan oleh paket gob, dengan penurunan performa yang ringan.)

Proses decoding caranya sama namun lebih sukar. Saat membaca sebuah nilai, paket gob menyimpan slice byte yang merepresentasikan sebuah nilai yang merepresentasikan tipe yang didefinisikan oleh encoder untuk dibaca, ditambah dengan nilai di mana ia akan disimpan. Paket gob kemudian membuat sebuah mesin untuk pasangan tersebut: tipe gob yang dikirim disilangkan dengan dengan tipe Go yang disediakan untuk decoding. Setelah mesin decoding tersebut dibuat, ia tidak lagi menggunakan refleksi (yang menggunakan method-method pada unsafe) supaya lebih cepat.

Penggunaan

Ada banyak hal yang terjadi di belakang penulisan dan pembacaan data dengan gob, namun hasilnya adalah sebuah sistem encoding yang efisien dan mudah digunakan untuk mengirim data. Berikut contoh komplit yang memperlihatkan perbedaan penulisan dan pembacaan dari beberapa tipe. Lihatlah bagaimana mudah-nya mengirim dan menerima nilai; apa yang harus kita lakukan hanyalah memberi nilai dan variabel ke paket gob dan ia akan melakukan semuanya.

package main

import (
	"bytes"
	"encoding/gob"
	"fmt"
	"log"
)

type P struct {
	X, Y, Z int
	Name    string
}

type Q struct {
	X, Y *int32
	Name string
}

func main() {
	// Inisialiasi encoder dan decoder.  Biasanya enc dan dec akan terikat
	// dengan koneksi jaringan dan berjalan pada proses yang berbeda.
	var network bytes.Buffer        // Penampung koneksi jaringan
	enc := gob.NewEncoder(&network) // Akan menulis ke jaringan.
	dec := gob.NewDecoder(&network) // Akan membaca dari jaringan.

	// Encode (kirim) nilai.
	err := enc.Encode(P{3, 4, 5, "Pythagoras"})
	if err != nil {
		log.Fatal("encode error:", err)
	}

	// Decode (terima) nilainya.
	var q Q
	err = dec.Decode(&q)
	if err != nil {
		log.Fatal("decode error:", err)
	}
	fmt.Printf("%q: {%d,%d}\n", q.Name, *q.X, *q.Y)
}

Anda bisa meng-compile dan menjalankan contoh kode ini dalam Playground Go.

Paket rpc dibangun dari gob untuk mengubah otomatisasi tulis/baca seperti di atas ke dalam sebuah transpor pemanggilan method dalam jaringan.

Rinci

Dokumentasi paket gob, terutama berkas doc.go, menjelaskan lebih rinci dari apa yang dibahas di sini dan mengikutkan contoh lengkap yang memperlihatkan bagaimana encoding merepresentasikan data. Jika tertarik dengan dalaman dari implementasi gob, berkas tersebut adalah tempat yang bagus untuk memulai.