Golang数据库编程详解 | 深入浅出Go语言原生数据库编程

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,高可用系列 2核4GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: Golang数据库编程详解 | 深入浅出Go语言原生数据库编程


前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站https://www.captainbed.cn/kitie


前言

对数据库的CURD是现代应用程序的必备功能,Go语言当然也对数据库的操作提供了非常完善的支持。


尽管在Go语言社区中有很多优秀的ORM库或框架(比如GORM,后面也会发文)能让我们更方便地操作数据库,不过要更好地使用ORM库,掌握Go原生操作数据库database/sql包的使用还是有必要的。

所以,在这篇文章中,我们先来学习一下database/sql包的使用吧。


database/sql简介

我们可以把标准库中的database/sql包看作一个数据库操作抽象层,因为database/sql并不直接连接并操作数据库,而是为不同的数据库驱动提供了统一的API,database/sql包与驱动包(driver)的关系如下图所示:

对数据库的具体操作由对应数据库的驱动包来完成,访问不同的类型的数据库需要导入不同的驱动包,而所有驱动包都必须实现database/sql/driver包下的相关接口,因此database/sql、database/sql/driver以及驱动包的关系如下图所示:

这样做的好处在于,如果某一天我们想迁移数据库,比如从MySQL切换为Postgres,只需要更换驱动包即可,而不需要把数据库操作的代码重写一遍。

连接数据库

连接到数据库,获取一个数据库操作句柄,简单来说可以分为三步:


选择对应数据库的驱动,并导入。

配置连接数据库所需的DSN。

使用sql.Open()函数打开数据连接,获得*sql.DB对象。

DSN是Data Source Name,包含了连接数据库所需的参数,比如用户,密码、数据编码、数据库名称等。


下面我们以MySQL,Postgres,SQLite为例介绍在Go语言中如何连接到数据库。

MySQL

连接MySQL驱动包推荐用github.com/go-sql-driver/mysql库,其完整的DSN为如以所示,可以看到MySQL的DSN各个部分都是可选的:

[username[:password]@][protocol[host[:port]]]/dbname[?param1=value1&...&paramN=valueN]


  • username:用户名。
  • password:密码,与用户名之间要用冒号分隔。
  • protocol:网络协议,默认用tcp即可。
  • host:数据库地址。
  • port:端口号,默认为3306
  • dbname:数据库名称
  • param1~paramN:可选参数。
package main 
 
import (
  "database/sql"
 
  // 匿名导入
  _ "github.com/go-sql-driver/mysql"
)
 
func main(){
  //db为*sql.DB
  //DSN要根据实际替换
  db, err := sql.Open("mysql", "user:password@/dbname")
  if err != nil {
    panic(err)
  }
}


Postgres

连接Postgres数据库推荐用github.com/lib/pq,其连接DSN为:

host=192.168.0.1 user=postgres dbname=postgres port=5432 password=123456 sslmode=disable
  • user:用户名。
  • password:密码,与用户名之间要用冒号分隔。
  • dbname:数据库名称。
  • sslmode:是否使用ss模式,其值可以是disable,required,verify-ca ,verify-full等。
  • host:主机
  • port:端口号
package main 
 
import (
  "context"
  "fmt"
  "os"
 
  "github.com/lib/pq"
)
 
func main(){
  
  dsn := "user=postgres dbname=postgres password=123456 sslmode=disable"
  db, err := sql.Open("postgres", dsn)
  if err != nil {
    log.Fatal(err)
  }
 
  defer db.Close()
}

SQLite

连接SQLite数据库推荐用github.com/mattn/go-sqlite3,SQLite是嵌入式数据库,因此它的DSN是数据库文件所在的路径:

import (
  "database/sql"
 
  _ "github.com/mattn/go-sqlite3"
)
 
func main(){
  dsn := "./test.db"
  db,err := sql.Open("sqlite3",dsn)
}

关闭连接

打开数据库连接要记得关闭数据连接,一般在defer语句后面调用sql.DB的Close()方法:

import (
  "database/sql"
 
  _ "github.com/mattn/go-sqlite3"
)
 
func main(){
  dsn := "./test.db"
  db,err := sql.Open("sqlite3",dsn)
  if err != nil{
    panic(err)
  }
  //关闭数据库连接
  defer db.Close()
}

连接池设置

sql.DB对象内包含一个数据库连池,并支持通过以下几个方法设置连接池的相关配置:

  db.SetConnMaxLifetime(0)  //最大打开的连接时间
  db.SetMaxIdleConns(50) //最大闲置连接数
  db.SetMaxOpenConns(50) //最大打开的连接数


示例数据表

为了后面更好的进行实例演示,我们需要一个演示数据库,这里以Mysql数据库为例,数据库名称为test,创建一个users数据表:

CREATE DATABASE IF NOT EXISTS test;
 
USE test;
 
CREATE TABLE IF NOT EXISTS users(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
gender TINYINT NOT NULL DEFAULT 0,
mobile VARCHAR(255) NOT NULL DEFAULT ''
);

sql.DB

前面我们调用sql.Open()获得了sql.DB对象,这是操作数据的句柄,也可以理解为一个数据库连接池,可以通过sql.DB的Conn()可以获得数据库连接池里的单个连接对象sql.Conn:

ctx, _ := context.WithCancel(context.Background())
conn, err := db.Conn(ctx)

上面的方法执行后返回sql.Result接口的实例,这个接口只有两个方法:

type Result interface {
  //执行insert语句时,返回自增id
  LastInsertId() (int64, error)
  //影响行数
  RowsAffected() (int64, error)
}

示例代码:

package main
 
import (
  "database/sql"
  "fmt"
 
  _ "github.com/mattn/go-sqlite3"
)
 
func main() {
 
  db, err := sql.Open("sqlite3", "../test.db")
 
  if err != nil {
    panic(err)
  }
  defer db.Close()
 
  insertSql := "INSERT INTO users(name,gender,mobile) VALUES(?,?,?),(?,?,?)"
  insertResult, err := db.Exec(insertSql, "小白", 2, "166xxxxxxxx", "小张", 1, "13493023333")
 
  if err != nil {
    panic(err)
  }
  lastInsertId, _ := insertResult.LastInsertId()
  fmt.Printf("最新记录id:%d\n", lastInsertId)
  rowsAffected, _ := insertResult.RowsAffected()
  fmt.Println("影响行数:", rowsAffected)
 
  updateSql := "UPDATE users SET mobile = ? WHERE id = ?"
  updateResult, err := db.Exec(updateSql, "136xxxxxxxx", 1)
 
  if err != nil {
    panic(err)
  }
 
  rowsAffected, _ = updateResult.RowsAffected()
  fmt.Printf("影响行数:%d\n", rowsAffected)
 
  deleteSql := "DELETE FROM users WHERE id = ?"
  deleteResult, err := db.Exec(deleteSql, 14)
 
  if err != nil {
    panic(err)
  }
 
  rowsAffected, _ = deleteResult.RowsAffected()
  fmt.Printf("影响行数:%d\n", rowsAffected)
}

如果你只想从数据表查询一行数据,可以调用QueryRow()和QueryRowContext()方法:

func (db *DB) QueryRow(query string, args ...any) *Row
func (db *DB) QueryRowContext(ctx context.Context, query string, args ...any) *Row

上面的方法返回的是sql.Row对象,代表查询的那一行数据,这个对象只有Scan方法可以获取对象里的数据,调用Scan方法时传进去参数个数必须与查询返回的数据列数一致:

package main
 
import (
  "database/sql"
  "fmt"
 
  _ "github.com/mattn/go-sqlite3"
)
 
func main() {
 
  db, err := sql.Open("sqlite3", "./test.db")
 
  if err != nil {
    panic(err)
  }
  defer db.Close()
 
  selectOne := "SELECT * FROM users WHERE id = ?"
  row := db.QueryRow(selectOne, 1)
  var (
    id     int
    name   string
    gender uint8
    mobile string
  )
  //扫描数据
  err = row.Scan(&id, &name, &gender, &mobile)
  if err != nil {
    panic(err)
  }
  genderText := "未知"
  switch gender {
  case 1:
    genderText = "男"
  case 2:
    genderText = "女"
  }
 
  fmt.Printf("用户:%s | 性别:%s | 手机:%s\n", name, genderText, mobile)
}

