前言
学了很多Golang的基础语法和零碎知识,总感觉无法把Golang像Python一样灵活运用到实际项目开发之中。
Gin框架源码解析
Gin框架路由详解
Gin框架中间详解
Go操作MySQL数据库
1.database/sql操作MySQL
在golang内置了1个database/sql包,database/sql提供了连接数据库的泛用的接口,但并不提供具体数据库驱动。
所以使用database/sqx包时必须注入(至少)一个数据库驱动。
如果我们需要连接不同的数据就需要基于database/sqlx的数据库驱动,下载不同的数据驱动。
我们常用的数据库基本上都有完整的第三方实现。例如:MySQL驱动。
公共的接口+不同的实现插件,这就是1个接口思想。
2.mysql数据库和表创建
MariaDB [(none)]> create database web default charset=utf8;
创建user表
CREATE TABLE `user` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT, `name` VARCHAR(20) DEFAULT '', `age` INT(11) DEFAULT '0', PRIMARY KEY(`id`) )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
3.下载MySQL驱动
go get -u github.com/go-sql-driver/mysql
3.sql.Open()使用驱动
sql.Open会返回1个db对象,db对象维护了1个数据库连接池,并发安全。
func UserDriver()(err error){ rds:="zhanggen:123.com@tcp(192.168.56.18:3306)/web" db,err= sql.Open("mysql", rds) //做完错误检查之后再进行defer db.close() if err!=nil{ return } //确保sql.Open()返回的不是空指针! defer db.Close() return }
4.db.Ping()测试数据库连接
sql.Open()不会去真正连接数据库,我们可以使用db.ping尝试与数据建立连接。
//全局连接池对象 var db *sql.DB func InitDB() (err error) { dns := "zhanggen:123.com@tcp(192.168.56.18:3306)/web" //Open函数只是校验其参数格式是否正确?实际并不会创建数据库连接! db, err = sql.Open("mysql", dns) if err != nil { return } //db有可能返回Nil //做完了错误检查之后,再defer,db.close释放掉数据库连接资源,确保db不为nill //defer db.Close() //尝试与数据库建立连接 err = db.Ping() if err!=nil{ fmt.Printf("连接数据库失败%v ",err) return } //连接最长的时间 db.SetConnMaxLifetime(time.Second*10) //最大连接数 db.SetMaxOpenConns(200) //最大闲置连接 db.SetMaxIdleConns(10) return }
5.db.QueryRow查询单条记录
注意在我查询的数据的使用了QueryRow就相当于在连接池中获取了1个数据库连接,要使用Scan方法对当前进行释放。
package main import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" //自动初始化执行mysql包中的init() "time" ) //全局连接池对象 var db *sql.DB func InitDB() (err error) { dns := "zhanggen:123.com@tcp(192.168.56.18:3306)/web" //Open函数只是校验其参数格式是否正确?实际并不会创建数据库连接! db, err = sql.Open("mysql", dns) if err != nil { return } //db有可能返回Nil //做完了错误检查之后,再defer,db.close释放掉数据库连接资源,确保db不为nill //defer db.Close() //尝试与数据库建立连接 err = db.Ping() if err!=nil{ fmt.Printf("连接数据库失败%v ",err) return } //连接最长的时间 db.SetConnMaxLifetime(time.Second*10) //最大连接数 db.SetMaxOpenConns(200) //最大闲置连接 db.SetMaxIdleConns(10) return } type user struct { id int name string age int } func main() { if err := InitDB(); err != nil { fmt.Printf("connect to db faild,err:%v ", err) } defer db.Close() sqlStr:="select id,name,age from user where id=?" var u user //注意:每1次QueryRow之后都要scan把当前连接释放掉!否则会一直占用单前连接! row:=db.QueryRow(sqlStr,1) //相当于直接传了3个变量,如果不直接传1个结构体字段名无需大写! err := row.Scan(&u.id, &u.name, &u.age) if err!=nil{ fmt.Println(err) return } fmt.Printf("id:%d name:%s age:%d ",u.id,u.name,u.age) }
5.db.Query()查询多条记录
//查询多条记录 func querymultiple() { sqlString := "select id,name,age from user limit 10;" rows, err := db.Query(sqlString) if err != nil { fmt.Println(err) return } var userList []user //防止for循环过程出错!提前注册关闭! defer rows.Close() //循环获取结果集中的数据,知道Rows.Next()=flase for rows.Next() { var u user err := rows.Scan(&u.id, &u.name, &u.age) if err != nil { return } userList = append(userList, u) } fmt.Println(userList) }
7.db.Exec()
db.Exec()可以对数据库进行insert、update、delete操作。
insert
插入操作可以通过db.Exec()执行之后返回的结果描述中获取LastInsertId
func insertRow() { sqlString := "insert into user(name,age)values(?,?)" ret, err := db.Exec(sqlString, "张根", 25) if err != nil { fmt.Println(err) return } id, err := ret.LastInsertId() if err != nil { fmt.Println("获取最后一条插入记录的ID失败!", err) } fmt.Printf("插入成功,最新记录的id为%d ", id) }
update
我们在使用db.Exec()执行update操作时,会返回1个RowsAffected,也就是受影响的行数。
func updateRow(){ sqlSring:="update user set age=? where id=?;" ret,err := db.Exec(sqlSring,27,3) if err!=nil{ fmt.Println("更新数据失败",err) return } //受影响的行数 n,err:=ret.RowsAffected() if err!=nil{ fmt.Println(err) } if n==0{ fmt.Println("未更新!") return } fmt.Printf("更新%d条数据 ",n) }
delete
我们在使用db.Exec()执行delete操作时,会返回RowsAffected(受影响的行数),仅SQL语句不同。
func deleteRow() { sqlString := "delete from user where id=?;" ret, err := db.Exec(sqlString, 4) if err != nil { fmt.Println("执行删除失败", err) return } //受影响的行数!! var affectedRow int64 affectedRow, err = ret.RowsAffected() if err != nil { fmt.Println(err) return } if affectedRow == 0 { fmt.Println("删除未成功!", affectedRow) return } fmt.Printf("删除%d数据 ", affectedRow) }
8.db.Prepare(sql)SQL预处理
SQL预处理有2大好处
1.避免SQL注入问题。
func prepareQuery(){ sql:="select id,name,age from user where name=?" //1.把SQL先发送到MySQL-server端去预处理 prepared,err := db.Prepare(sql) if err!=nil{ fmt.Printf("SQL预处理失败%v",err) } //3.最后记得关闭连接 defer prepared.Close() //2.把sql参数发送到MySQL-server端进行拼接 row:= prepared.QueryRow("Tom") u:=new(user) row.Scan(&u.id,&u.name,&u.age) fmt.Println(u) }
2.SQL预处理是优化MySQL服务器频繁执行重复SQL的方法,可以提升服务器性能,提前让服务器编译,服务端一次编译多次执行,节省后续编译的成本。
我们在做监控系统时,数据的写入不能延迟,所以无法使用批量插入,可以使用SQL预编译来进行优化。
func prepareInsert() { sqlString := "insert into user(name,age)values(?,?);" preparedStatement, err := db.Prepare(sqlString) if err != nil { fmt.Println("sql预处理失败", err) return } defer preparedStatement.Close() //实时插入 preparedStatement.Exec("Amy", 19) preparedStatement.Exec("Jack", 89) preparedStatement.Exec("Foina", 29) }
9.事务操作
Go语言中使用以下三个方法实现MySQL中的事务操作。
开始事务
begin, err := db.Begin()
提交事务
begin.Commit()
回滚事务
begin.Rollback()
代码
func transactionDemo() { //1.开启事务 begin, err := db.Begin() if err != nil { if begin != nil { //事务回滚 begin.Rollback() } fmt.Println("事务操作开始失败", err) return } sql1 := "update user set age=59 where id=?" _, err = begin.Exec(sql1, 1) if err != nil { //事务回滚 begin.Rollback() fmt.Println("事务操作时执行SQL1失败!", err) return } sql2 := "update user set age=60 where id=?" _, err = begin.Exec(sql2, 2) if err != nil { //事务回滚 begin.Rollback() fmt.Println("事务操作时执行SQL2失败", err) return } //2.提交事务 err = begin.Commit() if err != nil { //事务回滚 begin.Rollback() fmt.Println("执行事务操作提交事务时失败", err) return } }
使用sqlx操作数据
sqlx是基于内置database/sql包扩展出来的超集,它的功能更加强大,更加易用。官网。
1.sqlx.Connect()
连接数据库:sqlx是基于database/sql进行的扩展,它的connect()就是对sql.Open()和sql.Ping()的封装。
func initDB() (err error) { //parseTime=True解析时间类型 dsn := "zhanggen:123.com@tcp(192.168.56.18:3306)/web?charset=utf8mb4&parseTime=True" //Connect()=Open()+Ping() db, err = sqlx.Connect("mysql", dsn) if err != nil { fmt.Println("数据库连接失败", err) return } //设置连接池参数 db.SetConnMaxLifetime(time.Second) db.SetMaxIdleConns(10) return }
2.db.Get(&u, sqlString,参数)
查询单条数据
func queryOne(){ sqlString:="select id,name,age from user where id=?;" var u user //&u:查询结果要赋值到的结构体,sql语句,sql的参数 err := db.Get(&u, sqlString,1) if err!=nil{ fmt.Println("查询数据失败",err) return } fmt.Println(u) }
3.db.Select(&users, sqlString, 10)
查询多条数据
func queryMutil(){ sqlString:="select id,name,age from user limit ?" var users []user //不需要for rows.Next()逐一进行Scan获取了! err := db.Select(&users, sqlString, 10) if err!=nil{ fmt.Println(err) return } fmt.Printf("%#v ",users) }
4.db.Exec()
我们可以使用db.Exec()完成对数据库的insert/update/delete操作。
增
func InsertOne() { sqlStr := "insert into user(name,age)values(?,?);" ret, err := db.Exec(sqlStr, "郑爽", 28) if err != nil { fmt.Printf("数据插入失败%v ", err) return } var id int64 id, err = ret.LastInsertId() if err != nil { fmt.Println(err) return } fmt.Println(id) }
改
func update() { sqlString:="update user set age=? where id=?;" ret, err := db.Exec(sqlString, 35, 5) if err!=nil{ fmt.Println("数据库更新失败",err) } var n int64 n,err= ret.RowsAffected() if err!=nil{ fmt.Println("获取受影响的行失败",err) return } fmt.Println(n) }
删
func deleteOne() { sqlString:="delete from user where id=?;" ret, err := db.Exec(sqlString,4) if err!=nil{ fmt.Println("数据库更新失败",err) } var n int64 n,err= ret.RowsAffected() if err!=nil{ fmt.Println("获取受影响的行失败",err) return } fmt.Println(n) }
5.NamedExec()命名执行
在Python里面我们做字符串替换时可以使用%s占位符也可以使用format(**dict)进行字符串替换。
在sqlx中可以支持使用map和struct进行字符串替换。
func namedInsert() { sqlString:="insert into user(name,age)values(:name,:age);" db.NamedExec(sqlString,map[string]interface{}{"name":"张大千","age":3037}) return }
6.NamedQuery()命名查询
和NamedExec()一样支持在查询时通过map做字符串替换。这也就是意味着我们可以使用NamedQuery从前端接收json数据然后做组合查询。
func namedSelect() { sqlString := "select id,age,name from user where name=:name and age=:age;" rows, err := db.NamedQuery(sqlString, map[string]interface{}{"name": "张大千", "age": 3037}) if err != nil { fmt.Println(err) return } defer rows.Close() for rows.Next() { var u user //把查询到的结果Scan映射到1个结构体里面 err := rows.StructScan(&u) if err != nil { fmt.Println(err) return } fmt.Println(u) } }
7.sqlx事务操作
sqlx的事务操作和database/sql是一样的, 事务就是一组SQL语句Anyone执行遇到异常就RollBACK,确保全部执行成功再进行Transaction的Commit。
//sqlx事务操作:transactionsn: Noun(一笔)交易,业务,买卖;办理;处理 func sqxTransaction() (err error) { var transaction *sqlx.Tx transaction, err = db.Beginx() //开启事务 if err != nil { fmt.Println("开始事务失败", err) return err } //defer延迟执行函数,最后检查程序执行过程中是否出现panic?是否返回错误? defer func() { //程序执行出现panic之后回滚 if p := recover(); p != nil { transaction.Rollback() panic(p) //程序执行返回错误之后回滚。 } else if err != nil { fmt.Println("事务回滚:", err) transaction.Rollback() //程序执行过程没出现panic、也没有返回err信息,提交事务。 } else { err = transaction.Commit() fmt.Println("事务执行成功") } }() //执行SQL1 sqlStr1 := "update user set age=20 where id=?" ret, err := transaction.Exec(sqlStr1, 1) if err != nil { fmt.Println(err) return err } affectedRow, err := ret.RowsAffected() if err != nil { return err } if affectedRow != 1 { return errors.New("执行SQL1时未对数据库进行修改! ") } //执行SQL1成功之后开始执行SQL2 sqlStr2 := "update user set age=19 where id=?" ret, err = transaction.Exec(sqlStr2, 2) if err != nil { return err } affectedRow, err = ret.RowsAffected() if err != nil { return err } if affectedRow != 1 { return errors.New("执行SQL2时未对数据库进行修改! ") } return err }
8.自己拼接SQL语句实现批量插入
//在不使用sqlx.In的情况下,自己拼接SQL语句,实现批量插入数据。 func BatchInsertUsers(userList []*user) (err error) { //存放(?,?)的slice placeholderList := make([]string, 0, len(userList)) //存放values的slice valueList := make([]interface{}, 0, len(userList)*2) //遍历userList准备相关的数据 for _, u := range userList { //此处占位符要和插入值得个数对应 //SQL占位符:[(?,?) (?,?)] placeholderList = append(placeholderList, "(?,?)") //SQL执行时占位符对应的值:[扎木合 930 铁木真 931] valueList = append(valueList, u.Name, u.Age) } //拼接占位符以及占位符对应的值: sql := fmt.Sprintf("insert into user (name,age) values %s", strings.Join(placeholderList, ",")) fmt.Println(sql) fmt.Println(valueList...) _,err=db.Exec(sql, valueList...) if err!=nil{ fmt.Println(err) } return }
ps:这种SQL拼接参数的数据库操作会引发SQL注入吗?
查询占位符 ?在MySQL内部称为bindvars(查询占位符),它非常重要。你应该始终使用它们向数据库发送值,因为它们可以防止SQL注入攻击。
因为bindvars(查询占位符)不可以使用table名称、column名称进行字符串替换。很多人都不知道这个?占位符的替换规则。
// ?不能用来插入表名(做SQL语句中表名的占位符) db.Query("SELECT * FROM ?", "mytable") // ?也不能用来插入列名(做SQL语句中列名的占位符) db.Query("SELECT ?, ? FROM people", "name", "location")
9.sqlx.In实现批量插入
使用sqlx.In实现批量插入的前提是我们的struct必须要实现driver.Vauler接口。
type user struct { ID int `db:"id"` Age int `db:"age"` Name string `db:"name"` } //实现sqlx的Value接口 func (u user) Value() (driver.Value, error) { return []interface{}{u.Name, u.Age}, nil }
sqlx批量插入数据
func SqlxBatchInsert(userList []interface{}) (err error) { placeholders:=make([]string,0,len(userList)) for i:=0;i<len(userList);i++{ placeholders=append(placeholders,"(?)") } //有几条数据就写几个占位符? sqlStrings := "insert into user (name,age) values %s" sqlStrings=fmt.Sprintf(sqlStrings,strings.Join(placeholders,",")) //如果args实现了driver.Valuer接口,sqlx.In就会通过调用value()方法自动展开结构体 sql, args, err := sqlx.In(sqlStrings, userList...) if err != nil { err=err fmt.Println("sqlx.In把结构体拼接成参数是时,执行时出错!",err) return err } fmt.Println(sql) fmt.Printf("%#v ", args) _, err = db.Exec(sql, args...) if err != nil { fmt.Println("sql执行时出错!",err) } return }
userlist切片
users := []interface{}{user{Name: "俺巴孩汗",Age: 932},user{Name: "古儿汗",Age: 857},user{Name: "成吉思汗",Age: 859}} //BatchInsertUsers(users) SqlxBatchInsert(users)
10.NamedExec()实现批量插入
我们还可以使用NamedExec实现批量插入,不过该功能目前有人已经推了#285 PR,但是作者还没有发release
,所以想要使用下面的方法实现批量插入需要暂时使用master
分支的代码:
更新sqlx包
在项目目录下执行以下命令下载并使用master
分支代码:
D:GinSourceCodeAnalysis>go get github.com/jmoiron/sqlx@master go: github.com/jmoiron/sqlx master => v1.3.2-0.20210128211550-a1d5e6473423 go: downloading github.com/jmoiron/sqlx v1.3.2-0.20210128211550-a1d5e6473423
db.NamedExec()
func NamedBatchInsert(userList []*user) (err error) { //Any named placeholder parameters are replaced with fields from arg. //把切片中结构体的所有字段和占位符进行替换。 _, err = db.NamedExec("insert into user (name,age) values (:name, :age)",userList) if err != nil { fmt.Println("批量插入错误",err) return err } return }
11.sqlx.In实现批量查询
如何使用sqlx动态填充多个ID进行批量查询呢?
func queryByIDs(ids []int) (userList []user, err error) { sqlString := "SELECT name, age FROM user WHERE id IN (?)" query, args, err := sqlx.In(sqlString, ids) if err != nil { fmt.Println("SQL字符替换失败", err) return } // sqlx.In 返回带 `?` bindvar的查询语句, 我们使用Rebind()重新绑定它 query = db.Rebind(query) db.Select(&userList, query, args...) return }
查询到数据之后我们需要对批量查询到的数据按一定的规则进行排序。我们可以在程序层进行排序,也可以在数据库层进行排序。
MySQL的order by find_in_set(x_filed,particular_order),基于x字段根据某个字段根据指定的顺序进行排序。
func queryByIDs(ids []int) (userList []user, err error) { //1.把输入切片中的 int id,转换成字符串类型做参数使用。 strIDs := make([]string, 0, len(ids)) for _, id := range ids { strIDs = append(strIDs, fmt.Sprintf("%d", id)) } //2.通过order by find_in_set(字段,[1,2])可以对批量查询的结果进行特定规则排序! sqlString := "select name, age from user where id in (?) order by find_in_set(id,?)" query, args, err := sqlx.In(sqlString, ids,strings.Join(strIDs,",")) if err != nil { fmt.Println("SQL字符替换失败", err) return } // sqlx.In 返回带 `?` bindvar的查询语句, 我们使用Rebind()重新绑定它 query = db.Rebind(query) db.Select(&userList, query, args...) return }
go-redis操作redis数据库
我们可以使用redigo和go-redis库对redis数据库进行操作。
redigo这个库的特点是支持redis命令行,而redis-go支持连接哨兵和集群模式的Redis.
1.下载go-redis包从github上
go get -u github.com/go-redis/redis
2.连接单机redis
var rdb *redis.Client func initClient() (err error) { //普通连接 rdb = redis.NewClient(&redis.Options{ Addr: "192.168.56.18:6379", Password: "", DB: 0, //使用的数据库 PoolSize: 100, //连接池大小 }) _, err = rdb.Ping().Result() if err != nil { fmt.Println("连接redis失败", err) } return } func main() { if err := initClient();err!=nil{ fmt.Println(err) } fmt.Println("redis连接成功!") }
3.连接哨兵模式redis数据库
4.连接集群模式的redis数据库
搭建Go web开发脚手架
zap日志库
Viper配置管理库
优雅关机和平滑重启
CLD代码分层
项目实战
分布式ID生成
JWT认证
基于MySQL实现主要业务
基于redis实现投票业务
基于docker搭建开发环境
代码发布和项目部署
参考