在 Go 项目开发中,操作数据库是必备核心技能 。很多新手一上来就用 GORM、XORM 等 ORM 框架,但掌握原生 SQL 操作才是根基 ------ 性能更高、可控性更强、适配所有复杂查询。
Go 官方标准库 database/sql 提供了统一的数据库操作接口,搭配对应数据库驱动(如 MySQL、PostgreSQL),即可完成增删改查、事务、预处理、连接池等所有数据库操作。
本文全程使用 Go 原生标准库,无任何第三方 ORM 依赖,包含:环境搭建、增删改查、预处理语句、事务、连接池配置、注意事项、避坑指南
一、前置知识与环境准备
1.1 核心标准库
database/sql:Go 官方通用数据库接口,定义所有操作规范。- 数据库驱动 :具体数据库实现(如
github.com/go-sql-driver/mysql)。
1.2 支持的数据库
- MySQL / MariaDB
- PostgreSQL
- SQLite
- SQL Server
- 其他兼容 SQL 标准的数据库
1.3 安装 MySQL 驱动
go get github.com/go-sql-driver/mysql
1.4 测试数据表
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(32) NOT NULL,
`age` int NOT NULL,
`email` varchar(64) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
二、连接数据库(初始化连接池)
理论知识点
sql.Open():不会建立真实连接,仅初始化驱动配置。db.Ping():真正建立连接并校验连通性。*sql.DB:线程安全,内部自带连接池,全局单例即可。- 必须设置连接池参数,防止连接泄漏。
代码示例
package main
import (
"database/sql"
"fmt"
"time"
_ "github.com/go-sql-driver/mysql"
)
// 全局数据库对象(单例)
var db *sql.DB
// 初始化数据库
func initDB() (err error) {
// DSN 格式:用户名:密码@tcp(IP:端口)/数据库名?charset=utf8mb4&parseTime=True
dsn := "root:your-password@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
// 打开数据库(仅初始化,不建立连接)
db, err = sql.Open("mysql", dsn)
if err != nil {
return err
}
// 真正建立连接并校验
err = db.Ping()
if err != nil {
return err
}
// 设置连接池参数(非常重要)
db.SetMaxOpenConns(10) // 最大打开连接数
db.SetMaxIdleConns(5) // 最大空闲连接数
db.SetConnMaxLifetime(time.Minute * 3) // 连接最大生命周期
db.SetConnMaxIdleTime(time.Minute * 1) // 空闲最大时间
fmt.Println("数据库连接成功!")
return nil
}
func main() {
if err := initDB(); err != nil {
fmt.Printf("连接失败:%v\n", err)
return
}
// 延迟关闭数据库
defer db.Close()
}
注意事项
_导入驱动:仅执行驱动初始化,不直接使用包内函数。parseTime=True必须加,否则时间类型无法映射。*sql.DB是连接池,不要每次操作都 Open/Close。- 必须设置连接池参数,否则会导致连接数过多崩溃。
三、增删改查(CRUD)全操作
3.1 插入数据(Create)
代码示例
// 添加用户
func addUser(name string, age int, email string) (int64, error) {
// SQL 语句
sqlStr := "INSERT INTO user(name, age, email) VALUES(?, ?, ?)"
// 执行 SQL
result, err := db.Exec(sqlStr, name, age, email)
if err != nil {
return 0, err
}
// 获取自增 ID
return result.LastInsertId()
}
3.2 查询单条数据(Retrieve One)
代码示例
// User 结构体
type User struct {
ID int
Name string
Age int
Email string
}
// 根据 ID 查询单个用户
func getUserByID(id int) (*User, error) {
sqlStr := "SELECT id, name, age, email FROM user WHERE id = ?"
// 查询单行
row := db.QueryRow(sqlStr, id)
// 定义变量接收数据
var u User
// 扫描数据(必须和查询字段顺序一致)
err := row.Scan(&u.ID, &u.Name, &u.Age, &u.Email)
if err != nil {
return nil, err
}
return &u, nil
}
3.3 查询多条数据(Retrieve List)
代码示例
// 查询所有用户
func getUserList() ([]*User, error) {
sqlStr := "SELECT id, name, age, email FROM user"
// 查询多行
rows, err := db.Query(sqlStr)
if err != nil {
return nil, err
}
// 延迟关闭行对象(非常重要)
defer rows.Close()
// 定义结果切片
var userList []*User
// 循环遍历结果
for rows.Next() {
var u User
// 扫描
err := rows.Scan(&u.ID, &u.Name, &u.Age, &u.Email)
if err != nil {
return nil, err
}
userList = append(userList, &u)
}
// 返回遍历错误
return userList, rows.Err()
}
3.4 更新数据(Update)
代码示例
// 更新用户年龄
func updateUserAge(id, age int) (int64, error) {
sqlStr := "UPDATE user SET age = ? WHERE id = ?"
result, err := db.Exec(sqlStr, age, id)
if err != nil {
return 0, err
}
// 返回受影响行数
return result.RowsAffected()
}
3.5 删除数据(Delete)
代码示例
// 删除用户
func deleteUser(id int) (int64, error) {
sqlStr := "DELETE FROM user WHERE id = ?"
result, err := db.Exec(sqlStr, id)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
四、预处理语句(Prepared Statement)
理论知识点
- 预编译 SQL,防止 SQL 注入。
- 多次执行相同 SQL 时,性能更高。
- 服务器只编译一次,多次执行。
代码示例
// 预处理插入
func prepareAddUser(name string, age int, email string) (int64, error) {
sqlStr := "INSERT INTO user(name, age, email) VALUES(?, ?, ?)"
// 预处理
stmt, err := db.Prepare(sqlStr)
if err != nil {
return 0, err
}
defer stmt.Close()
// 执行
result, err := stmt.Exec(name, age, email)
if err != nil {
return 0, err
}
return result.LastInsertId()
}
五、数据库事务(Transaction)
理论知识点
- 原子性:要么全部成功,要么全部回滚。
- 核心方法:
Begin()、Commit()、Rollback()。 - 事务中必须使用
tx.Exec,不能用db.Exec。
代码示例
// 事务示例:同时添加两个用户
func transactionDemo() error {
// 开启事务
tx, err := db.Begin()
if err != nil {
return err
}
// 延迟处理:出错回滚
defer func() {
if r := recover(); r != nil {
_ = tx.Rollback()
fmt.Println("事务回滚:", r)
}
}()
// 执行 SQL1
sql1 := "INSERT INTO user(name, age) VALUES(?, ?)"
_, err = tx.Exec(sql1, "事务用户1", 20)
if err != nil {
tx.Rollback()
return err
}
// 执行 SQL2
sql2 := "INSERT INTO user(name, age) VALUES(?, ?)"
_, err = tx.Exec(sql2, "事务用户2", 21)
if err != nil {
tx.Rollback()
return err
}
// 提交事务
return tx.Commit()
}
六、空值处理(NULL 字段)
理论知识点
数据库字段为 NULL 时,直接用基础类型接收会报错,必须使用:
sql.NullStringsql.NullInt64sql.NullBoolsql.NullFloat64
代码示例
// 处理 NULL 字段
func getUserWithNullEmail(id int) (*User, error) {
sqlStr := "SELECT id, name, age, email FROM user WHERE id = ?"
row := db.QueryRow(sqlStr, id)
var u User
// email 可能为 NULL
var email sql.NullString
err := row.Scan(&u.ID, &u.Name, &u.Age, &email)
if err != nil {
return nil, err
}
// 判断是否为 NULL
if email.Valid {
u.Email = email.String
} else {
u.Email = "无邮箱"
}
return &u, nil
}
七、核心 API 总结
| 方法 | 作用 |
|---|---|
db.Exec() |
执行增删改,返回受影响行数 |
db.QueryRow() |
查询单行数据 |
db.Query() |
查询多行数据 |
db.Prepare() |
创建预处理语句 |
db.Begin() |
开启事务 |
tx.Commit() |
提交事务 |
tx.Rollback() |
回滚事务 |
rows.Scan() |
将结果扫描到变量 |
rows.Close() |
关闭结果集(必须) |
八、避坑指南(生产环境必看)
- 禁止字符串拼接 SQL ,必须使用
?占位符,防止 SQL 注入。 rows.Close()必须 defer,否则连接泄漏。*sql.DB全局单例,不要频繁 Open/Close。- 必须设置连接池参数,否则高并发下连接耗尽。
- 时间字段必须加
parseTime=True。 - NULL 值必须用
sql.NullXXX接收。 - 事务中所有操作必须使用
tx对象,不能用db。 - 上线前必须用
go vet检查未关闭的资源。
九、完整 main 函数测试
func main() {
// 1. 初始化数据库
if err := initDB(); err != nil {
fmt.Printf("连接失败:%v\n", err)
return
}
defer db.Close()
// 2. 添加用户
id, _ := addUser("张三", 18, "zhangsan@qq.com")
fmt.Println("新增ID:", id)
// 3. 查询单个
user, _ := getUserByID(1)
fmt.Println("单个用户:", user)
// 4. 查询列表
list, _ := getUserList()
fmt.Println("用户列表:", list)
// 5. 更新
rows, _ := updateUserAge(1, 20)
fmt.Println("更新行数:", rows)
// 6. 删除
delRows, _ := deleteUser(1)
fmt.Println("删除行数:", delRows)
// 7. 事务
_ = transactionDemo()
fmt.Println("事务执行完成")
}
十、高频面试题
-
sql.Open()会建立连接吗? 答:不会,仅初始化配置,Ping()才会真正建立连接。 -
*sql.DB是连接吗? 答:不是,是连接池,线程安全,全局单例使用。 -
为什么要关闭
rows? 答:不关闭会占用数据库连接,导致连接泄漏。 -
**预处理语句的好处?**答:防 SQL 注入、提升多次执行性能。
-
**Go 原生 SQL 对比 ORM 优势?**答:性能更高、可控性强、无学习成本、支持复杂 SQL。
知识图谱(文字版)
Go 原生 SQL 操作数据库
├── 标准库:database/sql + 数据库驱动
├── 核心对象:*sql.DB(连接池)
├── 基础操作:增删改查
├── 高级特性:预处理、事务、NULL处理
├── 性能优化:连接池配置
├── 安全规范:防SQL注入、连接泄漏
└── 避坑指南
十一、Go 原生 SQL + MySQL 生产级项目模板(可直接上线)
11.1 前言
前面讲解了 Go 原生 database/sql 操作 MySQL 的基础用法,本章节给出可用于生产环境的完整项目模板 ,包含:配置分离、数据库连接池封装、CRUD 通用封装、事务封装、NULL 空值处理、连接泄漏防护、全局单例、日志打印、错误统一处理。
全程无 ORM、无第三方框架,纯原生标准库,适配企业项目规范,可直接用于毕业设计、后端项目开发。
11.2 项目目录结构
gosql-demo/
├── config/
│ └── db.go # 数据库配置
├── dao/
│ ├── user.go # 用户表 CRUD
│ └── common.go # 通用数据库方法封装
├── model/
│ └── user.go # 结构体模型
├── db/
│ └── mysql.go # 数据库初始化、连接池、事务
├── main.go # 入口测试
└── go.mod
11.3 go.mod 依赖
module gosql-demo
go 1.21
require github.com/go-sql-driver/mysql v1.7.0
安装驱动
go get github.com/go-sql-driver/mysql
11.4 config/db.go 数据库配置(解耦配置)
package config
import "time"
// MySQLConfig MySQL配置结构体
type MySQLConfig struct {
User string
Passwd string
Host string
Port string
DBName string
Charset string
ParseTme bool
Loc string
// 连接池配置
MaxOpenConns int // 最大打开连接
MaxIdleConns int // 最大空闲连接
ConnMaxLifetime time.Duration // 连接最大生命周期
ConnMaxIdleTime time.Duration // 空闲连接最大时间
}
// DefaultMySQLConfig 默认配置
func DefaultMySQLConfig() *MySQLConfig {
return &MySQLConfig{
User: "root",
Passwd: "root",
Host: "127.0.0.1",
Port: "3306",
DBName: "testdb",
Charset: "utf8mb4",
ParseTme: true,
Loc: "Local",
MaxOpenConns: 20,
MaxIdleConns: 10,
ConnMaxLifetime: 3 * time.Minute,
ConnMaxIdleTime: 1 * time.Minute,
}
}
// DSN 拼接DSN连接串
func (c *MySQLConfig) DSN() string {
return c.User + ":" + c.Passwd + "@tcp(" + c.Host + ":" + c.Port + ")/" +
c.DBName + "?charset=" + c.Charset + "&parseTime=" + bool2Str(c.ParseTme) + "&loc=" + c.Loc
}
func bool2Str(b bool) string {
if b {
return "true"
}
return "false"
}
11.5 model/user.go 模型定义(含 NULL 空值兼容)
package model
import "database/sql"
// User 用户表模型
type User struct {
ID int `db:"id"`
Name string `db:"name"`
Age int `db:"age"`
Email sql.NullString `db:"email"` // 允许为NULL
}
// GetEmail 处理NULL,返回默认值
func (u *User) GetEmail() string {
if u.Email.Valid {
return u.Email.String
}
return ""
}
11.6 db/mysql.go 数据库底层封装(全局单例、连接池、事务)
package db
import (
"database/sql"
"fmt"
"gosql-demo/config"
"log"
_ "github.com/go-sql-driver/mysql"
)
// DB 全局数据库连接池单例
var DB *sql.DB
// InitMySQL 初始化数据库
func InitMySQL(cfg *config.MySQLConfig) error {
var err error
DB, err = sql.Open("mysql", cfg.DSN())
if err != nil {
return fmt.Errorf("open mysql failed: %w", err)
}
// 校验连通性
if err = DB.Ping(); err != nil {
return fmt.Errorf("ping mysql failed: %w", err)
}
// 设置连接池
DB.SetMaxOpenConns(cfg.MaxOpenConns)
DB.SetMaxIdleConns(cfg.MaxIdleConns)
DB.SetConnMaxLifetime(cfg.ConnMaxLifetime)
DB.SetConnMaxIdleTime(cfg.ConnMaxIdleTime)
log.Println("MySQL 连接池初始化成功")
return nil
}
// Close 关闭数据库
func Close() {
if DB != nil {
_ = DB.Close()
log.Println("MySQL 连接池已关闭")
}
}
// BeginTx 开启事务
func BeginTx() (*sql.Tx, error) {
return DB.Begin()
}
11.7 dao/common.go 通用数据库方法封装(复用 CRUD)
package dao
import (
"database/sql"
"gosql-demo/db"
)
// Exec 通用执行增删改
func Exec(sqlStr string, args ...interface{}) (int64, error) {
result, err := db.DB.Exec(sqlStr, args...)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
// Insert 通用插入,返回自增ID
func Insert(sqlStr string, args ...interface{}) (int64, error) {
result, err := db.DB.Exec(sqlStr, args...)
if err != nil {
return 0, err
}
return result.LastInsertId()
}
// QueryRow 通用查询单行
func QueryRow(sqlStr string, args ...interface{}) *sql.Row {
return db.DB.QueryRow(sqlStr, args...)
}
// Query 通用查询多行
func Query(sqlStr string, args ...interface{}) (*sql.Rows, error) {
return db.DB.Query(sqlStr, args...)
}
// Prepare 预处理语句
func Prepare(sqlStr string) (*sql.Stmt, error) {
return db.DB.Prepare(sqlStr)
}
11.8 dao/user.go 用户表业务层 CRUD(完整封装)
package dao
import (
"gosql-demo/model"
)
// AddUser 新增用户
func AddUser(name string, age int, email string) (int64, error) {
sqlStr := "INSERT INTO user(name,age,email) VALUES(?,?,?)"
return Insert(sqlStr, name, age, email)
}
// GetUserByID 根据ID查询
func GetUserByID(id int) (*model.User, error) {
sqlStr := "SELECT id,name,age,email FROM user WHERE id=?"
row := QueryRow(sqlStr, id)
var u model.User
err := row.Scan(&u.ID, &u.Name, &u.Age, &u.Email)
if err != nil {
return nil, err
}
return &u, nil
}
// ListUser 查询所有用户
func ListUser() ([]model.User, error) {
sqlStr := "SELECT id,name,age,email FROM user"
rows, err := Query(sqlStr)
if err != nil {
return nil, err
}
defer rows.Close()
var list []model.User
for rows.Next() {
var u model.User
if err := rows.Scan(&u.ID, &u.Name, &u.Age, &u.Email); err != nil {
return nil, err
}
list = append(list, u)
}
return list, rows.Err()
}
// UpdateUserAge 更新年龄
func UpdateUserAge(id int, age int) (int64, error) {
sqlStr := "UPDATE user SET age=? WHERE id=?"
return Exec(sqlStr, age, id)
}
// DeleteUser 删除用户
func DeleteUser(id int) (int64, error) {
sqlStr := "DELETE FROM user WHERE id=?"
return Exec(sqlStr, id)
}
11.9 事务封装示例(dao/tx.go 可选)
package dao
import (
"gosql-demo/db"
"log"
)
// TxAddTwoUser 事务:同时新增两个用户,原子性
func TxAddTwoUser(name1 string, age1 int, name2 string, age2 int) error {
tx, err := db.BeginTx()
if err != nil {
return err
}
defer func() {
if r := recover(); r != nil {
_ = tx.Rollback()
log.Println("事务异常回滚:", r)
}
}()
// 执行第一条
_, err = tx.Exec("INSERT INTO user(name,age) VALUES(?,?)", name1, age1)
if err != nil {
_ = tx.Rollback()
return err
}
// 执行第二条
_, err = tx.Exec("INSERT INTO user(name,age) VALUES(?,?)", name2, age2)
if err != nil {
_ = tx.Rollback()
return err
}
return tx.Commit()
}
11.10 main.go 入口测试(完整调用示例)
package main
import (
"fmt"
"gosql-demo/config"
"gosql-demo/dao"
"gosql-demo/db"
)
func main() {
// 1. 初始化数据库
cfg := config.DefaultMySQLConfig()
if err := db.InitMySQL(cfg); err != nil {
fmt.Println("数据库初始化失败:", err)
return
}
defer db.Close()
// 2. 新增用户
uid, err := dao.AddUser("李四", 22, "lisi@qq.com")
if err != nil {
fmt.Println("新增失败:", err)
} else {
fmt.Println("新增用户ID:", uid)
}
// 3. 查询单个
user, _ := dao.GetUserByID(int(uid))
fmt.Printf("查询用户:%+v,邮箱:%s\n", user, user.GetEmail())
// 4. 查询列表
list, _ := dao.ListUser()
fmt.Println("用户列表:", list)
// 5. 更新
affect, _ := dao.UpdateUserAge(int(uid), 25)
fmt.Println("更新行数:", affect)
// 6. 事务测试
err = dao.TxAddTwoUser("事务A", 20, "事务B", 21)
if err != nil {
fmt.Println("事务失败:", err)
} else {
fmt.Println("事务执行成功")
}
}
11.11 生产环境核心规范(必看)
1. 连接池配置规范
- MaxOpenConns:一般设置 20--50,不要过大
- MaxIdleConns:为最大连接数一半
- 必须设置连接超时,防止僵死连接
2. 安全规范
- 禁止字符串拼接 SQL,全部使用
?占位符,防 SQL 注入 - 敏感配置(账号密码)不要硬编码,使用环境变量 / 配置文件
3. 资源释放规范
- 所有 rows 对象必须 defer Close ()
- 预处理语句 stmt 必须延迟关闭
- 程序退出时必须关闭数据库连接池
4. 错误处理规范
- 所有数据库操作必须判断错误,禁止忽略
- 错误信息需携带上下文,方便排查(使用
fmt.Errorf("xxx: %w", err))
十二、知识图谱(文字版)
Go 原生 SQL 操作 MySQL
├── 标准库:database/sql + 数据库驱动
├── 核心对象:*sql.DB(连接池)
├── 基础操作:增删改查
├── 高级特性:预处理、事务、NULL处理
├── 性能优化:连接池配置
├── 安全规范:防SQL注入、连接泄漏
├── 避坑指南
└── 生产级项目模板
├── 配置分离(config)
├── 模型定义(model)
├── 数据库封装(db)
├── 业务CRUD(dao)
└── 入口测试(main)
版权声明
本文为原创 Go 后端技术文章,CSDN 首发,纯原生标准库实战教程 + 生产级项目模板,禁止未经授权转载、抄袭与搬运,侵权必究!