Golang学习笔记-魂尊

十七、MySQL

前言:这里假设已经掌握MySQL安装和使用,本章重点介绍Go语言如何和MySQL结合使用

17.1、MySQL回顾

  • 常见的数据库:SQlLite,MySQL,SQLServer,postgreSQL,mongoDB,Oracle
  • 基本概念:库、表、行、列
  • 数据库的操作SQL 分类(声明式语言):

DDL:(Data Definition Language 数据定义语言) create ,drop,alter .. 字段修饰符主键,自增长,默认值,注释,是否唯一

DML:(Data Manipulation Language 数据操控语言) select,insert,delete,update 数据管理

DCL:(Data Control Language 数据控制语句) grant,revoke,deny 权限控制

  • 常见的MySQL存储引擎:MyISAM,InnoDB
MyIsam InnoDB
锁力度
事务支持
外键支持
全文索引支持
  • 事务 ,把多个SQL当做一个整体;数据库事务正确执行的四个基本要素ACID:

1、原子性(atomicity):要么成功要么失败没有中间状态

2、一致性(consistency):数据库从一个一致性状态达到另外一个一致性状态

3、隔离性(isolation):一个事务所做的修改在最终提交前,对其他事务是不可见的

4、持久性(durability):持久性是指事务一旦提交,它对数据库的改变就应该是永久性的

  • 多个事务如何执行,数据库的隔离级别:
    read-uncommitted 读未提交:一个事务可以读取到另一个事务还未提交的数据,会导致脏读
    read-committed 读提交:
    repeatable-read 可重读
    serializable 串行化
  • 查询优化:索引
  • 数据库设计范式
  • 查询:
    • 分页: limit $限制查询数量 offset $偏移
    • 分组:nginx的access.log中每个IP每个状态码出现的次数select host,status,count(status) from hosts group by host,status;
      • select [] from table group by col1,col2 [having $过滤条件]
      • select 元素必须是指定分组的列名或者聚合类结果
    • 联合查询:
      • left join 以左为基准,右侧没数据则null
      • right join
      • inner join
    • 排序:order by
    • 分组统计:having
    • like : %,指定转义符 ESCAPE '/'
    • between,in
    • 逻辑运算: and,or,not
  • sql函数与存储过程
    • 函数针对select:select now()/md5("test")/dateformat()
    • 存储过程:简单理解为方法

17.2、go连接MySQL

Go语言中的database/sql包只提供了保证SQL或类SQL数据库的泛用接口,并且提供了连接池,并不提供具体的数据库驱动。需要单独导入驱动

go get -u github.com/go-sql-driver/mysql //-u 更新
报错:go: missing Git command. 

解决方法:
1、安装git https://git-scm.com/downloads,如果没有在PATH中,手动添加
2、在$GOPATH/src 目录下生成

备注:其他数据库驱动获取地址:`https://github.com/golang/go/wiki/SQLDrivers`

sql包提供了保证sql或类SQL数据库的范用接口,使用sql包时必须注入(至少一个)数据库驱动

  • 连接MySQL函数
func Open(driverName, dataSourceName string) (*DB, error)
- dirverName:驱动名称
- dataSourceName:指定数据源,一般包(至少)括数据库文件名和(可能的)连接信息。
- DB:是一个数据库(操作)句柄,代表一个具有零到多个底层连接的连接池。它可以安全的被多个go程同时使用。
- Open函数:可能只是验证其参数,而不创建与数据库的连接。如果要检查数据源的名称是否合法,应调用返回值的Ping方法。Open函数返回的DB可以安全的被多个go程同时使用,并会维护自身的闲置连接池。这样一来,Open函数只需调用一次。很少需要关闭DB。

package main

import (
   "database/sql"  //通过内置的database/sql来连接数据库
   "fmt"
   _ "github.com/go-sql-driver/mysql" //内置的database/sql真实连接数据库需要数据库的驱动 ,使用了这个包的init()方法
   //导入了第三方mysql的包,但是没有使用它。github.com/go-sql-driver/driver.go 的init方法{sql.Register("mysql", &MySQLDriver{})},向进行注册database/sql
)

func main(){
   //1、数据库信息
   dsn := "admin:admin123@tcp(192.168.71.130:3306)/admindb?charset=utf8"
   //2、检测连接格式信息
   db,err1 := sql.Open("mysql",dsn)  //使用的是什么驱动"mysql"
   if err1 != nil {
      fmt.Println("数据库连接格式信息有误..",dsn,err1)
      return
   }
    defer db.Close()
   //3、连接数据库测试
   err2 := db.Ping()
   if err2 != nil {
      fmt.Println("open db failed..",dsn,err2)
      return
   }
}
  • 连接池
    • db.SetMaxOpenConns设置与数据库建立连接的最大数目。 如果n大于0且小于最大闲置连接数,会将最大闲置连接数减小到匹配最大开启连接数的限制。 如果n<=0,不会限制最大开启连接数,默认为0(无限制)。
    • db.SetMaxIdleConns设置连接池中的最大闲置连接数。 如果n大于最大开启连接数,则新的最大闲置连接数会减小到匹配最大开启连接数的限制。 如果n<=0,不会保留闲置连接。

17.3、CRUD

17.3.1、Create/Update/Delete

插入、更新和删除操作都使用Exec方法。

func (db *DB) Exec(query string, args ...interface{}) (Result, error)

Exec执行一次命令(包括查询、删除、更新、插入等),返回的Result是对已执行的SQL命令的总结。参数args表示query中的占位参数。

package main

import (
	"database/sql"  //通过内置的database/sql来连接数据库
	"fmt"
	_ "github.com/go-sql-driver/mysql" //内置的database/sql真实连接数据库需要数据库的驱动 ,使用了这个包的init()方法
	//导入了第三方mysql的包,但是没有使用它。github.com/go-sql-driver/driver.go 的init方法{sql.Register("mysql", &MySQLDriver{})},向进行注册database/sql
)

var db *sql.DB //声明连接在外部,这样其他函数也可以用到

func initDB()(err error){
	//1、数据库信息
	dsn := "admin:4B8UNNY2JY15UmAv@tcp(127.0.0.1:3306)/school"
	//2、检测连接格式信息
	db,err = sql.Open("mysql",dsn)  //使用的是什么驱动"mysql"
	//注意这里不能用":="因为如果用了: 表示重新在main函数内声明了一个db变量,会提示报错"panic: runtime error: invalid memory address or nil pointer dereference"
	//fmt.Printf("Internal_DB:%v\n",db) //可以去掉和不去掉":="的":"对比效果
	if err != nil {
		return err
	}
	//3、连接数据库测试
	err = db.Ping()
	if err != nil {
		return err
	}
	//设置最大连接数
	db.SetMaxOpenConns(10)  //大于最大连接数后,新增的连接会被阻塞
	db.SetMaxIdleConns(5) //最大的空闲连接数
	return nil
}


func create()(err error) {
	//这里一次性执行一条语句。
	sqlStr := `
CREATE TABLE IF NOT EXISTS person (
	user_id int(11) NOT NULL AUTO_INCREMENT,
	username varchar(260) DEFAULT NULL,
	sex varchar(260) DEFAULT NULL,
	email varchar(260) DEFAULT NULL,
	PRIMARY KEY (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`

	sqlStr2 := `
CREATE TABLE IF NOT EXISTS place (
	country varchar(200),
	city varchar(200),
	telcode int
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
`

	_,err = db.Exec(sqlStr)
	if err != nil {
		fmt.Println("person表创建失败")
	}
	_,err = db.Exec(sqlStr2)
	if err != nil {
		fmt.Println("place表创建失败")
	}
	return err
}


//type Person struct {
//	UserId   int    `db:"user_id"`
//	Username string `db:"username"`
//	Sex      string `db:"sex"`
//	Email    string `db:"email"`
//}
//
//type Place struct {
//	Country string `db:"country"`
//	City    string `db:"city"`
//	TelCode int    `db:"telcode"`
//}

func insert(){
	//1、写入sql语句
	sql_insert := `insert into person(username,sex,email) values("王菲",'female',"123@qq.com");`
	sql_update := `update person set username="令狐冲" where user_id=2;`
	sql_delete := `delete from person where user_id=4;`

	//更新:sqlStr2 := `update  user set age=9000 where id=3;`
	//删除:sqlStr3 := `delete from user  where id=3;`

	//反引号:不支持转义,支持多行。
	//双引号:支持转义,不支持多行

	//2、exec
	_,err1 := db.Exec(sql_insert)
	_,err2 := db.Exec(sql_update)
	_,err3 := db.Exec(sql_delete)
	if err1 != nil {
		fmt.Println("执行失败",err1)
	}
	if err2 != nil {
		fmt.Println("执行失败",err2)
	}
	if err3 != nil {
		fmt.Println("执行失败",err3)
	}

}

func main(){
	//1、初始化连接
	err := initDB()
	//fmt.Printf("Global_DB:%v\n",db) ,如果initDB()函数用的是":=",结果为"nil"
	if err != nil {
		fmt.Println(err)
	}
	//2、创建表
	if err := create();err != nil {
		fmt.Println("创建表失败,错误为:",err)
	} else {
		fmt.Println("建表成功")
	}
	//3、插入数据
	insert()
}

17.3.2、Retrieve

  • 单行查询
import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql" )

var db *sql.DB

func initDB()(err error){
	//1、数据库信息
	dsn := "admin:4B8UNNY2JY15UmAv@tcp(127.0.0.1:3306)/school"
	//2、检测连接格式信息
	db,err = sql.Open("mysql",dsn)  //使用的是什么驱动"mysql"
	//注意这里不能用":="因为如果用了: 表示重新在main函数内声明了一个db变量,会提示报错"panic: runtime error: invalid memory address or nil pointer dereference"
	//fmt.Printf("Internal_DB:%v\n",db) //可以去掉和不去掉":="的":"对比效果
	if err != nil {
		return err
	}
	//3、连接数据库测试
	err = db.Ping()
	if err != nil {
		return err
	}
	//设置最大连接数
	db.SetMaxOpenConns(2)  //大于最大连接数后,新增的连接会被阻塞
	db.SetMaxIdleConns(2) //最大的空闲连接数
	return nil
}


func create()(err error) {
	//这里一次性执行一条语句。
	sqlStr := `
CREATE TABLE IF NOT EXISTS person (
	user_id int(11) NOT NULL AUTO_INCREMENT,
	username varchar(260) DEFAULT NULL,
	sex varchar(260) DEFAULT NULL,
	email varchar(260) DEFAULT NULL,
	PRIMARY KEY (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`

	sqlStr2 := `
CREATE TABLE IF NOT EXISTS place (
	country varchar(200),
	city varchar(200),
	telcode int
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
`
	_,err = db.Exec(sqlStr)
	if err != nil {
		fmt.Println("person表创建失败")
	}
	_,err = db.Exec(sqlStr2)
	if err != nil {
		fmt.Println("place表创建失败")
	}
	return err
}


type Person struct {
	UserId   int    `db:"user_id"`
	Username string `db:"username"`
	Sex      string `db:"sex"`
	Email    string `db:"email"`
}

type Place struct {
	Country string `db:"country"`
	City    string `db:"city"`
	TelCode int    `db:"telcode"`
}

