Go基础(四)之单元测试、反射、网络编程、操作Redis、操作MySQL

本文详细介绍了Go语言中的单元测试、反射机制、网络编程(TCP与UDP)以及如何操作Redis和MySQL数据库。在单元测试部分,阐述了测试框架和测试函数的使用;反射部分讲解了如何获取和修改变量信息,以及调用函数和方法;网络编程部分展示了TCP和UDP通信的基本操作;在数据库操作方面,不仅演示了如何连接和操作MySQL,还讨论了连接池和事务处理。此外,还提到了操作Redis的redigo库。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、单元测试

Go语言中自带有一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试。

  • 我们执行go test命令时,它会遍历该go包中所有以_test.go结尾的测试文件, 然后调用并执行测试文件中符合go test 规则的函数帮助我们实现自动化测试。

  • 其过程为生成1个临时的main包用于调用相应的测试函数,然后构建并运行测试文件中的函数、报告测试结果,最后清理测试中生成的临时文件。

单元测试的注意:

  • 在包目录内所有测试文件必须以_test.go结尾go build不会把这些测试文件编译到最终的可执行文件中。比如 cal_test.go , cal不是固定的

  • 测试用例函数必须以Test 开头,一般来说就是Test+被测试的函数名。即测试函数格式:

    func TestXxxx(*testing.T){...}
    
  • 每个单元测试函数的参数必须为*testing.T,参数t用于报告测试是否失败以及日志信息。

  • 运行测试用例指令

    • cmd>go test[如果运行正确,无日志,错误时,会输出日志]
    • cmd>go test -v[运行正确或是错误,都输出日志]
  • 当出现错误时,可以使用t.Fatalf来格式化输出错误信息,并退出程序

  • t.Logf方法可以输出相应的日志

  • PASS表示测试用例运行成功,FAIL表示测试用例运行失败

  • 测试单个文件,一定要带上被测试的原文件
    go test -v cal_test.go cal.go

  • 测试单个方法
    go test -v -test.run TestAddUpper

测试案例:

  • 文件结构:

    image-20220302175540118

  • 测试文件

    package cal
    import (
    	"fmt"
    	"testing" //引入go 的testing框架包
    )
    
    //编写要给测试用例,去测试addUpper是否正确
    func TestAddUpper(t *testing.T) {
    
    	//调用
    	res := addUpper(10)
    	if res != 55 {
    		//fmt.Printf("AddUpper(10) 执行错误,期望值=%v 实际值=%v\n", 55, res)
    		t.Fatalf("AddUpper(10) 执行错误,期望值=%v 实际值=%v\n", 55, res)
    	}
    
    	//如果正确,输出日志
    	t.Logf("AddUpper(10) 执行正确...")
    
    }
    
    func TestHello(t *testing.T) {
    
    	fmt.Println("TestHello被调用..")
    
    }
    
  • 待测试文件:

    package cal
    
    //一个被测试函数
    func addUpper(n int) int {
    	res := 0
    	for i := 1; i <= n; i++ {
    		res += i
    	}
    	return res
    }
    
    
  • 执行结果:

    image-20220302175641782

子测试:

  • 如果一个测试函数的函数名的不是以 Test 开头,那么在使用 go test 命令时默认不会执行,不过我们可以设置该函数时一个子测试函数,可以在其他测试函数里通过 t.Run 方法来执行子测试函数

    image-20220321204209776

TestMain(m *testing.M):

  • 测试文件中有 TestMain(m *testing.M)函数时,执行 go test 命令将直接运行 TestMain函数,不直接运行测试函数,只有在 TestMain 函数中执行 m.Run()时才会执行测试函数

    image-20220321204747633

二、反射

好的博客:

Go语言反射reflect

Go语言反射(reflect)

  • 反射可以在运行时动态获取变量的各种信息,比如变量的类型(type),类别(kind)
  • 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段、方法)
  • 通过反射,可以修改变量的值,可以调用关联的方法。
  • 使用反射,需要import (“reflect”)

2.1 两个重要函数和类型

两个重要的函数:

  • func TypeOf(i interface{}) Type

    TypeOf返回接口中保存的值的类型,TypeOf(nil)会返回nil。

  • Copyfunc ValueOf(i interface{}) Value

    ValueOf返回一个初始化为i接口保管的具体值的Value,ValueOf(nil)返回Value零值。

两个类型是 reflect.Typereflect.Value

变量、interface和 reflect.Value的相互转换:

image-20220315211740674

反射快速入门案例:

package main

import (
	"reflect"
	"fmt"
)

//专门演示反射
func reflectTest01(b interface{}) {
	//通过反射获取的传入的变量的 type ,kind, 值
	//1. 先获取到 reflect.Type
	rType := reflect.TypeOf(b)
	fmt.Println("rType=", rType) //rType= int
	
	//2. 获取到reflect.Value
	rVal := reflect.ValueOf(b)
	fmt.Printf("rVal=%v,rVal的type=%T\n", rVal, rVal) //rVal=100,rVal的type=reflect.Value

	n2 := 2 + rVal.Int() //不能写成 2 + rVal
	fmt.Println("n2=", n2) //n2= 102
	
	//下面我们将 rVal 转成 interface{}
	iV := rVal.Interface()
	//将 interface{} 通过断言转成需要的类型
	num2 := iV.(int)
	fmt.Printf("num2=%v,num2的type=%T\n", num2, num2) //num2=100,num2的type=int

	
}

//专门演示反射[对结构体的反射]
func reflectTest02(b interface{}) {

	//通过反射获取的传入的变量的 type , kind, 值
	//1. 先获取到 reflect.Type
	rType := reflect.TypeOf(b)
	fmt.Println("rType=", rType) //rType= main.Student

	//2. 获取到reflect.Value
	rVal := reflect.ValueOf(b)

	//3. 获取变量对应的Kind,下面两个得到的值一样
	kind2 := rType.Kind()
	kind1 := rVal.Kind()
	
	fmt.Printf("kind =%v kind=%v\n", kind1, kind2) //kind =struct kind=struct


	//下面我们将 rVal 转成 interface{}
	iV := rVal.Interface()
	fmt.Printf("iv=%v,iv的type=%T \n", iV, iV) //iv={tom 20},iv的type=main.Student
	//将 interface{} 通过断言转成需要的类型
	//这里,我们就简单使用了一带检测的类型断言.
	//同学们可以使用 swtich 的断言形式来做的更加的灵活
	stu, ok := iV.(Student)
	if ok {
		fmt.Printf("stu.Name=%v\n", stu.Name) //stu.Name=tom
	}


}

type Student struct {
	Name string
	Age int
}

func main() {

	//1. 先定义一个int
	var num int = 100
	reflectTest01(num)
	
	fmt.Println("-----------")

	//2. 定义一个Student的实例
	stu := Student{
		Name : "tom",
		Age : 20,
	}
	reflectTest02(stu)

}

执行结果:

image-20220315204805258

注意:使用反射的方式来获取变量的值(并返回对应的类型),要求数据类型匹配,比如x是int那么就应该使用reflect.Value(x).Int(),而不能使用其它的,否则报panic

