0x01 问题由来
最近使用gRPC-Gateway实现一个GRPC的服务HTTP网关时,遇到一个数据类型转换的问题。
在PB中定义的类型为uint64字段,在通过http接口访问返回JSON结果到本地之后却是一个string类型.
0x02 原因分析
为什么gRPC-Gateway要把int64转为string类型呢,他们的回答是遵循proto3的序列化规则,proto3的json-mapping中规定了int64/uint64/fixed64类型映射的json类型为string。
文档 里的有的话是这样说的:
int64, fixed64, uint64 string "1", "-10" JSON value will be a decimal string.
Either numbers or strings are accepted.
(PB文档地址:https://developers.google.com/protocol-buffers/docs/proto3#json)这里把文档里有关 JSON 格式映射的部分搬过来:
JSON Mapping
Proto3 supports a canonical encoding in JSON, making it easier to share data between systems. The encoding is described on a type-by-type basis in the table below.
If a value is missing in the JSON-encoded data or if its value is null
, it will be interpreted as the appropriate default value when parsed into a protocol buffer. If a field has the default value in the protocol buffer, it will be omitted in the JSON-encoded data by default to save space. An implementation may provide options to emit fields with default values in the JSON-encoded output.
proto3 | JSON | JSON example | Notes |
---|---|---|---|
message | object | {"fooBar": v, |
Generates JSON objects. Message field names are mapped to lowerCamelCase and become JSON object keys. If the json_ field option is specified, the specified value will be used as the key instead. Parsers accept both the lowerCamelCase name (or the one specified by the json_ option) and the original proto field name. null is an accepted value for all field types and treated as the default value of the corresponding field type. |
enum | string | "FOO_ |
The name of the enum value as specified in proto is used. Parsers accept both enum names and integer values. |
map<K,V> | object | {"k": v, |
All keys are converted to strings. |
repeated V | array | [v, |
null is accepted as the empty list [] . |
bool | true, false | true, |
|
string | string | "Hello World!" |
|
bytes | base64 string | "YWJjMTIzIT8kKiYoKSctPUB+" |
JSON value will be the data encoded as a string using standard base64 encoding with paddings. Either standard or URL-safe base64 encoding with/without paddings are accepted. |
int32, fixed32, uint32 | number | 1, |
JSON value will be a decimal number. Either numbers or strings are accepted. |
int64, fixed64, uint64 | string | "1", |
JSON value will be a decimal string. Either numbers or strings are accepted. |
float, double | number | 1. |
JSON value will be a number or one of the special string values "NaN", "Infinity", and "-Infinity". Either numbers or strings are accepted. Exponent notation is also accepted. -0 is considered equivalent to 0. |
Any | object |
{"@type": "url", |
If the Any contains a value that has a special JSON mapping, it will be converted as follows: {"@type": xxx, . Otherwise, the value will be converted into a JSON object, and the "@type" field will be inserted to indicate the actual data type. |
Timestamp | string | "1972-01-01T10:00:20. |
Uses RFC 3339, where generated output will always be Z-normalized and uses 0, 3, 6 or 9 fractional digits. Offsets other than "Z" are also accepted. |
Duration | string | "1. |
Generated output always contains 0, 3, 6, or 9 fractional digits, depending on required precision, followed by the suffix "s". Accepted are any fractional digits (also none) as long as they fit into nano-seconds precision and the suffix "s" is required. |
Struct | object |
{ … } |
Any JSON object. See struct. . |
Wrapper types | various types | 2, |
Wrappers use the same representation in JSON as the wrapped primitive type, except that null is allowed and preserved during data conversion and transfer. |
FieldMask | string | "f. |
See field_ . |
ListValue | array | [foo, |
|
Value | value | Any JSON value. Check google.protobuf.Value for details. | |
NullValue | null | JSON null | |
Empty | object | {} |
An empty JSON object |
JSON options
A proto3 JSON implementation may provide the following options:
- Emit fields with default values: Fields with default values are omitted by default in proto3 JSON output. An implementation may provide an option to override this behavior and output fields with their default values.
- Ignore unknown fields: Proto3 JSON parser should reject unknown fields by default but may provide an option to ignore unknown fields in parsing.
- Use proto field name instead of lowerCamelCase name: By default proto3 JSON printer should convert the field name to lowerCamelCase and use that as the JSON name. An implementation may provide an option to use proto field name as the JSON name instead. Proto3 JSON parsers are required to accept both the converted lowerCamelCase name and the proto field name.
- Emit enum values as integers instead of strings: The name of an enum value is used by default in JSON output. An option may be provided to use the numeric value of the enum value instead.
GITHUB上有关这个问题的讨论: https://github.com/grpc-ecosystem/grpc-gateway/issues/438
部分引用较早的问题评论:
gRPC 网关遵循 proto3 的序列化规则,proto3 规范规定 int64、fixed64 和 uint64 都是字符串。
这是因为 JS 使用 52 位尾数(1 表示符号,11 表示指数)实现的 IEEE 754 双精度。
这意味着 JS 在不损失精度的情况下可以表示的最大数字是 2^52。试试看:
(Math.pow(2,53) + 0) == (Math.pow(2,53) + 1)
true
因此,proto3 不是假装 Javascript 可以处理它(并丢失数据),而是更喜欢让客户端与字符串交互。
更少的注释==更好的库。
如果你想与 int64 作为数字进行交互,有一堆 JS bigint 库可以用来解析字符串,
执行你想要的任何操作并返回一个字符串。这可能是处理这个问题的更好方法。
0x03 解决方案
1. 如上所说,就统一用string吧.
2. 使用 double 类型代替 int64/uint64
如果使用的int64数值小于 MAX(double), 可以考虑使用 double 类型代替 int64/uint64 值
Go 语言中的浮点数采用IEEE-754 标准的表达方式,定义了两个类型:
float32
和 float64
,其中 float32
是单精度浮点数,可以精确到小数点后 7 位(类似 PHP、Java 等语言的 float
类型),float64
是双精度浮点数,可以精确到小数点后 15 位(类似 PHP、Java 等语言的 double
类型)。go语言里浮点型定义如下
由于JSON 数据类型里Number来自 Javascript , 而JS里 Number 采用 双精度浮点数 来保存数值,因此该值的范围只能在 +/- 253 的范围内。
所以,如果int64值是给JS端使用.也会遇到JS不能处理int64问题.
综上所述,为了统一,也可以在GO里使用double表示int64类型
0x04 END
参考文档: