Go Custom Date Type

Go นั้นมีข้อมูลแบบ time.Time ที่อยู่ที่ package time ให้เราใช้เก็บค่าของ วันที่ และ เวลา ซึ่งเมื่อเราใช้ร่วมกับ package encoding/json แล้วเวลาเรา Marshal ไปเป็น JSON ค่าของวันที่และเวลาจะแปลงเป็น string ในรูปแบบ RFC3999Nano ซึ่งเป็นรูปแบบเต็มที่มีตั้งวันที่และเวลา แต่ว่าถ้าเราต้องการให้ JSON ที่เราใช้ มันมีแค่ค่าวันที่ได้ โดยเราไม่สนใจค่าของเวลา จะทำยังไง มีทางเลือกแรกคือ ไม่ต้องเก็บเป็นค่า time.Time ให้เก็บเป็น string แต่ปัญหาคือถ้าเราต้องคำนวณเกี่ยวกับเวลา เราก็ต้องแปลงเป็น time.Time อยู่ดี

อีกทางออกนึงคือสร้าง Custom Type ใหม่ ที่ Base จาก time.Time แล้วให้เรา implements interfaces json.Marshaler และ json.Unmarshaler เพื่อแค่ค่าไปเป็น JSON และแปลงกลับจาก JSON ตามลำดับ

สำหรับ json.Marshaler กำหนด method ไว้แบบนี้

type Marshaler interface {
MarshalJSON() ([]byte, error)
}

นั่นคือเราต้อง implements method MarshalJSON() ([]byte, error)

สำหรับ json.Unmarshaler กำหนด method ไว้แบบนี้

type Unmarshaler interface {
UnmarshalJSON([]byte) error
}

นั่นคือเราต้อง implements method UnmarshalJSON([]byte) error

ต่อไปเริ่มสร้าง Custom Date Type กันโดยประกาศ type ใหม่แบบนี้

type Date time.Time

ต่อไปขอ สร้าง method String() string เพื่อ implements Stringer เพื่อให้พิมพ์ผ่อน fmt.Println ได้ผลลัพธ์แค่วันที่ แบบนี้

func (d Date) String() string { 
return time.Time(d).Format("2006-01-02")
}

จะเห็นว่าต้องแปลง type กลับเป็น time.Time ก่อนในกรณีที่ต้องการเรียกใช้ method ของ time.Time

ต่อไป ทำ MarshalJSON เพื่อแปลงข้อมูลเราไปเป็น []byte ซึ่งอันนี้ไม่ยากก็คล้ายๆที่เราทำกับ String แค่แปลงกลับเป็น []byte แล้วเพิ่ม "" ครอบเข้าไป

// MarshalJSON Date type
func (d Date) MarshalJSON() ([]byte, error) {
return []byte(time.Time(d).Format("\"2006-01-02\"")), nil
}

ต่อไป ทำ UnmarshalJSON ซึ่งก็คือทำตรงกันข้าม เราต้องแปลง date รูปแบบนี้ "\"2016-01-02\"" กลับไปเป็นข้อมูลแบบ time.Time แล้วครอบด้วย Date จะเห็นว่าเวลา json.Unmarshal ส่ง []byte เข้ามาให้จะเอา double qoute เข้ามาให้เราด้วย เพราะฉะนั้นเราก็ต้อง parse ด้วย layout แบบเดียวกัน โค้ดที่ได้เลยเป็นแบบนี้

// UnmarshalJSON Date type
func (d *Date) UnmarshalJSON(b []byte) error {
tm, err := time.Parse("\"2006-01-02\"", string(b))
if err != nil {
return json.Unmarshal(b, (*time.Time)(d))
}
*d = Date(tm)
return nil
}

จากโค้ด พยายามแปลงเป็น format "\"2006-01-02\"" ก่อน ถ้าไม่สำเร็จก็จะให้การแปลงปกติของ time.Time โดยแปลง *Date เป็น (*time.Time) ก่อนแล้วส่งให้ json.Unmarshal แต่ถ้าสำเร็จก็เอาค่า tm ที่ได้กำหนดกลับให้ ตำแหน่งที่ d ชี้อยู่โดยใช้ * ช่วย

ทีนี้ดูโค้ดตัวอย่างทั้งหมด ผลลัพธ์การพิมพ์เป็นดังส่วนที่ comment // Output: เอาไว้นะครับ

package main
import (
"encoding/json"
"fmt"
"time"
)
// Date type for marshal/unmarshal JSON without time
type Date time.Time
func (d Date) String() string { 
return time.Time(d).Format("2006-01-02")
}
// UnmarshalJSON Date type
func (d *Date) UnmarshalJSON(b []byte) error {
tm, err := time.Parse("\"2006-01-02\"", string(b))
if err != nil {
return json.Unmarshal(b, (*time.Time)(d))
}
*d = Date(tm)
return nil
}
// MarshalJSON Date type
func (d Date) MarshalJSON() ([]byte, error) {
return []byte(time.Time(d).Format(`"2006-01-02"`)), nil
}
func main() {
d := Date(time.Date(2017, 05, 16, 0, 0, 0, 0, time.Local))
fmt.Println(d) // Output: 2017-05-16
        b, _ := json.Marshal(d)
fmt.Printf("%s\n", b) // Output: "2017-05-16"
        var dd Date
json.Unmarshal([]byte(`"2017-08-16"`), &dd)
fmt.Println(dd) // Output: 2017-08-16
}

จากรูปแบบโค้ด จะเห็นว่า การแปลงข้อมูลไม่ว่าจะเป็น JSON หรือแบบอื่น จะอาศัยการสร้าง Interface เอาไว้เพื่อให้แต่ละ type สามารถ implements ได้เองว่าจะแปลงยังไง แม้ว่าการแปลงที่ standard package ทำให้จะไม่ถูกใจเรา เราก็สามารถสร้าง type ใหม่ แล้ว implements Interface นั้นเองได้เช่นกัน

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.