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

Golang操作Postgres数组报错:无法扫描至data.Image类型

Go解析Postgres数组到自定义结构体切片时出错

错误信息

sql: Scan error on column index 8, name "image_paths": pq: parsing array element index 0: pq: scanning to data.Image is not implemented; only sql.Scanner

数据库记录

Database Image

相关代码

func (s SpaceDetailsModel) GetForUser(filters Filters, id int64) ([]*SpaceDetails, Metadata, error) {
    if id < 1 {
        return nil, Metadata{}, ErrRecordNotFound
    }

    query := `
        SELECT count(*) OVER(), id, created_At, space_owner_id, location_text, post_code, longitude, latitude, image_paths, space_description, booking_count, number_of_reviews, average_rating, version
        FROM spaces
        WHERE space_owner_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`

    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    args := []interface{}{id, filters.limit(), filters.offset()}

    rows, err := s.DB.QueryContext(ctx, query, args...)
    if err != nil {
        return nil, Metadata{}, err
    }

    defer rows.Close()

    totalRecords := 0
    spaces := []*SpaceDetails{}

    for rows.Next() {
        var space SpaceDetails

        err := rows.Scan(
            &totalRecords,
            &space.ID,
            &space.CreatedAt,
            &space.SpaceOwnerId,
            &space.LocationText,
            &space.PostCode,
            &space.Coordinates.Longitude,
            &space.Coordinates.Latitude,
            pq.Array(&space.Images),
            &space.SpaceDescription,
            &space.BookingCount,
            &space.NumberOfReviews,
            &space.AverageRating,
            &space.Version,
        )
        if err != nil {
            return nil, Metadata{}, err
        }

        spaces = append(spaces, &space)
    }

    if err = rows.Err(); err != nil {
        return nil, Metadata{}, err
    }

    metadata := calculateMetadata(totalRecords, filters.Page, filters.PageSize)

    return spaces, metadata, nil
}

堆栈跟踪

