Pendahuluan

Dengan suka cita kami merilis revisi mayor dari Go API untuk protocol buffers, format pertukaran data yang mendukung banyak bahasa pemrograman dari Google.

Motivasi bagi API yang baru

Protocol buffers yang pertama untuk Go diumumkan oleh Rob Pike pada bulan Maret 2010. Go 1 belum dirilis sampai dua tahun kemudian.

Satu dekade sejak rilis yang pertama, paket tersebut teluh tumbuh dan berkembang bersama dengan Go. Kebutuhan pengguna pun semakin besar juga.

Banyak orang ingin menulis program yang menggunakan refleksi untuk memeriksa message dalam protocol buffer. Paket reflect menyediakan tipe dan nilai untuk Go, tetapi mengindahkan informasi dari sistem tipe protocol buffer. Misalnya, kita ingin memiliki sebuah fungsi yang memeriksa isi sebuah pesan log dan menghapus setiap field yang berisi data sensitif yang sebelumnya telah diberi anotasi. Anotasi tersebut bukan bagian dari sistem tipe Go.

Salah satu kebutuhan umum lainnya yaitu menggunakan struktur data yang bukan dihasilkan oleh compiler protocol buffer, seperti tipe message yang dinamis yang dapat merepresentasikan message yang tipenya tidak diketahui saat di-kompilasi.

Kami juga menelaah bahwa sebuah sumber permasalahan yang sering ditemukan yaitu interface proto.Message, yang mengidentifikasi nilai dari tipe Message yang dibangkitkan (generated), memiliki manfaat yang sedikit dalam menjelaskan perilaku dari tipe-tipe tersebut. Saat pengguna membuat tipe yang mengimplementasikan interface tersebut (sering kali dengan menanam Message di dalam struct yang lain) dan mengirim nilai dari tipe tersebut ke fungsi yang mengharapkan nilai message yang hasil pembangkitan, program menjadi crash atau tidak terprediksi.

Ketiga permasalahan ini punya penyebab yang sama, dan sebuah solusi yang sama: interface Message seharusnya secara penuh menspesifikasikan perilaku dari sebuah message, dan fungsi-fungsi yang mengoperasikan nilai dari Message seharusnya dapat menerima tipe apa pun yang secara benar mengimplementasikan interface tersebut.

Secara kita tidak bisa mengubah definisi dari tipe Message yang sekarang dan tetap menjaga kompatibilitas dari paket API, kami memutuskan untuk mulai membuat versi mayor yang baru yang tidak kompatibel dengan modul protobuf.

Hari ini, kita merilis modul baru tersebut. Kami harap Anda menyukainya.

Refleksi

Refleksi adalah fitur andalan dari implementasi yang baru. Mirip dengan bagaimana paket reflect menyediakan sebuah tipe dan nilai pada Go, paket google.golang.org/protobuf/reflect/protoreflect menyediakan sebuah nilai menurut sistem tipe protocol buffer.

Deskripsi lengkap dari paket protoreflect akan terlalu panjang bila dijelaskan di sini, tetapi mari kita lihat bagaimana kita dapat menulis fungsi yang membersihkan pesan log seperti yang kita sebut sebelumnya.

Pertama, kita tulis berkas .proto mendefinisikan ekstensi dari tipe google.protobuf.FieldOptions supaya kita dapat menambahkan anotasi pada field-field yang berisi informasi yang sensitif atau tidak.

syntax = "proto3";
import "google/protobuf/descriptor.proto";
package golang.example.policy;
extend google.protobuf.FieldOptions {
    bool non_sensitive = 50000;
}

Kita kemudian menggunakan opsi ini untuk menandai field-field tertentu yang tidak sensitif.

message MyMessage {
    string public_name = 1 [(golang.example.policy.non_sensitive) = true];
}

Selanjutnya, kita tulis sebuah fungsi Go yang menerima nilai Message apa pun dan menghapus semua field-field yang sensitif.

// Redact clears every sensitive field in pb.
func Redact(pb proto.Message) {
   // ...
}

