You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 06:45:58