2.2 类型(Type)与种类(Kind)

  • Go 程序中的类型(Type)指的是系统原生数据类型,如 int、string、bool、float32 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称。例如使用 type A struct{} 定义结构体时,A 就是 struct{} 的类型。

  • 种类(Kind)指的是对象归属的品种,在 reflect 包中有如下定义:

    const (
        Invalid Kind = iota
        Bool
        Int
        Int8
        Int16
        Int32
        Int64
        Uint
        Uint8
        Uint16
        Uint32
        Uint64
        Uintptr
        Float32
        Float64
        Complex64
        Complex128
        Array
        Chan
        Func
        Interface
        Map
        Ptr
        Slice
        String
        Struct
        UnsafePointer
    )
    

比如:var num int = 10,numTypeint, Kind 也是 int

比如: var stu Student stuTypepackageXXX.Student , Kindstruct

2.3 通过反射获取值信息

2.3.1 从反射值对象获取值

  • 反射值获取原始值的方法:

    方法名说 明
    Interface() interface{}将值以 interface{} 类型返回,可以通过类型断言转换为指定类型
    Int() int64将值以 int 类型返回,所有有符号整型均可以此方式返回
    Uint() uint64将值以 uint 类型返回,所有无符号整型均可以此方式返回
    Float() float64将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回
    Bool() bool将值以 bool 类型返回
    Bytes() []bytes将值以字节数组 []bytes 类型返回
    String() string将值以字符串类型返回
  • 代码示例:

    package main
    
    import (
    	"fmt"
    	"reflect"
    )
    
    func main() {
    
    	//声明整型变量a并赋初值
    	var a int = 1024
    
    	//获取变量a的反射值对象
    	valueOfA := reflect.ValueOf(a)
    
    	//获取interface{}类型的值,通过类型断言转换
    	var getA int = valueOfA.Interface().(int)
    
    	//获取64位的值,强制类型转换为int类型
    	var getB int = int(valueOfA.Int())
    
    	fmt.Println(getA, getB)	//1024 1024
    }
    

2.3.2 通过反射访问结构体成员的值

  • 反射值对象(reflect.Value)提供对结构体访问的方法,通过这些方法可以完成对结构体任意值的访问,如下表所示:

    方 法备 注
    Field(i int) Value根据索引,返回索引对应的结构体成员字段的反射值对象。当值不是结构体或索引超界时发生宕机
    NumField() int返回结构体成员字段数量。当值不是结构体或索引超界时发生宕机
    FieldByName(name string) Value根据给定字符串返回字符串对应的结构体字段。没有找到时返回零值,当值不是结构体或索引超界时发生宕机
    FieldByIndex(index []int) Value多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的值。 没有找到时返回零值,当值不是结构体或索引超界时发生宕机
    FieldByNameFunc(match func(string) bool) Value根据匹配函数匹配需要的字段。找到时返回零值,当值不是结构体或索引超界时发生宕机
  • 代码示例:

    package main
    import (
    	"fmt"
    	"reflect"
    )
    //定义了一个Monster结构体
    type Monster struct {
    	Name  string `json:"name"`
    	Age   int `json:"monster_age"`
    	Score float32 `json:"成绩"`
    	Sex   string
    }
    
    //方法, 接收四个值,给s赋值
    func (s Monster) Set(name string, age int, score float32, sex string) {
    	s.Name = name
    	s.Age = age
    	s.Score = score
    	s.Sex = sex
    }
    
    func TestStruct(a interface{}) {
    	//获取reflect.Type 类型
    	typ := reflect.TypeOf(a)
    	//获取reflect.Value 类型
    	val := reflect.ValueOf(a)
    	//获取到a对应的类别
    	kd := val.Kind()
    	//如果传入的不是struct,就退出
    	if kd !=  reflect.Struct {
    		fmt.Println("expect struct")
    		return
    	}
    
    	//获取到该结构体有几个字段
    	num := val.NumField()
    
    	fmt.Printf("struct has %d fields\n", num) //4
    	//变量结构体的所有字段
    	for i := 0; i < num; i++ {
    		fmt.Printf("Field %d: 值为=%v\n", i, val.Field(i))
    		//获取到struct标签, 注意需要通过reflect.Type来获取tag标签的值
    		tagVal := typ.Field(i).Tag.Get("json")
    		//如果该字段于tag标签就显示,否则就不显示
    		if tagVal != "" {
    			fmt.Printf("Field %d: tag为=%v\n", i, tagVal)
    		}
    	}
    
    }
    func main() {
    	//创建了一个Monster实例
    	var a Monster = Monster{
    		Name:  "黄鼠狼精",
    		Age:   400,
    		Score: 30.8,
    	}
    	//将Monster实例传递给TestStruct函数
    	TestStruct(a)	
    }
    

    执行结果:

    image-20220316091219191

2.3.3 判断反射值的空和有效性

反射值对象的零值和有效性判断方法:

方 法说 明
IsNil() bool返回值是否为 nil。如果值类型不是通道(channel)、函数、接口、map、指针或 切片时发生 panic,类似于语言层的v== nil操作
IsValid() bool判断值是否有效。 当值本身非法时,返回 false,例如 reflect Value不包含任何值,值为 nil 等。

代码示例:

func main() {

	//*int的空指针
	var a *int
	fmt.Println("var a *int:", reflect.ValueOf(a).IsNil())

	//nil值
	fmt.Println("nil:", reflect.ValueOf(nil).IsValid())

	//*int类型的空指针
	fmt.Println("(*int)(nil):", reflect.ValueOf((*int)(nil)).Elem().IsValid())

	//实例化一个结构体
	s := struct {}{}

	//尝试从结构体中查找一个不存在的字段
	fmt.Println("不存在的结构体成员:", reflect.ValueOf(s).FieldByName("").IsValid())

	//尝试从结构体中查找一个不存在的方法
	fmt.Println("不存在的方法:", reflect.ValueOf(s).MethodByName("").IsValid())

	//实例化一个map
	m := map[int]int{}

	//尝试从map中查找一个不存在的键
	fmt.Println("不存在的键:", reflect.ValueOf(m).MapIndex(reflect.ValueOf(3)).IsValid())
}

输出结果:

var a *int: true
nil: false
(*int)(nil): false
不存在的结构体成员: false
不存在的方法: false
不存在的键: false

IsNil() 常被用于判断指针是否为空;IsValid() 常被用于判定返回值是否有效。

2.3.4 通过反射修改变量的值

反射值对象的判定及获取元素的方法:

方法名备 注
Elem() Value取值指向的元素值,类似于语言层*操作。当值类型不是指针或接口时发生宕机,空指针时返回 nil 的 Value
Addr() Value对可寻址的值返回其地址,类似于语言层&操作。当值不可寻址时发生宕机
CanAddr() bool表示值是否可寻址
CanSet() bool返回值能否被修改。要求值可寻址且是导出的字段

值修改相关方法:

方法名备注
Set(x Value)将值设置为传入的反射值对象的值
Setlnt(x int64)使用 int64 设置值。当值的类型不是 int、int8、int16、 int32、int64 时会发生宕机
SetUint(x uint64)使用 uint64 设置值。当值的类型不是 uint、uint8、uint16、uint32、uint64 时会发生宕机
SetFloat(x float64)使用 float64 设置值。当值的类型不是 float32、float64 时会发生宕机
SetBool(x bool)使用 bool 设置值。当值的类型不是 bod 时会发生宕机
SetBytes(x []byte)设置字节数组 []bytes值。当值的类型不是 []byte 时会发生宕机
SetString(x string)设置字符串值。当值的类型不是 string 时会发生宕机