Fungsi ini menerima sebuah proto.Message, tipe interface yang diimplementasikan oleh semua tipe message hasil pembangkitan. Tipe tersebut adalah alias dari yang tipe yang didefinisikan dalam paket protoreflect:

type ProtoMessage interface {
    ProtoReflect() Message
}

Untuk menghindari penuhnya namespace dari Message hasil pembangkitan, interface tersebut hanya berisi sebuah method yang mengembalikan protoreflect.Message, yang menyediakan akses ke isi message.

Kenapa alias? Karena protoreflect.Message memiliki method yang mengembalikan proto.Message yang asli, dan kita harus menghindari pengulangan impor (import cycle) antara kedua paket tersebut.

Method protoreflect.Message.Range memanggil sebuah fungsi untuk setiap field dalam sebuah Message.

m := pb.ProtoReflect()
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
    // ...
    return true
})

Fungsi Range dipanggil dengan sebuah protoreflect.FieldDescriptor yang mendeskripsikan tipe protocol buffer dari field, dan sebuah protoreflect.Value yang berisi nilai dari field.

Method protoreflect.FieldDescriptor.Options mengembalikan field sebagai sebuah google.protobuf.FieldOptions.

opts := fd.Options().(*descriptorpb.FieldOptions)

(Kenapa pakai asersi tipe? Karena paket descriptorpb bergantung pada protoreflect, paket protoreflect tidak dapat mengembalikan tipe konkrit dari Options tanpa mengakibatkan pengulangan impor.)

Kemudian kita dapat memeriksa opts untuk melihat nilai dari ekstensi boolean kita sebelumnya:

if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
    return true // don't redact non-sensitive fields
}

Ingatlah bahwa yang perlu diperhatikan di sini yaitu field descriptor. Informasi yang ingin kita ketahui berada dalam sistem tipe protocol buffer, bukan dalam sistem tipe Go.

Hal ini juga merupakan contoh wilayah di mana kita telah menyederhanakan API dari paket proto. proto.GetExtension yang aslinya mengembalikan sebuah nilai dan sebuah error. Fungsi proto.GetExtension yang baru mengembalikan hanya nilai, atau nilai baku dari field bila tidak ada. Kesalahan dekode dari ekstensi dilaporkan saat Unmarshal.

Saat kita mengetahui field mana yang perlu dihilangkan, kode untuk menghapus field tersebut cukup dengan:

m.Clear(fd)

Bila semua kode di atas digabung, fungsi Redact kita menjadi:

// Redact clears every sensitive field in pb.
func Redact(pb proto.Message) {
    m := pb.ProtoReflect()
    m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
        opts := fd.Options().(*descriptorpb.FieldOptions)
        if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
            return true
        }
        m.Clear(fd)
        return true
    })
}

Implementasi yang komplit bisa secara rekursif sampai ke field-field dalam Message. Kami berharap dengan contoh sederhana ini dapat memberi Anda bayangan terhadap penggunaan refleksi pada protocol buffer.

Versi

Kami menyebut versi asli dari Go protocol buffer sebagai APIv1, dan yang baru APIv2. Karena APIv2 tidak kompatibel dengan APIv1, kita membutuhkan path modul yang berbeda.

(Versi API ini tidak sama dengan versi dari bahasa protocol buffer: proto1, proto2, dan proto3. APIv1 dan APIv2 adalah implementasi dalam Go yang mendukung versi bahasa proto2 dan proto3.)

Modul github.com/golang/protobuf adalah APIv1.

Modul google.golang.org/protobuf adalah APIv2. Kami telah mengambil mengubah path impor dengan beralih ke tempat yang tidak bergantung pada penyedia layanan hosting. (Kami juga mempertimbangkan google.golang.org/protobuf/v2, untuk memperjelas bahwa ini adalah versi mayor kedua dari API, tetapi kemudian memutuskan untuk memilih path yang pendek untuk keuntungan jangka panjang.)

