本文结合sqlx给出一些操作数据库的示例,并将常见功能封装了函数接口形式。
问题提出
几年前,笔者写了个golang工程,是和数据库打交道的,当时曾经调查过xorm,但并不合适,于是使用较原始的方式实现功能,后来一直沿用。原始方式有一个好处,可以自由组合sql,达到不同的查询目的。
最近又要和数据库打交道,考虑到工程实用性,经研究,决定使用sqlx将常见功能做成内部接口形式。
实践
设计说明
本文使用前面文章生成的结构体作为演示数据表字段。为简单起见,id不设置自增。另外,使用反射手段进行建表、写表、查表等操作。
结构体定义:
type ResultData_t struct {
Id string `json:"id" db:"id"`
Num int `json:"num" db:"num"`
Etype int `json:"etype" db:"etype"`
Timestamp string `json:"timestamp" db:"timestamp"`
MCode string `json:"mCode" db:"mCode"`
MName string `json:"mName" db:"mName"`
Age string `json:"age" db:"age"`
Addr string `json:"addr" db:"addr"`
Mac string `json:"mac" db:"mac"`
Remark string `json:"remark" db:"remark"`
}
变量定义:
var gTableName = "result_data"
var dbstr = "file:./database/result_data.db3?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=on"
引入第三方库
库如下:
"github.com/jmoiron/sqlx"
_ "modernc.org/sqlite"
要说明的是,本文采用"modernc.org/sqlite",这个库实现了sqlite3,但不使用cgo,因此十分合适跨平台,注意,该库使用的数据库驱动名为sqlite,而不是sqlite3。
笔者近年使用go作为主力语言,一个工程,如果不涉及底层库,都会优先考虑跨平台性。比如,同一套代码,在windows开发调试,再交叉编译成linux或arm版本,基本不用开虚拟机了。
初始化数据库
代码如下,代码会自动创建database/result_data.db3文件,默认表为result_data。
type ResultWriter struct {
db *sqlx.DB
}
func NewDB3Writer() *ResultWriter {
return &ResultWriter{}
}
func getDB3File(dbname string) string {
if !strings.HasPrefix(dbname, "file:") {
return ""
}
filePart := dbname[5:] // 移除 "file:"
parts := strings.Split(filePart, "?")
if len(parts) == 0 {
return ""
}
return com.GetFileDir(parts[0])
}
func (w *ResultWriter) CreateResultWriter(dbname string) (err error) {
// SQLite 连接字符串(包含并发优化参数)
// "file:./database/result_data.db3?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=on"
// 确保目录存在
filePath := getDB3File(dbname)
com.MkDir(filePath)
dsn := dbname
w.db, err = sqlx.Connect("sqlite", dsn)
if err != nil {
return err
}
w.db.SetMaxOpenConns(1) // SQLite 推荐单个连接
w.db.SetMaxIdleConns(1)
w.db.SetConnMaxLifetime(time.Hour)
err = w.initTables()
if err != nil {
klog.Println("创建内部数据库失败: ", err.Error())
}
return nil
}
func (w *ResultWriter) initTables() error {
// w.initTableSimple()
// 使用反射创建表
err := dbutil.CreateTableFromStruct(w.db, gTableName, ResultData_t{}, "id")
if err != nil {
return fmt.Errorf("创建 result_data 表失败: %v", err)
}
return nil
}
建表
在初始化的同时创建了数据表。可以调用initTableSimple,也可以调用CreateTableFromStruct。其中initTableSimple为直接指定字段创建表,实现函数如下:
// 直接指定字段创建表
func (w *ResultWriter) initTableSimple() error {
createTableSQL := `
CREATE TABLE IF NOT EXISTS result_data (
id TEXT PRIMARY KEY,
num INTEGER NOT NULL,
etype INTEGER NOT NULL,
timestamp TEXT NOT NULL,
mCode TEXT NOT NULL,
mName TEXT NOT NULL,
age TEXT NOT NULL,
addr TEXT NOT NULL,
mac TEXT NOT NULL,
remark TEXT
)
`
_, err := w.db.Exec(createTableSQL)
if err != nil {
return err
}
// 创建常用索引
indexes := []string{
"CREATE INDEX IF NOT EXISTS idx_result_data_timestamp ON result_data(timestamp)",
}
for _, indexSQL := range indexes {
_, err = w.db.Exec(indexSQL)
if err != nil {
return fmt.Errorf("创建索引失败: %v", err)
}
}
return nil
}
CreateTableFromStruct则使用结构体建表,通用性强一些,实现如下:
func CreateTableFromStruct(db *sqlx.DB, tableName string, model interface{}, primaryKey string, indices ...string) error {
if db == nil {
return fmt.Errorf("数据库连接为空")
}
var t reflect.Type
if model == nil {
return fmt.Errorf("model 参数不能为 nil")
}
v := reflect.ValueOf(model)
if v.Kind() == reflect.Ptr {
v = v.Elem()
t = v.Type()
} else {
t = reflect.TypeOf(model)
}
if t.Kind() != reflect.Struct {
return fmt.Errorf("model 必须是结构体")
}
var columns []string
var hasPrimaryKey bool
// 遍历结构体字段
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// 获取字段名(优先使用 db tag,其次使用字段名)
columnName := field.Tag.Get("db")
if columnName == "" || columnName == "-" {
// 如果 db tag 为空或为 "-",跳过该字段
continue
}
// 获取字段类型
fieldType := field.Type
// 处理指针类型
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
}
// 根据字段类型确定 SQL 类型
var sqlType string
switch fieldType.Kind() {
case reflect.String:
sqlType = "TEXT"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
sqlType = "INTEGER"
case reflect.Float32, reflect.Float64:
sqlType = "REAL"
case reflect.Bool:
sqlType = "INTEGER" // SQLite 用 0/1 表示布尔值
case reflect.Struct:
// 处理 time.Time 类型
if fieldType.String() == "time.Time" {
sqlType = "TEXT"
} else {
sqlType = "TEXT" // 其他结构体序列化为 JSON 存储
}
case reflect.Slice, reflect.Array, reflect.Map:
// 数组/切片/映射序列化为 JSON 存储
sqlType = "TEXT"
default:
// 默认使用 TEXT 类型
sqlType = "TEXT"
}
// 构建列定义
columnDef := fmt.Sprintf("%s %s", columnName, sqlType)
// 检查是否是主键
// isPrimaryKey := false
if primaryKey != "" {
// 使用指定的主键
if columnName == primaryKey {
columnDef += " PRIMARY KEY"
// isPrimaryKey = true
hasPrimaryKey = true
}
} else {
// 自动判断:如果字段名为 "id" 且没有显式指定主键
if columnName == "id" && !hasPrimaryKey {
columnDef += " PRIMARY KEY AUTOINCREMENT"
// isPrimaryKey = true
hasPrimaryKey = true
}
}
// 检查 NOT NULL 约束
notNullTag := field.Tag.Get("dbNotNull")
if notNullTag == "true" || notNullTag == "1" {
columnDef += " NOT NULL"
}
// 检查 UNIQUE 约束
uniqueTag := field.Tag.Get("dbUnique")
if uniqueTag == "true" || uniqueTag == "1" {
columnDef += " UNIQUE"
}
// 添加 DEFAULT 值
defaultTag := field.Tag.Get("dbDefault")
if defaultTag != "" {
columnDef += " DEFAULT " + defaultTag
}
columns = append(columns, columnDef)
}
if len(columns) == 0 {
return fmt.Errorf("没有找到有效的字段")
}
// 建表语句
createTableSQL := fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (
%s
)
`, tableName, strings.Join(columns, ",\n\t\t"))
// 执行
_, err := db.Exec(createTableSQL)
if err != nil {
return fmt.Errorf("创建表 %s 失败: %v", tableName, err)
}
// fmt.Printf("debug 创建表语句:[%v]\n", createTableSQL)
// 创建索引
for _, field := range indices {
if field != "" {
indexSQL := fmt.Sprintf("CREATE INDEX IF NOT EXISTS idx_%s_%s ON %s(%s)",
tableName, field, tableName, field)
_, err = db.Exec(indexSQL)
if err != nil {
return fmt.Errorf("创建索引 %s 失败: %v", field, err)
}
}
}
return nil
}
写表
func InsertTable[T any](db *sqlx.DB, tableName string, model []T, skipid ...string) (int64, string, error) {
if len(model) == 0 {
return 0, "", nil
}
// 获取第一个记录的结构信息
t := reflect.TypeOf(model[0])
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
// 可指定过滤的字段名,有的库可能会使用自增的id,因此要过滤
getskip := func(id string) bool {
for i := range skipid {
if skipid[i] == id {
return true
}
}
return false
}
// 收集所有 db tag(排除 id 字段)
var fieldNames []string
var dbTags []string
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
dbTag := field.Tag.Get("db")
if dbTag != "" && dbTag != "-" && !getskip(dbTag) {
fieldNames = append(fieldNames, field.Name)
dbTags = append(dbTags, dbTag)
}
}
// 构建带命名参数的 SQL
var namedParams []string
for _, tag := range dbTags {
namedParams = append(namedParams, ":"+tag)
}
query := fmt.Sprintf(`
INSERT INTO %s (%s)
VALUES (%s)
`, tableName,
strings.Join(dbTags, ", "),
strings.Join(namedParams, ", "))
// 执行批量插入
result, err := db.NamedExec(query, model)
if err != nil {
return 0, query, fmt.Errorf("表 %s: %v", tableName, err)
}
// fmt.Printf("debug insert: [%v]\n", query)
rows, err := result.RowsAffected()
if err != nil {
return int64(len(model)), query, nil // 有些数据库可能不支持
}
return rows, query, nil
}
查表
查表同样使用了反射的技术,主要调用genSelectItem将结构体各字段组成select语句。
比如:
SELECT id, num, etype, timestamp, mCode, mName, age, addr, mac, remark FROM result_data WHERE id = 'id_8'
SELECT id, num, etype, timestamp, mCode, mName, age, addr, mac, remark FROM result_data
事实上,在结构体ResultData_t定义时,就使用了db指定了各字段的名称,如下:
Id string `json:"id" db:"id"`
Num int `json:"num" db:"num"`
这也是genSelectItem可处理任意结构体的原因,该函数见下文的实现。
查表分2类,一是单个查询QueryOne,调用者知道只有一个结果时,可使用该函数。如果查询多个,则使用QueryTable函数。
这2个函数实现如下:
func QueryOne[T any](db *sqlx.DB, tableName string, model T, where string, args ...interface{}) (*T, error) {
sqlstr := genSelectItem(model, tableName, where)
var result T
err := db.Get(&result, sqlstr, args...)
if err != nil {
return nil, fmt.Errorf("表 %s: %v", tableName, err)
}
return &result, nil
}
func QueryTable[T any](db *sqlx.DB, tableName string, model T, where string) ([]T, string, error) {
mysqlstr := fmt.Sprintf("SELECT * FROM %v %v", tableName, where)
sqlstr := genSelectItem(model, tableName, where)
var results []T
err := db.Select(&results, sqlstr)
if err != nil {
return nil, mysqlstr, fmt.Errorf("表 %s: %v", tableName, err)
}
return results, mysqlstr, nil
}
接口工具函数集
本文所用的工具函数集如下:
package db
import (
"fmt"
"reflect"
"strings"
"github.com/jmoiron/sqlx"
)
func CreateTableFromStruct(db *sqlx.DB, tableName string, model interface{}, primaryKey string, indices ...string) error {
if db == nil {
return fmt.Errorf("数据库连接为空")
}
var t reflect.Type
if model == nil {
return fmt.Errorf("model 参数不能为 nil")
}
v := reflect.ValueOf(model)
if v.Kind() == reflect.Ptr {
v = v.Elem()
t = v.Type()
} else {
t = reflect.TypeOf(model)
}
if t.Kind() != reflect.Struct {
return fmt.Errorf("model 必须是结构体")
}
var columns []string
var hasPrimaryKey bool
// 遍历结构体字段
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// 获取字段名(优先使用 db tag,其次使用字段名)
columnName := field.Tag.Get("db")
if columnName == "" || columnName == "-" {
// 如果 db tag 为空或为 "-",跳过该字段
continue
}
// 获取字段类型
fieldType := field.Type
// 处理指针类型
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
}
// 根据字段类型确定 SQL 类型
var sqlType string
switch fieldType.Kind() {
case reflect.String:
sqlType = "TEXT"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
sqlType = "INTEGER"
case reflect.Float32, reflect.Float64:
sqlType = "REAL"
case reflect.Bool:
sqlType = "INTEGER" // SQLite 用 0/1 表示布尔值
case reflect.Struct:
// 处理 time.Time 类型
if fieldType.String() == "time.Time" {
sqlType = "TEXT"
} else {
sqlType = "TEXT" // 其他结构体序列化为 JSON 存储
}
case reflect.Slice, reflect.Array, reflect.Map:
// 数组/切片/映射序列化为 JSON 存储
sqlType = "TEXT"
default:
// 默认使用 TEXT 类型
sqlType = "TEXT"
}
// 构建列定义
columnDef := fmt.Sprintf("%s %s", columnName, sqlType)
// 检查是否是主键
// isPrimaryKey := false
if primaryKey != "" {
// 使用指定的主键
if columnName == primaryKey {
columnDef += " PRIMARY KEY"
// isPrimaryKey = true
hasPrimaryKey = true
}
} else {
// 自动判断:如果字段名为 "id" 且没有显式指定主键
if columnName == "id" && !hasPrimaryKey {
columnDef += " PRIMARY KEY AUTOINCREMENT"
// isPrimaryKey = true
hasPrimaryKey = true
}
}
// 检查 NOT NULL 约束
notNullTag := field.Tag.Get("dbNotNull")
if notNullTag == "true" || notNullTag == "1" {
columnDef += " NOT NULL"
}
// 检查 UNIQUE 约束
uniqueTag := field.Tag.Get("dbUnique")
if uniqueTag == "true" || uniqueTag == "1" {
columnDef += " UNIQUE"
}
// 添加 DEFAULT 值
defaultTag := field.Tag.Get("dbDefault")
if defaultTag != "" {
columnDef += " DEFAULT " + defaultTag
}
columns = append(columns, columnDef)
}
if len(columns) == 0 {
return fmt.Errorf("没有找到有效的字段")
}
// 建表语句
createTableSQL := fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (
%s
)
`, tableName, strings.Join(columns, ",\n\t\t"))
// 执行
_, err := db.Exec(createTableSQL)
if err != nil {
return fmt.Errorf("创建表 %s 失败: %v", tableName, err)
}
// fmt.Printf("debug 创建表语句:[%v]\n", createTableSQL)
// 创建索引
for _, field := range indices {
if field != "" {
indexSQL := fmt.Sprintf("CREATE INDEX IF NOT EXISTS idx_%s_%s ON %s(%s)",
tableName, field, tableName, field)
_, err = db.Exec(indexSQL)
if err != nil {
return fmt.Errorf("创建索引 %s 失败: %v", field, err)
}
}
}
return nil
}
// 自动从结构体生成SELECT字段列表
func genSelectItem(model interface{}, tableName string, where1 ...string) string {
t := reflect.TypeOf(model)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
var fields []string
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
dbTag := field.Tag.Get("db")
if dbTag != "" && dbTag != "-" {
fields = append(fields, dbTag)
}
}
where := ""
if len(where1) > 0 {
where = where1[0]
}
return fmt.Sprintf("SELECT %s FROM %s %s", strings.Join(fields, ", "), tableName, where)
}
///////////////////////////
func QueryOne[T any](db *sqlx.DB, tableName string, model T, where string, args ...interface{}) (*T, error) {
sqlstr := genSelectItem(model, tableName, where)
var result T
err := db.Get(&result, sqlstr, args...)
if err != nil {
// if err == sql.ErrNoRows {
// return nil, err // 没有找到记录,返回 nil
// }
return nil, fmt.Errorf("表 %s: %v", tableName, err)
}
return &result, nil
}
func QueryTable[T any](db *sqlx.DB, tableName string, model T, where string) ([]T, string, error) {
// start := time.Now()
// defer func() {
// stats := db.Stats()
// fmt.Printf("查询耗时: %v, 等待连接数量: %d\n",
// time.Since(start), stats.WaitCount)
// }()
mysqlstr := fmt.Sprintf("SELECT * FROM %v %v", tableName, where)
sqlstr := genSelectItem(model, tableName, where)
var results []T
err := db.Select(&results, sqlstr)
if err != nil {
return nil, mysqlstr, fmt.Errorf("表 %s: %v", tableName, err)
}
return results, mysqlstr, nil
}
///////////////////
// BatchInsert 通用批量插入函数
func InsertTable[T any](db *sqlx.DB, tableName string, model []T, skipid ...string) (int64, string, error) {
if len(model) == 0 {
return 0, "", nil
}
// 获取第一个记录的结构信息
t := reflect.TypeOf(model[0])
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
// 可指定过滤的字段名,有的库可能会使用自增的id,因此要过滤
getskip := func(id string) bool {
for i := range skipid {
if skipid[i] == id {
return true
}
}
return false
}
// 收集所有 db tag(排除 id 字段)
var fieldNames []string
var dbTags []string
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
dbTag := field.Tag.Get("db")
if dbTag != "" && dbTag != "-" && !getskip(dbTag) {
fieldNames = append(fieldNames, field.Name)
dbTags = append(dbTags, dbTag)
}
}
// 构建带命名参数的 SQL
var namedParams []string
for _, tag := range dbTags {
namedParams = append(namedParams, ":"+tag)
}
query := fmt.Sprintf(`
INSERT INTO %s (%s)
VALUES (%s)
`, tableName,
strings.Join(dbTags, ", "),
strings.Join(namedParams, ", "))
// 执行批量插入
result, err := db.NamedExec(query, model)
if err != nil {
return 0, query, fmt.Errorf("表 %s: %v", tableName, err)
}
// fmt.Printf("debug insert: [%v]\n", query)
rows, err := result.RowsAffected()
if err != nil {
return int64(len(model)), query, nil // 有些数据库可能不支持
}
return rows, query, nil
}
// ////////////////
// QueryCount 查询记录数量
func QueryCount(db *sqlx.DB, tableName, where string, args ...interface{}) (int64, error) {
sqlstr := fmt.Sprintf("SELECT COUNT(*) FROM %s", tableName)
if where != "" {
sqlstr += " WHERE " + where
}
var count int64
err := db.Get(&count, sqlstr, args...)
if err != nil {
return 0, fmt.Errorf("表 %s: %v", tableName, err)
}
return count, nil
}
测试
建表打印信息:
debug 创建表语句:[
CREATE TABLE IF NOT EXISTS result_data (
id TEXT PRIMARY KEY,
num INTEGER,
etype INTEGER,
timestamp TEXT,
mCode TEXT,
mName TEXT,
age TEXT,
addr TEXT,
mac TEXT,
remark TEXT
)
]
插入表打印信息:
debug insert: [
INSERT INTO result_data (id, num, etype, timestamp, mCode, mName, age, addr, mac, remark)
VALUES (:id, :num, :etype, :timestamp, :mCode, :mName, :age, :addr, :mac, :remark)
]
数据表内容:

