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

Go实现通用Protobuf结构体持久化Postgres的空指针问题求助

问题:泛型实现Protobuf结构体的PostgreSQL序列化存储失败

希望将Protobuf结构体以字节数组形式存储到PostgreSQL,为避免为每个新类型定义包装结构体,尝试用泛型实现Scan()和Value()方法,但运行时出现runtime error: invalid memory address or nil pointer dereference错误。

使用的代码如下:

type SqlProto[T proto.Message] struct {
    Valid bool
    Proto T
}

func (d SqlProto[T]) Scan(value interface{}) error {
    if value == nil {
        d.Valid = false
        return nil
    }

    bytes, ok := value.([]byte)
    if !ok {
        return fmt.Errorf("invalid value for %T: %v", value, value)
    }

    d.Proto = *new(T)
    err := proto.Unmarshal(bytes, d.Proto)
    if err != nil {
        return fmt.Errorf("failed to parse %T: %w", value, err)
    }

    d.Valid = true

    return nil
}

func (d SqlProto[T]) Value() (driver.Value, error) {
    if !d.Valid {
        return nil, nil
    }

    return proto.Marshal(d.Proto)
}

错误核心:new(T)返回的是指向T类型的指针,而T本身属于proto.Message(本质是指针类型),因此*new(T)得到的是nil指针,proto.Unmarshal无法对nil指针完成反序列化;同时原代码Scan方法用值接收者,修改的是结构体副本,无法同步到原实例。


解决方案

关键修改点

  1. 将Scan方法的接收者改为指针类型,确保修改能作用到原结构体实例
  2. 通过反射正确创建T类型的实例,避免nil指针问题

修正后的代码

import (
    "database/sql/driver"
    "fmt"
    "reflect"

    "google.golang.org/protobuf/proto"
)

type SqlProto[T proto.Message] struct {
    Valid bool
    Proto T
}

// 改为指针接收者,确保修改同步到原实例
func (d *SqlProto[T]) Scan(value interface{}) error {
    if value == nil {
        d.Valid = false
        d.Proto = nil
        return nil
    }

    bytes, ok := value.([]byte)
    if !ok {
        return fmt.Errorf("invalid value type: expected []byte, got %T", value)
    }

    // 创建T类型的有效实例:T是*MyProto类型,先获取底层结构体类型,再创建指针
    msgType := reflect.TypeOf(*new(T)).Elem()
    msg := reflect.New(msgType).Interface().(T)

    err := proto.Unmarshal(bytes, msg)
    if err != nil {
        return fmt.Errorf("failed to unmarshal protobuf: %w", err)
    }

    d.Proto = msg
    d.Valid = true
    return nil
}

// 改为指针接收者保持一致性,同时增加空值判断
func (d *SqlProto[T]) Value() (driver.Value, error) {
    if !d.Valid || d.Proto == nil {
        return nil, nil
    }

    return proto.Marshal(d.Proto)
}

代码说明

  • 指针接收者:Scan和Value方法使用*SqlProto[T]作为接收者,确保对结构体字段的修改直接作用到原实例,而非副本。
  • 实例创建:通过reflect.TypeOf(*new(T)).Elem()获取Protobuf消息的底层结构体类型,再用reflect.New创建对应指针实例(即T类型),确保proto.Unmarshal有非nil的实例可以写入数据。
  • 边界处理:Value方法增加d.Proto == nil的判断,避免调用proto.Marshal时触发nil指针错误。

内容的提问来源于stack exchange,提问作者Jacopo Rufini

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 03:33:26