Jared Mackey
Jared Mackey

Reputation: 4158

Create custom type that will seem like the another when checking types Golang

I am a experienced python programmer but I am still new to Golang so my apologies if this is an obvious or silly question. But I am trying to create my own type that I want to act exactly like the base type with the exception of several methods being overridden. The reason for this is because several libraries I am using are checking the type against time.Time and I want it to match.

type PythonTime struct {
    time.Time
}

var pythonTimeFormatStr = "2006-01-02 15:04:05-0700"

func (self *PythonTime) UnmarshalJSON(b []byte) (err error) {
    // removes prepending/trailing " in the string
    if b[0] == '"' && b[len(b)-1] == '"' {
        b = b[1 : len(b)-1]
    }
    self.Time, err = time.Parse(pythonTimeFormatStr, string(b))
    return
}

func (self *PythonTime) MarshalJSON() ([]byte, error) {
    return []byte(self.Time.Format(pythonTimeFormatStr)), nil
}

type OtherType struct {
    Uuid     string     `json:"uuid`
    Second   PythonTime `json:"second"`
    Location string     `json:"location"`
    Action   string     `json:"action"`
    Duration int        `json:"duration"`
    Value    string     `json:"value"`
}

So the the above works fine for marshalling and unmarshalling JSON. However, for my library that I am using (gocql and cqlr) they are checking if the type is a time.Time type so they can make some other modifications before putting it in C*. How do I get my PythonTime type to equate to either show as time.Time or override the default marshalling/unmarshalling for a time.Time object just for the use of my OtherType objects?

My temporary solution has been to modify their code and add a special case for the PythonTime type that does the same thing as the time.Time type. However, this is causing me circular imports and is not the best solution. Here is their code with my modifications.

func marshalTimestamp(info TypeInfo, value interface{}) ([]byte, error) {
    switch v := value.(type) {
    case Marshaler:
        return v.MarshalCQL(info)
    case int64:
        return encBigInt(v), nil
    case time.Time:
        if v.IsZero() {
            return []byte{}, nil
        }
        x := int64(v.UTC().Unix()*1e3) + int64(v.UTC().Nanosecond()/1e6)
        return encBigInt(x), nil
    case models.PythonTime:
        x := int64(v.UTC().Unix()*1e3) + int64(v.UTC().Nanosecond()/1e6)
        return encBigInt(x), nil
    }

    if value == nil {
        return nil, nil
    }

    rv := reflect.ValueOf(value)
    switch rv.Type().Kind() {
    case reflect.Int64:
        return encBigInt(rv.Int()), nil
    }
    return nil, marshalErrorf("can not marshal %T into %s", value, info)
}

Upvotes: 3

Views: 2197

Answers (3)

Mr_Pink
Mr_Pink

Reputation: 109475

Just like you have done with the json.Marshaler and json.Unamrshaler, you can also implement the gocql.Marshaler gocql.Unamrshaler interfaces.

func (t *PythonTime) MarshalCQL(info gocql.TypeInfo) ([]byte, error) {
    b := make([]byte, 8)
    x := t.UnixNano() / int64(time.Millisecond)
    binary.BigEndian.PutUint64(b, uint64(x))
    return b, nil
}

func (t *PythonTime) UnmarshalCQL(info gocql.TypeInfo, data []byte) error {
    x := int64(binary.BigEndian.Uint64(data)) * int64(time.Millisecond)
    t.Time = time.Unix(0, x)
    return nil
}

(note, untested in the context of CQL, but this does round-trip with itself)

Upvotes: 1

Sean
Sean

Reputation: 1088

Unfortunately, that will not work in Go. Your best option would be to create some import and export methods, so that you can cast your PythonTime to a time.Time and vice versa. That will give you flexibility you desire along with compatibility with other libraries.

package main

import (

    "fmt"
    "reflect"
    "time"

)

func main() {

    p, e := NewFromTime(time.Now())
    if e != nil {
        panic(e)
    }

    v, e := p.MarshalJSON()
    if e != nil {
         panic(e)
    }

    fmt.Println(string(v), reflect.TypeOf(p))

    t, e := p.GetTime()
    if e != nil {
        panic(e)
    }

    fmt.Println(t.String(), reflect.TypeOf(t))

}

type PythonTime struct {
    time.Time
}

var pythonTimeFormatStr = "2006-01-02 15:04:05-0700"

func NewFromTime(t time.Time) (*PythonTime, error) {

b, e := t.GobEncode()
if e != nil {
    return nil, e
}

p := new(PythonTime)
e = p.GobDecode(b)
if e != nil {
    return nil, e
}

    return p, nil
}

func (self *PythonTime) GetTime() (time.Time, error) {
    return time.Parse(pythonTimeFormatStr, self.Format(pythonTimeFormatStr))
}

func (self *PythonTime) UnmarshalJSON(b []byte) (err error) {
    // removes prepending/trailing " in the string
    if b[0] == '"' && b[len(b)-1] == '"' {
        b = b[1 : len(b)-1]
    }
    self.Time, err = time.Parse(pythonTimeFormatStr, string(b))
    return
}

func (self *PythonTime) MarshalJSON() ([]byte, error) {
    return []byte(self.Time.Format(pythonTimeFormatStr)), nil
}

That should give output like this:

2016-02-04 14:32:17-0700 *main.PythonTime

2016-02-04 14:32:17 -0700 MST time.Time

Upvotes: 0

Adam Smith
Adam Smith

Reputation: 54273

Don't do this. You're checking for a time.Time object when you should be checking that it satisfies an interface.

type TimeLike interface {
    Day() int
    Format(string) string
    ...  // whatever makes a "time" object to your code!
    // looks like in this case it's
    UTC() time.Time
    IsZero() bool
}

then any code that expects a time.Time that can be substituted with a PythonTime, expect a TimeLike instead.

function Foo(value interface{}) int {
    switch v := value.(type) {
    case TimeLike:
        return v.Day()  // works for either time.Time or models.PythonTime
    }
    return 0
}

Upvotes: 2

Related Questions