func insert(){
	//1、写入sql语句
	sql_insert := `insert into person(username,sex,email) values("王菲",'female',"123@qq.com");`
	sql_update := `update person set username="令狐冲" where user_id=2;`
	sql_delete := `delete from person where user_id=4;`

	//更新:sqlStr2 := `update  user set age=9000 where id=3;`
	//删除:sqlStr3 := `delete from user  where id=3;`

	//反引号:不支持转义,支持多行。
	//双引号:支持转义,不支持多行

	//2、exec
	_,err1 := db.Exec(sql_insert)
	_,err2 := db.Exec(sql_update)
	_,err3 := db.Exec(sql_delete)
	if err1 != nil {
		fmt.Println("执行失败",err1)
	}
	if err2 != nil {
		fmt.Println("执行失败",err2)
	}
	if err3 != nil {
		fmt.Println("执行失败",err3)
	}
}

func querrOne(){
	//1、写查询单条记录的sql语句
	sqlStr1 := `select user_id,username,sex,email from person where user_id=1; `
	sqlStr2 := `select user_id,username,sex,email from person where user_id=?; `  //这里的?相当于占位符,可以在 db.QueryRow(sqlStr,1)的sqlStr后面存放

	//2、从连接池中使用一个连接去数据库查询单条记录
	rowObj1 := db.QueryRow(sqlStr1) //
	rowObj2 := db.QueryRow(sqlStr2,2) //rowObj := db.QueryRow(sqlStr,1) 1是问号占位符
	//注意:要确保 QueryRow()方法之后,要调用Scan,否则会出现数据库连接不释放的情况,,Scan()方法内部有"defer r.rows.Close()"方法

	var u1,u2 Person
	//3、查询结果赋值为结构体
	rowObj1.Scan(&u1.UserId,&u1.Username,&u1.Sex,&u1.Email) //修改的是u的原有的值
	rowObj2.Scan(&u2.UserId,&u2.Username,&u2.Sex,&u2.Email) //修改的是u的原有的值
	//db.QueryRow(sqlStr,id).Scan(&u.id,&u.name,&u.age) 可以放在一起。2行用一行
	fmt.Println(u1,u2)
}

func main(){
	//1、初始化连接
	err := initDB()
	//fmt.Printf("Global_DB:%v\n",db) ,如果initDB()函数用的是":=",结果为"nil"
	if err != nil {
		fmt.Println(err)
	}
	//2、创建表
	if err := create();err != nil {
		fmt.Println("创建表失败,错误为:",err)
	} else {
		fmt.Println("建表成功")
	}
    defer db.Close()
	//3、插入数据
	//insert()
	querrOne()
}

17.3.3、多行查询

单行查询使用db.QueryRow,多行使用db.Query()

func (db *DB) QueryRow(query string, args ...interface{}) *Row
func (db *DB) Query(query string, args ...interface{}) (*Rows, error)
func queryMore(){
	sqlStr := `select user_id,username,sex,email from person where user_id>?; `

	rowObj,err := db.Query(sqlStr,1) //
	if err !=nil {
		fmt.Println("查询失败",err)
	}
	defer rowObj.Close()  //多行查询注意释放链接
	var p Person
	for rowObj.Next() {
		err = rowObj.Scan(&p.UserId,&p.Username,&p.Sex,&p.Email)  //注意和查询单条的Scan不一样
		if err != nil {
			fmt.Println("查询失败1",err)
		}
		fmt.Printf("%#v\n",p)
	}
}

17.4、MySQL预处理

重复执行相同的语句,只是部分变化的时候

普通SQL语句执行过程:

  1. 客户端对SQL语句进行占位符替换得到完整的SQL语句。
  2. 客户端发送完整SQL语句到MySQL服务端
  3. MySQL服务端执行完整的SQL语句并将结果返回给客户端。

预处理执行过程:

  1. 把SQL语句分成两部分,命令部分与数据部分。
  2. 先把命令部分发送给MySQL服务端,MySQL服务端进行SQL预处理。
  3. 然后把数据部分发送给MySQL服务端,MySQL服务端对SQL语句进行占位符替换。
  4. MySQL服务端执行完整的SQL语句并将结果返回给客户端。

为什么需要预处理

  1. 优化MySQL服务器重复执行SQL的方法,可以提升服务器性能,提前让服务器编译,一次编译多次执行,节省后续编译的成本。
  2. 避免SQL注入问题。
func prepareInsert(){
   sqlStr := `insert into user(name,age) values(?,?);`
   stmp,err := db.Prepare(sqlStr)
   if err !=nil {
      fmt.Println("Prepasre failed.",err)
      return
   }
   defer stmp.Close() //注意关闭
   //后续只需要拿到smtp进行操作
   var m = map[string]int{
      "小明":28,
      "小红红":20,
   }
   for k,v := range m {
      stmp.Exec(k,v)
   }
}

func main(){
   prepareInsert()
   queryMore()
}

17.5、MySQL事务

  • 什么是事务

事务:一个最小的不可再分的工作单元;通常一个事务对应一个完整的业务(例如银行账户转账业务,该业务就是一个最小的工作单元),同时这个完整的业务需要执行多次的DML(insert、update、delete)语句共同联合完成。A转账给B,这里面就需要执行两次update操作。

在MySQL中只有使用了Innodb数据库引擎的数据库或表才支持事务。事务处理可以用来维护数据库的完整性,保证成批的SQL语句要么全部执行,要么全部不执行。

  • 事务的ACID

通常事务必须满足4个条件(ACID):原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。

条件 解释
原子性 一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
隔离性 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
持久性 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
  • 事务相关方法:
func (db *DB) Begin() (*Tx, error) //开始

func (tx *Tx) Commit() error //提交
func (tx *Tx) Rollback() error //回滚
package main

import (
   "database/sql"  //通过内置的database/sql来连接数据库
   _ "github.com/go-sql-driver/mysql" //内置的database/sql真实连接数据库需要数据库的驱动 ,使用了这个包的init()方法
   
   "fmt"
)

var db *sql.DB //声明连接在外部,这样其他函数也可以用到

func initDB()(err error){
   //1、数据库信息
   dsn := "admin:admin123@tcp(192.168.71.130:3306)/admindb"
   //2、检测连接格式信息
   db,err = sql.Open("mysql",dsn)  //使用的是什么驱动"mysql"
   //注意这里不能用":="因为如果用了: 表示重新在main函数内声明了一个db变量,会提示报错"panic: runtime error: invalid memory address or nil pointer dereference"
   //fmt.Printf("Internal_DB:%v\n",db) //可以去掉和不去掉":="的":"对比效果
   if err != nil {
      return err
   }
   //3、连接数据库测试
   err = db.Ping()
   if err != nil {
      return err
   }
   //设置最大连接数
   db.SetMaxOpenConns(10)  //大于最大连接数后,新增的连接会被阻塞
   db.SetMaxIdleConns(5) //最大的空闲连接数
   return nil
}

type user struct {
   id int
   name string
   age int
}

func queryMore(){
   queryStr := `select id,name,age from user where id>?;`
   rowObj,err := db.Query(queryStr,0)
   if err !=nil {
      fmt.Println("查询失败",err)
   }
   defer rowObj.Close()  //注意: 需要及时关闭连接
   var u user
   for rowObj.Next(){
      err = rowObj.Scan(&u.id,&u.name,&u.age)  //注意和查询单条的Scan不一样
      if err != nil {
         fmt.Println("查询失败1",err)
      }
      fmt.Printf("%#v\n",u)
   }
}

func translationDemon(){
   //1、开启事务
   tx,err := db.Begin()
   if err != nil {
      fmt.Println("开启事务失败,",err)
      if tx != nil {
		tx.Rollback() // 回滚
	  }
   }
   //sqlStr1 := `update user set age=age+2 where id=3;`  //数据库中没有id=3的满足,这种不会报错,因此事务还会继续执行
   sqlStr1 := `update xxx set age=age+2 where id=3;`  //数据库中没有id=3的满足,这种不会报错,因此事务还会继续执行
   sqlStr2 := `update user set age=age-2 where id=7;`
   //注意:如果更新一个不存在的id 不会报错,因此事务会继续,如果要排除这种场景,可以添加 if判断

   // 2、执行sql1和sql 2
   _,err1 = tx.Exec(sqlStr1)
   _,err2 = tx.Exec(sqlStr2)
   if err ==nil && err2 == nil  {
      err = tx.Commit()
       if err !=nil {
          tx.Rollback()
       } 
   } else {
        tx.Rollback()
   		fmt.Println("执行sql1 或者 sql2错误",err1,err2)
   }
   return  
}

func main(){
   //1、建立连接
   err := initDB()
   if err != nil {
      fmt.Println(err)
   }
   fmt.Println("连接成功")

   //2、查询多条记录
   fmt.Println("query more")
   queryMore()

   //3、事务
   translationDemon()
   queryMore()
}

17.6、SQLx

17.6.1、基本使用

第三方库sqlx简化操作,在database/sql的基础上扩展,简化使用方法

安装:go get  .com/jmoiron/sqlx

  • sqlx的Handle Type ssqlx设计和database/sql使用方法是一样的。包含有4中主要的handle types:

    - sqlx.DB - 和sql.DB相似,表示数据库
    - sqlx.Tx - 和sql.Tx相似,表示transacion
    - sqlx.Stmt - 和sql.Stmt相似,表示prepared statement
    - sqlx.NamedStmt - 表示prepared statement(支持named parameters)

  • 数据库基本使用:

package main

import (
	_ "github.com/go-sql-driver/mysql"
	"github.com/jmoiron/sqlx"
	"fmt"
)

var db *sqlx.DB

//一、建立连接
func initDB()(err error){
	dsn := "admin:admin123@tcp(192.168.71.130:3306)/admindb?charset=utf8mb4&parseTime=True"
	db,err = sqlx.Open("mysql",dsn)
	//返回一个DB实例,一个DB实例并不是一个链接,但是抽象表示了一个数据库,内部维护有连接池
	if err !=nil {
		fmt.Println("连接失败",err)
		return err
	}
    defer db.Close()
	err = db.Ping()
	if err !=nil {
		fmt.Println("建立连接失败",err)
		return err
	}
	return nil
}

func initDB2()(err error){
	dsn := "admin:admin123@tcp(192.168.71.130:3306)/admindb?charset=utf8mb4&parseTime=True"
	db,err = sqlx.Connect("mysql",dsn) //sqlx.Connect比sqlx.Open多了一个db.Ping()
	//返回一个DB实例,一个DB实例并不是一个链接,但是抽象表示了一个数据库,内部维护有连接池
	if err !=nil {
		fmt.Println("连接失败",err)
		return err
	}
	return nil
}

type user struct {
	ID int
	NAME string
	AGE int
}

//二、单行查询
func queryOne(){
	sqlStr := `select id, name, age from user where id=?`
	var u user
	//方法1:"database/sql"的QueryRow
	db.QueryRow(sqlStr,1).Scan(&u.ID,&u.NAME,&u.AGE)
	fmt.Println("value:",u)
	defer db.Close()
	//方法2:"database/sql"的Query
	rowObj,_ := db.Query(sqlStr,1)
	for rowObj.Next() {
		err := rowObj.Scan(&u.ID,&u.NAME,&u.AGE)
		if err != nil {
			fmt.Println("方法2查询失败",err)
		}
		fmt.Printf("%#v\n",u)
	}
	//方法3:"sqlx"的Get,注意sqlx.Exec()执行select语句也可以实现查询
	//注意,前两个user这个struct中的元素名可以不用大写开头,但是方法3一定要求是大写开头,否则会报错"scannable dest type struct with >1 columns (3) in result"  
    //
    
	var u3 user
    err := db.Get(&u3,sqlStr,1) 
    
	if err != nil {
		fmt.Println("方法3查询失败",err)
	}
	fmt.Printf("%#v\n",u3)
}