值可被修改的两个条件:

  • 可被寻址,简单地说就是这个变量必须能被修改,需要传入对应的指针类型
    • v := reflect.ValueOf(&a),将a的地址传递给了ValueOf,值传递复制的就是a的地址。
    • v = v.Elem(),这部分很关键,因为传递的是a的地址,那么对应ValueOf函数的入参的值就是一个地址,地址是禁止修改的。v.Elem()就是解引用,返回的v就是变量a真正的reflection Value
  • 结构体成员中,字段可以被导出,即字段需要大写

代码示例:

func main() {

	type dog struct {
		// 如果希望通过反射能修改该值,则必须保证首字母大写
		LegCount int
	}

	//获取dog实例的反射值对象
	//如果希望反射能修改值,则reflect.ValueOf必须传入指针类型
	valueOfDog := reflect.ValueOf(&dog{})

  	// 取出dog实例地址的元素
	valueOfDog = valueOfDog.Elem()

	//获取legCount字段的值
	vLegCount := valueOfDog.FieldByName("LegCount")

	//尝试设置legCount的值
	vLegCount.SetInt(4)

	fmt.Println(vLegCount.Int())	//4
}

2.3.5 通过类型信息创建实例

func New(typ Type) Value

  • New返回一个Value类型值,该值持有一个指向类型为typ的新申请的零值的指针,返回值的Type为PtrTo(type)

当已知 reflect.Type 时,可以动态地创建这个类型的实例,实例的类型为指针。例如 reflect.Type 的类型为 int 时,创建 int 的指针,即*int,代码如下:

func main() {

	var a int

	//取变量a的反射类型对象
	typeOfA := reflect.TypeOf(a)

	//根据反射类型对象创建类型实例
	aIns := reflect.New(typeOfA)

	//输出Value的类型和种类
	fmt.Println(aIns.Type(), aIns.Kind())	//*int ptr
}

2.3.6 通过反射调用函数

如果反射值对象(reflect.Value)中值的类型为函数时,可以通过 reflect.Value 调用该函数。使用反射调用函数时,需要将参数使用反射值对象的切片 []reflect.Value 构造后传入 Call() 方法中,调用完成时,函数的返回值通过 []reflect.Value 返回。

反射调用函数:

//普通函数
func add(a, b int) int {
	return a + b
}

func main() {

	//将函数包装为反射值对象
	funcValue := reflect.ValueOf(add)

	//构造函数参数,传入两个整形值
	paramList := []reflect.Value{reflect.ValueOf(2), reflect.ValueOf(3)}

	//反射调用函数
	retList := funcValue.Call(paramList)

	fmt.Println(retList[0].Int())
}

2.3.7 通过反射调用方法

调用方法和调用函数是一样的,只不过结构体需要先通过rValue.Method()先获取方法再调用

package main
import (
	"fmt"
	"reflect"
)
//定义了一个Monster结构体
type Monster struct {
	Name  string `json:"name"`
	Age   int `json:"monster_age"`
	Score float32 `json:"成绩"`
	Sex   string
}

//方法,返回两个数的和
func (s Monster) GetSum(n1, n2 int) int {
	return n1 + n2
}
//方法, 接收四个值,给s赋值
func (s Monster) Set(name string, age int, score float32, sex string) {
	s.Name = name
	s.Age = age
	s.Score = score
	s.Sex = sex
}

//方法,显示s的值
func (s Monster) Print() {
	fmt.Println("---start~----")
	fmt.Println(s)
	fmt.Println("---end~----")
}
func TestStruct(a interface{}) {
	//获取reflect.Value 类型
	val := reflect.ValueOf(a)

	//获取到该结构体有多少个方法
	numOfMethod := val.NumMethod()
	fmt.Printf("struct has %d methods\n", numOfMethod)
	
	//var params []reflect.Value
	//方法的排序默认是按照 函数名的排序(ASCII码)
	val.Method(1).Call(nil) //获取到第二个方法。调用它

	
	//调用结构体的第1个方法Method(0)
	var params []reflect.Value  //声明了 []reflect.Value
	params = append(params, reflect.ValueOf(10))
	params = append(params, reflect.ValueOf(40))
	res := val.Method(0).Call(params) //传入的参数是 []reflect.Value, 返回[]reflect.Value
	fmt.Println("res=", res[0].Int()) //返回结果, 返回的结果是 []reflect.Value*/

}
func main() {
	//创建了一个Monster实例
	var a Monster = Monster{
		Name:  "黄鼠狼精",
		Age:   400,
		Score: 30.8,
	}
	//将Monster实例传递给TestStruct函数
	TestStruct(a)	
}

执行结果:

image-20220316091750438

三、网络编程

3.1 TCP通信

TCP通信常用API:

Server端:

  • Listen函数

    img

    • network: 选用的协议:TCP、UDP 如: “tcp"或"udp”
    • address:IP地址+端口号 如: “127.0.0.1:8000"或”:8000"
  • Listener接口

    img

  • Conn接口

    img

Client端:

  • Dial函数:用于创建一个链接(如tcp或udp)

    img

    • network: 选用的协议:TCP、UDP 如: “tcp"或"udp”
    • address:IP地址+端口号 如: “127.0.0.1:8000"或”:8000"

服务端程序的处理流程:

  1. 监听端口
  2. 接收客户端请求建立链接
  3. 创建goroutine处理链接。

服务端代码:

package main
import (
	"fmt"
	"net" //做网络socket开发时,net包含有我们需要所有的方法和函数
)

func process(conn net.Conn) {

	//这里我们循环的接收客户端发送的数据
	defer conn.Close() //关闭conn

	for {
		//创建一个新的切片
		buf := make([]byte, 1024)
		//conn.Read(buf)
		//1. 等待客户端通过conn发送信息
		//2. 如果客户端没有wrtie[发送],那么协程就阻塞在这里
		//fmt.Printf("服务器在等待客户端%s 发送信息\n", conn.RemoteAddr().String())
		n , err := conn.Read(buf) //从conn读取
		if err != nil {
			fmt.Printf("客户端退出 err=%v", err)
			return //!!!
		}
		//3. 显示客户端发送的内容到服务器的终端
		fmt.Print(string(buf[:n]))
		//回写数据给客户端
		conn.Write([]byte("数据已收到(来自服务端的消息)"))
	}

}

func main() {

	fmt.Println("服务器开始监听....")
	//net.Listen("tcp", "0.0.0.0:8888")
	//1. tcp 表示使用网络协议是tcp
	//2. 0.0.0.0:8888 表示在本地监听 8888端口
	listen, err := net.Listen("tcp", "0.0.0.0:8888")
	if err != nil {
		fmt.Println("listen err=", err)
		return 
	}
	defer listen.Close() //延时关闭listen

	//循环等待客户端来链接我
	for {
		//等待客户端链接
		fmt.Println("等待客户端来链接....")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("Accept() err=", err)
			
		} else {
			fmt.Printf("Accept() suc con=%v 客户端ip=%v\n", conn, conn.RemoteAddr().String())
		}
		//这里准备其一个协程,为客户端服务
		go process(conn)
	}
	
	//fmt.Printf("listen suc=%v\n", listen)
}