Kami tahu bahwa tidak semua pengguna akan pindah ke versi mayor yang baru pada saat bersamaan. Beberapa akan langsung pindah; yang lain bisa jadi tetap menggunakan versi lama seterusnya. Bahkan dalam sebuah program, beberapa bagian bisa jadi pakai API yang lama dan bagian lain menggunakan yang baru. Sangat penting bahwa kami terus mendukung program yang menggunakan APIv1.

  • github.com/golang/protobuf@v1.3.4 adalah versi paling terbaru pra-APIv2 dari APIv1.

  • github.com/golang/protobuf@v1.4.0 adalah versi APIv1 yang diimplementasikan dengan APIv2. API-nya tetap sama, tetapi implementasi dibelakangnya menggunakan yang baru. Versi ini berisi fungsi-fungsi untuk mengonversi interface proto.Message antara APIv1 dan APIv2 untuk memudahkan transisi antara keduanya.

  • google.golang.org/protobuf@v1.20.0 adalah APIv2. Modul ini bergantung pada github.com/golang/protobuf@v1.4.0, sehingga program apa pun yang menggunakan APIv2 akan secara otomatis mengambil versi APIv1 yang terintegrasi dengannya.

(Kenapa mulai dengan versi v1.20.0? Supaya lebih jelas. Kami tidak berharap APIv1 akan sampai ke v1.12.0, sehingga nomor versi itu sendiri sudah cukup untuk membedakan antara APIv1 dan APIv2.)

Kami tetap mendukung APIv1 selamanya.

Pengorganisasian ini memastikan supaya semua program akan menggunakan implementasi tunggal dari protocol buffer, tanpa memperhatikan versi API mana yang digunakan. Ia membolehkan program untuk mengadopsi API baru secara gradual, atau tidak sama sekali, namun tetap mendapatkan keuntungan dari implementasi yang baru. Prinsip dari pemilihan versi minimum (minimum version selection) yaitu bahwa sebuah program bisa terus menggunakan implementasi yang lama sampai pengelola memilih untuk memperbarui ke yang baru (baik secara langsung, atau lewat pembaruan dependensi).

Catatan fitur tambahan

Paket google.golang.org/protobuf/encoding/protojson mengonversi protocol buffer Message dari dan ke JSON menggunakan pemetaan JSON kanonis, dan memperbaiki sejumlah isu dengan paket jsonpb yang lama yang sulit diubah tanpa menyebabkan masalah bagi pengguna yang ada.

Paket google.golang.org/protobuf/types/dynamicpb menyediakan sebuah implementasi dari proto.Message untuk message yang tipe protocol buffer-nya dibangkitkan saat runtime.

Paket google.golang.org/protobuf/testing/protocmp menyediakan fungsi-fungsi untuk membandingkan Message protocol buffer dengan paket github.com/google/go-cmp/cmp.

Paket google.golang.org/protobuf/compiler/protogen menyediakan dukungan untuk menulis plugin untuk compiler protocol buffer.

Kesimpulan

Modul google.golang.org/protobuf adalah perbaikan mayor dari dukungan Go terhadap protocol buffer, menyediakan dukungan kelas-satu untuk refleksi, implementasi kustomisasi Message, dan pembersihan API. Kami ingin memelihara API yang lama selamanya sebagai pembungkus dari yang baru, membolehkan pengguna mengadopsi API baru secara inkremental.

Tujuan dari pembaruan ini yaitu untuk meningkatkan API yang lama dan membereskan masalah-masalah mereka yang terdahulu. Saat kita menyelesaikan setiap komponen dari implementasi yang baru, kami langsung gunakan dalam basis kode Google. Rilis secara inkremental memberikan kita sebuah kepercayaan diri terhadap penggunaan dari API baru berikut dengan kinerja dan ketepatan dari implementasi yang baru. Kami percaya ia siap untuk digunakan untuk lingkungan production.

Kami sangat senang dengan rilis ini dan berharap ia dapat melayani ekosistem Go untuk sepuluh tahun ke depan dan seterusnya!