PS C:\Users\bcart\OneDrive\Desktop\Project\Project Z\Code\Parkit> 
go run ./cmd/api
{"level":"INFO","time":"2022-07-28T13:40:14Z","message":"database connection pool established"}
{"level":"INFO","time":"2022-07-28T13:40:14Z","message":"starting server","properties":{"addr":":4000","env":"development"}}
{"level":"ERROR","time":"2022-07-28T13:40:19Z","message":"sql: Scan error on column index 8, name \"image_paths\": pq: parsing array element index 0: pq: scanning to data.Image is not implemented; only sql.Scanner","properties":{"request_method":"POST","request_url":"/api/v1/days_bookings_record"},"trace":"goroutine 22 [running]:\nruntime/debug.Stack(0x31011630, 0xeda7488c3, 0x0)\n\tC:/Program Files/Go/src/runtime/debug/stack.go:24 +0xa5\nparkit/internal/jsonlog.(*Logger).print(0xc000054600, 0xc0000fe001, 0xc0000fe000, 0x99, 0xc00009cc60, 0x0, 0x0, 0x0)\n\tC:/Users/bcart/OneDrive/Desktop/Project/Project Z/Code/Parkit/internal/jsonlog/jsonlog.go:80 +0x475\nparkit/internal/jsonlog.(*Logger).PrintError(0xc000054600, 0xbd9ba0, 0xc00009a2e0, 0xc00009cc60)\n\tC:/Users/bcart/OneDrive/Desktop/Project/Project Z/Code/Parkit/internal/jsonlog/jsonlog.go:52 +0x69\nmain.(*application).logError(0xc0000be000, 0xc0000c6300, 0xbd9ba0, 0xc00009a2e0)\n\tC:/Users/bcart/OneDrive/Desktop/Project/Project Z/Code/Parkit/cmd/api/errors.go:9 +0x145\nmain.(*application).serverErrorResponse(0xc0000be000, 0xbe03e0, 0xc0000883c0, 0xc0000c6300, 0xbd9ba0, 0xc00009a2e0)\n\tC:/Users/bcart/OneDrive/Desktop/Project/Project Z/Code/Parkit/cmd/api/errors.go:26 +0x54\nmain.(*application).createDaysBookingsRecordHandler(0xc0000be000, 0xbe03e0, 0xc0000883c0, 0xc0000c6300)\n\tC:/Users/bcart/OneDrive/Desktop/Project/Project Z/Code/Parkit/cmd/api/spacebookings.go:39 +0x6cd\nnet/http.HandlerFunc.ServeHTTP(0xc0000a4340, 0xbe03e0, 0xc0000883c0, 0xc0000c6300)\n\tC:/Program Files/Go/src/net/http/server.go:2069 +0x4b\ngithub.com/julienschmidt/httprouter.(*Router).Handler.func1(0xbe03e0, 0xc0000883c0, 0xc0000c6300, 0x0, 0x0, 0x0)\n\tC:/Users/bcart/go/pkg/mod/github.com/julienschmidt/httprouter@v1.3.0/router.go:275 +0x1e7\ngithub.com/julienschmidt/httprouter.(*Router).ServeHTTP(0xc000088180, 0xbe03e0, 0xc0000883c0, 0xc0000c6300)\n\tC:/Users/bcart/go/pkg/mod/github.com/julienschmidt/httprouter@v1.3.0/router.go:387 +0xc7e\nmain.(*application).authenticate.func1(0xbe03e0, 0xc0000883c0, 0xc0000c6200)\n\tC:/Users/bcart/OneDrive/Desktop/Project/Project Z/Code/Parkit/cmd/api/middleware.go:128 +0x423\nnet/http.HandlerFunc.ServeHTTP(0xc00009a0a0, 0xbe03e0, 0xc0000883c0, 0xc0000c6200)\n\tC:/Program Files/Go/src/net/http/server.go:2069 +0x4b\nmain.(*application).rateLimit.func2(0xbe03e0, 0xc0000883c0, 0xc0000c6200)\n\tC:/Users/bcart/OneDrive/Desktop/Project/Project Z/Code/Parkit/cmd/api/middleware.go:84 +0x82\nnet/http.HandlerFunc.ServeHTTP(0xc00009c2a0, 0xbe03e0, 0xc0000883c0, 0xc0000c6200)\n\tC:/Program Files/Go/src/net/http/server.go:2069 +0x4b\nmain.(*application).enableCORS.func1(0xbe03e0, 0xc0000883c0, 0xc0000c6200)\n\tC:/Users/bcart/OneDrive/Desktop/Project/Project Z/Code/Parkit/cmd/api/middleware.go:208 +0x3ec\nnet/http.HandlerFunc.ServeHTTP(0xc00009a0c0, 0xbe03e0, 0xc0000883c0, 0xc0000c6200)\n\tC:/Program Files/Go/src/net/http/server.go:2069 +0x4b\nmain.(*application).recoverPanic.func1(0xbe03e0, 0xc0000883c0, 0xc0000c6200)\n\tC:/Users/bcart/OneDrive/Desktop/Project/Project Z/Code/Parkit/cmd/api/middleware.go:30 +0x98\nnet/http.HandlerFunc.ServeHTTP(0xc00009a0e0, 0xbe03e0, 0xc0000883c0, 0xc0000c6200)\n\tC:/Program Files/Go/src/net/http/server.go:2069 +0x4b\ngithub.com/felixge/httpsnoop.CaptureMetrics.func1(0xbe03e0, 0xc0000883c0)\n\tC:/Users/bcart/go/pkg/mod/github.com/felixge/httpsnoop@v1.0.3/capture_metrics.go:29 +0x53\ngithub.com/felixge/httpsnoop.(*Metrics).CaptureMetrics(0xc0000960a8, 0xbdfe70, 0xc0000c80e0, 0xc000073ab0)\n\tC:/Users/bcart/go/pkg/mod/github.com/felixge/httpsnoop@v1.0.3/capture_metrics.go:84 +0x1e7\ngithub.com/felixge/httpsnoop.CaptureMetricsFn(...)\n\tC:/Users/bcart/go/pkg/mod/github.com/felixge/httpsnoop@v1.0.3/capture_metrics.go:39\ngithub.com/felixge/httpsnoop.CaptureMetrics(0xbda7e0, 0xc00009a0e0, 0xbdfe70, 0xc0000c80e0, 0xc0000c6200, 0x970ca7, 0xc0000aa2ec, 0x1e0edbf0778)\n\tC:/Users/bcart/go/pkg/mod/github.com/felixge/httpsnoop@v1.0.3/capture_metrics.go:28 +0xae\nmain.(*application).metrics.func1(0xbdfe70, 0xc0000c80e0, 0xc0000c6200)\n\tC:/Users/bcart/OneDrive/Desktop/Project/Project Z/Code/Parkit/cmd/api/middleware.go:223 +0x87\nnet/http.HandlerFunc.ServeHTTP(0xc0000aa0c0, 0xbdfe70, 0xc0000c80e0, 0xc0000c6200)\n\tC:/Program Files/Go/src/net/http/server.go:2069 +0x4b\nnet/http.serverHandler.ServeHTTP(0xc0000c8000, 0xbdfe70, 0xc0000c80e0, 0xc0000c6200)\n\tC:/Program Files/Go/src/net/http/server.go:2887 +0xaa\nnet/http.(*conn).serve(0xc00008a140, 0xbe0f20, 0xc0000aa240)\n\tC:/Program Files/Go/src/net/http/server.go:1952 +0x8cd\ncreated by net/http.(*Server).Serve\n\tC:/Program Files/Go/src/net/http/server.go:3013 +0x3b8\n"}
2022/07/28 14:40:19 http: superfluous response.WriteHeader call from github.com/felixge/httpsnoop.(*Metrics).CaptureMetrics.func1.1 (capture_metrics.go:53)