TCP客户端进行TCP通信的流程:

  1. 建立与服务端的链接
  2. 进行数据收发
  3. 关闭链接

客户端代码:

func main() {

	conn, err := net.Dial("tcp", "127.0.0.1:8888")
	if err != nil {
		fmt.Println("client dial err=", err)
		return 
	}
	defer conn.Close() //关闭连接
	//功能一:客户端可以发送单行数据,然后就退出
	reader := bufio.NewReader(os.Stdin) //os.Stdin 代表标准输入[终端]

	for {

		//从终端读取一行用户输入,并准备发送给服务器
		line, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("readString err=", err)
		}
		//如果用户输入的是 exit就退出
		line = strings.Trim(line, " \r\n")
		if line == "exit" {
			fmt.Println("客户端退出..")
			break
		}

		//再将line 发送给 服务器
		_, err = conn.Write([]byte(line + "\n"))
		if err != nil {
			fmt.Println("conn.Write err=", err)	
		}
		//接收服务端发送的数据
		buf := [512]byte{}
        n, err := conn.Read(buf[:])
        if err != nil {
            fmt.Println("recv failed, err:", err)
            return
        }
        fmt.Println(string(buf[:n]))
	}
	
}

执行结果:

image-20220316105914856

3.2 UDP通信

UDP通信常用API:

  • 创建监听地址

    func ResolveUDPAddr(network, address string) (*UDPAddr, error)
    

    ResolveUDPAddr将addr作为UDP地址解析并返回。参数addr格式为"host:port"或"[ipv6-host%zone]:port",解析得到网络名和端口名;net必须是"udp"、“udp4"或"udp6”。
    IPv6地址字面值/名称必须用方括号包起来,如"[::1]:80"、"[ipv6-host]:http"或"[ipv6-host%zone]:80"。

  • 创建用于通信的socket

    func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)
    

    ListenUDP创建一个接收目的地是本地地址laddr的UDP数据包的网络连接。net必须是"udp"、“udp4”、“udp6”;如果laddr端口为0,函数将选择一个当前可用的端口,可以用Listener的Addr方法获得该端口。返回的*UDPConn的ReadFrom和WriteTo方法可以用来发送和接收UDP数据包(每个包都可获得来源地址或设置目标地址)。

  • 接受UDP数据

    func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err error)
    

    ReadFromUDP从c读取一个UDP数据包,将有效负载拷贝到b,返回拷贝字节数和数据包来源地址。

    ReadFromUDP方法会在超过一个固定的时间点之后超时,并返回一个错误。

  • 写出数据到UDP

    func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
    

    WriteToUDP通过c向地址addr发送一个数据包,b为包的有效负载,返回写入的字节。

    WriteToUDP方法会在超过一个固定的时间点之后超时,并返回一个错误。在面向数据包的连接上,写入超时是十分罕见的。

服务端代码:

package main

import (
    "fmt"
    "net"
)

func main() {

    //0.本应从步骤1开始,但是在写步骤1的时候发现,步骤1还需要*UDPAddr类型的参数,所以需要先创建一个*DUPAddr
    //组织一个udp地址结构,指定服务器的IP+port
    udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("net.ResolveUDPAddr()函数执行出错,错误为:%v\n", err)
        return
    }
    fmt.Printf("UDP服务器地址结构创建完成!!!\n")

    //1.创建用户通信的socket
    //由于ListenUDP需要一个*UDPAddr类型的参数,所以我们还需要先创建一个监听地址
    udpConn, err := net.ListenUDP("udp", udpAddr)
    if err != nil {
        fmt.Printf("net.ListenUDP()函数执行出错,错误为:%v\n", err)
        return
    }
    defer udpConn.Close()
    fmt.Printf("UDP服务器通信socket创建完成!!!\n")

    for {
        //2.读取客户端发送的数据(阻塞发生在ReadFromUDP()方法中)
        buf := make([]byte, 4096)
        //ReadFromUDP()方法返回三个值,分别是读取到的字节数,客户端的地址,error
        n, clientUDPAddr, err := udpConn.ReadFromUDP(buf)
        if err != nil {
            fmt.Printf("*UDPAddr.ReadFromUDP()方法执行出错,错误为:%v\n", err)
            continue
        }
        //3.模拟处理数据
        fmt.Printf("服务器读到%v的数据:%s\n",clientUDPAddr, buf[:n])

        //4.回写数据给客户端
        _, err = udpConn.WriteToUDP([]byte("I am OK!"), clientUDPAddr)
        if err != nil {
            fmt.Printf("*UDPAddr.WriteToUDP()方法执行出错,错误为:%v\n", err)
            continue
        }
    }
}

客户端代码:

package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    conn, err := net.Dial("udp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("net.Dial()函数执行出错,错误为:%v\n", err)
        return
    }
    defer conn.Close()

    go func() {
        buf := make([]byte, 4096)
        for {
            //从键盘读取内容,放入buf
            n, err := os.Stdin.Read(buf)
            if err != nil {
                fmt.Printf("os.Stdin.Read()执行出错,错误为:%v\n", err)
                return
            }
            //给服务器发送
            conn.Write(buf[:n])
        }
    }()
    for {
        buf := make([]byte, 4096)
        n, err := conn.Read(buf)
        if err != nil {
            fmt.Printf("Conn.Read()方法执行出错,错误为:%v\n", err)
            return
        }
        fmt.Printf("服务器发来数据:%s\n", buf[:n])
    }
}

执行结果:

image-20220316112001045

四、Golang操作Redis

Golang 操作 Redis 的三方库主要有 go-redisredigo,这里主要介绍 redigo,它也是 Golang 官方推荐使用的 Redis 客户端。

go-redis 三方库为我们封装了很多函数来执行 Redis 命令,而 redigo 三方库只有一个 Do 函数执行 Redis 命令,更接近使用 redis-cli 操作 Redis。

go-redis的使用参考:Go操作Redis实战

4.1 安装redigo库

  • 使用 go get 命令安装 redigo:(在GOPATH路径下执行)

    go get github.com/gomodule/redigo/redis
    

    安装成功后,可以看到如下包

    image-20220316150444343

4.2 Redis的连接和操作

Redis连接和关闭:

  • 使用Dail()方法来连接服务:

    func Dial(network, address string, options ...DialOption) (Conn, error) {}
    

    network表示网络类型,address是服务地址,options是一些可选的选项,如果连接成功将返回一个redis.Conn对象,在连接使用完毕后,使用Close()关闭。

执行命令:

  • 执行redis命令使用Do()方法:

    Do(commandName string, args ...interface{}) (reply interface{}, err error)
    

    commandName是redis命令,后面接参数

    package main
    import (
    	"fmt"
    	"github.com/gomodule/redigo/redis" //引入redis包
    )
    
    func main() {
    	//通过go 向redis 写入数据和读取数据
    	//1. 链接到redis
    	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
    	if err != nil {
    		fmt.Println("redis.Dial err=", err)
    		return 
    	}
    	defer conn.Close() //关闭..
    
    	//2. 通过go 向redis写入数据 string [key-val]
    	_, err = conn.Do("Set", "name", "tomjerry猫猫")
    	if err != nil {
    		fmt.Println("set  err=", err)
    		return 
    	}
    
    	//3. 通过go 向redis读取数据 string [key-val]
    
    	r, err := redis.String(conn.Do("Get", "name"))
    	if err != nil {
    		fmt.Println("set  err=", err)
    		return 
    	}
    
    	//因为返回 r是 interface{}
    	//因为 name 对应的值是string ,因此我们需要转换
    	//nameString := r.(string)
    
    	fmt.Println("操作ok ", r)
    }
    