然而,更多的时候,我们需要查询多行数据,可以调用Query()和QueryContext()方法:

func (db *DB) Query(query string, args ...any) (*Rows, error)
func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)

上面两个方法返回的是sql.Rows对象,代表查询回来的多行数据,可以在for循环语句中通过Next()和Scan()方法扫描每一行数据,传给Scan方法的参数数量必须与查询回来的列数相同,另外,要记得调用Close()方法关闭sql.Rows对象:

package main
 
import (
  "database/sql"
  "fmt"
  "log"
 
  _ "github.com/mattn/go-sqlite3"
)
 
func main() {
 
  db, err := sql.Open("sqlite3", "./test.db")
 
  if err != nil {
    panic(err)
  }
  defer db.Close()
 
  selectMany := "SELECT * FROM users"
  rows, err := db.Query(selectMany)
  if err != nil {
    panic(err)
  }
  defer rows.Close()
 
  for rows.Next() {
    var (
      id     int
      name   string
      gender uint8
      mobile string
    )
    if err := rows.Scan(&id, &name, &gender, &mobile); err != nil {
      log.Fatal(err)
    }
 
    genderText := "未知"
    switch gender {
    case 1:
      genderText = "男"
    case 2:
      genderText = "女"
    }
 
    fmt.Printf("用户:%s | 性别:%s | 手机:%s\n", name, genderText, mobile)
  }
 
}


sql.Stmt

SQL语句预编译机制允许先把带有参数占位符的SQL语句发送给数据库,数据库会提前对SQL语句进行编译,之后我们再发送对应占位符的参数给数据库 。


SQL语句的好处在于:

  • 预防SQL注入攻击。
  • 复杂的SQL语句,提前编译提高SQL语句执行效率。

调用sql.DB、sql.Conn和sql.Tx(下面会讲到)的Prepare()方法或者PrepareContext()会返回一个sql.Stmt:

stmt, err := db.Prepare("INSERT INTO users(name,) VALUES(?,?,?),(?,?,?)")

因为SQL语句已经预发送给数据库,因此调用sql.Stmt的ExecXXX()和QueryXXX()时,只需要发送参数即可:

Stmt.Exec("张三",2,"10086","李四",1,"10086111")


sql.Stmt对象里的方法返回值与sql.DB一样是sql.Result,具体使用参考前面的例子。


sql.Stmt对象里的方法返回值与sql.DB一样是sql.Result,具体使用参考前面的例子。

sql.Tx

sql.Tx表示一个数据库事务对象,调用sql.DB或者sql.Conn对象的Begin()或者BeginTx()方法会返回一个sql.Tx:

tx,err := db.Begin()


sql.Tx与sql.DB执行CURD的方法基本相同,所不同的是,通过sql.Tx执行的语句,最后要调用sql.Tx的Commit()方法提交事务,如果执行时有错误发生,则应该调用Rollback()方法回滚事务:

package main
 
import (
  "database/sql"
 
  _ "github.com/mattn/go-sqlite3"
)
 
func main() {
 
  db, err := sql.Open("sqlite3", "./test.db")
 
  if err != nil {
    panic(err)
  }
  defer db.Close()
 
  tx, err := db.Begin()
 
  if err != nil {
    panic(err)
  }
 
  if _, err := tx.Exec("INSERT INTO users VALUES(?,?,?,?)", 1, "小龙", 1, "137xxxxxxxx"); err != nil {
    tx.Rollback()
  }
 
  if _, err := tx.Exec("INSERT INTO users VALUES(?,?,?,?)", 2, "小明", 1, "137xxxxxxxx"); err != nil {
    tx.Rollback()
  }
 
  tx.Commit()
}


总结


通过本文的学习,我们能够掌握Go语言数据库编程的基本技能,并能够独立进行数据库应用程序的开发。Go语言数据库编程是一个实践性很强的领域,需要不断地学习和实践。希望本文能够为你的学习之路提供指导和帮助。


