kaisawind's blog
  • 关于
  • 所有帖子

Go MySQL 数据转换 - Wed, Sep 8, 2021

Go 语言结构体与 MySQL 之间的数据转换技巧。使用 template 和 reflect 实现 CRUD,使用 reflect 将 rows 转换为结构体。

Go 语言结构体与 MySQL 之间的数据转换技巧。使用 template 和 reflect 实现 CRUD,使用 reflect 将 rows 转换为结构体。

注意: MySQL 8.0 已成为主流版本,本文档可能基于旧版本编写。

概述

Go 语言中的数据库操作通常需要处理结构体和数据库之间的转换。本文介绍如何使用反射和模板实现通用的数据转换。

核心思路:

  • 使用 template 和 reflect 实现 CRUD 操作
  • 使用 reflect 将 rows 转换为结构体
  • 使用 JSON tag 作为数据库字段映射

查询

查询操作最为简单,使用模板生成 SQL 语句。

Go 模板文件

SELECT * FROM {{.table}}
{{- if .query}} WHERE {{.query}} {{end -}}
{{- if .limit -}}
    {{- if ne .limit 0 -}}LIMIT {{.limit}} {{end -}}
{{- end -}}
{{- if .skip -}}
    {{- if ne .skip 0 -}}OFFSET {{.skip}} {{end -}}
{{- end -}}
;

Go 代码

// SQLQuery query sql string
func SQLQuery(table string, limit, skip int64, sort, query string) (sql string) {
	params := map[string]interface{}{
		"table": table,
		"limit": limit,
		"skip":  skip,
		"query": query,
		"sort":  sort,
	}
	tpl, err := template.New("query").Parse(Query)
	if err != nil {
		return
	}
	buf := &bytes.Buffer{}
	err = tpl.Execute(buf, params)
	if err != nil {
		return
	}
	sql = buf.String()
	return
}

插入

使用反射获取 JSON tag 作为 key,取值作为值。使用 insert 语句,将 key 和 value 一一对应。

注意事项:

  • 字符串需要加单引号
  • 数字不需要加单引号
  • 将 map 转换为字符串填充

Go 模板文件

INSERT INTO {{.table}}({{.keys}}) VALUES({{.values}});

Go 代码

// SQLInsert insert sql string
func SQLInsert(table string, in proto.Message) (sql string) {
	refValue := reflect.ValueOf(in)
	if refValue.Kind() == reflect.Ptr {
		refValue = refValue.Elem()
	}
	refType := refValue.Type()
	params := map[string]interface{}{
		"table": table,
	}
	tmp := map[string]string{}
	for i := 0; i < refType.NumField(); i++ {
		ft := refType.Field(i)
		if !ft.IsExported() {
			continue
		}
		fv := refValue.FieldByName(ft.Name)
		tag := ft.Tag.Get("json")
		switch fv.Kind() {
		case reflect.Map:
			v, _ := json.Marshal(fv.Interface())
			tmp[tag] = fmt.Sprintf("'%s'", v)
		case reflect.String:
			tmp[tag] = fmt.Sprintf("'%v'", fv.Interface())
		case reflect.Int32:
			// 注意: Sprintf 会调用 String 函数
			mf := fv.MethodByName("String")
			if mf.IsValid() {
				tmp[tag] = fmt.Sprintf("'%v'", fv.Interface())
			}
		default:
			// 注意: Sprintf 会调用 String 函数
			mf := fv.MethodByName("String")
			if mf.IsValid() {
				tmp[tag] = fmt.Sprintf("'%v'", fv.Interface())
			} else {
				if tag == "created_at" || tag == "updated_at" {
					tmp[tag] = "now()"
				} else {
					tmp[tag] = fmt.Sprintf("%v", fv.Interface())
				}
			}
		}
	}
	var keys []string
	var values []string
	for k, v := range tmp {
		keys = append(keys, k)
		values = append(values, v)
	}
	params["keys"] = strings.Join(keys, ",")
	params["values"] = strings.Join(values, ",")
	tpl, err := template.New("insert").Parse(Insert)
	if err != nil {
		return
	}
	buf := &bytes.Buffer{}
	err = tpl.Execute(buf, params)
	if err != nil {
		return
	}
	sql = buf.String()
	return
}

查询结果转换为结构体

将 scan 出的列与结构体反射出的 JSON tag 一一对应,通过 fv.Addr().Interface() 提取字段指针,使用 rows.Scan 直接对反射出的指针赋值。

Go 代码

// Scan scan rows to message
func Scan(rows *sql.Rows, in proto.Message) (err error) {
	refValue := reflect.ValueOf(in)
	if refValue.Kind() == reflect.Ptr {
		refValue = refValue.Elem()
	}
	cols, err := rows.Columns()
	if err != nil {
		return
	}
	refType := refValue.Type()
	columns := make([]interface{}, len(cols))
	for i := 0; i < refValue.NumField(); i++ {
		ft := refType.Field(i)
		if !ft.IsExported() {
			continue
		}
		fv := refValue.FieldByName(ft.Name)
		tag := ft.Tag.Get("json")
		for j, col := range cols {
			if col == tag {
				columns[j] = fv.Addr().Interface()
			}
		}
	}
	err = rows.Scan(columns...)
	if err != nil {
		logrus.WithError(err).Errorln("rows scan error")
		return
	}
	logrus.Infoln(in)
	return
}

最佳实践

  1. 类型安全:使用反射时要小心类型转换错误
  2. 性能优化:反射操作较慢,考虑缓存反射结果
  3. 错误处理:确保正确处理 SQL 错误和反射错误
  4. 字段映射:使用明确的字段 tag,避免歧义
  5. SQL 注入防护:使用参数化查询,避免 SQL 注入

替代方案

考虑使用成熟的 ORM 框架:

  • GORM: 功能完善的 Go ORM
  • sqlx: 增强的 database/sql
  • ent: Facebook 的实体框架

这些框架提供了更安全、更高效的数据转换方式。


辽ICP备2021007608号 | © 2026 | kaisawind

Facebook Twitter GitHub