操作string类型:

//string类型数据命令
//redis命令:set key val
func set(key,val string){
	_,err := c.Do("set",key,val)

	if err != nil {
		log.Fatal(err)
	}
}
//redis命令:mset key1 val1 key2 val2 key3 val3 ...
func mset(key1,val1,key2,val2,key3,val3 string){
	_,err := c.Do("mset",key1,val1,key2,val2,key3,val3)

	if err != nil {
		log.Fatal(err)
	}
}
//redis命令:get key
func get(key string){
	val,err := redis.String(c.Do("get",key))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(val)
}
//redis命令:mget key1 key2 key3 ...
func mget(key1,key2,key3 string){
	vals,err := redis.Values(c.Do("mget",key1,key2,key3))

	if err != nil {
		log.Fatal(err)
	}

	for k,v := range vals {
		fmt.Printf("k = %v  v = %s\n",k,v)
	}
}
//redis命令:del key1 key2 ...
func del(key1,key2,key3 string){
	_,err := c.Do("del",key1,key2,key3)

	if err != nil {
		log.Fatal(err)
	}
}
//redis命令:getrange key start end
func getrange(key string,start,end int){
	s,err := redis.String(c.Do("getrange",key,start,end))

	if err != nil{
		log.Fatal(err)
	}

	fmt.Println(s)
}
//redis命令:exists key
func exists(key string){
	isExists,err := redis.Bool(c.Do("exists",key))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(isExists)
}
//redis命令:setex key 10 val
func setex(key,val string,expire int){
	_,err := c.Do("setex",key,expire,val)

	if err != nil {
		log.Fatal(err)
	}
}

操作Hash类型:

//hash类型数据命令
//redis命令:hset hashTable key val
func hset(hashTable,key,val string){
	_,err := c.Do("hset",hashTable,key,val)

	if err != nil {
		log.Fatal(err)
	}
}
//redis命令:hmset hashTable key1 val1 key2 val2 key3 val3 ...
func hmset(hashTable,key1,val1,key2,val2,key3,val3 string){
	_,err := c.Do("hmset",hashTable,key1,val1,key2,val2,key3,val3)

	if err != nil {
		log.Fatal(err)
	}
}
//redis命令:hget hashTable key
func hget(hashTable,key string){
	val,err := redis.String(c.Do("hget",hashTable,key))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(val)
}
//redis命令:hmget hashTable key1 key2 key3 ...
func hmget(hashTable,key1,key2,key3 string){
	vals,err := redis.Values(c.Do("hmget",hashTable,key1,key2,key3))

	if err != nil {
		log.Fatal(err)
	}

	for k,v := range vals {
		fmt.Printf("k = %v  v = %s\n",k,v)
	}
}
//redis命令:hdel hashTable key1 key2 key3
func hdel(hashTable,key1,key2,key3 string){
	//批量删除一个或若干个hash表中的键值对,如果是一次性删除多个,只要至少删除掉一个即返回true
	isDelSuccessful,err := redis.Bool(c.Do("hdel",hashTable,key1,key2,key3))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(isDelSuccessful)
}
//redis命令:hgetall hashTable
func hgetall(hashTable string){
	vals,err := redis.Values(c.Do("hgetall",hashTable))

	if err != nil {
		log.Fatal(err)
	}

	for k,v := range vals {
		fmt.Printf("k = %v  v = %s\n",k,v)
	}
}
//redis命令:hkeys hashTable
func hkeys(hashTable string){
	keys,err := redis.Values(c.Do("hkeys",hashTable))

	if err != nil {
		log.Fatal(err)
	}

	for k,v := range keys {
		fmt.Printf("k = %v v = %s\n",k,v)
	}
}
//redis命令:hvals hashTable
func hvals(hashTable string){
	vals,err := redis.Values(c.Do("hvals",hashTable))

	if err != nil {
		log.Fatal(err)
	}

	for k,v := range vals {
		fmt.Printf("k = %v v = %s\n",k,v)
	}
}
//redis命令:hlen hashTable
func hlen(hashTable string){
	len,err := redis.Int(c.Do("hlen",hashTable))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(len)
}
//redis命令:hexists hashTable
func hexists(hashTable,key string){
	isExists,err := redis.Bool(c.Do("hexists",hashTable,key))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(isExists)
}

操作list类型:

//list类型数据命令
//redis命令:lpush list val1 val2 val3 ...
func lpush(list,val1,val2,val3 string){
	_,err := c.Do("lpush",list,val1,val2,val3)

	if err != nil {
		log.Fatal(err)
	}
}
//redis命令:rpush list val1 val2 val3 ...
func rpush(list,val1,val2,val3 string){
	_,err := c.Do("rpush",list,val1,val2,val3)

	if err != nil {
		log.Fatal(err)
	}
}
//redis命令:lpop list
func lpop(list string){
	v,err := redis.String(c.Do("lpop",list))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("删除列表首个元素并返回:" + v)
}
//redis命令:rpop list
func rpop(list string){
	v,err := redis.String(c.Do("rpop",list))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("删除列表末尾元素并返回:" + v)
}
//redis命令:lrem list count val
func lrem(list,val string,count int){
	n,err := redis.Int(c.Do("lrem",list,count,val))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(n)
}
//redis命令:lset list index val
func lset(list,val string,index int){
	result,err := c.Do("lset",list,index,val)

	if err != nil {
		log.Fatal(err)
	}
	//成功修改则返回OK
	fmt.Println(result)
}
//redis命令:lindex list index
func lindex(list string,index int){
	v,err := redis.String(c.Do("lindex",list,index))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("列表%s中索引为%d对应的字符串为%s\n",list,index,v)
}
//redis命令:lrange list start end
func lrange(list string,start,end int){
	vals,err := redis.Values(c.Do("lrange",list,start,end))

	if err != nil {
		log.Fatal(err)
	}

	for k,v := range vals {
		fmt.Printf("k = %v v = %s\n",k,v)
	}
}
//redis命令:llen list
func llen(list string){
	len,err := redis.Int(c.Do("llen",list))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(len)
}
//redis命令:ltrim list start end
func ltrim(list string,start,end int){
	//这里加不加redis.String都可以,如果操作成功返回的都是字符串OK
	result,err := redis.String(c.Do("ltrim",list,start,end))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result)
}

操作无序集合set类型:

