
本文介绍如何利用 Go 的 reflect 包,在运行时根据任意结构体类型动态创建其实例,并将 sql.Rows 中的数据安全、通用地扫描进该实例,实现真正的“泛型式”数据库结果映射。
本文介绍如何利用 go 的 `reflect` 包,在运行时根据任意结构体类型动态创建其实例,并将 `sql.rows` 中的数据安全、通用地扫描进该实例,实现真正的“泛型式”数据库结果映射。
在 Go 中,若需将 SQL 查询结果自动映射为结构体切片,通常会为每种实体(如 User、Post)编写专用的 Scan 逻辑。但当业务需要统一处理多种结构体类型(例如构建通用 ORM 层或 API 数据导出工具),硬编码结构体类型显然违背“动态性”需求。此时,reflect 是唯一可行的底层方案。
核心思路是:不传结构体值,而传其类型(或零值指针),再通过 reflect.New() 动态构造新实例。原代码中 var user User 是具体类型变量,无法复用;而 u interface{} 接收的是任意结构体的值(如 User{}),但 reflect.TypeOf(u) 实际得到的是 User 类型(非指针),直接 reflect.New(t) 才能获得可寻址的指针,满足 rows.Scan 要求。
以下是完整、健壮的实现:
import (
"database/sql"
"fmt"
"reflect"
)
// StructRow 根据结构体模板动态解析 SQL rows,返回 []interface{} 切片
func StructRow(template interface{}, rows *sql.Rows) ([]interface{}, error) {
if rows == nil {
return nil, fmt.Errorf("rows is nil")
}
defer rows.Close()
t := reflect.TypeOf(template)
if t.Kind() == reflect.Ptr {
t = t.Elem() // 解包指针,获取实际 struct 类型
}
if t.Kind() != reflect.Struct {
return nil, fmt.Errorf("template must be a struct or *struct, got %s", t.Kind())
}
var result []interface{}
for rows.Next() {
// 动态创建该 struct 类型的新实例(指针)
newInstance := reflect.New(t).Interface()
// 构造 Scan 所需的 []interface{} 参数
scanArgs := structForScan(newInstance)
if err := rows.Scan(scanArgs...); err != nil {
return nil, fmt.Errorf("scan failed: %w", err)
}
// 将解引用后的 struct 值(非指针)加入结果
result = append(result, reflect.ValueOf(newInstance).Elem().Interface())
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return result, nil
}
// structForScan 返回结构体字段地址切片,用于 rows.Scan
func structForScan(instance interface{}) []interface{} {
v := reflect.ValueOf(instance)
if v.Kind() != reflect.Ptr || v.IsNil() {
panic("instance must be a non-nil pointer")
}
elem := v.Elem()
if elem.Kind() != reflect.Struct {
panic("pointer must point to a struct")
}
n := elem.NumField()
args := make([]interface{}, n)
for i := 0; i < n; i++ {
field := elem.Field(i)
if !field.CanAddr() {
panic(fmt.Sprintf("field %d of struct is unaddressable (must be exported)", i))
}
args[i] = field.Addr().Interface()
}
return args
}使用示例:
type User struct {
ID int `json:"id_user"`
Name string `json:"name"`
Email string `json:"email"`
Username string `json:"username"`
}
func handler(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query("SELECT id_user, name, email, username FROM users")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 传入结构体零值(或任意 User 实例),仅用于推导类型
data, err := StructRow(User{}, rows)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(data) // 输出: [{"id_user":1,"name":"Alice",...}]
}⚠️ 关键注意事项:
- 结构体字段必须首字母大写(exported),否则 reflect 无法获取地址;
- 数据库列顺序、数量、类型必须与结构体字段严格匹配(按声明顺序);
- json 标签不影响扫描逻辑,仅用于序列化;如需字段名映射,需额外解析 reflect.StructTag 并关联列名(需配合 rows.Columns());
- 生产环境应避免高频反射调用,可考虑缓存 reflect.Type 和字段信息以提升性能。
此方案在保持类型安全的前提下,实现了真正的动态结构体绑定,是构建轻量级数据访问层的重要基础能力。

















