Определения Protobuf такие:
syntax = "proto3"
message HugeMessage {
// omitted
}
message Request {
string name = 1;
HugeMessage payload = 2;
}
В ситуации я получил HugeMessage
от кого-то, и я хочу упаковать его дополнительными полями, а затем передать сообщение кому-то еще. Так что мне нужно распаковать бинарник HugeMessage
в структуру Go, упаковать его в Request
и снова маршалировать. Из-за большого размера HugeMessage
стоимость Unmarshal и Marshal недоступна. так могу ли я повторно использовать двоичный файл HugeMessage
без изменения определений protobuf?
func main() {
// receive it from file or network, not important.
bins, _ := os.ReadFile("hugeMessage.dump")
var message HugeMessage
_ = proto.Unmarshal(bins, &message) // slow
request := Request{
name: "xxxx",
payload: message,
}
requestBinary, _ := proto.Marshal(&request) // slow
// send it.
os.WriteFile("request.dump", requestBinary, 0644)
}
Короткий ответ: нет, простого или стандартного способа добиться этого не существует.
Наиболее очевидная стратегия — сделать то, что вы делаете сейчас: разархивировать HugeMessage
, установить его в Request
, а затем снова упорядочить. Поверхность API golang protobuf на самом деле не предоставляет средств для чего-то большего — и на то есть веские причины.
Тем не менее, есть способы достичь того, что вы хотите сделать. Но они не обязательно безопасны или надежны, поэтому вам нужно сопоставить эту стоимость со стоимостью того, что у вас есть сейчас.
Один из способов избежать немаршалирования — это воспользоваться способом, которым сообщение обычно сериализуется;
message Request {
string name = 1;
HugeMessage payload = 2;
}
.. эквивалентно
message Request {
string name = 1;
bytes payload = 2;
}
.. где payload
содержит результат вызова Marshal(...)
для некоторого HugeMessage
.
Итак, если у нас есть следующие определения:
syntax = "proto3";
message HugeMessage {
bytes field1 = 1;
string field2 = 2;
int64 field3 = 3;
}
message Request {
string name = 1;
HugeMessage payload = 2;
}
message RawRequest {
string name = 1;
bytes payload = 2;
}
Следующий код:
req1, err := proto.Marshal(&pb.Request{
Name: "name",
Payload: &pb.HugeMessage{
Field1: []byte{1, 2, 3},
Field2: "test",
Field3: 948414,
},
})
if err != nil {
panic(err)
}
huge, err := proto.Marshal(&pb.HugeMessage{
Field1: []byte{1, 2, 3},
Field2: "test",
Field3: 948414,
})
if err != nil {
panic(err)
}
req2, err := proto.Marshal(&pb.RawRequest{
Name: "name",
Payload: huge,
})
if err != nil {
panic(err)
}
fmt.Printf("equal? %t\n", bytes.Equal(req1, req2))
выходы equal? true
Неясно, является ли эта «причуда» полностью надежной, и нет никаких гарантий, что она будет работать бесконечно долго. И, очевидно, тип RawRequest
должен полностью отражать тип Request
, что не идеально.
Другой альтернативой является создание сообщения более ручным способом, то есть с использованием пакета protowire — опять же, наугад, рекомендуется соблюдать осторожность.
Вкратце это можно сделать с помощью protowire, и это не очень сложно, если повторно используемая структура не сложна.
Я задавал этот вопрос не так давно и, наконец, решил его, вдохновленный постом @nj_ . Согласно главе кодирования protobuf, сообщение буфера протокола представляет собой серию пар поле-значение, и порядок этих пар не имеет значения. Мне приходит в голову очевидная идея: просто работает как компилятор protoc, вручную формирует встроенное поле и добавляет его в конец запроса.
В этой ситуации мы хотим повторно использовать HugeMessage
в Request
, поэтому пара ключ-значение поля будет 2:{${HugeMessageBinary}}
. Таким образом, код (немного другой) может быть:
func binaryEmbeddingImplementation(messageBytes []byte, name string) (requestBytes []byte, err error) {
// 1. create a request with all ready except the payload. and marshal it.
request := protodef.Request{
Name: name,
}
requestBytes, err = proto.Marshal(&request)
if err != nil {
return nil, err
}
// 2. manually append the payload to the request, by protowire.
requestBytes = protowire.AppendTag(requestBytes, 2, protowire.BytesType) // embedded message is same as a bytes field, in wire view.
requestBytes = protowire.AppendBytes(requestBytes, messageBytes)
return requestBytes, nil
}
Сообщите номер поля, тип поля и байты. Вот и все. Обычный способ такой.
func commonImplementation(messageBytes []byte, name string) (requestBytes []byte, err error) {
// receive it from file or network, not important.
var message protodef.HugeMessage
_ = proto.Unmarshal(messageBytes, &message) // slow
request := protodef.Request{
Name: name,
Payload: &message,
}
return proto.Marshal(&request) // slow
}
Некий эталон.
$ go test -bench=a -benchtime 10s ./pkg/
goos: darwin
goarch: arm64
pkg: pbembedding/pkg
BenchmarkCommon-8 49 288026442 ns/op
BenchmarkEmbedding-8 201 176032133 ns/op
PASS
ok pbembedding/pkg 80.196s
package pkg
import (
"github.com/stretchr/testify/assert"
"golang.org/x/exp/rand"
"google.golang.org/protobuf/proto"
"pbembedding/pkg/protodef"
"testing"
)
var hugeMessageSample = receiveHugeMessageFromSomewhere()
func TestEquivalent(t *testing.T) {
requestBytes1, _ := commonImplementation(hugeMessageSample, "xxxx")
requestBytes2, _ := binaryEmbeddingImplementation(hugeMessageSample, "xxxx")
// They are not always equal int bytes. you should compare them in message view instead of binary from
// due to: https://developers.google.com/protocol-buffers/docs/encoding#implications
// I'm Lazy.
assert.NotEmpty(t, requestBytes1)
assert.Equal(t, requestBytes1, requestBytes2)
var request protodef.Request
err := proto.Unmarshal(requestBytes1, &request)
assert.NoError(t, err)
assert.Equal(t, "xxxx", request.Name)
}
// actually mock one.
func receiveHugeMessageFromSomewhere() []byte {
buffer := make([]byte, 1024*1024*1024)
_, _ = rand.Read(buffer)
message := protodef.HugeMessage{
Data: buffer,
}
res, _ := proto.Marshal(&message)
return res
}
func BenchmarkCommon(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := commonImplementation(hugeMessageSample, "xxxx")
if err != nil {
panic(err)
}
}
}
func BenchmarkEmbedding(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := binaryEmbeddingImplementation(hugeMessageSample, "xxxx")
if err != nil {
panic(err)
}
}
}
Вы правы, я думаю. Тот же вывод из этого поста: Быстрое кодирование с использованием пре-сериализации.