//无序集合set数据类型命令
//redis命令:sadd myset val1 val2 val3 ...
func sadd(myset,val1,val2,val3 string){
	_,err := c.Do("sadd",myset,val1,val2,val3)

	if err != nil {
		log.Fatal(err)
	}
}
//redis命令:srem myset val
func srem(myset,val string){
	//c.Do删除成功返回1,然后用redis.Bool转换成bool值true
	isDel,err := redis.Bool(c.Do("srem",myset,val))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(isDel)
}
//redis命令:spop myset
func spop(myset string){
	val,err := redis.String(c.Do("spop",myset))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("随机删除一个元素并返回:" + val)
}
//redis命令:smembers myset
func smembers(myset string){
	vals,err := redis.Values(c.Do("smembers",myset))

	if err != nil {
		log.Fatal(err)
	}

	for k,v := range vals {
		fmt.Printf("k = %v v = %s\n",k,v)
	}
}
//redis命令:scard myset
func scard(myset string){
	len,err := redis.Int(c.Do("scard",myset))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("集合长度为:%d\n",len)
}
//redis命令:sismember myset val
func sismember(myset,val string){
	isMember,err := redis.Bool(c.Do("sismember",myset,val))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(isMember)
}
//redis命令:srandmember myset count
func srandmember(myset string,count int){
	vals,err := redis.Values(c.Do("srandmember",myset,count))

	if err != nil {
		log.Fatal(err)
	}

	for k,v := range vals {
		fmt.Printf("k = %v v = %s\n",k,v)
	}
}
//redis命令:smove myset myset2 val
func smove(myset,myset2,val string){
	isMoveSuccessful,err := redis.Bool(c.Do("smove",myset,myset2,val))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("从集合%s中移动元素%s到集合%s,移动结果:" + strconv.FormatBool(isMoveSuccessful) + "\n",myset,val,myset2)
}
//redis命令:sunion myset myset2 ...
func sunion(myset,myset2 string){
	vals,err := redis.Values(c.Do("sunion",myset,myset2))

	if err != nil {
		log.Fatal(err)
	}

	for k,v := range vals {
		fmt.Printf("k = %v v = %s\n",k,v)
	}
}
//redis命令:sunionstore myset3 myset myset2 ...
func sunionstore(myset,myset2,myset3 string){
	//将集合myset、myset2取并集后存入myset3集合
	len,err := redis.Int(c.Do("sunionstore",myset3,myset,myset2))

	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("集合%s与集合%s取交集后得到的集合%s有%d个元素\n",myset,myset2,myset3,len)

	//打印集合myset3验证结果
	smembers("myset3")
}
//redis命令:sinter myset myset2 ...
func sinter(myset,myset2 string){
	vals,err := redis.Values(c.Do("sinter",myset,myset2))

	if err != nil {
		log.Fatal(err)
	}

	for k,v := range vals {
		fmt.Printf("k = %v v = %s\n",k,v)
	}
}
//redis命令:sinterstore myset3 myset myset2 ...
func sinterstore(myset,myset2,myset3 string){
	len,err := redis.Int(c.Do("sinterstore",myset3,myset,myset2))

	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("集合%s与集合%s取交集后得到的集合%s有%d个元素\n",myset,myset2,myset3,len)

	//打印集合myset3验证结果
	smembers("myset3")
}
//redis命令:sdiff myset myset2 ...
func sdiff(myset,myset2 string){
	vals,err := redis.Values(c.Do("sdiff",myset,myset2))

	if err != nil {
		log.Fatal(err)
	}

	for k,v := range vals {
		fmt.Printf("k = %v v = %s\n",k,v)
	}
}
//redis命令:sdiffstore myset3 myset myset2 ...
func sdiffstore(myset,myset2,myset3 string){
	len,err := redis.Int(c.Do("sdiffstore",myset3,myset,myset2))

	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("集合%s与集合%s取交集后得到的集合%s有%d个元素\n",myset,myset2,myset3,len)

	//打印集合myset3验证结果
	smembers("myset3")

}

操作有序集合zset类型:

//有序集合zset数据类型命令
//redis命令:zadd myzset val1 val2 val3 ...
func zadd(myzset,val1,val2,val3 string,score1,score2,score3 float64){
	_,err := c.Do("zadd",myzset,score1,val1,score2,val2,score3,val3)

	if err != nil {
		log.Fatal(err)
	}
}
//redis命令:zrem myzset val1 val2 val3 ...
func zrem(myzset,val1,val2,val3 string){
	len,err := redis.Int(c.Do("zrem",myzset,val1,val2,val3))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("本次有%d个元素被成功删除\n",len)
}
//redis命令:zrange myzset start end [withscores]
func zrange(myzset string,start,end,flag int){
	var vals []interface{}
	var err error

	if flag == 0 {
		//不加withscores
		vals,err = redis.Values(c.Do("zrange","myzset",start,end))
	}else if flag == 1{
		//加withscores
		vals,err = redis.Values(c.Do("zrange","myzset",start,end,"withscores"))
	}

	if err != nil {
		log.Fatal(err)
	}

	for k,v := range vals {
		fmt.Printf("k = %v v = %s\n",k,v)
	}
}
//redis命令:zrevrange myzset start end [withscores]
func zrevrange(myzset string,start,end,flag int){
	var vals []interface{}
	var err error

	if flag == 0 {
		//不加withscores
		vals,err = redis.Values(c.Do("zrevrange",myzset,start,end))
	}else if flag == 1 {
		//加withscores
		vals,err = redis.Values(c.Do("zrevrange",myzset,start,end,"withscores"))
	}

	if err != nil {
		log.Fatal(err)
	}

	for k,v := range vals {
		fmt.Printf("k = %v v = %s\n",k,v)
	}

}
//redis命令:redis zrangebyscore myzset start end [withscores]
func zrangebyscore(myzset string,start,end,flag int){
	var vals []interface{}
	var err error

	if flag == 0 {
		//不加withscores
		vals,err = redis.Values(c.Do("zrangebyscore",myzset,start,end))
	}else if flag == 1 {
		//加withscores
		vals,err = redis.Values(c.Do("zrangebyscore",myzset,start,end,"withscores"))
	}

	if err != nil {
		log.Fatal(err)
	}

	for k,v := range vals {
		fmt.Printf("k = %v v = %s\n",k,v)
	}
}
//redis命令:zcard myzset
func zcard(myzset string){
	len,err := redis.Int(c.Do("zcard",myzset))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("有序集合%s中有%d个元素\n",myzset,len)
}
//redis命令:zcount myzset minscore maxscore
func zcount(myzset string,minscore,maxscore float64){
	len,err := redis.Int(c.Do("zcount",myzset,minscore,maxscore))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("有序集合%s位于%.2f和%.2f分数区间内的元素有%d个\n",myzset,minscore,maxscore,len)
}
//redis命令:zrank myzset val
func zrank(myzset,val string){
	index,err := redis.Int(c.Do("zrank",myzset,val))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("有序集合%s中的元素%s对应的索引为%d\n",myzset,val,index)
}
//redis命令:zscore myzset val
func zscore(myzset,val string){
	score,err := redis.Float64(c.Do("zscore",myzset,val))

	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("有序集合%s中的元素%s对应的分数为%.2f\n",myzset,val,score)
}

4.3 Redis连接池

连接池:事先初始化一定数量的链接,放入到链接池,当Go需要操作Redis 时,直接从 Redis链接池取出链接即可。这样可以节省临时获取Redis 链接的时间,从而提高效率。

  • 应用程序调用Get方法从池中获取连接,并使用连接的Close方法将连接的资源返回到池