原因分析

错误核心是:Postgres的pq.Array无法直接将数组元素映射到自定义的data.Image结构体,因为该结构体没有实现sql.Scanner接口,pq驱动不知道如何将数据库中的字符串(从图片链接数组来看,数据库里是文本数组)转换为data.Image实例。

解决方法

方法一:让data.Image实现sql.Scanner接口

假设data.Image结构体定义如下:

package data

type Image struct {
    URL string
}

为其实现Scan方法:

func (i *Image) Scan(value interface{}) error {
    str, ok := value.(string)
    if !ok {
        return fmt.Errorf("expected string, got %T", value)
    }
    i.URL = str
    return nil
}

实现后,pq驱动就能自动将数组中的每个字符串元素转换为Image实例,原代码中的pq.Array(&space.Images)可以正常使用。

方法二:先扫描到字符串切片,再手动转换

如果不想修改Image结构体,可以临时用字符串切片接收数据,再转换为Image切片:
修改rows.Scan部分代码:

var imagePaths []string
err := rows.Scan(
    &totalRecords,
    &space.ID,
    &space.CreatedAt,
    &space.SpaceOwnerId,
    &space.LocationText,
    &space.PostCode,
    &space.Coordinates.Longitude,
    &space.Coordinates.Latitude,
    pq.Array(&imagePaths), // 先扫描到字符串切片
    &space.SpaceDescription,
    &space.BookingCount,
    &space.NumberOfReviews,
    &space.AverageRating,
    &space.Version,
)
if err != nil {
    return nil, Metadata{}, err
}
// 手动转换为Image切片
space.Images = make([]data.Image, len(imagePaths))
for idx, path := range imagePaths {
    space.Images[idx] = data.Image{URL: path}
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 11:15:34