Blog Go
API mới của Go cho Protocol Buffers
Giới thiệu
Chúng tôi vui mừng thông báo về bản phát hành sửa đổi lớn của API Go cho protocol buffers, định dạng trao đổi dữ liệu trung lập với ngôn ngữ của Google.
Lý do cần API mới
Các binding protocol buffer đầu tiên cho Go được Rob Pike thông báo vào tháng 3 năm 2010. Go 1 sẽ không được phát hành cho đến hai năm sau đó.
Trong thập kỷ kể từ bản phát hành đầu tiên đó, gói này đã phát triển và tiến hóa cùng với Go. Yêu cầu của người dùng cũng ngày càng tăng.
Nhiều người muốn viết các chương trình sử dụng reflection để kiểm tra
các thông điệp protocol buffer. Gói
reflect
cung cấp một góc nhìn về các kiểu và giá trị Go,
nhưng bỏ qua thông tin từ hệ thống kiểu của protocol buffer. Chẳng hạn,
chúng ta có thể muốn viết một hàm duyệt qua một mục nhật ký và xóa bất kỳ
trường nào được chú thích là chứa dữ liệu nhạy cảm. Các chú thích đó không
phải là một phần của hệ thống kiểu Go.
Một nhu cầu phổ biến khác là sử dụng các cấu trúc dữ liệu khác ngoài những cấu trúc được tạo ra bởi trình biên dịch protocol buffer, chẳng hạn như kiểu thông điệp động có thể biểu diễn các thông điệp mà kiểu của chúng không được biết tại thời điểm biên dịch.
Chúng tôi cũng nhận thấy rằng một nguồn gốc thường gặp của các vấn đề là
interface
proto.Message,
vốn xác định các giá trị của các kiểu thông điệp được tạo ra, lại mô tả
quá ít về hành vi của những kiểu đó. Khi người dùng tạo ra các kiểu cài đặt
interface đó (thường là vô tình bằng cách nhúng một thông điệp vào một struct
khác) và truyền các giá trị của những kiểu đó vào các hàm mong đợi một giá trị
thông điệp được tạo ra, chương trình sẽ bị crash hoặc hoạt động không thể đoán
trước được.
Cả ba vấn đề này đều có một nguyên nhân chung và một giải pháp chung:
Interface Message nên mô tả đầy đủ hành vi của một thông điệp, và các hàm
thao tác trên các giá trị Message nên chấp nhận tự do bất kỳ kiểu nào cài
đặt interface đó đúng cách.
Vì không thể thay đổi định nghĩa hiện có của kiểu Message trong khi vẫn
duy trì tính tương thích API của gói, chúng tôi quyết định đã đến lúc bắt
đầu làm việc trên một phiên bản mới, không tương thích ngược của module protobuf.
Hôm nay, chúng tôi vui mừng phát hành module mới đó. Hy vọng bạn sẽ thích nó.
Reflection
Reflection là tính năng hàng đầu của cài đặt mới. Tương tự như cách gói
reflect cung cấp góc nhìn về các kiểu và giá trị Go, gói
google.golang.org/protobuf/reflect/protoreflect
cung cấp góc nhìn về các giá trị theo hệ thống kiểu của protocol buffer.
Mô tả đầy đủ về gói protoreflect sẽ quá dài cho bài viết này, nhưng hãy
cùng xem cách chúng ta có thể viết hàm xóa thông tin nhạy cảm trong nhật ký
mà chúng tôi đã đề cập trước đó.
Trước tiên, chúng ta sẽ viết một tệp .proto định nghĩa phần mở rộng của kiểu
google.protobuf.FieldOptions
để chúng ta có thể chú thích các trường là có chứa thông tin nhạy cảm hay không.
syntax = "proto3";
import "google/protobuf/descriptor.proto";
package golang.example.policy;
extend google.protobuf.FieldOptions {
bool non_sensitive = 50000;
}
Chúng ta có thể dùng tùy chọn này để đánh dấu một số trường là không nhạy cảm.
message MyMessage {
string public_name = 1 [(golang.example.policy.non_sensitive) = true];
}
Tiếp theo, chúng ta sẽ viết một hàm Go nhận bất kỳ giá trị thông điệp nào và xóa tất cả các trường nhạy cảm.
// Redact clears every sensitive field in pb.
func Redact(pb proto.Message) {
// ...
}
Hàm này nhận một
proto.Message,
một kiểu interface được cài đặt bởi tất cả các kiểu thông điệp được tạo ra.
Kiểu này là một alias cho kiểu được định nghĩa trong gói protoreflect:
type ProtoMessage interface{
ProtoReflect() Message
}
Để tránh làm đầy namespace của các thông điệp được tạo ra, interface chỉ
chứa một phương thức duy nhất trả về một
protoreflect.Message,
cung cấp quyền truy cập vào nội dung của thông điệp.
(Tại sao lại dùng alias? Vì protoreflect.Message có một phương thức tương
ứng trả về proto.Message gốc, và chúng ta cần tránh vòng lặp import giữa
hai gói.)
Phương thức
protoreflect.Message.Range
gọi một hàm cho mỗi trường được điền trong một thông điệp.
m := pb.ProtoReflect()
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
// ...
return true
})
Hàm range được gọi với một
protoreflect.FieldDescriptor
mô tả kiểu protocol buffer của trường, và một
protoreflect.Value
chứa giá trị của trường.
Phương thức
protoreflect.FieldDescriptor.Options
trả về các tùy chọn của trường dưới dạng thông điệp google.protobuf.FieldOptions.
opts := fd.Options().(*descriptorpb.FieldOptions)
(Tại sao lại có type assertion? Vì gói descriptorpb được tạo ra phụ thuộc
vào protoreflect, gói protoreflect không thể trả về kiểu tùy chọn cụ thể
mà không gây ra vòng lặp import.)
Sau đó chúng ta có thể kiểm tra các tùy chọn để xem giá trị của boolean mở rộng của mình:
if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
return true // don't redact non-sensitive fields
}
Lưu ý rằng chúng ta đang xem descriptor của trường ở đây, không phải giá trị của trường. Thông tin chúng ta quan tâm nằm trong hệ thống kiểu của protocol buffer, không phải hệ thống kiểu Go.
Đây cũng là một ví dụ về khu vực mà chúng ta đã đơn giản hóa API của gói
proto. Hàm
proto.GetExtension
ban đầu trả về cả một giá trị và một lỗi. Hàm
proto.GetExtension
mới chỉ trả về một giá trị, trả về giá trị mặc định cho trường nếu nó không
có mặt. Lỗi giải mã extension được báo cáo tại thời điểm Unmarshal.
Khi đã xác định được một trường cần xóa thông tin nhạy cảm, việc xóa nó rất đơn giản:
m.Clear(fd)
Tổng hợp tất cả những điều trên, hàm xóa thông tin nhạy cảm hoàn chỉnh của chúng ta là:
// 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
})
}
Một cài đặt đầy đủ hơn có thể đệ quy đi sâu vào các trường có giá trị là thông điệp. Hy vọng rằng ví dụ đơn giản này cho thấy một chút về reflection của protocol buffer và các ứng dụng của nó.
Các phiên bản
Chúng tôi gọi phiên bản gốc của Go protocol buffers là APIv1, và phiên bản mới là APIv2. Vì APIv2 không tương thích ngược với APIv1, chúng ta cần sử dụng các đường dẫn module khác nhau cho mỗi phiên bản.
(Các phiên bản API này không giống với các phiên bản của ngôn ngữ protocol
buffer: proto1, proto2 và proto3. APIv1 và APIv2 là các cài đặt cụ thể
trong Go, đều hỗ trợ các phiên bản ngôn ngữ proto2 và proto3.)
Module
github.com/golang/protobuf
là APIv1.
Module
google.golang.org/protobuf
là APIv2. Chúng tôi đã tận dụng sự cần thiết phải thay đổi đường dẫn import
để chuyển sang một đường dẫn không gắn với một nhà cung cấp hosting cụ thể.
(Chúng tôi đã cân nhắc google.golang.org/protobuf/v2, để làm rõ rằng đây là
phiên bản lớn thứ hai của API, nhưng cuối cùng chọn đường dẫn ngắn hơn vì nó
là lựa chọn tốt hơn về lâu dài.)
Chúng tôi biết rằng không phải tất cả người dùng sẽ chuyển sang phiên bản lớn mới của một gói với cùng tốc độ. Một số sẽ chuyển đổi nhanh chóng; những người khác có thể vẫn ở phiên bản cũ vô thời hạn. Ngay cả trong một chương trình đơn lẻ, một số phần có thể sử dụng API này trong khi những phần khác sử dụng API kia. Do đó, điều cần thiết là chúng ta tiếp tục hỗ trợ các chương trình sử dụng APIv1.
-
github.com/golang/protobuf@v1.3.4là phiên bản APIv1 mới nhất trước APIv2. -
github.com/golang/protobuf@v1.4.0là phiên bản APIv1 được cài đặt theo thuật ngữ của APIv2. API vẫn giống nhau, nhưng cài đặt bên dưới được hỗ trợ bởi cái mới. Phiên bản này chứa các hàm để chuyển đổi giữa interfaceproto.Messagecủa APIv1 và APIv2 để dễ dàng chuyển tiếp giữa hai phiên bản. -
google.golang.org/protobuf@v1.20.0là APIv2. Module này phụ thuộc vàogithub.com/golang/protobuf@v1.4.0, do đó bất kỳ chương trình nào sử dụng APIv2 sẽ tự động chọn một phiên bản APIv1 tích hợp với nó.
(Tại sao bắt đầu ở phiên bản v1.20.0? Để rõ ràng hơn. Chúng tôi không
dự đoán APIv1 sẽ đạt đến v1.20.0, vì vậy số phiên bản một mình đã đủ để
phân biệt rõ ràng giữa APIv1 và APIv2.)
Chúng tôi có kế hoạch duy trì hỗ trợ cho APIv1 vô thời hạn.
Tổ chức này đảm bảo rằng bất kỳ chương trình nào cũng sẽ chỉ sử dụng một cài đặt protocol buffer duy nhất, bất kể phiên bản API nào nó sử dụng. Nó cho phép các chương trình áp dụng API mới một cách dần dần, hoặc không áp dụng chút nào, trong khi vẫn được hưởng lợi từ cài đặt mới. Nguyên tắc chọn phiên bản tối thiểu có nghĩa là các chương trình có thể vẫn ở cài đặt cũ cho đến khi người bảo trì chọn cập nhật lên cái mới (trực tiếp hoặc bằng cách cập nhật một dependency).
Các tính năng đáng chú ý khác
Gói
google.golang.org/protobuf/encoding/protojson
chuyển đổi các thông điệp protocol buffer sang và từ JSON sử dụng
ánh xạ JSON chuẩn,
và khắc phục một số vấn đề với gói jsonpb cũ mà rất khó thay đổi mà không
gây ra vấn đề cho người dùng hiện tại.
Gói
google.golang.org/protobuf/types/dynamicpb
cung cấp một cài đặt proto.Message cho các thông điệp mà kiểu protocol buffer
của chúng được suy ra tại thời gian chạy.
Gói
google.golang.org/protobuf/testing/protocmp
cung cấp các hàm để so sánh các thông điệp protocol buffer với gói
github.com/google/cmp.
Gói
google.golang.org/protobuf/compiler/protogen
cung cấp hỗ trợ để viết các plugin của trình biên dịch protocol.
Kết luận
Module google.golang.org/protobuf là sự cải tổ lớn về hỗ trợ protocol buffers
của Go, cung cấp hỗ trợ hạng nhất cho reflection, các cài đặt thông điệp tùy
chỉnh, và một bề mặt API được làm sạch hơn. Chúng tôi có kế hoạch duy trì API
trước đó vô thời hạn như một lớp bọc của cái mới, cho phép người dùng áp dụng
API mới dần dần theo tốc độ của riêng họ.
Mục tiêu của chúng tôi trong bản cập nhật này là cải thiện những lợi ích của API cũ trong khi giải quyết những thiếu sót của nó. Khi chúng tôi hoàn thành từng thành phần của cài đặt mới, chúng tôi đưa nó vào sử dụng trong codebase của Google. Việc triển khai dần dần này đã cho chúng tôi sự tự tin vào cả khả năng sử dụng của API mới lẫn hiệu năng và tính đúng đắn của cài đặt mới. Chúng tôi tin rằng nó đã sẵn sàng cho môi trường production.
Chúng tôi rất hào hứng với bản phát hành này và hy vọng rằng nó sẽ phục vụ hệ sinh thái Go trong mười năm tới và xa hơn nữa!
Bài tiếp theo: Go, Cộng đồng Go, và Đại dịch
Bài trước: Go 1.14 đã được phát hành
Mục lục blog