Go操作MySQL数据库编程实战:从数据插入到条件查询

发布时间:2026/10/20 21:48:31
Go操作MySQL数据库编程实战:从数据插入到条件查询 1. 项目背景与整体思路1.1 这个实战到底在做什么标题里带着“go-mysql”“数据查询”“输入数据”“实战2”几个词核心其实就一件事用 Go 操作 MySQL把数据写进去再按条件查出来。这套流程我从数据库连接、单条插入、批量写入、事务控制、单行查询、多行遍历、动态条件搜索一路跑下来发现真正卡人的不是 SQL 语法而是 database/sql 里的各种边界情况。本文对应一个代码级的练习项目我给它起的内部代号叫“模拟项目 X”。这个项目的需求很简单一张用户表支持录入新用户、按姓名和年龄搜索用户、批量导入数据。功能不多但刚好能覆盖“输入数据”和“数据查询”两大核心主题。适合正在学 Go 数据库编程的开发者也适合准备写内部管理后台、运维工具、接口聚合服务的同学参考。项目整体不依赖任何 ORM全部基于 Go 标准库 database/sql把最底层的操作逻辑先弄明白后面再上框架心里才有底。这套流程里最值得花时间研究的不是“怎么写 SQL”而是连接生命周期、占位符参数、NULL 值扫描、事务边界和连接池调优。这些内容在官方文档里都有但往往分散在各处没有人告诉你它们会以什么顺序坑你。这篇我按照实操顺序把代码、报错和规避方法一次性整理清楚。1.2 为什么用原生 database/sql 而不是 ORM我在选择方案时对比过两条路线一条是直接用 ORM把结构体和表映射起来增删改查几行代码搞定另一条是用 database/sql 写原生 SQL。初期觉得 ORM 很省事但真跑起来后一旦涉及动态条件、批量插入、事务回滚框架会把细节包得很严实出了问题反而难排查。database/sql 虽然被很多人说“太底层”但它的核心概念其实很少Open、Exec、Query、QueryRow、Begin掌握这几个方法就能覆盖绝大多数场景。它本质是一个统一的 SQL 接口层具体协议交给第三方驱动处理MySQL 就用 go-sql-driver/mysql。这样做的好处是每一次网络往返、每一条 SQL 的走向都透明可见程序做了什么完全在掌控范围内。另一个理由是可迁移性。用 database/sql 写的代码换到 PostgreSQL、SQLite 或者其他支持数据库时业务代码基本不用大改只需要换驱动和 DSN。而如果绑死在某个 ORM 上迁移成本往往会高出很多。2. 环境准备与数据表初始化2.1 Go 环境与 MySQL 驱动安装先确认基础环境Go 1.20 以上版本MySQL 8.0 社区版本地能通过命令行连上 MySQL。项目目录我用的是普通 Go Module 结构提前执行 go mod init 初始化模块。安装 MySQL 驱动只需要一条命令go get github.com/go-sql-driver/mysql导入方式有个细节值得说明。代码里写的是import ( database/sql _ github.com/go-sql-driver/mysql )这里使用匿名导入也就是包名前面的下划线。原因是 database/sql 只定义接口不会自动加载任何第三方驱动。MySQL 驱动在 init 函数里通过 sql.Register 完成注册匿名导入就是为了触发这个注册副作用。如果你只导入而不用它Go 编译器不会报错但如果你用了普通导入又不引用编译直接失败。这个规律在接其他驱动时也适用。2.2 设计一张能覆盖多种场景的业务表项目里要跑通输入数据和数据查询表结构不能太简单也不能太复杂。我最终设计了一张 users 表包含自增主键、用户名、邮箱、年龄、状态、创建时间六个字段CREATE DATABASE IF NOT EXISTS demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE demo; CREATE TABLE users ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL, email VARCHAR(128) NOT NULL, age TINYINT UNSIGNED NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_email (email) ) ENGINEInnoDB;这张表每个字段都有用处id 自增用来测试 LastInsertIdemail 唯一用来验证重复写入时的报错age 允许为 NULL用来练习 Scan 阶段的 NULL 处理status 是常见查询条件created_at 是时间字段用来验证 parseTime 配置。存储引擎选择 InnoDB因为后续要用事务MyISAM 不支持事务一开始就选错的话后面全白做。2.3 基础连接代码与连接池参数初始化数据库连接的初始化代码是整个项目的骨架。我把它封装成一个 initDB 函数在 main 函数里调用package main import ( database/sql fmt log time _ github.com/go-sql-driver/mysql ) var db *sql.DB func initDB() error { dsn : root:你的密码tcp(127.0.0.1:3306)/demo?charsetutf8mb4parseTimeTruelocLocal var err error db, err sql.Open(mysql, dsn) if err ! nil { return err } db.SetMaxOpenConns(20) db.SetMaxIdleConns(10) db.SetConnMaxLifetime(30 * time.Minute) if err db.Ping(); err ! nil { return err } return nil } func main() { if err : initDB(); err ! nil { log.Fatalf(初始化数据库失败: %v, err) } defer db.Close() fmt.Println(数据库连接成功) }DSN 是数据库连接串里面每个参数都有讲究。charsetutf8mb4 解决中文乱码问题以前很多人还在用 utf8但在 MySQL 里 utf8 并不完整支持四字节字符统一用 utf8mb4 最稳妥。parseTimeTrue 的作用是让驱动把 DATETIME、TIMESTAMP 扫描成 time.Time而不是字符串。locLocal 用来指定时区避免插入和查询时间出现 8 小时偏移。这里要强调一个初学者最容易误解的地方sql.Open 并不会立刻创建连接它只是初始化连接池配置真正和数据库建立连接的是 Ping。因此 Open 返回的 error 几乎总是 nil即使 DSN 里的密码是错的也一样必须通过 Ping 才能提前发现。3. 数据输入从单条插入到批量写入3.1 单条记录插入与自增ID获取“输入数据”的第一步是单条插入。这段代码看起来简单但里面藏着 Exec、Query 方法选型的问题func insertUser(username, email string, age int) (int64, error) { result, err : db.Exec( INSERT INTO users (username, email, age) VALUES (?, ?, ?), username, email, age, ) if err ! nil { return 0, err } return result.LastInsertId() }Exec 方法专门用来执行不返回结果集的 SQLINSERT、UPDATE、DELETE 都属于这一类。它返回的 sql.Result 里有两个常用方法LastInsertId 获取自增 IDRowsAffected 获取影响行数。为什么不用 Query 执行 INSERTQuery 会把 SQL 当作可能返回结果集的语句来处理多走一步结果集相关逻辑而且如果没有正确关闭结果集连接状态会被弄乱。实战里我见过有人用 Query 执行 UPDATE然后忘记 Close最后连接池耗尽这就是方法选型的代价。关于 LastInsertId它只在 INSERT 语句成功且表里有 AUTO_INCREMENT 字段时才有意义。如果表是 UUID 主键或者业务主键它返回的数值没有参考价值。另一个注意点是如果执行的是 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE且最终因为唯一键冲突没有插入新行LastInsertId 的行为会不同需要按实际情况判断。3.2 批量写入的三种实现方式单条插入测试通过之后我开始批量导入用户数据。批量写入有三种常见写法执行效率和稳定性差别很大。第一种是循环 Exec代码最简单但是效率最低func batchInsertLoop(users []User) error { for _, u : range users { _, err : db.Exec( INSERT INTO users (username, email, age) VALUES (?, ?, ?), u.Username, u.Email, u.Age, ) if err ! nil { return err } } return nil }这种写法的本质是 N 次网络往返。每条 INSERT 都要经历一次连接获取、SQL 发送、执行、结果返回的过程。小批量数据感觉不到但导入 10000 条时耗时会明显拉长。第二种是拼接多值 SQL一次把所有数据塞进一条语句func batchInsertValues(users []User) error { valueStrings : make([]string, 0, len(users)) valueArgs : make([]interface{}, 0, len(users)*3) for _, u : range users { valueStrings append(valueStrings, (?, ?, ?)) valueArgs append(valueArgs, u.Username, u.Email, u.Age) } sqlStr : INSERT INTO users (username, email, age) VALUES strings.Join(valueStrings, ,) _, err : db.Exec(sqlStr, valueArgs...) return err }优势是一次网络往返就能完成所有插入。代价是 SQL 语句可能非常长受到 max_allowed_packet 参数限制默认值在 64MB 左右。实测中我一次插 500 条没问题但不建议盲目加大尤其是单条记录里还有大文本字段时拼接出来的 SQL 会迅速膨胀。另一个风险是如果一条语句里某条记录触发错误整个批次都会失败定位问题时不容易确定是哪条数据出了问题。第三种是 Prepared Statement 预编译后循环执行func batchInsertStmt(users []User) error { stmt, err : db.Prepare( INSERT INTO users (username, email, age) VALUES (?, ?, ?), ) if err ! nil { return err } defer stmt.Close() for _, u : range users { if _, err : stmt.Exec(u.Username, u.Email, u.Age); err ! nil { return err } } return nil }这条路线是我最终推荐的方案。SQL 在 MySQL 端预编译一次后面每次 Exec 只需要传参数网络开销比循环 Exec 小又避开了多值 SQL 的长度隐患。几千条数据的场景下相比循环 Exec 能明显感觉到速度提升而且代码可读性高错误处理也更自然。如果你要导入的是上万条数据建议把 Prepared Statement 和事务结合起来每 500 到 1000 条提交一次事务避免一次事务持有锁太久。3.3 事务保护下的一致性插入批量导入场景有一个业务上的硬要求要么全部成功要么全部回滚。如果前 999 条插进去了最后 1 条邮箱重复业务上不能留下 999 条半成品。这时必须使用事务。func batchInsertInTx(users []User) error { tx, err : db.Begin() if err ! nil { return err } defer tx.Rollback() stmt, err : tx.Prepare( INSERT INTO users (username, email, age) VALUES (?, ?, ?), ) if err ! nil { return err } defer stmt.Close() for _, u : range users { if _, err : stmt.Exec(u.Username, u.Email, u.Age); err ! nil { return err } } return tx.Commit() }事务操作里有几个关键点必须写清楚。第一事务里的 Prepare 和 db 级别的 Prepare 不共享。db.Prepare 得到的语句句柄不会自动绑定到某个事务上所以事务内必须用 tx.Prepare。第二defer tx.Rollback() 这种写法不会造成二次回滚的问题。如果事务已经 Commit 成功再调用 Rollback 会返回 sql.ErrTxDone但通常我们不会去关心这个返回值它只是提示“事务已经结束”。这样写的好处是中间任何一步出错 return 时连接都会回到正确的状态不会留下未提交的事务。第三Commit 本身也可能失败比如连接断开、锁等待超时。Commit 失败后要立刻返回错误让上层决定重试还是放弃。不要忽略 Commit 的返回值。第四事务要短。事务内不要发起 HTTP 请求、等待用户输入、执行耗时的外部调用。InnoDB 的行锁会一直持有到事务提交或回滚长事务会让大量写请求排队数据库 CPU 都有可能被拖高。3.4 插入数据的字段校验与去重策略输入数据时除了基本的类型检查还要处理“重复数据”的问题。假设邮箱必须在库里唯一业务上有两种写法效果差别很大。第一种是先 SELECT 再 INSERT比如先查邮箱是否存在不存在才插入。这种写法在高并发下有问题两个请求同时查到“不存在”然后同时插入其中一个必然报唯一键冲突错误。它不是原子操作。第二种是直接用 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE把去重逻辑交给数据库唯一索引_, err : db.Exec( INSERT INTO users (username, email, age) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE username VALUES(username), age VALUES(age), username, email, age, )邮箱冲突时这条语句会把原来的 username 和 age 更新成新值看起来像“存在就覆盖”。需要注意 VALUES() 函数在 MySQL 8.0.20 以后被标记为已弃用很多版本还能用但如果你希望彻底避开可以写成INSERT INTO users (username, email, age) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE username ?, age ?后面的参数需要把 username 和 age 再传一遍。在 Go 程序里读取唯一键冲突的报错时可以这样判断if err ! nil { var mysqlErr *mysql.MySQLError if errors.As(err, mysqlErr) mysqlErr.Number 1062 { log.Println(邮箱重复执行更新逻辑) } }这段逻辑依赖 errors.As 拿到驱动层返回的 MySQLError然后用错误码判断。直接字符串匹配错误信息在英文环境能用但如果你改了语言或字符集字符串判对比错误码脆弱得多。4. 数据查询从单行查询到多条件组合4.1 单行查询与对应的错误处理数据查询里最常见的是按主键或唯一条件查单条记录。database/sql 里对应的是 QueryRow Scanvar ( id int username string email string age int ) err : db.QueryRow( SELECT id, username, email, age FROM users WHERE id ?, 1, ).Scan(id, username, email, age) if errors.Is(err, sql.ErrNoRows) { log.Println(记录不存在) } else if err ! nil { log.Fatal(err) }QueryRow 最多返回一行结果Scan 负责把数据库返回的字段按位置填充到变量里。它和普通 Query 最大的区别是错误处理方式如果没查到记录QueryRow 不会直接返回 err而是等到 Scan 时才返回 sql.ErrNoRows。所以判断“数据是否存在”用的是 errors.Is而不是 err ! nil。Scan 是这段代码里最容易翻车的地方。数据库字段的返回顺序固定为 SELECT 后面写的字段顺序所以 Scan 参数的顺序必须和 SELECT 字段顺序一一对应。变量名叫什么不关键关键是位置。4.2 多行查询与结果集遍历多行查询使用的是 db.Query返回一个 rows 句柄。遍历方式和 Scan 类似但多了几个明显的坑rows, err : db.Query( SELECT id, username, email, age FROM users WHERE age ? ORDER BY id DESC LIMIT 20, 25, ) if err ! nil { log.Fatal(err) } defer rows.Close() for rows.Next() { var ( id int username string email string age int ) if err : rows.Scan(id, username, email, age); err ! nil { log.Fatal(err) } fmt.Printf(id%d username%s email%s age%d\n, id, username, email, age) } if err : rows.Err(); err ! nil { log.Fatal(err) }第一坑是忘记 Close。rows 打开时占着数据库连接如果只遍历到一半就 return或者把你完全忘记连接不会自动归还。连接池里连接数量本来就有限泄漏到一定量后新的查询只能排队。第二坑是遍历结束后没有检查 rows.Err()。严格来说遍历过程中可能发生网络中断、数据截断等错误这些错误不会在 Next 里直接暴露而是累积在 rows 内部。必须用 rows.Err() 检查一遍否则可能拿到不完整的结果还以为查询很成功。第三坑是不要在遍历期间发起新的查询。rows 未关闭期间底层连接被占用同一时刻用 db.Query 发起新请求确实会找连接池里的其他连接这在连接池空闲时通常没问题但如果连接池已经见底会出现死锁或长时排队。一个安全的惯例是先把 rows 里的数据全部 Scan 到内存切片Close 之后再处理后续逻辑。4.3 多条件动态搜索的实现业务查询很少是固定一条 SQL更多场景是搜索框里选了多个条件有就加没有就跳过。下面的代码我调试过很多次可以直接当模板用func searchUsers(keyword string, minAge int, status int, limit int) ([]User, error) { baseSQL : SELECT id, username, email, age, status FROM users WHERE 1 1 var args []interface{} if keyword ! { baseSQL AND username LIKE ? args append(args, %keyword%) } if minAge 0 { baseSQL AND age ? args append(args, minAge) } if status 0 { baseSQL AND status ? args append(args, status) } baseSQL ORDER BY id DESC LIMIT ? args append(args, limit) rows, err : db.Query(baseSQL, args...) if err ! nil { return nil, err } defer rows.Close() var result []User for rows.Next() { var u User if err : rows.Scan(u.ID, u.Username, u.Email, u.Age, u.Status); err ! nil { return nil, err } result append(result, u) } return result, rows.Err() }WHERE 1 1 是很多人争议的写法。它在性能上确实没有太大帮助除非你用的优化器极端到连这个恒真条件都要重新评估。但它有巨大的维护优势所有条件都可以统一用 AND 开头不需要单独记录“这是第几个条件要不要加 WHERE”。在条件数量持续变动的查询里这个写法让代码清晰很多。如果追求极致完全可以用一个布尔变量做同样的事但最终可读性不一定比得上 1 1。这里有一个绝对红线所有条件值都必须放入 args 切片再通过占位符传给 Query。不能用 fmt.Sprintf 把字符串拼进 SQL。后面我会专门展开讲。4.4 Scan 阶段常见坑NULL、类型与时间字段users 表的 age 字段允许 NULL。如果某条记录的 age 是 NULLScan 到一个 int 变量会直接报错错误内容是 converting NULL to int is unsupported。因为数据库的 NULL 不是 0它表示“未知”Go 语言的 int 没有表达“未知”的能力。这时必须用 sql.NullInt64var age sql.NullInt64 err : rows.Scan(id, username, email, age) if age.Valid { fmt.Println(年龄:, age.Int64) } else { fmt.Println(年龄未填写) }sql.NullInt64 有两个字段Int64 保存实际数值Valid 表示是否为 NULL。Valid 为 true 时读 Int64为 false 时说明数据库里就是 NULL。类似的还有 sql.NullString、sql.NullFloat64、sql.NullTime。时间字段的坑在 DSN 的 parseTime 参数上。如果 parseTimeFalse驱动会把 DATETIME 返回成字符串Scan 到 time.Time 会报错如果 parseTimeTrue就可以直接用 time.Time 接收。另一个常见异常是时区偏移插入和查出来的时间差了 8 小时这往往不是代码问题而是 DSN 里没有指定 locLocal服务器和客户端时区不一致导致的。5. 实战中提炼的性能与安全细节5.1 Prepared Statement 为何是默认选择前面批量插入时用过 Prepared Statement这里把它的价值归纳一下。第一MySQL 端预编译节省语法解析时间。一条 SQL 第一次 Prepare 时完成编译后面再执行就只是传入参数执行计划可以复用。单条查询收益不明显但批量场景收益很大。第二参数走二进制协议传输。Prepared Statement 的参数和 SQL 结构是分开传输的参数内容永远不会被当作 SQL 语法解析这从原理上杜绝了拼接注入。第三database/sql 内部的连接池会自动管理语句句柄和连接的关系。如果某个连接被回收语句句柄下次使用时会通过重新绑定机制保证可用开发者不需要手工干预。但也要说清楚不是所有情况都必须手动 Prepare。如果只是一次性查询一条记录直接 db.Query 带占位符driver 内部大概率也会走 prepare 路径差别不大。需要主动 Prepare 的核心场景是“一条 SQL 反复执行、参数不同”。5.2 参数占位符与 SQL 注入防护Go 的 database/sql 使用问号 ? 作为占位符这是 MySQL 驱动支持的风格。任何一个从外部进来的值都应该通过占位符传参。有一种特殊情况表名、字段名、排序关键字不能使用占位符。因为参数编译阶段会被当作值而不是标识符你写 ORDER BY ? 时 MySQL 会把参数当成字符串常量解析结果完全不对。这种场景下必须做白名单映射orderMap : map[string]string{ id: id, age: age, time: created_at, } orderCol, ok : orderMap[req.Order] if !ok { orderCol id } orderDir : ASC if req.Desc { orderDir DESC } sql ORDER BY orderCol orderDir排序方向只用三元判断排序字段只从固定映射表里取值。无论前端传进来什么都只能命中预设的选项这是一条很实用的边界安全实践。5.3 连接池参数调优的几条经验初始化代码里的三个连接池参数值得单独拉出来讲清楚。SetMaxOpenConns 是连接池允许打开的最大连接数。不设置时默认不限制高并发瞬间可能把数据库连接数打爆。设置太小则并发请求会排队等待空闲连接。具体值要根据业务并发量来压测不要拍脑袋。SetMaxIdleConns 是连接池保留的空闲连接数。建议大于零否则每个请求都要新建连接连接建立本身有 TCP 握手开销。但也不要设得特别大因为每个空闲连接都占用 MySQL 端内存。SetConnMaxLifetime 是连接的最大存活时间。MySQL 服务端默认 wait_timeout 是 8 小时如果连接被服务器断开而连接池不知道下次拿这条连接执行查询时会报 invalid connection。把它设成 30 分钟或者更短可以让连接池定期换新连接有效规避这个问题。还有一个容易忽视的点SetConnMaxLifetime 是生存时间不代表到点立刻关闭而是在空闲或归还连接时替换。如果你发现某个时间点数据库连接数异常上升先检查这三个参数是不是没设过。6. 常见报错与排查速查表6.1 连接类报错连接问题是最早出现的也很容易识别。我整理了一个速查表格现象常见原因处理方式dial tcp 127.0.0.1:3306: connect: connection refusedMySQL 服务未启动或端口不对检查 MySQL 进程和端口监听状态Access denied for user rootlocalhost用户名密码或权限错误核对账号密码、执行授权并刷新权限dial tcp: i/o timeout网络不通或防火墙拦截检查防火墙规则、安全组、ping 和 telnet 连通性Error 1040: Too many connections连接数达到 MySQL 上限控制应用连接池大小或调大 MySQL 的 max_connectionsDSN 编码问题也经常导致连接失败。如果密码里有 、#、:、/ 这类特殊字符必须做 URL 编码否则 DSN 解析会错位提示奇怪的认证失败。最稳妥的办法是先写一个最小 DSN 测试连接通了再往上加参数。6.2 SQL 执行类报错SQL 执行时报错的原因比较集中基本围绕语法、参数、约束三方面。参数数量不匹配是最常见的。SQL 里写了 3 个 ?但传参时只传了 2 个参数驱动会报参数数量不匹配。排查方法是数清楚占位符数量再数 args 切片长度。动态拼接条件时尤其容易漏掉。唯一键冲突会返回 Error 1062 duplicate entry。如果业务允许覆盖更新改用 ON DUPLICATE KEY UPDATE如果不允许就用 errors.As 抓 MySQLError 错误码做分支处理。表不存在、字段不存在属于开发期问题检查库名、表名、字段名。注意 MySQL 里的标识符在 Linux 下区分大小写表名大小写不一致也会报错。慢插入、锁等待超时在大批量写入时比较明显。如果插入时长时间卡住最后报 lock wait timeout exceeded多半是另一个事务占用行锁没提交。敲 SHOW PROCESSLIST 看当前会话找出持锁的线程。6.3 数据读取类报错读取阶段最常见的错误集中在 Scan。Scan NULL 到基础类型是不允许的。解决方式是字段可空时使用 sql.NullInt64、sql.NullString 等包装类型。字段顺序不匹配也很好排查。SELECT id, username, emailScan 就必须按 id、username、email 的顺序给变量。如果 SELECT 的列在前面Scan 的变量在后面位置错位不会报编译错误但数据会错得离谱。类型不匹配时报错也很直接。比如 VARCHAR 字段 Scan 到 int驱动转换失败。最常见的是时间字段parseTimeFalse 时 Scan 到 time.Time 的类型不对parseTimeTrue 时再 Scan 到 string 也有问题。把 DSN 的 parseTime 参数和字段类型都检查一遍。还有一点容易遇到rows 遍历中再发起新查询可能导致连接池等待如果 MaxOpenConns 设置得小甚至会卡死。需要先收集数据、关闭 rows再执行后续查询。6.4 数据写入后的即时查询异常“INSERT 完立刻 SELECT 却查不到”是高频问题。先检查是否在同一事务里事务中 INSERT 未提交之前其他连接默认看不到变化。如果确定要立刻读到就直接在事务里查询或者拿到 LastInsertId 后开新查询。另一个原因是查询条件错误。比如自增 ID 是数字但程序把 ID 传成字符串可能会因为隐式类型转换查不到数据。还有 LIKE 查询没有处理百分号业务输入里如果包含 % 或 _会被当作通配符导致结果超出预期。这时候需要转义或者限制关键词长度。MySQL 默认的隔离级别是 REPEATABLE READ一个事务内反复查询即使其他事务已提交也看不到新变化。如果业务上要求同一事务读到最新提交数据需要重新评估隔离级别的选择但建议默认保持默认不要轻易改动。7. 写在最后的实操心得所有代码逻辑跑通之后真正影响项目稳定性的往往是那些不起眼的配置细节。第一个体会是 DSN 里的 parseTime 和 loc 必须从一开始就设置。很多项目做了一半才发现时间字段读出来是字符串或者插入时间差 8 个小时。这时候再改代码涉及字段类型、比较逻辑、报表展示改动面会迅速扩大。宁可最开始多花一分钟配置也不要后期补坑。第二个体会是批量导入一定要配合事务和分批提交。我实测过普通配置下单批 500 条用户数据在事务内执行耗时大约几百毫秒2000 条以上时耗时明显上升锁等待概率也会提高。如果导入几十万条一个事务到底是不可取的建议每 500 到 1000 条一个事务分片提交。第三个体会是排查问题时把 SQL 打印出来。我在调试时会在代码里临时加日志把最终 SQL 和 args 打印到控制台然后在 mysql 客户端里手动执行同样的 SQL对比执行计划。很多时候问题出在索引缺失而不是代码逻辑。比如 age、status 这类经常作为查询条件的字段建一个联合索引性能提升立竿见影。第四个体会是database/sql 不是 ORM它只是把数据库连接和结果集暴露给你。用它的最大收获不是减少代码量而是逼你把 SQL、连接、事务这些概念彻底弄明白。后续如果项目继续做第三期我大概率会把重心放在查询性能优化上比如 explain 分析、覆盖索引、大偏移分页的优化。这篇先写到这里希望能给正在写 Go 和 MySQL 实战项目的朋友带来一些实际帮助。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询