//三、多行查询
func queryMore(){
	//方法1:"database/sql"的Query
	sqlStr1 := `select count(id) from user where id>?`
	sqlStr := `select id, name, age from user where id>?`

	var sum int
	db.QueryRow(sqlStr1,4).Scan(&sum)  //计算总行数
	var u = make([]user,sum)  //一定注意切片初始化
	
	rowObj,_ := db.Query(sqlStr,4)

	for i:=0;rowObj.Next();i++ {  //注意 .Next()第二次执行的时候内容就会丢失
		err := rowObj.Scan(&u[i].ID,&u[i].NAME,&u[i].AGE)
		if err != nil {
			fmt.Println("方法2查询失败",err)
		}
	}
	defer rowObj.Close()
	fmt.Printf("%v\n",u)

	//方法2:"sqlx"的select 
	var u3 []user  //可以不手动初始化
	err := db.Select(&u3,sqlStr,3)
	if err != nil {
		fmt.Println("方法3查询失败",err)
	}
	fmt.Printf("%v\n",u3)
}

type user2 struct {
	name string
	age int
}

func Update(){
	insterSql := `insert into user(name,age) values(?,?);`
	m2 := []user2{user2{"小黄",18},user2{"令狐冲",28}}
	for _,v := range m2 {
		ret,err := db.Exec(insterSql,v.name,v.age)
		if err != nil {
			fmt.Println("插入数据失败",err)
			return
		}
		id,_ := ret.LastInsertId()
		fmt.Println("插入成功,ID:",id)
	}
}


func main(){
	//1、建立连接
	if err := initDB2();err !=nil {
		fmt.Println("初始化连接失败,",err)
	}
	fmt.Println("建立连接成功")

	//2、查询
	queryOne()

	//3、多行查询
	queryMore()

	//4、插入和更新
	Update()
}

17.6.2、NamedExec和NamedQuery

func Named(){
   //1、NamedExec()绑定SQL语句与结构体或map中的同名字段。
   sqlStr := "insert into user (name,age) values (:nm,:ag);"
   ret, err := db.NamedExec(sqlStr,
   map[string]interface{}{
      "nm": "岳不群",
      "ag": 28,
   })
   if err !=nil {
      fmt.Println("插入失败",err)
   }
   id,_ := ret.LastInsertId()
   fmt.Println("插入id为:",id)

   //2、NamedQuery()同 NamedExec()
   sqlStr = "select * from user where name=:lv"
   rows, err := db.NamedQuery(sqlStr, map[string]interface{}{"lv":"岳不群"})
   if err != nil {
      fmt.Printf("NamedQuery() exec failed, err:%v\n", err)
      return
   }
   defer rows.Close()  //注意查询后关闭
   for rows.Next(){
      var u user
      err := rows.StructScan(&u) //结构体Scan
      if err != nil {
         fmt.Printf("scan failed, err:%v\n", err)
         continue
      }
      fmt.Printf("user:%#v\n", u)
   }
}

17.6.3、批量操作

  • 不同的数据库中,SQL语句使用的占位符语法不尽相同。
数据库 占位符语法
MySQL ?
PostgreSQL $1, $2
SQLite ?$1
Oracle :name

批量操作的方法:

  • 1、手动拼接字符串
  • 2、使用sqlx.In实现
  • 使用NamedExec()
package main

import (
   _ "github.com/go-sql-driver/mysql"
   "github.com/jmoiron/sqlx"
   "fmt"
   "strings"
   "database/sql/driver"
)

var db *sqlx.DB

//一、建立连接
func initDB()(err error){
   dsn := "admin:admin123@tcp(192.168.71.130:3306)/admindb?charset=utf8mb4&parseTime=True"
   db,err = sqlx.Open("mysql",dsn)
   //返回一个DB实例,一个DB实例并不是一个链接,但是抽象表示了一个数据库,内部维护有连接池
   if err !=nil {
      fmt.Println("连接失败",err)
      return err
   }
   err = db.Ping()
   if err !=nil {
      fmt.Println("建立连接失败",err)
      return err
   }
   return nil
}

func initDB2()(err error){
   dsn := "admin:admin123@tcp(192.168.71.130:3306)/admindb?charset=utf8mb4&parseTime=True"
   db,err = sqlx.Connect("mysql",dsn) //sqlx.Connect比sqlx.Open多了一个db.Ping()
   //返回一个DB实例,一个DB实例并不是一个链接,但是抽象表示了一个数据库,内部维护有连接池
   if err !=nil {
      fmt.Println("连接失败",err)
      return err
   }
   return nil
}

type user struct {
   ID int
   NAME string
   AGE int
}

//批量操作
type user2 struct {
   name string
   age int
}

//1、手动批量插入数据
func manualBat(users []*user2)(err error){  //如果要引用,操作的是切片内部的指针,而不是指针的切片
   //手动拼接sql实现批量操作
   //1、创建2个切片存储参数信息
   bindvar := make([]string,0,len(users))  //存储占位符?,一对儿"? ?",初始化长度为0否则会出现0补齐的情况
   value := make([]interface{},0,len(users)*2) //存储实际要插入的值,有2个fields,因此len*2,按照name,age,name,age的顺序存储

   //2、存储
   for _,u := range users {
      bindvar = append(bindvar, "(?,?)")
      value = append(value,u.name)
      value = append(value,u.age)
   }

   fmt.Println(strings.Join(bindvar,","))  //(?,?),(?,?),(?,?)
   fmt.Printf("TYPE:%T\n%#v\n",value,value)  //[孙红雷 28 江疏影 26 江浩坤 32]
   //3、字串拼接
   sql1 := fmt.Sprintf("insert into user(name,age) values %s",strings.Join(bindvar,","))
   fmt.Printf("%#v\n",sql1)
   ret,err := db.Exec(sql1,value...)
   //注意:这里的value一定要展开,不然会报错"数据插入失败, sql: converting argument $1 type: unsupported type []interface {}, a slice of interface"
   //展开和直接传递一个"[]interface {}"是不一样的
   num,_ := ret.RowsAffected()
   if err !=nil {
      fmt.Println("执行错误,",err)
      return err
   } else {
      fmt.Printf("%v lines affected\n",num)
   }
   return err
}

//2、使用Sql.In插入数据;前提需要保证我们的结构体user2 实现了driver.Value接口
func (u user) Value() (driver.Value, error) {
   return []interface{}{u.NAME, u.AGE}, nil
}

func Batch(users []interface{})(err error){
   //1、sqlx.In实现批量插入
   query,args,_ := sqlx.In(
      "insert into user(name,age) values(?),(?),(?)",
      users...,
   )
   fmt.Println("query:",query)
   fmt.Printf("TYPE:%T value
              :%v\n",args,args)
   _,err = db.Exec(query,args...) //这里如果没有实现driver.Value方法,就需要展开
   
   return err
}

func main(){
   //1、建立连接
   if err := initDB2();err !=nil {
      fmt.Println("初始化连接失败,",err)
   }
   fmt.Println("建立连接成功")
   defer db.Close()

   //2、批量插入-手动拼接方式
   //初始化方式1:
   a := user2{"孙红雷",28}
   b := user2{"江疏影",26}
   c := user2{"江浩坤",32}
   _ = []*user2{&a,&b,&c}

   //初始化方式2
   _ = []*user2{
      &(user2{"孙红雷",28}),
      &(user2{"江疏影",26}),
      &(user2{"江浩坤",32}),
   }

   //err := manualBat(u)  //注意传递的是切片型数组,还是数组型切片
   //if err !=nil {
   // fmt.Println("数据插入失败,",err)
   //}

   //3、批量插入-sqlx.In()
   d := user{NAME:"孙红雷",AGE:28}
   e := user{NAME:"江疏影",AGE:26}
   f := user{NAME:"江浩坤",AGE:32}
   u2 := []interface{}{d,e,f}
   err := Batch(u2)
   if err !=nil {
      fmt.Println("sqlx.In数据插入失败,",err)
   }
}

备注:sqlx的其他用法请参考: https://github.com/jmoiron/sqlx/blob/master/README.md

17.6.4、sql注入

我们任何时候都不应该自己拼接SQL语句!

package main

import (
   _ "github.com/go-sql-driver/mysql" //内置的database/sql真实连接数据库需要数据库的驱动 ,使用了这个包的init()方法
   "github.com/jmoiron/sqlx"
   "fmt"
)

var db *sqlx.DB //声明连接在外部,这样其他函数也可以用到

func initDB()(err error){
   dsn := "admin:admin123@tcp(192.168.71.130:3306)/admindb"
   db,err = sqlx.Connect("mysql",dsn)
   if err != nil {
      fmt.Println("Connect to MySQL failed.",err)
      return err
   }
   db.SetMaxOpenConns(10)
   db.SetMaxIdleConns(5)
   return nil 
}

type user struct {  //这里的字段首字母有一定要大写,sqlx中用反射区获取对应的类型来对结构体赋值
   ID int
   NAME string
   AGE int
}

func sqlInjectDemo(name string) {
   sqlStr := fmt.Sprintf("select id, name, age from user where name='%s';", name)
   fmt.Printf("SQL:%s\n", sqlStr)

   var users []user
   err := db.Select(&users, sqlStr)
   if err != nil {
      fmt.Printf("exec failed, err:%v\n", err)
      return
   }
   for _, u := range users {
      fmt.Printf("user:%#v\n", u)
   }
}

func main(){
   //1、连接数据库
   err := initDB()
   if err !=nil {
      fmt.Println("建立连接失败",err)
   }

   //2、SQL注入的几种示例
   //sqlInjectDemo("小名")
   sqlInjectDemo("xxx' or 1=1  #")  //这样可以把所有的结果都查询出来,因为"1=1"永远成立,#后面是注释
   
   //sqlInjectDemo("xxx' union select * from user #")
   //sqlInjectDemo("xxx' and (select count(*) from user) <10 #")
}

17.7、学生信息系统-数据库版

17.7.1、主函数

package main

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"html/template"
	"net/http"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"
)

type Student struct {
	ID int
	NAME string
	AGE int
	SEX string
	STATUS int
	STARTTIME *time.Time
	STOPTIME *time.Time
}

func(s *Student) StatusStu()string{ //定义对象方法
	switch s.STATUS {
	case 0:
		return "在上课"
	case 1:
		return "在休息"
	case 2:
		return "在吃饭"
	default:
		return "未知"
	}
}

func(s *Student) GetStartTime()string{ //定义对象方法
	return s.STARTTIME.Format("2006/01/02/15/03/04")
}

func(s *Student) GetStopTime()string{ //定义对象方法
	return s.STOPTIME.Format("2006/01/02/15/03/04")
}

type StudentForm struct {  //存储post请求校验的数据
	NAME string
	AGE int
	SEX string
	STATUS int
	STARTTIME string
	STOPTIME string
}