提供方法:

  • func (*Pool) ActiveCount 返回active的连接数,包含空闲的和正在使用的
  • func (*Pool) Close 关闭连接
  • func (*Pool) Get 获取一个连接
  • func (*Pool) GetContext GetContext使用提供的上下文获取连接
  • func (*Pool) IdleCount 空闲连接数
  • func (*Pool) Stats 连接池统计信息

代码示例:

package main
import (
	"fmt"
	"github.com/gomodule/redigo/redis"
)

//定义一个全局的pool
var pool *redis.Pool

//当启动程序时,就初始化连接池
func init() {
	pool = &redis.Pool{
		MaxIdle: 8, //最大空闲链接数
		MaxActive: 0, // 表示和数据库的最大链接数, 0 表示没有限制
		IdleTimeout: 100, // 最大空闲时间
		Dial: func() (redis.Conn, error) { // 初始化链接的代码, 链接哪个ip的redis
		return redis.Dial("tcp", "localhost:6379")
		},
	}
}

func main() {
	//先从pool 取出一个链接
	conn := pool.Get()
	defer conn.Close()

	_, err := conn.Do("Set", "name", "汤姆猫~~")
	if err != nil {
		fmt.Println("conn.Do err=", err)
		return
	}

	//取出
	r, err := redis.String(conn.Do("Get", "name"))
	if err != nil {
		fmt.Println("conn.Do err=", err)
		return
	}

	fmt.Println("r=", r)

	//如果我们要从pool 取出链接,一定保证链接池是没有关闭
	//pool.Close()
	conn2 := pool.Get()

	_, err = conn2.Do("Set", "name2", "汤姆猫~~2")
	if err != nil {
		fmt.Println("conn.Do err~~~~=", err)
		return
	}

	//取出
	r2, err := redis.String(conn2.Do("Get", "name2"))
	if err != nil {
		fmt.Println("conn.Do err=", err)
		return
	}

	fmt.Println("r=", r2)

	//fmt.Println("conn2=", conn2)

}

五、操作MySQL

5.1 获取数据库链接

  • Go语言中的database/sql包提供了保证SQL或类SQL数据库的泛用接口,并不提供具体的数据库驱动。database/sql/driver 包定义了应被数据库驱动实现的接口,这些接口会被 sql 包使用。所有使用database/sql包时必须注入(至少)一个数据库驱动。

  • 我们常用的数据库基本上都有完整的第三方实现。例如:MySQL驱动

下载依赖:

go get -u github.com/go-sql-driver/mysql

初始化连接:

  • Open打开数据库函数

    func Open(driverName, dataSourceName string) (*DB, error)
    

    Open打开一个dirverName指定的数据库,dataSourceName指定数据源,一般包至少括数据库文件名和(可能的)连接信息。

    Open函数可能只是验证其参数,而不创建与数据库的连接。如果要检查数据源的名称是否合法,应调用返回值的Ping方法。

    • 参数 dataSourceName 的格式:

      数据库用户名:数据库密码@[tcp(localhost:3306)]/数据库名

  • DB结构体说明

    type DB struct {
        // 内含隐藏或非导出字段
    }
    
    • DB是一个数据库(操作)句柄,代表一个具有零到多个底层连接的连接池。它可以安全的被多个go程同时使用。

    • sql包会自动创建和释放连接;它也会维护一个闲置连接的连接池。如果数据库具有单连接状态的概念,该状态只有在事务中被观察时才可信。一旦调用了BD.Begin,返回的Tx会绑定到单个连接。当调用事务Tx的Commit或Rollback后,该事务使用的连接会归还到DB的闲置连接池中。连接池的大小可以用SetMaxIdleConns方法控制。

  • Ping()方法测试连接是否有效

    func (db *DB) Ping() error
    

    Ping检查与数据库的连接是否仍有效,如果需要会创建连接。

**初始化连接实例:**可以将数据库对象的初始化放到init()函数中

package main

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

var (
	Db *sql.DB
	err error
)

func init() {
	Db, err = sql.Open("mysql", "root:root@tcp(localhost:3306)/test")
	if err != nil {
		panic(err.Error())
	}
	// 尝试与数据库建立连接(校验数据源是否正确)
	err = Db.Ping()
	if err != nil {
		panic(err.Error())
	}
	fmt.Println("数据库初始化成功")
}

func main() {
	fmt.Println("数据库连接是:", *Db)
}

执行结果:

image-20220321142656948

数据库的设置:

  • func (*DB) SetMaxOpenConns,设置与数据库建立连接的最大数目

    func (db *DB) SetMaxOpenConns(n int)
    
    • 如果n大于0且小于最大闲置连接数,会将最大闲置连接数减小到匹配最大开启连接数的限制。
    • 如果n<=0,不会限制最大开启连接数,默认为0(无限制)。
  • func (*DB) SetMaxIdleConns,设置连接池中的最大闲置连接数

    func (db *DB) SetMaxIdleConns(n int)
    
    • 如果n大于最大开启连接数,则新的最大闲置连接数会减小到匹配最大开启连接数的限制。
    • 如果n<=0,不会保留闲置连接。

5.2 增删改查操作

  • 在连接的 test 数据库中创建一个 users

    CREATE TABLE users(
        id INT PRIMARY KEY AUTO_INCREMENT,
        username VARCHAR(100) UNIQUE NOT NULL,
        PASSWORD VARCHAR(100) NOT NULL,
        email VARCHAR(100)
    )
    
  • 创建连接数据库的工具包

    package utils
    
    import (
    	"database/sql"
    	_ "github.com/go-sql-driver/mysql"
    )
    
    var (
    	Db *sql.DB
    	err error
    )
    
    func init() {
    	Db, err = sql.Open("mysql", "root:root@tcp(localhost:3306)/test")
    	if err != nil {
    		panic(err.Error())
    	}
    	// 尝试与数据库建立连接(校验dsn是否正确)
    	err = Db.Ping()
    	if err != nil {
    		panic(err.Error())
    	}
    }
    

5.2.1 增删改操作

  • *func (db DB) Exec执行一次命令,插入、更新和删除操作该方法。

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

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

插入数据:

//添加User
func (user *User) AddUser2() error {
	//1.写sql语句
	sqlStr := "insert into users(username,password,email) values(?,?,?)"
	//2.执行
	result, err := utils.Db.Exec(sqlStr, "admin4", "666666", "admin2@sina.com")
	if err != nil {
		fmt.Println("执行出现异常:", err)
		return err
	}
	theID, err := result.LastInsertId() // 新插入数据的id
	if err != nil {
		fmt.Printf("get lastinsert ID failed, err:%v\n", err)
		return err
	}
	fmt.Printf("insert success, the id is %d.\n", theID)	//insert success, the id is 5.
	return nil
}

更新数据:

