1

I am trying to perform a postgres query that contains a custom geoPoint type but get a Unexpected EOF error. Any ideas to what I am doing wrong?

type Account struct {
    Id             uint   `json:"id" db:"id"`
    RegistrationId string `json:"registration_id" db:"registration_id"`
    PhoneNumber    string `json:"phone_number" db:"phone_number"`
    AuthToken      string `json:"auth_token" db:"auth_token"`
    // Role           string `json:"-" db:"role"`
    CreatedAt   time.Time `json:"-" db:"created_at"`
    ActivatedAt time.Time `json:"-" db:"activated_at"`

    Location GeoPoint `json:"location" db:"location"`
}

// THE FAILING FUNCTION
func FindAccountByToken(db *sqlx.DB, token string) (Account, error) {
    var account Account
    log.Println("FindAcountByToken", token)
    err := db.Get(&account, "select * from accounts where auth_token = $1", token)
    return account, err
}

type GeoPoint struct {
    Latitude  float64 `json:"latitude" db:"latitude"`
    Longitude float64 `json:"longitude" db:"longitude"`
}

// String value
func (g *GeoPoint) String() string {
    return fmt.Sprintf("(%v, %v)", g.Latitude, g.Longitude)
}

// Value of the geoPoint to be stored in the db based on the .String() method
func (g GeoPoint) Value() (driver.Value, error) {
    return g.String(), nil
}

// Scan converts the db []byte array value to the geoPoint value
func (g *GeoPoint) Scan(src interface{}) error {
    var source []byte
    var gp GeoPoint

    switch src.(type) {
    case []byte:
        source = src.([]byte)
    default:
        return errors.New("Unable to perform geopoint conversion")
    }

    log.Println("bytes -> ", source)

    reader := bytes.NewReader(source)
    if err := binary.Read(reader, binary.BigEndian, &gp); err != nil {
        log.Println("BinaryRead Error", err)
        return err
    }

    *g = gp

    return nil
}
3
  • How are you writing your geopoints to the database? Are you using binary.Write? Hard to tell what's going on without this. The code of GeoPoint.Value() suggests that you're using some kind of arbitrary human-friendly string representation. If that's the case, then binary.Read won't be an appropriate way to deserialize that representation. Commented Apr 26, 2015 at 19:56
  • That makes sense and after just casting the []byte array to a string it works. This is what happens when you blindly follow someone else's example. If you want to add your comment as an answer that would be awesome. Thanks for the help. Commented Apr 26, 2015 at 20:19
  • Uh... please add test cases. If all you've done is add some arbitrary casts from bytes to strings, you've made a huge conceptual mistake. Commented Apr 26, 2015 at 20:20

1 Answer 1

1

The GeoPoint implementation of Scanner and Valuer interfaces looks questionable. They should be symmetric, but in one case, it represents the GeoPoint as a arbitrary-variable-length string of the form:

"(<latitude>, <longitude>)"

but in the other direction, expects a representation of exactly 16 bytes (two 64-bit floats in big-endian byte order). This does not seem compatible.

Have you tested that the output value of Value() can be fed back into Scan() to get the same value? Essentially, you should be able to do:

p1 := GeoPoint{3.1415926, 2.71828}
bs, _ := p1.Value()
p2 := GeoPoint{}
p2.Scan(bs)
fmt.Printf("%#v\n", p2) 

and see the same value in p2 as in p1.

For example, something like:

// Value of the geoPoint to be stored in the db
func (g *GeoPoint) Value() (driver.Value, error) {
    var buf bytes.Buffer
    binary.Write(&buf, binary.BigEndian, g)
    return buf.Bytes(), nil
}

// Scan converts the db []byte array value to the geoPoint value
func (g *GeoPoint) Scan(src interface{}) error {
    var source []byte
    switch src.(type) {
    case []byte:
        source = src.([]byte)
    default:
        return errors.New("Unable to perform geopoint conversion")
    }
    reader := bytes.NewReader(source)
    return binary.Read(reader, binary.BigEndian, g)
}

should satisfy this property. Scan() and Value() have to be conceptually consistent: otherwise, they wouldn't be useful.

Sign up to request clarification or add additional context in comments.

2 Comments

You are right that the Valuer and Scanner were incompatible. However from the postgres[1] docs it accepts (x, y) and the format for points, so I don't think I want to save the whole GeoPoint struct, but rather just the (x, y) coords. When using this approach the Scan method seems a little dirty since it has to parse out the lat, lng values from a "(lat,lng)" string, but it does work. When trying to implement what you suggested point values of (0,0) were always saved. 1. postgresql.org/docs/8.4/static/datatype-geometric.html
Sure, yes. That's related to your database schema. If the type of accounts.location is of postgres type point, then yes, what you're doing sounds reasonable. Without the schema in hand, all I could say was that Value and Scan needed to match. If the underlying type in the database were a binary data type, like bytea, that'd be a factor.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.