/*
 CREATE TABLE `student` (
  `id` int auto_increment,
  `name` varchar(200) DEFAULT NULL,
  `age` int DEFAULT 0,
  `SEX` varchar(2) DEFAULT NULL,
  `STATUS` int,
  `STARTTIME` datetime,
  `STOPTIME` datetime,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
status : 0在上课,1在休息,2在吃饭

mysql> select * from student;
+----+--------------+------+------+--------+---------------------+---------------------+
| id | name         | age  | SEX  | STATUS | STARTTIME           | STOPTIME            |
+----+--------------+------+------+--------+---------------------+---------------------+
|  1 | 小红         |   18 | 女   |      0 | 2022-05-06 19:15:28 | 2022-05-06 19:15:28 |
|  2 | 小刚         |   28 | 男   |      1 | 2022-05-06 19:15:28 | 2022-05-06 19:15:28 |
|  3 | 令狐冲       |   38 | 男   |      2 | 2022-05-06 19:15:28 | 2022-05-06 19:15:28 |
|  4 | 独孤求败     |   50 | 男   |      2 | 2022-05-06 19:15:28 | 2022-05-06 19:15:28 |
|  5 | 小红         |   18 | 女   |      0 | 2022-05-06 19:15:41 | 2022-05-06 19:15:41 |
|  6 | 小刚         |   28 | 男   |      1 | 2022-05-06 19:15:41 | 2022-05-06 19:15:41 |
|  7 | 令狐冲       |   38 | 男   |      2 | 2022-05-06 19:15:41 | 2022-05-06 19:15:41 |
|  8 | 独孤求败     |   50 | 男   |      2 | 2022-05-06 19:15:41 | 2022-05-06 19:15:41 |
|  9 | 东方不败     |   18 | 女   |      1 | NULL                | NULL                |
+----+--------------+------+------+--------+---------------------+---------------------+

遗留问题:
在传递的内容是: *Student的时候
  <label for="">姓名:</label> <input type="text" name="name" value="{{.Task.NAME}}" /> <br/> 在首次访问 task值为空的时候,下面的html中的元素也显示不出来
修改为值类型就可以了,因为如果是指针的话,没有值对应nil,如果为空值则还有内容。只不过为空
 */
const (
	SELECTSTR = ""
)


func main(){
	addr := ":8888"
	dsn := "golang:golang@tcp(192.168.74.128:3306)/school?timeout=5s"

	//1、探测数据库
	db,err1 := sql.Open("mysql",dsn)  //使用的是什么驱动"mysql"
	if err1 != nil {
		fmt.Println("数据库连接格式信息有误..",dsn,err1)
		return
	}

	err2 := db.Ping()
	if err2 != nil {
		fmt.Println("open db failed..",dsn,err2)
		return
	} else {
		defer db.Close()
	}

	//2、定义各个处理函数
	http.HandleFunc("/list/", func(resp http.ResponseWriter,req  *http.Request){
		Students := make([]Student,0,20)
		sqlStr := `select * from student; `
		rowObj,err := db.Query(sqlStr) //
		if err !=nil {
			fmt.Println("查询失败",err)
		}
		defer rowObj.Close()  //多行查询注意释放链接

		for rowObj.Next() {
			var stu Student
			var t1,t2 string
			err = rowObj.Scan(&stu.ID,&stu.NAME,&stu.AGE,&stu.SEX,&stu.STATUS,&t1,&t2)  //注意和查询单条的Scan不一样
			if err != nil {
				fmt.Println("查询失败1",err)
			}
			t11,err := time.Parse("2006-01-02 15:04:05",t1)
			if err == nil {
				stu.STARTTIME = &t11
			}

			fmt.Printf("TTTT:%#vT1T:%v\n",t11,t1) //时间格式为: 2022-05-06 19:15:28 +0000 UTC
			/*
			TTTT:time.Date(2022, time.May, 6, 19, 15, 28, 0, time.UTC)T1T:2022-05-06 19:15:28
			TTTT:time.Date(2022, time.May, 6, 19, 15, 28, 0, time.UTC)T1T:2022-05-06 19:15:28
			TTTT:time.Date(2022, time.May, 6, 19, 15, 28, 0, time.UTC)T1T:2022-05-06 19:15:28
			TTTT:time.Date(2022, time.May, 6, 19, 15, 28, 0, time.UTC)T1T:2022-05-06 19:15:28
			TTTT:time.Date(2022, time.May, 6, 19, 15, 41, 0, time.UTC)T1T:2022-05-06 19:15:41
			TTTT:time.Date(2022, time.May, 6, 19, 15, 41, 0, time.UTC)T1T:2022-05-06 19:15:41
			TTTT:time.Date(2022, time.May, 6, 19, 15, 41, 0, time.UTC)T1T:2022-05-06 19:15:41
			TTTT:time.Date(2022, time.May, 6, 19, 15, 41, 0, time.UTC)T1T:2022-05-06 19:15:41
			查询失败1 sql: Scan error on column index 5, name "STARTTIME": converting NULL to string is unsupported
			TTTT:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)T1T:
			 */

			/*
			优化:
				1、对开始时间和结束时间优化为格式" "2006-01-02 15:04" 简明显示
				2、对开始时间和结束时间为null的展示为 '--'
				3、根据状态展示: 0在上课,1在休息,2在吃饭
					在html中:  <td> {{ if eq .STATUS 0 }}在上课{{ else if eq .STATUS 1 }} 在休息 {{ else }}在吃饭  {{ end}} </td>
					在后端服务中:定义模板函数实现 template.FuncMap
					使用结构体方法:定义结构体方法 + 模板文件中调用 {{ .StatusStu}}""

				注意:一般不会直接查询数据库,而是通过方法进行获取
			 */
			t12,err := time.Parse("2006-01-02 15:04:05",t2) //如果用同一个变量进行parse要保证 layout一样,否则会导致nil的出现
			if err == nil {
				stu.STOPTIME = &t12
			}
			Students = append(Students,stu)
		}
		fmt.Printf("查询结果:%#v\n",Students)

		//优化时间格式
		funcs := template.FuncMap{
			"myformat": func(t *time.Time) string{
				if t == nil {
					return "--"
				}
				return t.Format("2006-01-02 15:04")
			},
			"mystatus": func(m int) string{
				switch m {
				case 0:
					return "在上课"
				case 1:
					return "在休息"
				case 2:
					return "在吃饭"
				default:
					return "未知"
				}
			},
		}

		tpl := template.Must(template.New("tpl").Funcs(funcs).ParseFiles("html/list.html"))
		tpl.ExecuteTemplate(resp,"list.html",Students)

	})

	http.HandleFunc("/add/", func(resp http.ResponseWriter,req  *http.Request){
		//第一次请求对应 Get内容为空->填写请求信息->成功:跳转到list页面 失败:回显信息,并提示失败部分
		var (
			stuForm StudentForm
			errors = make(map[string]string)
		)

		if req.Method == http.MethodPost { // 数据验证,成功->跳转到list页面,失败->当前页面数据回显,错误提示
			name := strings.TrimSpace(req.PostFormValue("name"))
			age := strings.TrimSpace(req.PostFormValue("age"))
			ag,_ := strconv.Atoi(age)
			sex := req.PostFormValue("sex")

			status := req.PostFormValue("status")
			sta,err := strconv.Atoi(status)
			if err != nil {
				fmt.Println("status strconv error ",err)
			}

			starttime := req.PostFormValue("starttime")
			stoptime := req.PostFormValue("stoptime")

			fmt.Println("GET POST VALUE:",name,ag,sex,status,starttime,stoptime)
			stuForm = StudentForm{
				NAME: name,
				AGE: ag,
				SEX: sex,
				STATUS: sta,
				STARTTIME: starttime,
				STOPTIME: stoptime,
			}

			errors = StuFormCheck(stuForm)

			fmt.Println("TASK==>",stuForm)
			//如果没有错误就添加数据
			if len(errors) == 0 {
				sqlCreate := "insert into student(name,age,sex,status,STARTTIME,STOPTIME) values (?,?,?,?,?,?)"
				result,err := db.Exec(sqlCreate,stuForm.NAME,stuForm.AGE,stuForm.SEX,stuForm.STATUS,stuForm.STARTTIME,stuForm.STOPTIME)
				if err != nil {
					fmt.Println("插入数据失败",err)
				} else {
					id,_ := result.LastInsertId()
					fmt.Println("插入的数据id为:",id)
				}
				//如果跳转成功就跳转
				http.Redirect(resp,req,"/list",302)
			} else {
				fmt.Println("有错误",errors)
			}
		}

		tpl := template.Must(template.ParseFiles("html/add_task.html"))
		tpl.ExecuteTemplate(resp,"add_task.html", struct {
			Task StudentForm
			Errors map[string]string
		}{stuForm,errors})
	})

	http.HandleFunc("/modify/", func(resp http.ResponseWriter,req  *http.Request){
		//修改的时候,回填信息
		var (
			id int
			stuForm StudentForm
			errors = make(map[string]string)
		)
		req.ParseForm()
		id,err := strconv.Atoi(req.FormValue("ID"))  //获取要修改的学号
		if err != nil {
			fmt.Printf("modify get ID error : %v",err)
		}

		sqlstr := "select name,status,age,sex,STARTTIME,STOPTIME from student where id=?"
		row := db.QueryRow(sqlstr,id)
		row.Scan(&stuForm.NAME,&stuForm.STATUS,&stuForm.AGE,&stuForm.SEX,&stuForm.STARTTIME,&stuForm.STOPTIME)

		if req.Method == http.MethodPost {  //针对提交的数据进行处理
			name := strings.TrimSpace(req.PostFormValue("name"))
			status := strings.TrimSpace(req.PostFormValue("status"))
			age := strings.TrimSpace(req.PostFormValue("age"))
			ag ,err := strconv.Atoi(age)
			sex := req.PostFormValue("sex")

			sta,err := strconv.Atoi(status)

			starttime := req.PostFormValue("starttime")
			stoptime := req.PostFormValue("stoptime")
			//atime,_ := time.Parse("2006-01-02",starttime)
			//btime,_ := time.Parse("2006-01-02",stoptime)

			stuForm.NAME = name
			stuForm.AGE = ag
			stuForm.SEX = sex
			stuForm.STATUS = sta
			//stuForm.STARTTIME = atime.Format("2006-01-02")
			//stuForm.STOPTIME = btime.Format("2006-01-02")
			stuForm.STARTTIME = starttime
			stuForm.STOPTIME = stoptime

			errors = StuFormCheck(stuForm)

			if len(errors) == 0 {
				//校验通过,则修改数据
				sqlstr := "update student set name=?,age=?,sex=?,status=?,STARTTIME=?,STOPTIME=? where id=?"
				_,err := db.Exec(sqlstr,stuForm.NAME,stuForm.AGE,stuForm.SEX,stuForm.STATUS,stuForm.STARTTIME,stuForm.STOPTIME,id)
				if err != nil {
					fmt.Println("执行更新语句失败:",err)
				}
			} else {
				fmt.Println("要修改的数据有问题:",err)
			}
			http.Redirect(resp,req,"/list",302)
		}

		fmt.Println("ID",id)

		tpl := template.Must(template.ParseFiles("html/modify.html"))
		tpl.ExecuteTemplate(resp,"modify.html",struct {
			ID int
			Task StudentForm
			Errors map[string]string
		}{id,stuForm,errors})
	})

	http.HandleFunc("/del/", func(resp http.ResponseWriter,req  *http.Request){
		req.ParseForm()
		id,err := strconv.Atoi(req.FormValue("ID"))
		if err != nil {
			fmt.Printf("del get ID error : %v",err)
		}

		//获取要删除的学生的信息,
		if req.Method == http.MethodGet {
			sqlStr := `delete  from student where id=?;`
			result,err := db.Exec(sqlStr,id)
			if err != nil {
				fmt.Printf("删除%v学生信息失败\n",id)
			} else {
				r,_ := result.RowsAffected()
				fmt.Printf("删除%v学生信息成功,受影响的共%v行\n",id,r)
			}
			http.Redirect(resp,req,"/list",302)
		}
	})
	http.ListenAndServe(addr,nil)
}


