
本文介绍如何利用 go 的反射机制,根据任意结构体类型动态创建其实例并扫描 sql 查询结果,避免硬编码具体结构体,实现真正的泛型化数据映射。
本文介绍如何利用 go 的反射机制,根据任意结构体类型动态创建其实例并扫描 sql 查询结果,避免硬编码具体结构体,实现真正的泛型化数据映射。
在 Go 中处理动态 SQL 查询结果时,常面临一个核心挑战:如何不依赖具体结构体(如 User)而通用地将 *sql.Rows 映射为一组结构体实例。Go 本身不支持泛型(在 Go 1.18 前),但可通过 reflect 包在运行时完成类型推导与实例构造,从而实现“类型驱动”的动态解码。
关键在于两点:
- 动态创建结构体指针:使用 reflect.New(reflect.TypeOf(u)) 获取指向新零值实例的指针;
- 统一生成扫描参数切片:通过 reflect.Value.Elem() 访问字段地址,构造 []interface{} 供 rows.Scan() 使用。
以下是完整、健壮的实现示例:
import (
"database/sql"
"encoding/json"
"fmt"
"reflect"
)
// StrutForScan 接收结构体指针,返回各字段地址构成的 interface{} 切片
func StrutForScan(u interface{}) []interface{} {
val := reflect.ValueOf(u).Elem() // 确保 u 是 *T 类型
if val.Kind() != reflect.Struct {
panic("StrutForScan: argument must be a pointer to struct")
}
v := make([]interface{}, val.NumField())
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
if !field.CanAddr() {
panic(fmt.Sprintf("StrutForScan: field %d is unaddressable", i))
}
v[i] = field.Addr().Interface()
}
return v
}
// StructRow 根据传入的结构体样板(零值)动态创建实例并扫描每一行
func StructRow(sample interface{}, rows *sql.Rows) ([]interface{}, error) {
defer rows.Close() // 避免资源泄漏
t := reflect.TypeOf(sample)
if t.Kind() == reflect.Ptr {
t = t.Elem() // 解引用指针,获取实际结构体类型
}
if t.Kind() != reflect.Struct {
return nil, fmt.Errorf("StructRow: sample must be struct or *struct, got %v", t)
}
var data []interface{}
for rows.Next() {
newInstance := reflect.New(t).Interface() // 创建 *T 实例
scanArgs := StrutForScan(newInstance)
if err := rows.Scan(scanArgs...); err != nil {
return nil, fmt.Errorf("scan failed: %w", err)
}
data = append(data, newInstance) // 存储 *T(非 T)
}
return data, rows.Err() // 检查 rows.Next() 循环后的错误
}
// 使用示例
type User struct {
IdUser int `json:"id_user"`
Name string `json:"name"`
Email string `json:"email"`
Username string `json:"username"`
}
func main() {
// 假设 db 已初始化
rows, err := db.Query("SELECT id_user, name, email, username FROM users")
if err != nil {
log.Fatal(err)
}
// 传入结构体零值(或其指针)作为类型模板
users, err := StructRow(User{}, rows)
if err != nil {
log.Fatal(err)
}
// 注意:users 中每个元素是 *User,需解引用才能 JSON 序列化
var userSlice []User
for _, u := range users {
userSlice = append(userSlice, *(u.(*User)))
}
json.NewEncoder(w).Encode(userSlice)
}⚠️ 重要注意事项:
- 传入 StructRow 的 sample 参数仅用于类型推导,可以是零值(User{})或 &User{},函数内部会自动处理指针层级;
- StrutForScan 要求输入必须是指向结构体的指针(*T),否则 .Elem() 将 panic;
- 字段必须可寻址(即不能是嵌入的不可寻址字段或未导出字段),且数据库列顺序必须严格匹配结构体字段声明顺序(按 tag 无法自动对齐,需手动保证);
- 生产环境建议配合 sqlx 或 gorp 等成熟 ORM/Query 工具,它们已内置更安全、高效的反射映射逻辑。
该方案在无泛型时代提供了简洁可控的动态映射能力,是理解 Go 反射与 SQL 交互的经典实践。


