// 更新数据
func updateUser() {
	sqlStr := "update users set password=? where id = ?"
	ret, err := utils.Db.Exec(sqlStr, "123456", 4)
	if err != nil {
		fmt.Printf("update failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 操作影响的行数
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("update success, affected rows:%d\n", n)
}

删除数据:

// 删除数据
func deleteUser() {
	sqlStr := "delete from users where id = ?"
	ret, err := utils.Db.Exec(sqlStr, 4)
	if err != nil {
		fmt.Printf("delete failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 操作影响的行数
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("delete success, affected rows:%d\n", n)
}

预编译语句:

  • Prepare方法

    func (db *DB) Prepare(query string) (*Stmt, error)
    

    Prepare创建一个准备好的状态用于之后的查询和命令。返回值可以同时执行多个查询和命令

  • Stmt 结构体及它的方法的说明

    image-20220321150009167

预编译的方法添加User:

func (user *User) AddUser() error {
	//1.写sql语句
	sqlStr := "insert into users(username,password,email) values(?,?,?)"
	//2.预编译
	inStmt, err := utils.Db.Prepare(sqlStr)
	if err != nil {
		fmt.Println("预编译出现异常:", err)
		return err
	}
	_, err2 := inStmt.Exec("admin", "123456", "admin@qq.com")
	if err2 != nil {
		fmt.Println("执行出现异常:", err2)
		return err
	}
	return nil
}

5.2.2 查询操作

  • func (*DB) Query,用于查询多行结果

    func (db *DB) Query(query string, args ...interface{}) (*Rows, error)
    

    Query执行一次查询,返回多行结果(即Rows),一般用于执行select命令。参数args表示query中的占位参数。

  • func (*DB) QueryRow,用于期望返回最多一行结果的查询

    func (db *DB) QueryRow(query string, args ...interface{}) *Row
    

    QueryRow执行一次查询,并期望返回最多一行结果(即Row)。QueryRow总是返回非nil的值,直到返回值的Scan方法被调用时,才会返回被延迟的错误

  • Row 结构体及它的方法的说明:

    image-20220321150900096

  • Rows 结构体及它的方法的说明:image-20220321151008612

单行查询:

//GetUserByID 根据用户的id从数据库中查询一条记录
func (user *User) GetUserByID() (*User, error) {
	//写sql语句
	sqlStr := "select id, username, password, email from users where id = ?"
	//执行
	row := utils.Db.QueryRow(sqlStr, user.ID)
	//声明
	var id int
	var username string
	var password string
	var email string
	err := row.Scan(&id, &username, &password, &email)
	if err != nil {
		return nil, err
	}
	u := &User{
		ID:       id,
		Username: username,
		Password: password,
		Email:    email,
	}
	return u, nil
}

多行查询:

//GetUsers 获取数据库中所有的记录
func (user *User) GetUsers() ([]*User, error) {
	//写sql语句
	sqlStr := "select id, username, password, email from users"
	//执行
	rows, err := utils.Db.Query(sqlStr)
	if err != nil {
		return nil, err
	}
	//创建User切片
	var users []*User
	for rows.Next() {
		//声明
		var id int
		var username string
		var password string
		var email string
		err := rows.Scan(&id, &username, &password, &email)
		if err != nil {
			return nil, err
		}
		u := &User{
			ID:       id,
			Username: username,
			Password: password,
			Email:    email,
		}
		users = append(users, u)
	}
	return users, nil
}

也可以使用预编译方法执行查询

5.3 Go实现MySQL事务

Go语言中使用以下三个方法实现MySQL中的事务操作

  • 开始事务

    func (db *DB) Begin() (*Tx, error)
    
  • 提交事务

    func (tx *Tx) Commit() error
    
  • 回滚事务

    func (tx *Tx) Rollback() error
    

**事务示例:**该事物操作能够确保两次更新操作要么同时成功要么同时失败,不会存在中间状态。

// 事务操作示例
func transactionDemo() {
	tx, err := db.Begin() // 开启事务
	if err != nil {
		if tx != nil {
			tx.Rollback() // 回滚
		}
		fmt.Printf("begin trans failed, err:%v\n", err)
		return
	}
	sqlStr1 := "Update user set age=30 where id=?"
	_, err = tx.Exec(sqlStr1, 2)
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec sql1 failed, err:%v\n", err)
		return
	}
	sqlStr2 := "Update user set age=40 where id=?"
	_, err = tx.Exec(sqlStr2, 4)
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec sql2 failed, err:%v\n", err)
		return
	}
	err = tx.Commit() // 提交事务
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("commit failed, err:%v\n", err)
		return
	}
	fmt.Println("exec trans success!")
}

5.4 sqlx使用

第三方库sqlx能够简化操作,提高开发效率。

  • 安装:

    go get github.com/jmoiron/sqlx
    

连接数据库:

var db *sqlx.DB

func initDB() (err error) {
	dsn := "user:password@tcp(127.0.0.1:3306)/test"
	// 也可以使用MustConnect连接不成功就panic
	db, err = sqlx.Connect("mysql", dsn)
	if err != nil {
		fmt.Printf("connect DB failed, err:%v\n", err)
		return
	}
	db.SetMaxOpenConns(20)
	db.SetMaxIdleConns(10)
	return
}

查询:

  • 查询单行数据示例代码如下:

    // 查询单条数据示例
    func queryRowDemo() {
    	sqlStr := "select id, name, age from user where id=?"
    	var u user
    	err := db.Get(&u, sqlStr, 1)
    	if err != nil {
    		fmt.Printf("get failed, err:%v\n", err)
    		return
    	}
    	fmt.Printf("id:%d name:%s age:%d\n", u.ID, u.Name, u.Age)
    }
    
  • 查询多行数据示例代码如下:

    // 查询多条数据示例
    func queryMultiRowDemo() {
    	sqlStr := "select id, name, age from user where id > ?"
    	var users []user
    	err := db.Select(&users, sqlStr, 0)
    	if err != nil {
    		fmt.Printf("query failed, err:%v\n", err)
    		return
    	}
    	fmt.Printf("users:%#v\n", users)
    }
    

插入、更新和删除:

sqlx中的exec方法与原生sql中的exec使用基本一致:

// 插入数据
func insertRowDemo() {
	sqlStr := "insert into user(name, age) values (?,?)"
	ret, err := db.Exec(sqlStr, "沙河小王子", 19)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	theID, err := ret.LastInsertId() // 新插入数据的id
	if err != nil {
		fmt.Printf("get lastinsert ID failed, err:%v\n", err)
		return
	}
	fmt.Printf("insert success, the id is %d.\n", theID)
}

// 更新数据
func updateRowDemo() {
	sqlStr := "update user set age=? where id = ?"
	ret, err := db.Exec(sqlStr, 39, 6)
	if err != nil {
		fmt.Printf("update failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 操作影响的行数
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("update success, affected rows:%d\n", n)
}

// 删除数据
func deleteRowDemo() {
	sqlStr := "delete from user where id = ?"
	ret, err := db.Exec(sqlStr, 6)
	if err != nil {
		fmt.Printf("delete failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 操作影响的行数
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("delete success, affected rows:%d\n", n)
}

事务操作:

对于事务操作,我们可以使用sqlx中提供的db.Beginx()和tx.MustExec()方法来简化错误处理过程。示例代码如下:

func transactionDemo() {
	tx, err := db.Beginx() // 开启事务
	if err != nil {
		if tx != nil {
			tx.Rollback()
		}
		fmt.Printf("begin trans failed, err:%v\n", err)
		return
	}
	sqlStr1 := "Update user set age=40 where id=?"
	tx.MustExec(sqlStr1, 2)
	sqlStr2 := "Update user set age=50 where id=?"
	tx.MustExec(sqlStr2, 4)
	err = tx.Commit() // 提交事务
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("commit failed, err:%v\n", err)
		return
	}
	fmt.Println("exec trans success!")
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值