func StuFormCheck(stuform StudentForm)(errors map[string]string){
	errors = make(map[string]string)
	/*
	1、开始时间和结束时间可以为空,大师开始时间必须小于等于结束时间
	2、性别必须是男或者女
	3、名字长度不能为空,最长12
	4、年龄取值范围1-120
	5、状态必须是0,1,2 中一个
	 */
	fmt.Println("获得需要检查的数据=>",stuform)


	atime,aerr := time.Parse("2006-01-02",stuform.STARTTIME)
	if aerr != nil {
		errors["STARTTIME"] = "开始时间解析错误"
		fmt.Println("STIME-=>",atime,aerr)
	}

	btime,berr := time.Parse("2006-01-02",stuform.STOPTIME)
	if berr != nil {
		errors["STOPTIME"] = "结束时间解析错误"
		fmt.Println("STIME-=>",btime,berr)
	}

	if aerr == nil && berr == nil  {
		a,_ := strconv.Atoi(atime.Format("20060102"))
		b,_ := strconv.Atoi(btime.Format("20060102"))
		if a > b {
			errors["STOPTIME"] = "结束时间不能小于开始时间"
		}
	}

	if stuform.SEX  != "男" && stuform.SEX  != "女"  {
		errors["SEX"] = "性别必须是男或者女"
	}

	nameLength := utf8.RuneCountInString(stuform.NAME)
	if nameLength == 0 || nameLength >= 12  {
		errors["NAME"] = "姓名不能为空并且长度不能大于12"
	}

	if stuform.AGE <= 0 || stuform.AGE >=  120 {
		errors["AGE"] = "年龄取值范围1-120"
	}

	if stuform.STATUS != 0 && stuform.STATUS != 1 && stuform.STATUS != 2 {
		errors["STATUS"] = "状态错误"
	}

	return  errors
}

17.7.2、静态页面

>>>>>>>>>>>>>>>>>>>>>> list.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    <table>
        <thead>
            <tr>
                <th>学号</th>
                <th>姓名</th>
                <th>年龄</th>
                <th>性别</th>
                <th>状态</th>
                <th>开始时间</th>
                <th>结束时间</th>
            </tr>
        </thead>
        <tbody>
            {{ range . }}
            <tr>
                <td>{{ .ID}}</td>
                <td>{{ .NAME}}</td>
                <td>{{ .AGE}}</td>
                <td>{{ .SEX}}</td>
                <td> {{ mystatus .STATUS }} </td>
                <td>{{ .StatusStu}}</td>
                <td>  {{ myformat .STARTTIME}}</td>
                <td>  {{ myformat .STOPTIME}}</td>
                <td><a href="/del/?ID={{ .ID}}">删除</a></td>
                <td><a href="/modify/?ID={{ .ID}}">修改</a></td>
            </tr>
        </tbody>
            {{ end }}
    </table> <br>

    <a href="/add/"> 新增</a>
</body>
</html>

>>>>>>>>>>>>>>>>>>>>>> add_task.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    TASKS: {{.Task}} <br>
    <form action="/add/" method="post">
        <label for="">姓名:</label> <input type="text" name="name" value="{{.Task.NAME}}" /> {{ .Errors.NAME }}  <br/>
        <label for="">年龄:</label> <input type="text" name="age" value="{{.Task.AGE}}" /> {{ .Errors.AGE }}   <br/>
        <label for="">性别:</label>
        <!-- 通过type+name进行分组,组内为单选。如果没有name则默认为多选,checked设置默认值--->
        {{ if eq .Task.SEX "男" }}
            <input type="radio" name="sex" value="男" checked="checked"><label for="">男</label>
            <input type="radio" name="sex" value="女"><label for="">女</label>  {{.Errors.SEX}}<br>
            {{ else if  eq .Task.SEX "女" }}
            <input type="radio" name="sex" value="男" ><label for="">男</label>
            <input type="radio" name="sex" value="女" checked="checked"><label for="">女</label>{{.Errors.SEX}} <br>
            {{ else }}
            <input type="radio" name="sex" value="男" ><label for="">男</label>
            <input type="radio" name="sex" value="女" ><label for="">女</label>{{.Errors.SEX}} <br>
        {{ end }}

        <label for="">状态:</label>
        <!-- 通过type+name进行分组,组内为单选。如果没有name则默认为多选,checked设置默认值--->
        {{ if eq .Task.STATUS 0 }}
            <input type="radio" name="status" value=0 checked="checked" ><label for="">在上课</label>
            <input type="radio" name="status" value=1><label for="">在休息</label>
            <input type="radio" name="status" value=2><label for="">在吃饭</label>{{.Errors.STATUS}} <br>
        {{ end }}
        {{ if eq .Task.STATUS 1 }}
            <input type="radio" name="status" value=0><label for="">在上课</label>
            <input type="radio" name="status" value=1 checked="checked" ><label for="">在休息</label>
            <input type="radio" name="status" value=2><label for="">在吃饭</label>{{.Errors.STATUS}} <br>
        {{ end }}
        {{ if eq .Task.STATUS 2 }}
            <input type="radio" name="status" value=0><label for="">在上课</label>
            <input type="radio" name="status" value=1><label for="">在休息</label>
            <input type="radio" name="status" value=2 checked="checked" ><label for="">在吃饭</label>{{.Errors.STATUS}} <br>
        {{ end }}

        <label for="">开始时间:</label>    <input type="date" name="starttime" value="{{.Task.STARTTIME}}">{{.Errors.STARTTIME}} <br/>
        <label for="">结束时间:</label>    <input type="date" name="stoptime" value="{{.Task.STOPTIME}}">{{.Errors.STOPTIME}} <br/>

        <input type="submit" value="新增">
    </form>
</body>
</html>


>>>>>>>>>>>>>>>>>>>>>> modify.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

要修改记录的学号是 {{ .Task}} <br>
错误: {{ .Errors}}

<form action="/modify/?ID={{ .ID}}" method="post">
    <label for="">姓名:</label>    <input type="text" name="name" value="{{.Task.NAME}}"> <br/>
    <label for="">年龄:</label>    <input type="text" name="age" value="{{.Task.AGE}}"> <br/>
    {{ if eq .Task.SEX "男" }}
        <input type="radio" name="sex" value="男" checked="checked"><label for="">男</label>
        <input type="radio" name="sex" value="女"><label for="">女</label> <br>
    {{ else if  eq .Task.SEX "女" }}
        <input type="radio" name="sex" value="男" ><label for="">男</label>
        <input type="radio" name="sex" value="女" checked="checked"><label for="">女</label> <br>
    {{ else }}
        <input type="radio" name="sex" value="男" ><label for="">男</label>
        <input type="radio" name="sex" value="女" ><label for="">女</label> <br>
    {{ end }}
    <label for="">状态:</label>

    {{ if eq .Task.STATUS 0 }}
        <input type="radio" name="status" value=0 checked="checked" ><label for="">在上课</label>
        <input type="radio" name="status" value=1><label for="">在休息</label>
        <input type="radio" name="status" value=2><label for="">在吃饭</label> <br>
    {{ end }}
    {{ if eq .Task.STATUS 1 }}
        <input type="radio" name="status" value=0><label for="">在上课</label>
        <input type="radio" name="status" value=1 checked="checked" ><label for="">在休息</label>
        <input type="radio" name="status" value=2><label for="">在吃饭</label> <br>
    {{ end }}
    {{ if eq .Task.STATUS 2 }}
        <input type="radio" name="status" value=0><label for="">在上课</label>
        <input type="radio" name="status" value=1><label for="">在休息</label>
        <input type="radio" name="status" value=2 checked="checked" ><label for="">在吃饭</label> <br>
    {{ end }}
    <label for="">开始时间:</label>    <input type="date" name="starttime" value="{{.Task.STARTTIME}}"> <br/>
    <label for="">结束时间:</label>    <input type="date" name="stoptime" value="{{.Task.STOPTIME}}"> <br/>
    <input type="submit" value="提交">
</form>
</body>
</html>

十八、Redis

18.1、Redis介绍

这里假设读者已经了解并且使用过Redis,只是简单回顾Redis。不做详细说明
redis主要用于:缓存、消息队列以及持久化存储
Redis数据类型主要有:string、hash、list、set、zset、...

其他数据库驱动:https://github.com/golang/go/wiki/SQLDriversRedis学习地址:http://www.redis.cn/documentation.html

Redis是一个开源的Key-Value类型的内存数据库,Redis提供了多种不同类型的数据结构,提供持久化功能

  • Redis的Value支持的数据结构: 字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)、位图(bitmaps)、hyperloglogs、带半径查询和流的地理空间索引等数据结构(geospatial indexes)
  • Redis特性:1、支持pipeline; 2、支持发布订阅范式 ;3、支持lua脚本;4、支持事务;5、支持分片和集群;6、支持分布式锁;等等

18.2、连接Redis

Redis驱动: https://redis.io/clients#go#go get -u github.com/go-redis/redis //安装redis驱动。依赖库出现访问失败#go get github.com/gomodule/redigo //推荐使用

18.3、pipeline

Pipeline 主要是一种网络优化。它本质上意味着客户端缓冲一堆命令并一次性将它们发送到服务器。这些命令不能保证在事务中执行。这样做的好处是节省了每个命令的网络往返时间(RTT)。而 TxPipeline是支持事务的Pipeline

  • 原生批量命令是原子性,Pipeline是非原子性的.
  • 原生批量命令是一个命令对应多个key,Pipeline支持多个命令.
  • 原生批量命令是Redis服务端支持实现的,而Pipeline需要服务端与客户端的共同实现.

Pipeline 基本示例如下:

package main

import (
	"fmt"
	"github.com/go-redis/redis"
	"time"
)

var rdb *redis.Client

// 初始化连接
func initClient() (err error) {
	rdb = redis.NewClient(&redis.Options{
		Addr:     "39.106.26.159:8379",
		Password: "lsyqTVZDfOXm", // no password set
		DB:       0,  // use default DB
	})

	_, err = rdb.Ping().Result()
	if err != nil {
		return err
	}
	return nil
}


func rediSet(){
	err := rdb.Set("score", 100, 0).Err()
	if err != nil {
		fmt.Printf("set score failed, err:%v\n", err)
		return
	}

	val, err := rdb.Get("score").Result()
	if err != nil {
		fmt.Printf("get score failed, err:%v\n", err)
		return
	}
	fmt.Println("score", val)

	val2, err := rdb.Get("name").Result()
	if err == redis.Nil {
		fmt.Println("name does not exist")
	} else if err != nil {
		fmt.Printf("get name failed, err:%v\n", err)
		return
	} else {
		fmt.Println("name", val2)
	}
}


