GORM自定义Point类型报错:panic: reflect: call of reflect.Value.Field on slice Value
修复GORM自定义Point类型查询时的反射panic问题
我需要在数据库中存储Point类型,参考相关文档编写了包含GormDataType、GormValue和Scan方法的自定义Point类型,但查询数据时出现错误:panic: reflect: call of reflect.Value.Field on slice Value。
问题详情
1. 模型定义
package models // ...imports type User struct { gorm.Model Id uint64 `gorm:"primaryKey;autoIncrement:true"` Name string `gorm:"size:255;index:idx_name,unique"` Password string `gorm:"size:64"` CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time Walks []Walk `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL"` } type Walk struct { gorm.Model Id uint64 `gorm:"primaryKey;autoIncrement:true"` UserId uint64 Coords types.Point Altitude float64 CreatedAt time.Time }
2. Point类型实现
package types // ...imports type Point struct { Latitude, Longitude float64 } func (p Point) GormDataType() string { return "point" } func (p Point) GormValue(ctx context.Context, db *gorm.DB) clause.Expr { return clause.Expr{ SQL: "ST_PointFromText(?)", Vars: []interface{}{fmt.Sprintf("POINT(%f %f)", p.Latitude, p.Longitude)}, } } func (p *Point) Scan(src interface{}) (err error) { switch src.(type) { case []byte: data, ok := src.([]byte) if !ok { return errors.New(fmt.Sprintf("(*Point).Scan: Error of scan source value of type %T", src)) } if len(data) != 25 { return errors.New(fmt.Sprintf("(*Point).Scan: Expected []bytes with length 25, got %d", len(data))) } var latitude, longitude float64 buf := bytes.NewReader(data[9:17]) err := binary.Read(buf, binary.LittleEndian, &latitude) if err != nil { return err } buf = bytes.NewReader(data[17:25]) err = binary.Read(buf, binary.LittleEndian, &longitude) if err != nil { return err } *p = Point{latitude, longitude} default: return errors.New(fmt.Sprintf("(*Point).Scan: Expected []byte for Point type, got %T", src)) } return nil }
3. 数据查询代码
db.Model(&models.User{}).Joins("Walks").First(&user, "users.name = ?", "***")
修复方案
1. 修正关联查询方式
Joins("Walks")会返回扁平的关联结果集(每条User对应一条Walk记录),搭配First方法仅能获取单条记录,GORM尝试将单条Walk数据映射到User.Walks切片字段时触发反射错误。将查询改为使用Preload预加载关联数据:
db.Preload("Walks").First(&user, "users.name = ?", "***")
2. 完善Point类型的数据库接口实现
自定义类型需要同时实现driver.Valuer接口,确保数据库驱动能正确处理该类型的写入操作。添加Value方法:
func (p Point) Value() (driver.Value, error) { // 保持与GormValue一致的POINT格式 return fmt.Sprintf("POINT(%f %f)", p.Latitude, p.Longitude), nil }
3. 可选:优化Scan方法的兼容性
手动解析二进制Point数据可能受数据库版本影响,可改用成熟的空间类型库(如github.com/twpayne/go-geom)来处理,避免硬编码字节长度:
import "github.com/twpayne/go-geom" func (p *Point) Scan(src interface{}) error { if data, ok := src.([]byte); ok { point, err := geom.UnmarshalWKB(data) if err != nil { return err } if point.Type() != geom.Point { return errors.New("not a point") } coords := point.Coords() *p = Point{Latitude: coords[1], Longitude: coords[0]} return nil } return errors.New(fmt.Sprintf("unsupported scan type: %T", src)) }
内容的提问来源于stack exchange,提问作者Aleksey Chelnokov
相关产品推荐
相关产品推荐