查表打印信息:
[2025-12-30 20:29:23 866] [INFO] query result: id_8 2025-12-30T20:29:23
[2025-12-30 20:29:23 867] [INFO] query results:
[2025-12-30 20:29:23 868] [INFO] id_0 2025-12-30T20:29:23
[2025-12-30 20:29:23 868] [INFO] id_1 2025-12-30T20:29:23
[2025-12-30 20:29:23 868] [INFO] id_2 2025-12-30T20:29:23
[2025-12-30 20:29:23 869] [INFO] id_3 2025-12-30T20:29:23
[2025-12-30 20:29:23 869] [INFO] id_4 2025-12-30T20:29:23
[2025-12-30 20:29:23 869] [INFO] id_5 2025-12-30T20:29:23
[2025-12-30 20:29:23 870] [INFO] id_6 2025-12-30T20:29:23
[2025-12-30 20:29:23 871] [INFO] id_7 2025-12-30T20:29:23
[2025-12-30 20:29:23 871] [INFO] id_8 2025-12-30T20:29:23
有插入数据的写表打印信息:
[2025-12-30 20:30:16 233] [INFO] insert sqlite failed: 表 result_data: constraint failed: UNIQUE constraint failed: result_data.id (1555)
[2025-12-30 20:30:16 234] [INFO] query result: id_8 2025-12-30T20:29:23
[2025-12-30 20:30:16 234] [INFO] query results:
[2025-12-30 20:30:16 235] [INFO] id_0 2025-12-30T20:29:23
[2025-12-30 20:30:16 236] [INFO] id_1 2025-12-30T20:29:23
[2025-12-30 20:30:16 236] [INFO] id_2 2025-12-30T20:29:23
[2025-12-30 20:30:16 236] [INFO] id_3 2025-12-30T20:29:23
[2025-12-30 20:30:16 237] [INFO] id_4 2025-12-30T20:29:23
[2025-12-30 20:30:16 237] [INFO] id_5 2025-12-30T20:29:23
[2025-12-30 20:30:16 237] [INFO] id_6 2025-12-30T20:29:23
[2025-12-30 20:30:16 238] [INFO] id_7 2025-12-30T20:29:23
[2025-12-30 20:30:16 238] [INFO] id_8 2025-12-30T20:29:23
小结
本文通过示例工程展示了sqlx操作sqlite数据库的方法,理论上只要数据库驱动合适,sqlx可以对任意数据库进行操作,在代码统一了方法,便于维护。