func pipeLine(){
	pipe := rdb.Pipeline()  //创建pipeline
	incr := pipe.Incr("pipeline_counter")  //添加key pipeline_counter
	incr2 := pipe.Incr("pipeline_counter2")  //添加key pipeline_counter2

	pipe.Expire("pipeline_counter", time.Hour)  //设置 key pipeline_counter 的超时时间为 3600秒
	pipe.Expire("pipeline_counter2",60*time.Second)

	defer pipe.Close() //关闭
	_, err := pipe.Exec()
	fmt.Println(incr.Val(), err)  // 4 <nil>
	fmt.Println(incr2.Val(), err) // 1 <nil>
/*
   127.0.0.1:8379> keys *
   1) "pipeline_counter"
   2) "score"
   3) "pipeline_counter2"
   127.0.0.1:8379> ttl "pipeline_counter2"
   (integer) 54
   127.0.0.1:8379> ttl "pipeline_counter1"  //不存在的key
   (integer) -2
   127.0.0.1:8379> ttl "pipeline_counter"
   (integer) 3588
 */
}

func main(){
	//1、建立链接
	err := initClient()
	if err != nil {
		fmt.Println("数据库建立链接失败",err)
	} else {
		defer rdb.Close()
		fmt.Println("redis建立链接成功")
	}
	
	rediSet()
	pipeLine()
}

上面的代码相当于将以下两个命令一次发给redis server端执行,与不使用Pipeline相比能减少一次RTT。

rdb.INCR pipeline_counterrdb.EXPIRE pipeline_counts 3600

也可以使用Pipelined

var incr *redis.IntCmd_, err := rdb.Pipelined(func(pipe redis.Pipeliner) error {	incr = pipe.Incr("pipelined_counter")	pipe.Expire("pipelined_counter", time.Hour)	return nil})fmt.Println(incr.Val(), err)

在某些场景下,当我们有多条命令要执行时,就可以考虑使用pipeline来优化。

18.4、事务

Redis是单线程的,因此单个命令始终是原子的,但是来自不同客户端的两个给定命令可以依次执行,例如在它们之间交替执行。但是,Multi/exec能够确保在multi/exec两个语句之间的命令之间没有其他客户端正在执行命令。

在这种场景我们需要使用TxPipelineTxPipeline总体上类似于上面的Pipeline,但是它内部会使用MULTI/EXEC包裹排队的命令。例如:

pipe := rdb.TxPipeline()incr := pipe.Incr("tx_pipeline_counter")pipe.Expire("tx_pipeline_counter", time.Hour)_, err := pipe.Exec()fmt.Println(incr.Val(), err)

上面代码相当于在一个RTT下执行了下面的redis命令:

MULTIINCR pipeline_counterEXPIRE pipeline_counts 3600EXEC

还有一个与上文类似的TxPipelined方法,使用方法如下:

var incr *redis.IntCmd_, err := rdb.TxPipelined(func(pipe redis.Pipeliner) error {	incr = pipe.Incr("tx_pipelined_counter")	pipe.Expire("tx_pipelined_counter", time.Hour)	return nil})fmt.Println(incr.Val(), err)

18.5、watch

在某些场景下,我们除了要使用MULTI/EXEC命令外,还需要配合使用WATCH命令。在用户使用WATCH命令监视某个键之后,直到该用户执行EXEC命令的这段时间里,如果有其他用户抢先对被监视的键进行了替换、更新、删除等操作,那么当用户尝试执行EXEC的时候,事务将失败并返回一个错误,用户可以根据这个错误选择重试事务或者放弃事务。

Watch(fn func(*Tx) error, keys ...string) error

Watch方法接收一个函数和一个或多个key作为参数。基本使用示例如下:

// 监视watch_count的值,并在值不变的前提下将其值+1key := "watch_count"err = client.Watch(func(tx *redis.Tx) error {	n, err := tx.Get(key).Int()	if err != nil && err != redis.Nil {		return err	}	_, err = tx.Pipelined(func(pipe redis.Pipeliner) error {		pipe.Set(key, n+1, 0)		return nil	})	return err}, key)

最后看一个官方文档中使用GET和SET命令以事务方式递增Key的值的示例:

const routineCount = 100

increment := func(key string) error {
	txf := func(tx *redis.Tx) error {
		// 获得当前值或零值
		n, err := tx.Get(key).Int()
		if err != nil && err != redis.Nil {
			return err
		}

		// 实际操作(乐观锁定中的本地操作)
		n++

		// 仅在监视的Key保持不变的情况下运行
		_, err = tx.Pipelined(func(pipe redis.Pipeliner) error {
			// pipe 处理错误情况
			pipe.Set(key, n, 0)
			return nil
		})
		return err
	}

	for retries := routineCount; retries > 0; retries-- {
		err := rdb.Watch(txf, key)
		if err != redis.TxFailedErr {
			return err
		}
		// 乐观锁丢失
	}
	return errors.New("increment reached maximum number of retries")
}

var wg sync.WaitGroup
wg.Add(routineCount)
for i := 0; i < routineCount; i++ {
	go func() {
		defer wg.Done()

		if err := increment("counter3"); err != nil {
			fmt.Println("increment error:", err)
		}
	}()
}
wg.Wait()

n, err := rdb.Get("counter3").Int()
fmt.Println("ended with", n, err)

十九、nsq消息队列

NSQ是Go语言编写的一个开源的实时分布式内存消息队列,其性能十分优异。 NSQ的优势有以下优势:

  1. NSQ提倡分布式和分散的拓扑,没有单点故障,支持容错和高可用性,并提供可靠的消息交付保证
  2. NSQ支持横向扩展,没有任何集中式代理。
  3. NSQ易于配置和部署,并且内置了管理界面。
  • nsqd: 是一个守护进程,它接收、排队并向客户端发送消息 ./nsqd -broadcast-address=127.0.0.1
  • nsqlookupd: 维护所有nsqd状态、提供服务发现的守护进程。它能为消费者查找特定topic下的nsqd提供了运行时的自动发现服务。 它不维持持久状态,也不需要与任何其他nsqlookupd实例协调以满足查询。因此根据你系统的冗余要求尽可能多地部署nsqlookupd节点。它们消耗的资源很少,可以与其他服务共存。我们的建议是为每个数据中心运行至少3个集群。
  • nsqadmin: 一个实时监控集群状态、执行各种管理任务的Web管理平台。 启动nsqadmin,指定nsqlookupd地址: ./nsqadmin -lookupd-http-address=127.0.0.1:4161

应用场景:异步处理、应用解耦、流量削峰

NSQ快速开始: https://nsq.io/overview/quick_start.html

19.1、nsqd概述

  • nsqd是接收、queue和向客户端传递消息的守护进程。可独立运行,通常在具有nslookupd实例的集群中配置(用于发布topics以及channel)
  • nsqlookupd: 管理拓扑信息的守护进程,客户端查询nslookupd用来发现topic 对应的 nsqd producers以及nsqd节点广播的topics和 channel信息
  • nsqadmin: 是一个web管理界面看,查看集群消息和各种管理任务
  • 工具:
    • nsq_stat :Polls /stats for all the producers of the specified topic/channel and displays aggregate stats
    • nsq_tail : Consumes the specified topic/channel and writes to stdout (in the spirit of tail(1))
    • nsq_to_file :Consumes the specified topic/channel and writes out to a newline delimited file, optionally rolling and/or compressing the file.
    • nsq_to_http :Consumes the specified topic/channel and performs HTTP requests (GET/POST) to the specified endpoints.
    • nsq_to_nsq :Consumes the specified topic/channel and re-publishes the messages to destination nsqd via TCP.
    • to_nsq :Takes a stdin stream and splits on newlines (default) for re-publishing to destination nsqd via TCP.

启动方式,参考:https://nsq.io/components/nsqd.html

19.2、topic和channel

每个nsqd实例旨在一次处理多个数据流。这些数据流称为“topics”,一个topic具有1个或多个“channels”。每个channel都会收到topic所有消息的副本,实际上下游的服务是通过对应的channel来消费topic消息。

topicchannel不是预先配置的。topic在首次使用时创建,方法是将其发布到指定topic,或者订阅指定topic上的channelchannel是通过订阅指定的channel在第一次使用时创建的。

topicchannel都相互独立地缓冲数据,防止缓慢的消费者导致其他chennel的积压(同样适用于topic级别)。

channel可以并且通常会连接多个客户端。假设所有连接的客户端都处于准备接收消息的状态,则每条消息将被传递到随机客户端。例如:

总而言之,消息是从topic -> channel(每个channel接收该topic的所有消息的副本)多播的,但是从channel -> consumers均匀分布(每个消费者接收该channel的一部分消息)

19.3、发送和接收消息

  • 消息默认不持久化,可以配置成持久化模式。nsq采用的方式时内存+硬盘的模式,当内存到达一定程度时就会将数据持久化到硬盘。
    • 如果将--mem-queue-size设置为0,所有的消息将会存储到磁盘。
    • 服务器重启时也会将当时在内存中的消息持久化。
  • 每条消息至少传递一次。
  • 消息不保证有序。

19.4、go操作nsq

1、启动

新开三个终端:终端1:#nsqlookupd终端2:#nsqd --lookupd-tcp-address=172.25.2.221:4160终端3:#nsqadmin --lookupd-http-address=172.25.2.221:4161浏览器访问: http://172.25.2.221:4171

安装依赖:go get -u github.com/nsqio/go-nsq

2、生产者

/*
1、构建nsq.NewProducer()对象
2、producer.Publish() //发布消息
*/

package main

import (
	"bufio"
	"fmt"
	"github.com/nsqio/go-nsq"
	"os"
	"strings"
)

var producer *nsq.Producer


func initProducer(str string )(err error){
	config := nsq.NewConfig()
	producer,err = nsq.NewProducer(str,config) 
	if err != nil {
		fmt.Println("创建producer失败,",err)
		return err
	}
	return  nil
}

func nsqProducer(){
	nsqAddress := "39.106.26.159:4150"
	err := initProducer(nsqAddress)
	if err != nil {
		fmt.Println("初始化失败,",err)
	}

	reader := bufio.NewReader(os.Stdin)
	for {
		data ,err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("读取数据有问题",err)
			continue
		}
		data = strings.TrimSpace(data)
		if strings.ToUpper(data) == "Q" {
			break
		}
		err = producer.Publish("topic_demo",[]byte(data))
		if err != nil {
			fmt.Println("发布消息失败",err)
			continue
		}
	}
}

func main(){
	nsqProducer()
}

3、消费者

/*
1、 nsq.NewConsumer 构建消费者对象c 
2、c.AddHandler(consumer) //consumer是一个接口,需要实现方法 HandleMessage(message *Message) error  
	函数用于执行对消息本身的操作,#消费者对象 注册消息处理函数
3、 c.ConnectToNSQD(address); //连接到nsqd
*/

package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/nsqio/go-nsq"
)

// NSQ Consumer Demo

// MyHandler 是一个消费者类型
type MyHandler struct {
	Title string
}

// HandleMessage 是需要实现的处理消息的方法
func (m *MyHandler) HandleMessage(msg *nsq.Message) (err error) {
	fmt.Printf("%s recv from %v, msg:%v\n", m.Title, msg.NSQDAddress, string(msg.Body))
	return
}

