Golang X GRPC: Protobuf nullable type

Waranchit Chaiwong
2 min readOct 1, 2020

ณ ปัจจุบันเราได้มีการเล่นกับ grpc-gateway อยู่พอสมควร ตอนแรก ๆ มันก็ดีมากนะ แปลกใหม่ดี จนมาเจอข้อกำหนดบางอย่างที่ว่า ในตอน response ออกไปหา client ถ้า field ไหนไม่มีค่า ให้แสดงค่า null แทน…. อ๊าววว จะเอาไงดีละทีเนี่ย เพราะโดยปกติตัว scalar type ของ protobuf message มันกำหนดเป็น null ไม่ได้ (ใน Go ก็คือของที่เป็น pointer type นั่นเอง).

Message field type with Message

โดยปกติแล้ว ถ้าอยากให้ message field สามารถป้อนค่า nil ได้ เราก็แค่กำหนด type ของ message field นั้น ๆ ให้เป็น message ซะก็จบ เช่น

message M1 {
M2 data = 1;
}

message M2 {
string value = 1;
}

จากตัวอย่างการประกาศ message ด้านบน หมายความว่า field data ใน message M1 สามารถ มีค่าหรือไม่มีค่าก็ได้ เพราะเมื่อเราสั่ง generate protobuf แล้ว มันจะได้ struct ตามด้านล่าง

type M1 struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields

Data *M2 `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
}

จากที่เห็นตาม code ด้านบน field data จะมี type เป็น *M2(Pointer of M2).

msg := M1{Data: nil}
marshaller := runtime.JSONPb{EmitDefaults: true}
b, _ := marshaller.Marshal(&msg)
Result
{"data":null}

จาก code ด้านบน ในที่สุดเราก็ได้ท่าที่ตอบโจทย์สักที เย้ๆๆๆๆๆ. แต่เดี๋ยวก่อน ในทางกลับกัน ถ้า field Data มีค่าละ json ที่ออกมามันก็ควรจะเป็น …

{“data”: “test”}

แต่ ดั๊นนนนมีเซอร์ไพรส์ซะงั้น. เพราะหลังจากที่เรากำหนดค่าให้ Data มันกลับได้ออกมาเป็น…

{“data”: {“value”: “test”}}

เอ๊…ทำไมมันถึงเป็นแบบนี้ละน๊อออออ ติ๊กต็อกๆๆ. ก็ไม่แปลกหรอก เพราะถ้าเราประกาศ message field typeให้เป็น message มันก็เหมือนเราทำ nested struct นั่นเอง. ถ้างั้นมันจะมีวิธีอื่นไหมละ ที่จะตอบโจทย์ทั้งแบบมีค่าและไม่มีค่า

Wrapper type (Well-Known Types)

จริงๆแล้ว protobuf จะมีสิ่งที่เรียกว่า Wrapper type ซึ่งจริงๆแล้วมันก็เหมือนการเอา scalar type มาห่อให้เป็น message นั่นเอง เช่น

string => google.protobuf.StringValue
int32 => google.protobuf.Int32Value
int64 => google.protobuf.Int64Value

จริงๆมีเยอะกว่านี้ อยากดูเพิ่มก็กดเข้าไปเลยยย https://developers.google.com/protocol-buffers/docs/reference/google.protobuf

จากนั้น เราก็ไปแก้ message field type data ให้เป็น wrapper type

message M1 {
google.protobuf.String data = 1;
}
// อย่าลืม import "google/protobuf/wrappers.proto"; ด้วยยย

หลังจากที่ generate proto แล้ว ทีนี่เราจะได้ struct ที่มีหน้าตา

type M1 struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields

Data *wrappers.StringValue `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
}

ไหน ๆ เราก็ได้ struct โฉมใหม่ละ ไหนลองซิ มันจะได้ตามที่เราต้องการหรือเปล่า

// 1. แบบไม่มีค่า
msg := M1{Data: nil}
marshaller := runtime.JSONPb{EmitDefaults: true}
b ,_ := marshaller.Marshal(&msg)
Result
{"data":null}
// 2. แบบมีค่า
msg := M1{Data: &wrapper.StringValue{Value: "test"}}
marshaller := runtime.JSONPb{EmitDefaults: true}
b ,_ := marshaller.Marshal(&msg)
Result
{"data":"test"}

ในที่สุดดดด เราก็ได้วิธีที่ตอบโจทย์ที่สุดละ นั่นคือการประกาศตัว message field type ให้เป็นแบบ Wrapper type…..

--

--