committed by
Ralf Haferkamp
parent
e270cdbfd2
commit
b5b15f29de
+50
@@ -6,6 +6,11 @@
|
||||
[](https://codecov.io/gh/shamaton/msgpack)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fshamaton%2Fmsgpack?ref=badge_shield)
|
||||
|
||||
## 📣 Announcement: `time.Time` decoding defaults to **UTC** in v3
|
||||
Starting with **v3.0.0**, when decoding MessagePack **Timestamp** into Go’s `time.Time`,
|
||||
the default `Location` will be **UTC** (previously `Local`). The instant is unchanged.
|
||||
To keep the old behavior, use `SetDecodedTimeAsLocal()`.
|
||||
|
||||
## Features
|
||||
* Supported types : primitive / array / slice / struct / map / interface{} and time.Time
|
||||
* Renaming fields via `msgpack:"field_name"`
|
||||
@@ -60,6 +65,51 @@ func handle(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
```
|
||||
|
||||
## 📣 Announcement: `time.Time` decoding defaults to **UTC** in v3
|
||||
|
||||
**TL;DR:** Starting with **v3.0.0**, when decoding MessagePack **Timestamp** into Go’s `time.Time`, the default `Location` will be **UTC** (previously `Local`). The **instant** is unchanged—only the display/location changes. This avoids host-dependent differences and aligns with common distributed systems practice.
|
||||
|
||||
### What is changing?
|
||||
|
||||
* **Before (v2.x):** Decoded `time.Time` defaults to `Local`.
|
||||
* **After (v3.0.0):** Decoded `time.Time` defaults to **UTC**.
|
||||
|
||||
MessagePack’s Timestamp encodes an **instant** (epoch seconds + nanoseconds) and does **not** carry timezone info. Your data’s point in time is the same; only `time.Time.Location()` differs.
|
||||
|
||||
### Why?
|
||||
|
||||
* Eliminate environment-dependent behavior (e.g., different hosts showing different local zones).
|
||||
* Make “UTC by default” the safe, predictable baseline for logs, APIs, and distributed apps.
|
||||
|
||||
### Who is affected?
|
||||
|
||||
* Apps that **display local time** without explicitly converting from UTC.
|
||||
* If your code already normalizes to UTC or explicitly sets a location, you’re likely unaffected.
|
||||
|
||||
### Keep the old behavior (Local)
|
||||
|
||||
If you want the v2 behavior on v3:
|
||||
|
||||
```go
|
||||
msgpack.SetDecodedTimeAsLocal()
|
||||
```
|
||||
|
||||
Or convert after the fact:
|
||||
|
||||
```go
|
||||
var t time.Time
|
||||
_ = msgpack.Unmarshal(data, &t)
|
||||
t = t.In(time.Local)
|
||||
```
|
||||
|
||||
### Preview the new behavior on v2 (optional)
|
||||
|
||||
You can opt into UTC today on v2.x:
|
||||
|
||||
```go
|
||||
msgpack.SetDecodedTimeAsUTC()
|
||||
```
|
||||
|
||||
## Benchmark
|
||||
This result made from [shamaton/msgpack_bench](https://github.com/shamaton/msgpack_bench)
|
||||
|
||||
|
||||
+11
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/shamaton/msgpack/v2/internal/encoding"
|
||||
streamdecoding "github.com/shamaton/msgpack/v2/internal/stream/decoding"
|
||||
streamencoding "github.com/shamaton/msgpack/v2/internal/stream/encoding"
|
||||
"github.com/shamaton/msgpack/v2/time"
|
||||
)
|
||||
|
||||
// StructAsArray is encoding option.
|
||||
@@ -82,3 +83,13 @@ func RemoveExtStreamCoder(e ext.StreamEncoder, d ext.StreamDecoder) error {
|
||||
func SetComplexTypeCode(code int8) {
|
||||
def.SetComplexTypeCode(code)
|
||||
}
|
||||
|
||||
// SetDecodedTimeAsUTC sets decoded time.Time values to UTC timezone.
|
||||
func SetDecodedTimeAsUTC() {
|
||||
time.SetDecodedAsLocal(false)
|
||||
}
|
||||
|
||||
// SetDecodedTimeAsLocal sets decoded time.Time values to local timezone.
|
||||
func SetDecodedTimeAsLocal() {
|
||||
time.SetDecodedAsLocal(true)
|
||||
}
|
||||
|
||||
+17
-5
@@ -48,7 +48,11 @@ func (td *timeDecoder) AsValue(offset int, k reflect.Kind, d *[]byte) (interface
|
||||
case def.Fixext4:
|
||||
_, offset = td.ReadSize1(offset, d)
|
||||
bs, offset := td.ReadSize4(offset, d)
|
||||
return time.Unix(int64(binary.BigEndian.Uint32(bs)), 0), offset, nil
|
||||
v := time.Unix(int64(binary.BigEndian.Uint32(bs)), 0)
|
||||
if decodeAsLocal {
|
||||
return v, offset, nil
|
||||
}
|
||||
return v.UTC(), offset, nil
|
||||
|
||||
case def.Fixext8:
|
||||
_, offset = td.ReadSize1(offset, d)
|
||||
@@ -56,9 +60,13 @@ func (td *timeDecoder) AsValue(offset int, k reflect.Kind, d *[]byte) (interface
|
||||
data64 := binary.BigEndian.Uint64(bs)
|
||||
nano := int64(data64 >> 34)
|
||||
if nano > 999999999 {
|
||||
return zero, 0, fmt.Errorf("In timestamp 64 formats, nanoseconds must not be larger than 999999999 : %d", nano)
|
||||
return zero, 0, fmt.Errorf("in timestamp 64 formats, nanoseconds must not be larger than 999999999 : %d", nano)
|
||||
}
|
||||
return time.Unix(int64(data64&0x00000003ffffffff), nano), offset, nil
|
||||
v := time.Unix(int64(data64&0x00000003ffffffff), nano)
|
||||
if decodeAsLocal {
|
||||
return v, offset, nil
|
||||
}
|
||||
return v.UTC(), offset, nil
|
||||
|
||||
case def.Ext8:
|
||||
_, offset = td.ReadSize1(offset, d)
|
||||
@@ -67,10 +75,14 @@ func (td *timeDecoder) AsValue(offset int, k reflect.Kind, d *[]byte) (interface
|
||||
secbs, offset := td.ReadSize8(offset, d)
|
||||
nano := binary.BigEndian.Uint32(nanobs)
|
||||
if nano > 999999999 {
|
||||
return zero, 0, fmt.Errorf("In timestamp 96 formats, nanoseconds must not be larger than 999999999 : %d", nano)
|
||||
return zero, 0, fmt.Errorf("in timestamp 96 formats, nanoseconds must not be larger than 999999999 : %d", nano)
|
||||
}
|
||||
sec := binary.BigEndian.Uint64(secbs)
|
||||
return time.Unix(int64(sec), int64(nano)), offset, nil
|
||||
v := time.Unix(int64(sec), int64(nano))
|
||||
if decodeAsLocal {
|
||||
return v, offset, nil
|
||||
}
|
||||
return v.UTC(), offset, nil
|
||||
}
|
||||
|
||||
return zero, 0, fmt.Errorf("should not reach this line!! code %x decoding %v", code, k)
|
||||
|
||||
+15
-3
@@ -33,7 +33,11 @@ func (td *timeStreamDecoder) IsType(code byte, innerType int8, dataLength int) b
|
||||
func (td *timeStreamDecoder) ToValue(code byte, data []byte, k reflect.Kind) (interface{}, error) {
|
||||
switch code {
|
||||
case def.Fixext4:
|
||||
return time.Unix(int64(binary.BigEndian.Uint32(data)), 0), nil
|
||||
v := time.Unix(int64(binary.BigEndian.Uint32(data)), 0)
|
||||
if decodeAsLocal {
|
||||
return v, nil
|
||||
}
|
||||
return v.UTC(), nil
|
||||
|
||||
case def.Fixext8:
|
||||
data64 := binary.BigEndian.Uint64(data)
|
||||
@@ -41,7 +45,11 @@ func (td *timeStreamDecoder) ToValue(code byte, data []byte, k reflect.Kind) (in
|
||||
if nano > 999999999 {
|
||||
return zero, fmt.Errorf("in timestamp 64 formats, nanoseconds must not be larger than 999999999 : %d", nano)
|
||||
}
|
||||
return time.Unix(int64(data64&0x00000003ffffffff), nano), nil
|
||||
v := time.Unix(int64(data64&0x00000003ffffffff), nano)
|
||||
if decodeAsLocal {
|
||||
return v, nil
|
||||
}
|
||||
return v.UTC(), nil
|
||||
|
||||
case def.Ext8:
|
||||
nano := binary.BigEndian.Uint32(data[:4])
|
||||
@@ -49,7 +57,11 @@ func (td *timeStreamDecoder) ToValue(code byte, data []byte, k reflect.Kind) (in
|
||||
return zero, fmt.Errorf("in timestamp 96 formats, nanoseconds must not be larger than 999999999 : %d", nano)
|
||||
}
|
||||
sec := binary.BigEndian.Uint64(data[4:12])
|
||||
return time.Unix(int64(sec), int64(nano)), nil
|
||||
v := time.Unix(int64(sec), int64(nano))
|
||||
if decodeAsLocal {
|
||||
return v, nil
|
||||
}
|
||||
return v.UTC(), nil
|
||||
}
|
||||
|
||||
return zero, fmt.Errorf("should not reach this line!! code %x decoding %v", code, k)
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package time
|
||||
|
||||
var decodeAsLocal = true
|
||||
|
||||
// SetDecodedAsLocal sets the decoded time to local time.
|
||||
func SetDecodedAsLocal(b bool) {
|
||||
decodeAsLocal = b
|
||||
}
|
||||
Reference in New Issue
Block a user