// 初始化消费者
func initConsumer(topic string, channel string, address string) (err error) {
	config := nsq.NewConfig()
	config.LookupdPollInterval = 15 * time.Second
	c, err := nsq.NewConsumer(topic, channel, config)
	if err != nil {
		fmt.Printf("create consumer failed, err:%v\n", err)
		return
	}
	consumer := &MyHandler{
		Title: "测试01",
	}
	c.AddHandler(consumer)

	if err := c.ConnectToNSQD(address); err != nil { // 直接连NSQD
	//if err := c.ConnectToNSQLookupd(address); err != nil { // 通过lookupd查询
		return err
	}
	return nil

}

func main() {
	err := initConsumer("topic_demo", "first", "39.106.26.159:4150")
	if err != nil {
		fmt.Printf("init consumer failed, err:%v\n", err)
		return
	}
	c := make(chan os.Signal)        // 定义一个信号的通道
	signal.Notify(c, syscall.SIGINT) // 转发键盘中断信号到c
	<-c                              // 阻塞
}

二十、ETCD

20.1、ETCD概述

etcd 是一个高可用强一致性的键值仓库在很多分布式系统架构中得到了广泛的应用,其最经典的使用场景就是服务发现。

作为一个受到 ZooKeeper启发而催生的项目,它除了拥有与之类似的功能外,更专注于以下四点。

  • 简单:易于部署,易使用。基于 HTTP+JSON 的 API 让你用 curl 就可以轻松使用。
  • 安全:可选 SSL 客户认证机制。
  • 快速:每个实例每秒支持一千次写操作。
  • 可信:使用一致性 Raft 算法充分实现了分布式。

适用于,存储配置信息,数据量小但是更新和访问频次都很高的数据进行存储。

服务发现要解决的是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接。本质上来说,服务发现就是想要了解集群中是否有进程在监听 udp 或 tcp 端口,并且通过名字就可以查找和连接。要解决服务发现的问题,需要有下面三大支柱,缺一不可。

  • 一个强一致性、高可用的服务存储目录。基于 Raft 算法的 etcd 就是一个强一致性高可用的服务存储目录。
  • 一种注册服务和监控服务健康状态的机制。用户可以在 etcd 中注册服务,并且对注册的服务设置 key TTL,定时保持服务的心跳以达到监控健康状态的效果。
  • 一种查找和连接服务的机制**。通过在 etcd 指定的主题(由服务名称构成的服务目录)下注册的服务也能在对应的主题下查找到。

20.1.1、组件

摘自链接:https://cloud.tencent.com/developer/article/1532452

图片

从 etcd 的架构图中我们可以看到,etcd 主要分为四个部分。

  • HTTP Server:用于处理用户发送的 API 请求以及其它 etcd 节点的同步与心跳信息请求。
  • Store:用于处理 etcd 支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等,是 etcd 对用户提供的大多数 API 功能的具体实现。
  • Raft:Raft 强一致性算法的具体实现,是 etcd 的核心。
  • WAL:Write Ahead Log(预写式日志),是 etcd 的数据存储方式。除了在内存中存有所有数据的状态以及节点的索引以外,etcd 就通过 WAL 进行持久化存储。WAL 中,所有的数据提交前都会事先记录日志。Snapshot 是为了防止数据过多而进行的状态快照;Entry 表示存储的具体日志内容。

通常,一个用户的请求发送过来,会经由 HTTP Server 转发给 Store 进行具体的事务处理,如果涉及到节点的修改,则交给 Raft 模块进行状态的变更、日志的记录,然后再同步给别的 etcd 节点以确认数据提交,最后进行数据的提交,再次同步。

  • 数据流向:etcd集群中所有的数据流向都是一个方向,从 Leader (主节点)流向 Follower。保证强一致性
  • 数据写入:
    • 如果写入请求来自Leader节点即可直接写入然后Leader节点会把写入分发给所有Follower
    • 如果写入请求来自其他Follower节点那么写入请求会给转发给Leader节点,由Leader节点写入之后再分发给集群上的所有其他节点。
  • 数据读取:所有节点均可读取

20.1.2、Leader选举

Raft算法使用随机Timer来初始化Leader选举流程。比如说在上面三个节点上都运行了Timer(每个Timer的持续时间是随机的),第一个节点率先完成了Timer,随后它就会向其他两个节点发送成为Leader的请求,其他节点接收到请求后会以投票回应然后第一个节点被选举为Leader。

成为Leader后,该节点会以固定时间间隔向其他节点发送通知,确保自己仍是Leader。有些情况下当Follower们收不到Leader的通知后,比如说Leader节点宕机或者失去了连接,其他节点会重复之前选举过程选举出新的Leader。

  • 判断写入是否成功?

etcd认为写入请求被Leader节点处理并分发给了多数节点后,就是一个成功的写入。如何界定多数节点呢?很简单,假设总结点数是N,那么多数节点 Majority=N/2+1,不过在etcd中使用的术语是 Quorum(法定人数)而不是 Majority(大部分)。所以界定多数节点的公式是 Quorum=N/2+1

注意:etcd客户端api本分为v2v3,版本差异较大,使用etcdctl默认为v2版本,如果想要使用客户端v3示例: ETCDCTL_API=3 etcdctl -h

20.2、go访问etcd

20.2.1、安装

安装: go get go.etcd.io/etcd/clientv3 这里使用第一个

报错:

D:\Program_language\Project1\src\mysql>    go get go.etcd.io/etcd/clientv3
go: found go.etcd.io/etcd/clientv3 in go.etcd.io/etcd v3.3.27+incompatible
# github.com/coreos/etcd/clientv3/balancer/resolver/endpoint
..\..\pkg\mod\github.com\coreos\etcd@v3.3.27+incompatible\clientv3\balancer\resolver\endpoint\endpoint.go:114:78: undefined: resolver.BuildOption
..\..\pkg\mod\github.com\coreos\etcd@v3.3.27+incompatible\clientv3\balancer\resolver\endpoint\endpoint.go:182:31: undefined: resolver.ResolveNowOption
# github.com/coreos/etcd/clientv3/balancer/picker
..\..\pkg\mod\github.com\coreos\etcd@v3.3.27+incompatible\clientv3\balancer\picker\err.go:37:44: undefined: balancer.PickOptions
..\..\pkg\mod\github.com\coreos\etcd@v3.3.27+incompatible\clientv3\balancer\picker\roundrobin_balanced.go:55:54: undefined: balancer.PickOptions

大概是说原因是google.golang.org/grpc 1.26后的版本是不支持clientv3的。

在go.mod文件最后添加,
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0

补充:context包

Context是作用于goroutine的上下文,如多个goroutine同时执行一个任务,多个条件满足一个即可退出。其中一个goroutine满足后,调整context,其他goroutine通过context得知并自动退出。

Background和TODO只是用于不同场景下: Background通常被用于主函数、初始化以及测试中,作为一个顶层的context,也就是说一般我们创建的context都是基于Background;而TODO是在不确定使用什么context的时候才会使用。

20.2.2、初始化链接

package main

import (
	"context"
	"fmt"
	"go.etcd.io/etcd/clientv3"
	"log"
	"time"
)

var etcdClient *clientv3.Client

func iniClient(){
	//1、初始化链接,初始化注意关闭
	var err error
	etcdClient,err = clientv3.New(clientv3.Config{
		Endpoints: []string{"39.106.26.159:8379"},
		DialTimeout: 5 * time.Second,
	})
	if err != nil {
		fmt.Println("connect to etcd server error :", err)
	} else  {
		fmt.Println("connect to etcd server success !")
	}
}

20.3、常见操作

20.3.1、Client

返回的 client,它的类型具体如下:

type Client struct {
    Cluster
    KV
    Lease
    Watcher
    Auth
    Maintenance
    // Username is a user name for authentication.
    Username string
    // Password is a password for authentication.
    Password string
    // contains filtered or unexported fields
}

类型中的成员是etcd客户端几何核心功能模块的具体实现,它们分别用于:

  • Cluster:向集群里增加etcd服务端节点之类,属于管理员操作。
  • KV:我们主要使用的功能,即K-V键值库的操作。
  • Lease:租约相关操作,比如申请一个TTL=10秒的租约(应用给key可以实现键值的自动过期)。
  • Watcher:观察订阅,从而监听最新的数据变化。
  • Auth:管理etcd的用户和权限,属于管理员操作。
  • Maintenance:维护etcd,比如主动迁移etcd的leader节点,属于管理员操作。

Client.KV是一个interface,提供了关于k-v操作的所有方法:

type KV interface {
	Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)
    //ctx: goroutine的上下文,后面是key和value。对于etcd来说,key=/test/key1只是一个字符串而已,但是对我们而言却可以模拟出目录层级关系
    //opts 可以添加租约,
    PutResponse:
    type (
       CompactResponse pb.CompactionResponse
       PutResponse     pb.PutResponse
       GetResponse     pb.RangeResponse
       DeleteResponse  pb.DeleteRangeResponse
       TxnResponse     pb.TxnResponse
    )
    
	Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)
    //例如rangeResp, err := kv.Get(context.TODO(), "/test/", clientv3.WithPrefix()) ,获取包含 /test/下的所有子元素 
    //etcd是一个有序的k-v存储,因此/test/为前缀的key总是顺序排列在一起。
    
	Delete(ctx context.Context, key string, opts ...OpOption) (*DeleteResponse, error)
	Compact(ctx context.Context, rev int64, opts ...CompactOption) (*CompactResponse, error)
    
	// Do applies a single Op on KV without a transaction.
	// Do is useful when creating arbitrary operations to be issued at a
	// later time; the user can range over the operations, calling Do to
	// execute them. Get/Put/Delete, on the other hand, are best suited
	// for when the operation should be issued at the time of declaration.
	Do(ctx context.Context, op Op) (OpResponse, error)  //接收一Op对象
	Txn(ctx context.Context) Txn
}

20.3.2、OP

Op (操作operation) 是一个抽象的操作,可以是Put/Get/Delete…;而OpResponse是一个抽象的结果,可以是PutResponse/GetResponse…

可以通过Client中定义的一些方法来创建Op:

  • func OpDelete(key string, opts …OpOption) Op
  • func OpGet(key string, opts …OpOption) Op
  • func OpPut(key, val string, opts …OpOption) Op
  • func OpTxn(cmps []Cmp, thenOps []Op, elseOps []Op) Op

其实和直接调用KV.Put,KV.GET没什么区别。

下面是一个例子:

cli, err := clientv3.New(clientv3.Config{
    Endpoints:   endpoints,
    DialTimeout: dialTimeout,
})
if err != nil {
    log.Fatal(err)
}
defer cli.Close()

ops := []clientv3.Op{
    clientv3.OpPut("put-key", "123"),
    clientv3.OpGet("put-key"),
    clientv3.OpPut("put-key", "456")}

for _, op := range ops {
    if _, err := cli.Do(context.TODO(), op); err != nil {
        log.Fatal(err)
    }
}

把Op交给Do方法执行,返回的opResp结构如下:

type OpResponse struct {
    put *PutResponse
    get *GetResponse
    del *DeleteResponse
    txn *TxnResponse
}

你的操作是什么类型,你就用哪个指针来访问对应的结果。

20.3.3、Lease

Client.Lease租约