简单总结一下:


sql.DB对象表示一个数据库句柄,其中包含一个数据库连接池。

sql.Conn对象表示一个连接池里的普通数据库连接。

sql.Tx表示一个数据库事务对象,通过该对象执行的SQL语句要调用Commit()方法才会生效,如果执行过程发生错误,要调用Rollback()方法回滚事务。

sql.Stmt表示一个预编译对象,通过这个对象执行的SQL语句会先发送给数据库,之后再发送参数,这样可以避免SQL注入以及提前编译语句,提高执行效率。

sql.Rows是调用QueryXXX这类方法返回的表示多行数据的对象,内置迭代器,可以调用for语句迭代查询回来的数据。

sql.Row是调用QueryRowXXX这类方法返回的对象,代表一行数据。

  • sql.Result是调用ExecXXX这类方法进行update,delete,insert操作之后返回的结果。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
相关文章
|
7月前
|
关系型数据库 MySQL Java
【YashanDB知识库】原生mysql驱动配置连接崖山数据库
【YashanDB知识库】原生mysql驱动配置连接崖山数据库
【YashanDB知识库】原生mysql驱动配置连接崖山数据库
|
5月前
|
设计模式 缓存 算法
Go如何进行高质量编程与性能调优实践
本文介绍了Go语言高质量编程与性能调优的实践方法。高质量编程包括良好的编码习惯(如清晰注释、命名规范)、代码风格与设计(如MVC模式)、简洁明了的代码原则,以及单元测试与代码重构的重要性。性能调优方面,涵盖算法优化、数据结构选择、I/O优化、内存管理、并行与并发处理优化及代码层面的改进。通过这些方法,可有效提升代码质量和系统性能。
108 13
|
5月前
|
分布式计算 Go C++
初探Go语言RPC编程手法
总的来说,Go语言的RPC编程是一种强大的工具,让分布式计算变得简单如同本地计算。如果你还没有试过,不妨挑战一下这个新的编程领域,你可能会发现新的世界。
124 10
|
7月前
|
SQL 数据库连接 Linux
数据库编程:在PHP环境下使用SQL Server的方法。
看看你吧,就像一个调皮的小丑鱼在一片广阔的数据库海洋中游弋,一路上吞下大小数据如同海中的珍珠。不管有多少难关,只要记住这个流程,剩下的就只是探索未知的乐趣,沉浸在这个充满挑战的数据库海洋中。
147 16
|
7月前
|
Go 开发者
go-carbon v2.6.0 重大版本更新,轻量级、语义化、对开发者友好的 golang 时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 Golang 时间处理库,提供了对时间穿越、时间差值、时间极值、时间判断、星座、星座、农历、儒略日 / 简化儒略日、波斯历 / 伊朗历的支持
121 3
|
8月前
|
中间件 关系型数据库 数据库
docker快速部署OS web中间件 数据库 编程应用
通过Docker,可以轻松地部署操作系统、Web中间件、数据库和编程应用。本文详细介绍了使用Docker部署这些组件的基本步骤和命令,展示了如何通过Docker Compose编排多容器应用。希望本文能帮助开发者更高效地使用Docker进行应用部署和管理。
211 19
|
8月前
|
存储 缓存 Java
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
809 3
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
|
10月前
|
SQL Java 数据库连接
JDBC编程安装———通过代码操控数据库
本文,教你从0开始学习JBCD,包括驱动包的下载安装调试设置,以及java是如何通过JBDC实现对数据库的操作,以及代码的分析,超级详细
|
11月前
|
JSON Go 开发者
go-carbon v2.5.0 发布,轻量级、语义化、对开发者友好的 golang 时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 Golang 时间处理库,提供了对时间穿越、时间差值、时间极值、时间判断、星座、星座、农历、儒略日 / 简化儒略日、波斯历 / 伊朗历的支持。
209 4
|
11月前
|
数据采集 监控 Java
go语言编程学习
【11月更文挑战第3天】
172 7

热门文章

最新文章

推荐镜像

更多