Golang中无法从PostgreSQL扫描datetime的问题

Golang中无法从PostgreSQL扫描datetime的问题 我想在Go Web服务器中使用PostgreSQL数据库,作为第一步,我只想实现对数据库的基本访问。我可以创建表并添加记录,但在检索该记录时,Go在处理时间戳时遇到了问题。

2023/05/24 14:15:36 sql: 扫描列索引3时出错,列名"created_at":不支持的扫描,将driver.Value类型存储到*time.Time类型中

这是出错的那一行代码。

        if err := db.QueryRow(query, 1).Scan(&id, &username, &password, &createdAt); err != nil {

我发现有提到MySQL的一个问题,涉及在连接字符串中设置某个参数(parseTime=true),但PostgreSQL没有类似的东西。我刚开始学习Go,我的大部分代码是从别处复制过来的——而且那些代码使用的是MySQL。

package main

import (
    "database/sql"
    "fmt"
    "log"
    "time"

    _ "github.com/lib/pq"
)

const (
  host     = "localhost"
  port     = 5432
  user     = "postgres"
  password = "*******"
  dbname   = "com_dev"
)


func main() {

    psqlInfo := fmt.Sprintf("host=%s port=%d user=%s "+
      "password=%s dbname=%s sslmode=disable",
      host, port, user, password, dbname)
    db, err := sql.Open("postgres", psqlInfo)
    if err != nil {
        log.Fatal(err)
    }
    if err := db.Ping(); err != nil {
        log.Fatal(err)
    }

    { // Create a new table
        fmt.Println("Trying to create user table")
        query := `
            CREATE TABLE users (
                id SERIAL PRIMARY KEY,
                username TEXT NOT NULL,
                password TEXT NOT NULL,
                created_at TIMESTAMP
            );`

        if _, err := db.Exec(query); err != nil {
            fmt.Println(err) 
            //log.Fatal(err)
        }
    }

    { // Insert a new user
        fmt.Println("Trying to add user")
        username := "johndoe"
        password := "secret"
        createdAt := time.Now()

        result, err := db.Exec("INSERT INTO users (username, password, created_at) VALUES ($1, $2, $3)", username, password, createdAt)
        if err != nil {
            fmt.Println("add failed")
            log.Fatal(err)
        }

        id, err := result.LastInsertId()
        fmt.Println(id)
    }

    { // Query a single user
        fmt.Println("Trying to find user")
        var (
            id        int
            username  string
            password  string
            createdAt time.Time
        )

        query := "SELECT id, username, password, created_at FROM users WHERE id = $1"
        if err := db.QueryRow(query, 1).Scan(&id, &username, &password, &createdAt); err != nil {
            log.Fatal(err)
        }

        fmt.Println(id, username, password, createdAt)
    }
}

更多关于Golang中无法从PostgreSQL扫描datetime的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

6 回复

我是从一个教程里复制粘贴的代码,那个教程就是这么做的。它用的是MySQL;也许timestamp不是内置的?或者它想说明某个观点?

更多关于Golang中无法从PostgreSQL扫描datetime的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我已切换到 pgx… 结果相同。

不过,这促使我删除了 pgAdmin 中的表,当我再次运行程序时,它成功了,所以我认为问题出在数据库中存在错误数据。

感谢您的帮助,这让我走上了正确的道路!

它使用了MySQL

我确实了解MySQL,但我认为让数据库创建时间戳很方便。

但问题是时间戳是否存储在数据库中?你是否使用pgAdmin4来“管理”数据库?强烈推荐用它来排查问题。它有一个内置的查询工具。

而且我同意 @Dean_Davidson 的观点,“pgx”是一个不错的选择。

引用:

        query := `
            CREATE TABLE users (
                id SERIAL PRIMARY KEY,
                username TEXT NOT NULL,
                password TEXT NOT NULL,
                created_at TIMESTAMP
            );`

但你为什么不让PostgreSQL自动创建时间戳呢?

created_at TIMESTAMP default CURRENT_TIMESTAMP,

… 你能在pgAdmin4中看到时间戳吗?它被存储了吗?

有趣。这个问题对我来说并不那么显而易见。如果你修改以下内容会怎样?

// 当前
query := "SELECT id, username, password, created_at FROM users WHERE id = $1"
// 为测试硬编码 time.Time 列
query := "SELECT id, username, password, now() FROM users WHERE id = $1"

对我来说,问题在于错误信息没有指出源类型。那个错误通常会说“将类型 someType 扫描到…”,但在你的情况下却缺失了。我还注意到你正在使用处于维护模式的 /lib/pq。从其自述文件中可以看到:

对于需要新功能或可靠修复已报告错误的用户,我们建议使用正在积极开发的 pgx

我个人在我的 Postgres 项目中使用 pgx。我很难想象像扫描到 time.Time 这样基础的功能在那个项目中会出问题,但我想知道如果你更换驱动程序,它是否会正常工作?为了保持使用 database/sql 接口,请查看这个 pgx 指南:

Getting started with pgx through database sql

简而言之就是:在你的导入中更换驱动程序,并且在 sql.Open 中使用 “pgx” 而不是 “postgres” 作为驱动程序名称。

最后,如果你直接查询数据库,那一行数据看起来是什么样的?

问题出在PostgreSQL的TIMESTAMP类型与Go的time.Time类型之间的映射上。PostgreSQL的TIMESTAMP默认返回的是time.Time不直接支持的类型。以下是解决方案:

// 修改查询部分,使用类型转换或调整数据库类型
{ // Query a single user
    fmt.Println("Trying to find user")
    var (
        id        int
        username  string
        password  string
        createdAt time.Time
    )

    // 方法1:在SQL查询中转换类型
    query := "SELECT id, username, password, created_at::timestamp FROM users WHERE id = $1"
    
    // 或者方法2:使用明确的类型转换
    // query := "SELECT id, username, password, created_at AT TIME ZONE 'UTC' FROM users WHERE id = $1"
    
    // 或者方法3:修改表结构使用timestamptz类型
    // CREATE TABLE users (
    //     id SERIAL PRIMARY KEY,
    //     username TEXT NOT NULL,
    //     password TEXT NOT NULL,
    //     created_at TIMESTAMPTZ
    // );

    if err := db.QueryRow(query, 1).Scan(&id, &username, &password, &createdAt); err != nil {
        log.Fatal(err)
    }

    fmt.Println(id, username, password, createdAt)
}

更完整的解决方案是修改表结构使用TIMESTAMPTZ

{ // Create a new table
    fmt.Println("Trying to create user table")
    query := `
        CREATE TABLE users (
            id SERIAL PRIMARY KEY,
            username TEXT NOT NULL,
            password TEXT NOT NULL,
            created_at TIMESTAMPTZ
        );`

    if _, err := db.Exec(query); err != nil {
        fmt.Println(err) 
    }
}

如果无法修改表结构,可以使用自定义扫描:

import (
    "database/sql/driver"
    "time"
)

type NullTime struct {
    Time  time.Time
    Valid bool
}

func (nt *NullTime) Scan(value interface{}) error {
    if value == nil {
        nt.Time, nt.Valid = time.Time{}, false
        return nil
    }
    
    switch v := value.(type) {
    case time.Time:
        nt.Time, nt.Valid = v, true
        return nil
    case []byte:
        t, err := time.Parse("2006-01-02 15:04:05", string(v))
        if err != nil {
            return err
        }
        nt.Time, nt.Valid = t, true
        return nil
    case string:
        t, err := time.Parse("2006-01-02 15:04:05", v)
        if err != nil {
            return err
        }
        nt.Time, nt.Valid = t, true
        return nil
    }
    return fmt.Errorf("unsupported type: %T", value)
}

func (nt NullTime) Value() (driver.Value, error) {
    if !nt.Valid {
        return nil, nil
    }
    return nt.Time, nil
}

// 使用自定义类型
{ // Query a single user
    fmt.Println("Trying to find user")
    var (
        id        int
        username  string
        password  string
        createdAt NullTime
    )

    query := "SELECT id, username, password, created_at FROM users WHERE id = $1"
    if err := db.QueryRow(query, 1).Scan(&id, &username, &password, &createdAt); err != nil {
        log.Fatal(err)
    }

    if createdAt.Valid {
        fmt.Println(id, username, password, createdAt.Time)
    }
}

最简单的解决方案是使用TIMESTAMPTZ类型创建表,这样Go的time.Time可以直接正确映射。

回到顶部