type Lease interface {
	// Grant creates a new lease.
	Grant(ctx context.Context, ttl int64) (*LeaseGrantResponse, error)
	//自动过期,
    //grantResp, err := lease.Grant(context.TODO(), 10)
    //kv.Put(context.TODO(), "/test/vanish", "vanish in 10s", clientv3.WithLease(grantResp.ID))
	//自动续约
    //keepResp, err := lease.KeepAliveOnce(context.TODO(), grantResp.ID)
    或者使用 KeepAlive()方法,其会返回 <-chan *LeaseKeepAliveResponse只读通道,每次自动续租成功后会向通道中发送信号。一般都用 KeepAlive()方法,KeepAlive和Put一样,如果在执行之前Lease就已经过期了,那么需要重新分配Lease。etcd并没有提供API来实现原子的Put with Lease,需要我们自己判断err重新分配Lease。
    
	// Revoke revokes the given lease.
	Revoke(ctx context.Context, id LeaseID) (*LeaseRevokeResponse, error)

	// TimeToLive retrieves the lease information of the given lease ID.
	TimeToLive(ctx context.Context, id LeaseID, opts ...LeaseOption) (*LeaseTimeToLiveResponse, error)

	// Leases retrieves all leases.
	Leases(ctx context.Context) (*LeaseLeasesResponse, error)

	// KeepAlive keeps the given lease alive forever. If the keepalive response
	// posted to the channel is not consumed immediately, the lease client will
	// continue sending keep alive requests to the etcd server at least every
	// second until latest response is consumed.
	//
	// The returned "LeaseKeepAliveResponse" channel closes if underlying keep
	// alive stream is interrupted in some way the client cannot handle itself;
	// given context "ctx" is canceled or timed out. "LeaseKeepAliveResponse"
	// from this closed channel is nil.
	//
	// If client keep alive loop halts with an unexpected error (e.g. "etcdserver:
	// no leader") or canceled by the caller (e.g. context.Canceled), the error
	// is returned. Otherwise, it retries.
	//
	// TODO(v4.0): post errors to last keep alive message before closing
	// (see https://github.com/coreos/etcd/pull/7866)
	KeepAlive(ctx context.Context, id LeaseID) (<-chan *LeaseKeepAliveResponse, error)

	// KeepAliveOnce renews the lease once. The response corresponds to the
	// first message from calling KeepAlive. If the response has a recoverable
	// error, KeepAliveOnce will retry the RPC with a new keep alive message.
	//
	// In most of the cases, Keepalive should be used instead of KeepAliveOnce.
	KeepAliveOnce(ctx context.Context, id LeaseID) (*LeaseKeepAliveResponse, error)

	// Close releases all resources Lease keeps for efficient communication
	// with the etcd server.
	Close() error
}

Lease提供了以下功能:

  • Grant:分配一个租约。
  • Revoke:释放一个租约。
  • TimeToLive:获取剩余TTL时间。
  • Leases:列举所有etcd中的租约。
  • KeepAlive:自动定时的续约某个租约。
  • KeepAliveOnce:为某个租约续约一次。
  • Close:释放当前客户端建立的所有租约。

20.3.4、Watch

Watch用于监听某个键的变化, Watch调用后返回一个 WatchChan,它的类型声明如下:

type WatchChan <-chan WatchResponse

type WatchResponse struct {
    Header pb.ResponseHeader
    Events []*Event

    CompactRevision int64

    Canceled bool

    Created bool
}

当监听的key有变化后会向 WatchChan发送 WatchResponse。Watch的典型应用场景是应用于系统配置的热加载,我们可以在系统读取到存储在etcd key中的配置后,用Watch监听key的变化。在单独的goroutine中接收WatchChan发送过来的数据,并将更新应用到系统设置的配置变量中,比如像下面这样在goroutine中更新变量appConfig,这样系统就实现了配置变量的热加载。

type AppConfig struct {
  config1 string
  config2 string
}

var appConfig Appconfig

func watchConfig(clt *clientv3.Client, key string, ss interface{}) {
    watchCh := clt.Watch(context.TODO(), key)
    go func() {
        for res := range watchCh {
            value := res.Events[0].Kv.Value
            if err := json.Unmarshal(value, ss); err != nil {
                fmt.Println("now", time.Now(), "watchConfig err", err)
                continue
            }
            fmt.Println("now", time.Now(), "watchConfig", ss)
        }
    }()
}

watchConfig(client, "config_key", &appConfig)

20.3.5、Txn事务

etcd中事务是原子执行的,只支持if … then … else …这种表达。首先来看一下Txn中定义的方法:

type Txn interface {
    // If takes a list of comparison. If all comparisons passed in succeed,
    // the operations passed into Then() will be executed. Or the operations
    // passed into Else() will be executed.
    If(cs ...Cmp) Txn

    // Then takes a list of operations. The Ops list will be executed, if the
    // comparisons passed in If() succeed.
    Then(ops ...Op) Txn

    // Else takes a list of operations. The Ops list will be executed, if the
    // comparisons passed in If() fail.
    Else(ops ...Op) Txn

    // Commit tries to commit the transaction.
    Commit() (*TxnResponse, error)
}

Txn必须是这样使用的:If(满足条件) Then(执行若干Op) Else(执行若干Op)。

If中支持传入多个Cmp比较条件,如果所有条件满足,则执行Then中的Op(上一节介绍过Op),否则执行Else中的Op。

首先,我们需要开启一个事务,这是通过KV对象的方法实现的:

txn := kv.Txn(context.TODO())

下面的测试程序,判断如果k1的值大于v1并且k1的版本号是2,则Put 键值k2和k3,否则Put键值k4和k5。

kv.Txn(context.TODO()).If(
 clientv3.Compare(clientv3.Value(k1), ">", v1),
 clientv3.Compare(clientv3.Version(k1), "=", 2)
).Then(
 clientv3.OpPut(k2,v2), clentv3.OpPut(k3,v3)
).Else(
 clientv3.OpPut(k4,v4), clientv3.OpPut(k5,v5)
).Commit()

类似于clientv3.Value()\用于指定key属性的,有这么几个方法:

  • func CreateRevision(key string) Cmp:key=xxx的创建版本必须满足…
  • func LeaseValue(key string) Cmp:key=xxx的Lease ID必须满足…
  • func ModRevision(key string) Cmp:key=xxx的最后修改版本必须满足…
  • func Value(key string) Cmp:key=xxx的创建值必须满足…
  • func Version(key string) Cmp:key=xxx的累计更新次数必须满足…

20.4、示例

package main

import (
	"context"
	"fmt"
	"go.etcd.io/etcd/clientv3"
	"log"
	"time"
)

var etcdClient *clientv3.Client

func iniClient(){
	//1、初始化链接,初始化注意关闭
	var err error
	etcdClient,err = clientv3.New(clientv3.Config{
		Endpoints: []string{"33.33.33.33:8379"},
		DialTimeout: 5 * time.Second,
	})
	if err != nil {
		fmt.Println("connect to etcd server error :", err)
	} else  {
		fmt.Println("connect to etcd server success !")
	}
}

func etcdPutGet(){
	/*
	etcd v3 uses gRPC for remote procedure calls. And clientv3 uses grpc-go to connect to etcd. Make sure to close the client after using it. If the client is not closed, the connection will have leaky goroutines. To specify client request timeout, pass context.WithTimeout to APIs:
	*/

	//PUT
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	resp, err := etcdClient.Put(ctx, "NO1", "令狐冲")

	cancel()
	if err != nil {
		fmt.Println("put数据失败",err)
		return
	} else {
		fmt.Println("resp:",resp)
	}

	//GET
	ctx, cancel = context.WithTimeout(context.Background(), time.Second)
	resp2, err := etcdClient.Get(ctx, "NO1")
	cancel()
	if err != nil {
		fmt.Println("put数据失败",err)
		return
	}	else {
		fmt.Println("resp:",resp2)
	}
	for _, ev := range resp2.Kvs {
		fmt.Printf("%s:%s\n", ev.Key, ev.Value)
	}
/*
   D:\Program_language\Project1\src\mysql>etcd.exe
   connect to etcd server success !
   resp: &{cluster_id:14841639068965178418 member_id:10276657743932975437 revision:2 raft_term:2  <nil> {} [] 0}
   resp: &{cluster_id:14841639068965178418 member_id:10276657743932975437 revision:2 raft_term:2  [key:"NO1" create_revision:2 mod_revision:2 version:1 value:"\344\273\244\347\213\220\345
   \206\262" ] false 1 {} [] 0}
   NO1:令狐冲

 */
}

func Watch(){
	//ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
	//rch := etcdClient.Watch(ctx,"NO1")
	//这两句,结合 cancel() ,实现监控 5s超时,默认不超时

	rch := etcdClient.Watch(context.Background(),"NO1")  //不超时的写法
	for v := range rch {
		for _,ev := range v.Events {
			fmt.Printf("Type: %s Key:%s Value:%s\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
		}
	}


	// cancel()

	/*
	[root@iZ2zef0llgs69lx3vc9rfgZ ~]# ETCDCTL_API=3 etcdctl --endpoints=172.25.2.221:8379 put NO1 "岳不群"
	OK
	[root@iZ2zef0llgs69lx3vc9rfgZ ~]# ETCDCTL_API=3 etcdctl --endpoints=172.25.2.221:8379 del NO1
	1
	[root@iZ2zef0llgs69lx3vc9rfgZ ~]# ETCDCTL_API=3 etcdctl --endpoints=172.25.2.221:8379 put NO1 "东方不败"
	OK

	执行的输出对应为:
	D:\Program_language\Project1\src\mysql>etcd.exe
	connect to etcd server success !
	Type: PUT Key:NO1 Value:岳不群
	Type: DELETE Key:NO1 Value:
	Type: PUT Key:NO1 Value:东方不败
	*/
}


func putLease(){
	resp,err := etcdClient.Grant(context.TODO(),5)  //在5秒的超时时间
	if err != nil {
		fmt.Println("grant失败:",err)
	}

	r,err := etcdClient.Put(context.TODO(),"NO2","左冷禅",clientv3.WithLease(resp.ID))
	if err != nil {
		fmt.Println("put数据失败",err)
	} else {
		fmt.Println(r)
	}
}

func keepLease(){
	resp, err := etcdClient.Grant(context.TODO(), 5)
	if err != nil {
		log.Fatal(err)
	}

	_, err = etcdClient.Put(context.TODO(), "NO3", "任我行", clientv3.WithLease(resp.ID))
	if err != nil {
		log.Fatal(err)
	}

	// the key 'foo' will be kept forever
	ch, kaerr := etcdClient.KeepAlive(context.TODO(), resp.ID)
	if kaerr != nil {
		fmt.Println(kaerr)
	}
	for {
		ka := <-ch
		fmt.Println("ttl:", ka.TTL)
	}
}

func main(){
	iniClient()  //1、初始化client
	defer etcdClient.Close()  //打开注意关闭

	//etcdPutGet()  //2、etcd基础操作put和get
	//Watch()  //3、etcd watch操作
	//putLease()  //4、带租约的put
	keepLease() //5、keep lease
}

二十一、数据结构

有时间再补充

21.1、数据结构

链表

队列

二叉树

散列表

21.2、常见算法

冒泡排序

快速排序

二分查找

选择排序

posted @ 2021-10-19 19:47  MT_IT  阅读(296)  评论(0编辑  收藏  举报