【Go语言】

Go语言

配置GOPATH

使用命令行查看GOPATH信息

go env

配置环境变量

D盘创建GO文件夹
里面创建三个文件夹
bin 编译生成可执行文件
pkg 用来存放编译后生成的归档文件
src 源码

添加变量

path: D:\GO\bin
GOPATH: D:\GO

Go项目目录结构

src > 域名 > 项目名称

Go语言基础

标识符

首字母大写表示暴露,公有的,外部可见

第一个Go语言文件

// 包名
package main
// 导入
import "fmt"

func main() {
	fmt.Println(123)
}

变量声明

var xx string 
var yy int

// 批量声明
var (
	a string
	b bool
	c int
)
func xxxx() (string, int) {
	return "x", 1
}
func main() {
    // 局部变量 必须使用 不使用会报错
	var cc string = "da"
    var xxx = "x"
	// 变量初始化
	a = "xx"
	b = true
	c = 12
	//简写 只能在函数中,也就是局部使用
	s1 :="ss"
	// 匿名变量
	_, s2 := xxxx()

	fmt.Printf("我是%s\n", a)
	fmt.Printf("我是%s\n", cc)
	fmt.Printf("我是%s\n", xxx)
	fmt.Printf("我是%s\n", s1)
	fmt.Printf("我是%d\n", s2)
	fmt.Println(123)
}

常量声明和iota


const x1 = 12
const x2 = "x2"

// 下面没赋值,默认和第一个一样
const(
	x3 = "xx"
	x4 
)

// iota  在const出现时重置为0  const中每新增一行常量声明 iota 计数一次(iota可以理解为const语句块中的行索引)
// 使用iota能简化定义,在定义枚举时很有用
const (
	x45 = 2
	x46 = 3
	x5  = iota  // 2
	x6  = 100   // 100
	x7  = iota  // 4
	x8  = iota  // 5
)

// iota 使用案例

const (
	_  = iota
	kb = 1 << (10 * iota)
	mb = 1 << (10 * iota)
	gb = 1 << (10 * iota)
	tb = 1 << (10 * iota)
)

fmt

%s 字符串, %d十进制 ,%b二进制,%o八进制,%x十六进制,%c 字符,%T 查看数据类型 ,%v任意类型,%#v输出时会加描述符

基础数据类型

整型

func main() {
// uintptr 无符号整型
	//定义 int8类型
	x0 := int8(9)
	x1 := 12
	x2 := 012  // 8进制 输出%o
	x3 := 0x65 // 16进制 输出%x
	fmt.Printf("x1=%d\n", x1)
	fmt.Printf("x1=%o\n", x1)
	fmt.Printf("x1=%x\n", x1)

	fmt.Printf("x2=%d\n", x2)
	fmt.Printf("x2=%o\n", x2)
	fmt.Printf("x2=%x\n", x2)

	fmt.Printf("x3=%d\n", x3)
	fmt.Printf("x3=%o\n", x3)
	fmt.Printf("x3=%x\n", x3)
	// %T 查看类型
	fmt.Printf("x0类型%T\n", x0)

}

浮点型复数和布尔值

// 浮点只有float32和float64  默认都是float64
func main() {
	x0 := 1.23456
	x1 := 1.23
	// 定义float32类型
	x3 := float32(1.23)

	fmt.Printf("x1=%f\n", x0)
	fmt.Printf("x2=%f\n", x1)
	fmt.Printf("x3=%f\n", x3)

	fmt.Printf("x0类型%T\n", x0)
	fmt.Printf("x1类型%T\n", x1)
	fmt.Printf("x3类型%T\n", x3)
}

布尔值

// 布尔值 只有true 和 false 两个值
var bool = true

注意:
1.布尔值的默认值为false
2. GO语言中不允许将整型强制转换为布尔型
3. 布尔型无法参与数值运算,也无法与其他类型进行转换

字符串

Go语言中字符串是双引号包裹!
Go语言中字符是单引号包裹!

//字符串
s1 :="hello 你好"
// 字符
z := 'z'
c := 'c'
b := '比'
//字节: 1字节= 8bit(8个二进制位)
// 1个字符A = 1个字节
//1个utf8编码的汉字‘比’ = 一般占3个字节
转义符
  • \r 回车
  • \n 换行
  • \t 制表符
  • ’ 单引号
  • " 双引号
  • \ 反斜杠
多行字符串
s1 := `sss
xxx
ccc
`
字符串常用操作
方法介绍示例
len(str)求长度i := len(str)
+或fmt.Sprintf拼接字符用i := s1 + s2 或 i := fmt.Speintf(“%s%s”,s1,s2)
strings.Split分割ret := strings.Split(s3,“,”)
strings.contains判断是否包含,返回真假strings.contains(s1,“xx”)
strings.HasPrefix,strings.HasSuffix前缀/后缀判断,返回真假strings.HasPrefix(s1,“xx”),strings.HasSuffix(s1,“xx”)
strings.Index(),strings.LastIndex()子串出现的位置,返回下标strings.Index(s1,“xx”)第一次,strings.LastIndex(s1,“xx”)最后一次
strings.Join(alstring,sep string)join操作strings.Join(ret, “+”)
byte和 rune

1.uint8类型,或者叫byte型,代表了ASCI码的一个字符。
2.rune类型,代表一个UTF-8字符。

修改字符串

注意:字符串不可以直接修改

package main

import "fmt"

func main() {
	x0 := "1.23456"
	x1 := []rune(x0) // 切片
	x1[0] = '0'
	fmt.Printf("x0=%v\n", x0)
	fmt.Println(string(x1))
}

if判断和for循环

if示例

package main

import "fmt"

func main() {
	x0 := "1.23456"
	x1 := []rune(x0)
	x1[0] = '0'
	fmt.Printf("x0=%v\n", x0)
	fmt.Println(string(x1))
	x0 = ""
	if x0 == "" {
		fmt.Println(12)
	} else if x0 == "1" {
		fmt.Println(1)
	} else {
		fmt.Println("xx")
	}
	// if中声明变量
	if c := 1; c > 0 {
		fmt.Println(c)
	}
}

for示例

GO语言只有for循环
break 跳出循环
continue 跳出本次循环

	// 基础循环
	for i := 0; i < 10; i++ {
		fmt.Printf("%d\n", i)
	}
	// 变种1
	o := 1
	for ; o < 10; o++ {
		fmt.Printf("%d\n", o)
	}
	// 变种2
	j := 1
	for j < 10 {
		fmt.Printf("%d\n", j)
		j++
	}
	// 无限循环
	// for {
	// 	fmt.Println("无限循环")
	// }
	// range 循环切片,map,数组,字符串,通道
	for _, v := range x0 {
		//fmt.Printf("i%d\n", i)
		fmt.Printf("%c\n", v)
	}
for range(键值循环)

1.数组、切片、字符串返回索引和值。
2.map返回键和值。
3.通道(channel)只返回通道内的值。

switch

	switch x0 {
	case 1:
		fmt.Println(x0)
	case 2:
		fmt.Println(x0)
	case 3:
		fmt.Println(x0)
	default:
		fmt.Println("default")
	}

	switch x := 3; x {
	case 1, 3, 5, 7, 9:
		fmt.Println("奇数")
	case 2, 4, 6, 8, 10:
		fmt.Println("偶数")
	default:
		fmt.Println("default")
	}

	x := 3
	switch {
	case x > 10:
		fmt.Println("大于10")
	case x == 10:
		fmt.Println("等于10")
	default:
		fmt.Println("小于10")
	}

	xc := 10
	switch {
	case xc > 10:
		fmt.Println("大于10")
	case xc == 10:
		fmt.Println("等于10")
		fallthrough
	default:
		fmt.Println("小于10")
	}

注意
fallthrough 可以让switch继续往下执行

goto

	for i := 0; i < 110; i++ {
		for j := 0; j < 10; j++ {
			for n := 0; n < 10; n++ {
				if n == 2 {
					// 退出到具体标签
					goto breakTag
				}
			}
		}
	}

// 标签
breakTag:
	fmt.Println("跳出")

运算符

&& and
|| or
! not

复合数据类型

Array数组

数组是同一种数据类型元素的集合。在G语言中,数组从声明时就确定,使用时可以修改数组成员,
但是数组大小不可变化。
必须指定存放的元素的类型和容量(长度)
数组的长度是数组类型的一分

	var arr [3]int
	arr = [3]int{4, 5, 6}

	var arr0 [3]int = [3]int{7, 8, 9}

	arr1 := [3]int{1, 2, 3}

	arr2 := [...]int{9, 6, 3}

	arr3 := [3]int{0: 3, 2: 2}

	fmt.Printf("%T\n%v\n", arr, arr)
	fmt.Printf("%v\n", arr0)
	fmt.Printf("%v\n", arr1)
	fmt.Printf("%v\n", arr2)
	fmt.Printf("%v\n", arr3)
排序

sort.Strings(s)

多维数组
	arr4 := [3][3]int{
		{1, 2, 3},
		{4, 5, 6},
		{7, 8, 9},
	}
	fmt.Printf("%v\n", arr4)
	for _, v := range arr4 {
		for _, x := range v {
			fmt.Println(x)
		}
	}

切片(Slice)

切片(Sc)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址、长度和容量。切片一般用于快速地操作一块数据集合。
cap(slice)查看容量

	var s []string
	var s1 []int = []int{1, 2}
	s = []string{"s", "s"}

	//数组生成切片,切片的容量取决于底层数组的容量(切片第一个到数组最后一个)
	a1 := [...]int{1, 2, 3, 4, 5, 6}
	s2 := a1[0:4]
	s3 := a1[0:]
	s4 := a1[:4]
	s5 := a1[:]
	
	// make函数 make(类型, 长度, 容量)
	s6 := make([]int, 5, 10)
	s6[0] = 10
	
	fmt.Println(s)
	fmt.Println(s1)
	fmt.Println(s2)
	fmt.Printf("%v/%v/%v\n", s3, len(s3), cap(s3))
	fmt.Printf("%v/%v/%v\n", s4, len(s4), cap(s4))
	fmt.Printf("%v/%v/%v\n", s5, len(s5), cap(s5))
	fmt.Printf("%v/%v/%v\n", s6, len(s6), cap(s6))

判断切片是否为空 len(切片) == 0

append和copy

append追加,copy复制

	s6 := make([]int, 5, 10)
	s6[0] = 10
	s6 = append(s6, 10)
	s6 = append(s6, 10, 11)
	s6 = append(s6, s5...)
	s7 := make([]int,4,4)
	copy(s7, s6)
	
删除切片第index元素

删除切片第index元素 append(s6[:index], s6[index+1:]…)

	// 删除1-7的元素
	s6 = append(s6[:1], s6[7:]...)

指针和make和new

对变量进行取地址(&)操作,可以获得这个变量的指针变量。
指针变量的值是指针地址,
对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

new()函数是申请一个内存地址,一般用来给基本数据类型申请地址,返回的是对应类型的指针

	var p *int
	p = new(int)
	*p = 100
	fmt.Println(*p)

make也是用于内存分配的,区别于nev,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本
身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。

map

map是一种无序的基于key-value的数据结构,Go语言中的map是引l用类型T必须初始化才能使用。

package main

import "fmt"

func main() {
	var m1 = make(map[string]int, 3)
	m2 := map[int]int{1: 2, 2: 3, 3: 4}
	m1["x"] = 1
	m1["z"] = 2
	m1["c"] = 3
	fmt.Printf("%v/%v\n", m1, len(m1))
	v, ok := m1["v"]
	if ok {
		fmt.Printf("%v\n", v)
	} else {
		fmt.Printf("没有\n")
	}
	for k, v := range m1 {
		fmt.Printf("%v/%v\n", k, v)

	}
}

删除键值对 delete(m1, “key”)

map排序
	s := make([]string, 0)
	for k:= range m1 {
		s = append(s, k)
	}
	sort.Strings(s)
	fmt.Printf("%v\n", s)

	for _, v := range s {
		fmt.Printf("%v\n", m1[v])
	}

切片与map结合

值为map类型的切片
	s1 := make([]map[string]int, 10)
	s1[0] = make(map[string]int, 2)
	s1[0]["x"] = 1
	s1[0]["c"] = 2
	s1[1] = map[string]int{"a": 4, "g": 5}
	fmt.Printf("%v\n", s1)
值为切片类型的map

	m3 := map[string][]int{"北京": {1, 2}, "上海": {4, 5}}
	fmt.Printf("%v\n", m3)

函数

函数内部不能定义带名字的函数,只能定义匿名函数

// 多参,多返回值
func add(a int, b int) (n string, x int) {
	x = (a + b)
	return "n", x
}
// 多参 类型简写
func add1(a, b int) int {
	return a + b
}
//无参
func add2() int {
	return 1
}
// 可变长参数
func add3(a int, b ...int) {
	fmt.Println(a)
	fmt.Println(b)
}
func main() {
	x, y := add(1, 2)
	fmt.Printf("%v/%v\n", x, y)

}
匿名函数
	f1 := func() {
		fmt.Println(666)
	}
	f1()
	
	// 立即执行函数
	f2 := func() {
		fmt.Println(666)
	}()
defer

defer把他后面的语句延迟到函数即将返回的时候再执行
一个图数中可人有多个defer语句
多个defer语言按照先进后出(后进先出)的顺序延迟执行

func add3(a int, b ...int) int {
	defer fmt.Println(a)
	fmt.Println(b)
	return 10
	
}
func main() {
	fmt.Printf("%v\n", c)
	// [2 3] 
	//1
    //10
}
闭包

闭包是一个函数,这个函数包含了他外部作用域的一个变量

package main

import (
	"fmt"
)

func f1(a func()) {
	a()
}
func f2(a, b int) {
	fmt.Println(666)
}

func f3(f func(int, int), x, y int) func() {
	tmp := func() {
		f(x, y)
	}
	return tmp
}

func main() {
	x := f3(f2, 1, 2)
	f1(x)

}

内置函数

内置函数介绍
close主要用来关闭channel
len用来求长度,比如string、array、slice、map、channel
new用来分配内存,主要用来分配值类型,比如it、struct。返回的是指针
make用来分配内存,主要用来分配引用类型,比如eham、map、slice
append用来追加元素到数组、slice中
panic和recover用来做错误处理
package main

import (
	"fmt"
)

func f1() {
	fmt.Println("a")
}
func f2() {
	defer func() {
		err := recover()
		fmt.Println(err) // 尝试修复,继续执行,必须搭配defer使用
		fmt.Println("释放数据库连接...")
	}()
	panic("发生错误")
	fmt.Println("b")
}

func f3() {
	fmt.Println("f3")
}

func main() {
	f1()
	f2()
	f3()

}

递归函数

自己调用自己就是递归
一定要有个明确的退出

5的阶乘

package main

import "fmt"

func f1(i uint64) uint64 {
	if i <= 1 {
		return i
	}
	return i * f1(i-1)
}

func main() {
	fmt.Println(f1(5))
}

类型别名和自定义类型

package main

import "fmt"

type xxx string // 自定义类型
type yyy = string // 类型别名

func main() {
	var name xxx = "1xx"
	var name2 yyy = "1yy"
	fmt.Println(name)
	fmt.Printf("%T\n", name) // main.xxx
	fmt.Println(name2)
	fmt.Printf("%T\n", name2) // string
}

结构体

结构体是值类型,赋值都是拷贝

package main

import "fmt"

type person struct {
	name  string
	age   int
	hobby []string
}

func main() {

	var xf person
	xf.name = "小明"
	xf.age = 18
	xf.hobby = []string{"篮球", "足球"}
	fmt.Println(xf)

	var xf2 = person{
		name:"小米",
		age:12,
		hobby:[]string{"羽毛球", "足球"},
	}
	xf2 := person{
		"小米",
		12,
		[]string{"羽毛球", "足球"},
	}
	fmt.Println(xf2)

}

匿名结构体

用于临时

	var xf3 struct {
		name string
		age  int
	}
	xf3.name = "xx"
	xf3.age = 12
指针结构体
type person struct {
	name  string
	age   int
	hobby []string
}

func f(x person) {
	x.age = 1 // 没有用
}
func f1(x *person) {
	x.age = 10 // 修改有用, 可以把*x省略为x
}
func main() {
	var xf person
	xf.name = "小明"
	xf.age = 18
	xf.hobby = []string{"篮球", "足球"}
	f(xf)
	f1(&xf)
	fmt.Println(xf)
	
	xf6 := new(person)// 或者xf6 := &person{}
	xf6.name = "ddd"
	xf6.age = 3
	fmt.Println(xf6)
}
结构体实现构造函数
package main

import "fmt"

type person struct {
	name string
	age  int
}

func newPerson(name string, age int) *person {
	return &person{
		name,
		age,
	}
}

func main() {
	sf := newPerson("小明", 18)
	fmt.Println(sf)
}

匿名字段

字段比较少也比较简单的场景
不常用!

type c struct {
	int
	string
}
func main() {
	xx := c{1, "xx"}
	fmt.Println(xx.int)    //1
	fmt.Println(xx.string) // xx

}
方法和接收者

方法是作用于特定类型的函数
接受者表示的是调用该方法的具体类型变量,多用类型名首字母小写表示

package main

import "fmt"

type dog struct {
	name string
}

func newPerson(name string) *dog {
	return &dog{
		name,
	}
}
func (d dog) wang() {
	fmt.Println("汪汪汪")
	fmt.Println(d.name)
}
func main() {
	d := newPerson("小明")
	d.wang()
}

值接收者和指针接收者

什么时候应该使用指针类型接收者

1.需要修改接收者中的值
2.接收者是播贝代价比较大的大对象
3.保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

package main

import "fmt"

type dog struct {
	age int
}

func newPerson(age int) *dog {
	return &dog{
		age,
	}
}
func (d dog) guon() {
	d.age++
}
func (d *dog) zguon() {
	d.age++
}
func main() {
	d := newPerson(10)
	d.guon()
	fmt.Println(d.age) // 10
	d.zguon()
	fmt.Println(d.age) // 11
}

结构体嵌套
package main

import "fmt"

type dog struct {
	age int
	c1  c
}
type c struct {
	int
	string
}

func main() {
	x := dog{
		12,
		c{1, "xx"},
	}
	fmt.Println(x)

}

匿名嵌套结构体
package main

import "fmt"

type dog struct {
	age int
	c
}
type c struct {
	int
	string
}

func main() {
	x := dog{
		age: 12,
		c:   c{1, "xx"},
	}
	fmt.Println(x)
	fmt.Println(x.int)

}
结构体的“继承”
package main

import "fmt"

// 动物类
type dongw struct {
	name string
}

func (d *dongw) move() {
	fmt.Println("动一下")
}

// 狗类
type dog struct {
	feet int
	// 模拟继承
	dongw
}

func (d *dog) wang() {
	fmt.Println("汪汪汪")
}

func main() {
	x := dog{
		feet:  4,
		dongw: dongw{name: "xx"},
	}
	fmt.Println(x)
	fmt.Println(x.name)
	x.wang()
	x.move()

}

结构体与json

1.序列化:
把Go语言中的结构体变量->json格式的字符串 json.Marshal(x)
2.反序列化:
json格式的字符串->Go语言中能够识别的结构体变量 json.Unmarshal([]byte(s), &x1)

package main

import (
	"encoding/json"
	"fmt"
)

// 动物类
type dongw struct {
	//name string
	//age  int

	Name string `json:"nnn"`
	Age  int
}

func main() {
	x := dongw{
		Name: "xx",
		Age:  12,
	}
	s, err := json.Marshal(x)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("%s\n", s)
	fmt.Println(x.Name)
	var x1 dongw
	json.Unmarshal([]byte(s), &x1)
	fmt.Println(x1)

}

练习: 结构体版学生管理系统

main.go
package main

import (
	"fmt"
	"os"
)

// 學生管理系統
var smr studentMgr

// 菜单函数
func showMenu() {
	fmt.Println("----------------welcome sms!-------------")
	fmt.Println(`
	1.查看所有学生
	2.添加学生
	3.修改学生
	4.删除学生
	5.退出`)
}
func main() {
	smr = studentMgr{
		allStudent: make(map[int64]student, 100),
	}
	for {
		showMenu()
		// 等待用户输入选项
		fmt.Print("请输入序号:")
		var choice int
		fmt.Scanln(&choice)
		fmt.Println("你输入的是:", choice)
		switch choice {
		case 1:
			smr.showstudents()
		case 2:
			smr.addstudent()
		case 3:
			smr.editstudent()
		case 4:
			smr.deletestudent()
		case 5:
			os.Exit(1)
		default:
			fmt.Println("输入错误...")
		}
	}

}

student_mar.go
package main

import "fmt"

// 学生类
type student struct {
	id   int64
	name string
}

// 学生管理员类
type studentMgr struct {
	allStudent map[int64]student
}

//查看学生
func (s studentMgr) showstudents() {
	for k, v := range s.allStudent {
		fmt.Printf("学号:%d, 姓名:%s\n", k, v.name)
	}
}

//增加学生
func (s studentMgr) addstudent() {
	var (
		newid   int64
		newname string
	)
	fmt.Println("请输入学号")
	fmt.Scanln(&newid)
	fmt.Println("请输入姓名")
	fmt.Scanln(&newname)
	newStu := student{
		id:   newid,
		name: newname,
	}
	s.allStudent[newStu.id] = newStu

}

//修改学生
func (s *studentMgr) editstudent() {
	//获取用户输入学号
	var newId int64
	var newName string
	fmt.Println("请输入学号")
	fmt.Scanln(&newId)
	v, ok := s.allStudent[newId]
	if !ok {
		fmt.Println("查无此人")
		return
	}
	fmt.Printf("你要修改的学生信息如下:\n学号:%d, 姓名:%s\n", v.id, v.name)
	fmt.Println("请输入学生姓名")
	fmt.Scanln(&newName)
	v.name = newName
	s.allStudent[newId] = v

}

//删除学生
func (s studentMgr) deletestudent() {
	//获取用户输入学号
	var newId int64
	fmt.Println("请输入学号")
	fmt.Scanln(&newId)
	v, ok := s.allStudent[newId]
	if !ok {
		fmt.Println("查无此人")
		return
	}
	fmt.Printf("你要修改的学生信息如下:\n学号:%d, 姓名:%s\n", v.id, v.name)
	delete(s.allStudent, newId)
	fmt.Println("删除成功.......")

}

接口类型(interface)

接口是一种类型,是一种特殊的类型它规定了变量有哪些方法
在编程中会遇到以下场景: 我不关心一个变星是什么类型,我只关心能洞用它的什么方法

初识接口
package main

import "fmt"

type car interface {
	run()
}
type falali struct {
	brand string
}

func (f falali) run() {
	fmt.Printf("%s速度70迈..\n", f.brand)
}

type baoshijie struct {
	brand string
}

func (b baoshijie) run() {
	fmt.Printf("%s速度80迈..\n", b.brand)
}
func drive(c car) {
	c.run()
}
func main() {
	f1 := falali{
		brand: "法拉利",
	}
	b1 := baoshijie{
		brand: "保时捷",
	}
	drive(f1)
	drive(b1)
	f1.run()
	b1.run()
}

接口的定义

type 接口名 interface {
方法名1(参数1,参致2…)(返回值1,返回值2…)
方法名2(参数1,参数2…)(返回值1,返回值2…)

}

用来给变量\参数返回值等设置类型

接口的实现

一个变量如果实现了接口中规定的所有的方法那么这个变量就实现了这个接口,可以称为这个接口类型的变量。

package main

import "fmt"

type animal interface {
	move()
	eat(string)
}
type cat struct {
	name string
	feet int8
}
type chicken struct {
	feet int8
}

func (c chicken) move() {
	fmt.Print("鸡动")
}
func (c chicken) eat() {
	fmt.Print("吃鸡饲料")
}
func (c cat) move() {
	fmt.Print("走猫步")
}
func (c cat) eat(s string) {
	fmt.Printf("猫吃%s...\n", s)
}
func main() {
	var a1 animal
	c1 := cat{
		name: "淘气",
		feet: 4,
	}
	a1 = c1
	a1.eat("小黄鱼")
	c2 := chicken{
		feet: 4,
	}
	c2.eat()
}

指针接收者实现接口和值接收者实现接口的区别

使用值接收者实现接口,结构体类型和结构体指针类型的变量都能存.
指针接收者实现接口只能存结构体指针类型的变量

package main

import "fmt"

type animal interface {
	move()
	eat(string)
}
type cat struct {
	name string
	feet int8
}
type chicken struct {
	feet int8
}

// 使用值接受者实现所有方法
func (c chicken) move() {
	fmt.Print("鸡动")
}
func (c chicken) eat(s string) {
	fmt.Printf("吃%s", s)
}

// 使用指针接受者实现所有方法
func (c *cat) move() {
	fmt.Print("走猫步")
}
func (c *cat) eat(s string) {
	fmt.Printf("猫吃%s...\n", s)
}
func main() {
	var a1 animal
	c1 := chicken{
		feet: 2,
	}
	a1 = c1
	a1.eat("鸡饲料")
	fmt.Println(a1)
	c2 := cat{
		name: "喵喵",
		feet: 2,
	}
	// a1 = c2  // 错误,指针接收者实现接口只能存结构体指针类型的变量
	a1 = &c2 // 正确
}

接口和类型的关系

多个类型可以实现同一个接口。
一个类型可以实现多个接口。

package main

import "fmt"

// 接口嵌套
type animal interface {
	mover
	eatr
}
type mover interface {
	move()
}
type eatr interface {
	eat(string)
}
type cat struct {
	name string
}

// 实现多个接口
// 实现了mover接口
func (c *cat) move() {
	fmt.Print("走猫步")
}

// 实现了eatr接口
func (c *cat) eat(s string) {
	fmt.Printf("猫吃%s...\n", s)
}
func xx(a animal, m mover, e eatr) {

}
func main() {
	c1 := cat{
		name: "喵喵",
	}
	xx(&c1, &c1, &c1)
}

空接口

没有必要起名字 , 通常定义成下面的格式:

interface{}// 空接口

所有的类型都实现了空接口也就是任意类型的变量都能保存到空接口中

package main

import "fmt"

func show(a interface{}) {
	fmt.Printf("%T/%v\n", a, a)
}
func main() {
	var m1 map[string]interface{}
	m1 = make(map[string]interface{}, 10)
	m1 = map[string]interface{}{
		"a": "a",
	}
	m2 := map[int]interface{}{
		1: 2,
		2: 3,
		3: 4,
		4: 5,
	}
	m1["b"] = 2
	m1["c"] = 3
	m1["d"] = 4
	m1["e"] = "e"
	m1["f"] = true
	fmt.Println(m1)
	fmt.Println(m2)
	show("xxx")
	show(true)
	show(123)
}

类型断言
a.(int)
package main

import "fmt"

func show(a interface{}) {
	fmt.Printf("%T/%v\n", a, a)
	str, ok := a.(string)
	if !ok {
		fmt.Println("猜错了")
		fmt.Println(ok)
		return
	}
	fmt.Println(str)

}

func justifyType(x interface{}) {
	switch v := x.(type) {
	case string:
		fmt.Printf("x is a string,value is %v\n", v)
	case int:
		fmt.Printf("x is a int is %v\n", v)
	case bool:
		fmt.Printf("x is a bool is %v\n", v)
	default:
		fmt.Println("unsupport type!")

	}
}

func main() {
	show("xxx")
	//show(true)
	show(123)
}

strconv包 实现类型转换

Go语言除了指定类型,其他类型不支持强转

字符串数字转int64类型

valueint, err := strconv.ParseInt(value, 10, 64)// 参数为,值,进制,位数 如;(value,10进制,int64)

字符串数字转int类型

func main() {
	s := "12"
	i, _ := strconv.Atoi(s)
	fmt.Printf("%T/%v\n", i, i)
}

int类型转字符串

func main() {
	s := "12"
	i, _ := strconv.Atoi(s)
	fmt.Printf("%T/%v\n", i, i)
	s1 := strconv.Itoa(i)
	fmt.Printf("%T/%v\n", s1, s1)
}

package 包

是多个Go源码的集合,是一种高级的代码复用方案,Go语言为我们提供了很多内置包,如fmt、 0s、i0等。

定义包

package 包名

注意事项:

  • 一个文件夹下面只的有一个包,同样一个包的文件不在多个文件夹下。
  • 包名可以不和文件夹的名字一样,包名不能包含符号。
  • 包名为main的包为应用程序的入口包,编译时不包含main包的源代码时不会得到可执行文件。

可见性

如果想在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识符必须是对外可见的
(public)。在Go语言中只需要将标识符的首字母大写就可以让标识符对外可见了。

init函数

引用以后自动执行
每个包导入的时候会自动执行一个名为iit)的函数它设有参数也设有返回值也不能手动调用

package jian

import "fmt"

func init() {
	fmt.Println("自动执行...")
}
func Jian() {

}

package add

import "fmt"

func Add() {
	fmt.Println("add")
}

package main

import (
	"pack/add"
	"pack/jian"
)

func main() {
	add.Add()
	jian.Jian()
}

文件操作

打开和关闭文件及读取文件

os.Open()函数能够打开一个文件,返叵1个*Fie和一个err。
对得到的文件实例调用close()方法能够关闭文件。

package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.Open("./os/main.go")
	if err != nil {
		fmt.Print("打开错误...", err)
		return
	}
	defer file.Close()
	tmp := [128]byte{}
	for {
		n, err := file.Read(tmp[:])
		if err != nil {
			fmt.Print("读取错误...", err)
			return
		}
		//fmt.Printf("读取了%d个字节\n", n)
		fmt.Println(string(tmp[:n]))
		if n < 128 {
			return
		}
	}

}


利用bufio包读取文件

func ReadFromFile2() {
	file, err := os.Open("./os/main.go")
	if err != nil {
		fmt.Print("打开错误...", err)
		return
	}
	defer file.Close()
	reader := bufio.NewReader(file)
	for {
		lin, err := reader.ReadString('\n')
		if err == io.EOF {
			return
		}
		if err != nil {
			fmt.Print("读取错误", err)
			return
		}
		fmt.Println(lin)
	}

}
利用ioutil包读取文件
// 利用ioutil包读取文件
func ReadFromFile3() {
	file, err := ioutil.ReadFile("./os/main.go")
	if err != nil {
		fmt.Print("打开错误...", err)
		return
	}
	fmt.Println(string(file))

}
使用bufio获取用户输入
package main

import (
	"bufio"
	"fmt"
	"os"
)

func writeDome1() string {
	wr := bufio.NewReader(os.Stdin)
	fmt.Println("请输入:")
	s, _ := wr.ReadString('\n')
	return s
}
func main() {
	fmt.Println(writeDome1())
}

写入文件

os.OpenFile() 函数能够以指定模式财打开文件,从而实现文件写入相关功能。

func OpenFile(name string,flag int,perm FileMode)(*File,error){}

其中:
name:要打开的文件名flag:打开文件的模式。模式有以下几种:

模式含义
os.O_WRONLY只写
os.O_CREATE创建文件
os.O_RDONLY只读
os.O_RDWR读写
os.O_TRUNC清空
os.O_ APPEND追加

perm:文件权限,一个八进制数。r(读)04,w(写)02,x(执行)01。

Write与WriteString实现写入
package main

import (
	"fmt"
	"os"
)

func main() {
	fileobj, err := os.OpenFile("goxuexi/lx27/xx.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	defer fileobj.Close()
	if err != nil {
		fmt.Print("错误", err)
		return
	}
	// Write写入字节
	fileobj.Write([]byte("xxcvbnnm"))
	// WriteString写入字符串
	fileobj.WriteString("我是你")
	fmt.Println(666)
}

使用bufio包实现写入
func writeDome2() {
	fileobj, err := os.OpenFile("goxuexi/lx27/xx.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	defer fileobj.Close()
	if err != nil {
		fmt.Print("错误", err)
		return
	}
	wr := bufio.NewWriter(fileobj)
	wr.WriteString("cnm\n")
	wr.Flush()
}
使用ioutil包实现写入
func writeDome3() {
	str := "nmmb"
	err := ioutil.WriteFile("goxuexi/lx27/xx.txt", []byte(str), 0666)
	if err != nil {
		fmt.Print("错误", err)
		return
	}
}

在文件中间读取内容

package main

import (
	"fmt"
	"os"
)

func writeDome1() {
	wr, err := os.OpenFile("goxuexi/lx29/xx.txt", os.O_RDWR, 0644)
	if err != nil {
		fmt.Print("错误", err)
	}
	defer wr.Close()
	wr.Seek(1, 0)
	ret := [1]byte{}
	n, err := wr.Read(ret[:])
	if err != nil {
		fmt.Print("读取错误!", err)
	}
	fmt.Println(string(ret[:n]))
}
func main() {
	writeDome1()
}

在文件中间插入内容

利用一个临时文件做修改

package main

import (
	"fmt"
	"io"
	"os"
)

func writeDome1(i int, s string) {
	wr, err := os.OpenFile("goxuexi/lx30/xx.txt", os.O_RDWR, 0644)
	if err != nil {
		fmt.Print("错误", err)
	}

	ret := make([]byte, i)
	n, err := wr.Read(ret[:])
	if err != nil {
		fmt.Print("读取错误!", err)
	}
	tmpflie, err := os.OpenFile("goxuexi/lx30/xx.tem", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		fmt.Printf("创建错误%s", err)
	}
	tmpflie.Write(ret[:n])
	tmpflie.WriteString(s)
	x := [1024]byte{}
	for {
		n, err := wr.Read(x[:])
		if err != io.EOF {
			tmpflie.Write(x[:n])
			fmt.Print("读取完成!", err)
			break
		}
		if err != nil {
			fmt.Print("读取错误!", err)
			return
		}
		tmpflie.Write(x[:n])
	}
	wr.Close()
	tmpflie.Close()
	os.Rename("goxuexi/lx30/xx.tem", "goxuexi/lx30/xx.txt")

}
func main() {
	writeDome1(2, "\n66")
}

time包

time包提供了时间的显示和测量用的函数。日历的计算采用的是公历。

时间类型

time.Time类型表示时间。我们可以过 time.Now()函数获取当前的时间对象,
然后获取时间对象的年月日时分秒等信息。示例代码如下:

	now := time.Now()
	fmt.Println(now.Year())
	fmt.Println(now.Month())
	fmt.Println(now.Date())
	fmt.Println(now.Hour())
	fmt.Println(now.Minute())
	fmt.Println(now.Second())

时间戳

是自1970年月日(08:00:00GT)至当前时间的总毫秒数。它也被称为Unix时间截(UnixTimestamp)。
基于时间对象获取时间戳的示例代码如下:

	xjc := now.Unix()
	fmt.Println(xjc)
	fmt.Println(now.UnixNano())
	fmt.Println(time.Unix(xjc, 0))

时间间隔

Duration类型代表两个时间点之间经过的时间,以纳秒为单位。可表示的最长时间段大约29o年。time包中定义的时间间
隔常是如下

const(
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second      = 1000 * Millisecond
Minute      =  60  * Second
Hour        =  60 * Minute
)

例如:

time.Duration 表示1纳秒 time.Second 表示1秒。

时间操作

Add

我们在日常的编码过程中可能会遇到要求时间+时间间隔的需求,Go语言的时间对象有提供Add方法如下:

 func (t Time)Add(d Duration) Time

举个例子,求一个小时之后的时间:

fmt.Println(now.Add(time.Hour))
Sub

求两个时间之间的差值:

func (t Time)Sub(u Time)Ducation

返回一个时间段t-u。如果结果超出了Duration可以表示的最大值/最小值,将返回最大值/最小值。
要获取时间点t-d(d为Duration),可以使用t.Add(-d),

Equal
func (t Time)Equal(u Time)bool

判断两个时间是否相同,会考虑时区的影响,因此不同时区标准的时间也可以正确比较。本方法和用t==u不同,这种方法 还会比较地点和时区信息。

Before
 func (t Time)Before(u Time)bool

如果t代表的时间点在之前,返回真;否则返回假。

After
func (t Time)After(u Time)bool

如果t代表的时间点在之后,返回真;否则返回假。

定时器

使用tine.Tick(时间间隔)来设置定时器,定时器的本质上是一个通道(channel)。

func main() {
	ticker := time.Tick(time.Second)
	for v := range ticker {
		fmt.Println(v) // 1秒执行一次
	}
}

等待Sleep

time.Sleep(n) n以后再往下执行, n 为time.Duration类型

time.Sleep(3 * time.Second)
时间格式化

时间类型有一个自带的方法
Format进行格式化,需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S而
是使用G0的诞生时间2006年1月215点04分(记忆口快为20061234)。也许这就是技术人员的浪漫吧。
补充:如果想格式化为12小时方式,需指定PM。

	now := time.Now()
	fmt.Println(now.Format("2006-01-02 3:04:00 PM"))
	fmt.Println(now.Format("2006-01-02 15:04:00"))
	fmt.Println(now.Format("2006-01-02"))
	fmt.Println(now.Format("2006/01/02"))
	fmt.Println(now.Format("15:04:00"))
字符串格式的时间转换为时间类型
	t, err := time.Parse("2006-01-02", "2005-12-30")
	if err != nil {
		fmt.Print(err)
		return
	}
	fmt.Println(t)
按照指定时区转换
	loc, err := time.LoadLocation("Asia/Shanghai")
	if err != nil {
		fmt.Println(err)
	}
	t2, err := time.ParseInLocation("2006-01-02 15:04:05", "2023-05-18 20:22:00", loc)

log

输出在终端

package main

import (
	"log"
	"time"
)

func main() {
	for {
		log.Println("测试")
		time.Sleep(time.Second * 3)
	}

}

输出在文件

package main

import (
	"fmt"
	"log"
	"os"
	"time"
)

func main() {
	file, err := os.OpenFile("goxuexi/lx35/xx.txt", os.O_RDWR|os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	defer file.Close()
	if err != nil {
		fmt.Println(err)
		return
	}
	log.SetOutput(file)
	for {
		log.Println("测试")
		time.Sleep(time.Second * 3)
	}

}

runtime.Caller()

package main

import (
	"fmt"
	"path"
	"runtime"
)

func f1() {
	pc, fiile, line, ok := runtime.Caller(2)
	if !ok {
		fmt.Println("错误")
	}
	funName := runtime.FuncForPC(pc).Name()
	fmt.Println(funName)
	fmt.Println(fiile) // 文件全路径
	fmt.Println(path.Base(fiile)) // 拿到最后文件的名字
	fmt.Print(line)
}
func f2() {
	f1()
}
func main() {
	f2()
}


获取文件详细信息

package main

import (
	"fmt"
	"os"
)

func main() {
	fileobj, err := os.Open("goxuexi/lx30/xx.txt")
	if err != nil {
		fmt.Println(err)
	}
	fmt.Printf("%T\n", fileobj)
	// 获取文件对象的详细信息
	fileinfo, err := fileobj.Stat()
	if err != nil {
		fmt.Println(err)
	}
	/*
		Name() string       // base name of the file
		Size() int64        // length in bytes for regular files; system-dependent for others
		Mode() FileMode     // file mode bits
		ModTime() time.Time // modification time
		IsDir() bool        // abbreviation for Mode().IsDir()
		Sys() any           // underlying data source (can return nil)
	*/
	fmt.Println(fileinfo.Name())
	fmt.Println(fileinfo.Size())
	fmt.Println(fileinfo.Mode())
	fmt.Println(fileinfo.ModTime())
	fmt.Println(fileinfo.IsDir())
	fmt.Println(fileinfo.Sys())
}

练习:日志库

需求分析:

  1. 支持往不同的地方输出日志
  2. 日志分级别
    1. Debug
    2. Trace
    3. Info
    4. Warning
    5. Error
    6. Fata
  3. 日志要支持开关控制
  4. 完整的日志记录要包含有时间、行号、文件名、日志级别、日志信息
  5. 日志文件要切割

完成地址:
https://gitee.com/yanxfei/GOlog.git

反射

什么是反射

​反射(reflection)是在 Java
出现后迅速流行起来的一种概念,通过反射可以获取丰富的类型信息,并可以利用这些类型信息做非常灵活的工作。大多数现代的高级语言都以各种形式支持反射功能,反射是把双刃剑,功能强大但代码可读性并不理想,若非必要并不推荐使用反射。

反射可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。

反射的弊端

  • 代码难以阅读和维护。
  • 编译期间不能发现类型错误,有些bug只能在运行很长时间才能发现,可能造成不良后果。
  • 反射性能差,通常比正常代码慢一到两个数量级。在对性能要求高或大量反复调用的代码块里建议不要使用反射。

reflect 包

在G0语言的反射机制中,任何接口值都由是一个具体类型和具体类型的值两部分组成的。在Go语言中反射的相关功能由内置的reflect 包提供,任意接口值在反射中都可以理解为由reflect.Type和reflect.Value两部分组成,并且reflect包提供了reflect.TypeOf()和reflect.Value()f两个函数来获取任意对象的Valuei和Type。

TypeOf
package main

import (
	"fmt"
	"reflect"
)

func reflectType(x interface{}) {
	v := reflect.TypeOf(x)
	fmt.Printf("type:%v\n", v)
}
func main() {
	x := float32(3.14)
	x1 := float64(3.15)
	s := "xxx"
	a := []byte{}
	a1 := [3]int{4, 5, 6}
	m := map[int]int{1: 2, 2: 3}
	reflectType(x)  // type:float32
	reflectType(x1) // type:float64
	reflectType(s)  // type:string
	reflectType(a)  // type:[]uint8
	reflectType(a1) // type:[3]int
	reflectType(m)  // type:map[int]int
}

type name和type kind

在反射中关于类型还划分为两种:

类型(Type)和种类(Kind)。因为在Go语言中我们可以使用type关键字构造很多自定义关型,而种类(Kind)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)。举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。

package main

import (
	"fmt"
	"reflect"
)

type sss struct{}
func reflectType2(x interface{}) {
	v := reflect.TypeOf(x)
	fmt.Printf("type.name:%v,type.kind:%v\n", v.Name(), v.Kind())
}
func main() {
	a := []byte{}
	a1 := [3]int{4, 5, 6}
	var s1 sss
	reflectType2(a)  // type.name:,type.kind:slice
	reflectType2(a1) // type.name:,type.kind:array
	reflectType2(s1) // type.name:sss,type.kind:struct
}

kind类型
const (
	Invalid Kind = iota //非法类型

	bool          //布尔型
	Int           //有符号整型
	Int8          //有符号8位整型
	Int16         //有符号16位整型
	Int32         //有符号32位整型
	Int64         //有符号64位整型
	Uint          //无符号整型
	Uint8         //无符号8位整型
	Uint16        //无符号16位整型
	Uint32        //无符号32位整型
	Uint64        //无符号64位整型
	Uintptr       //指针
	Float32       //单精度浮点数
	Float64       //双精度浮点数
	Complex64     //64位复数类型
	Complex128    //128位复教类型
	Array         //数组
	Chan          //通道
	Func          //函数
	Interface     //接口
	Map           //映射
	Ptr           //指针
	slice         //切片
	String        //字符用
	Struct        //结构体
	UnsafePointer //底层指针
)
ValueOf()

reflect.Valueof()返回的是ref1ect.Value类型,其中包含了原始值的值信息。
ref1ect.Value与原始值之间可以直相换,

reflect.Value
类型提供的获取原始值的方法如下:

方法说明
Interface()interface将值以interface()类型返回,可以通过类型断言转换为指定类型
Int() int64将值以int类型返回,所有有符号整型均可以此方式返回
Uint() uint64将值以uint类型返回,所有无符号整型均可以此方式返回
Float() float64将值以双精度(float64)类型返回,所有浮点数(float32、1oat64)均可以此方式返回
Bool() bool将值以bool类型返回
Bytes()[]bytes将值以字节数组[]bytes类型返回
String() string将值以字符串类型返回
func reflectValue(x interface{}) {
	v := reflect.ValueOf(x)
	k := v.Kind() //值的类型种类
	switch k {
	case reflect.Int64:
		//v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
		fmt.Printf("type is int64,value is %d\n", int64(v.Int()))
	case reflect.Float32:
		//v.F1oat()从反射中获取整型的原始值,然后通过f1oat32()强制类型转换
		fmt.Printf("type is float32,value is %f\n", float32(v.Float()))
	case reflect.Float64:
		//v.F1oat()从反射中获取整型的原始值,然后通过f1oat64()强制类型转换
		fmt.Printf("type is float64,value is %f\n", float64(v.Float()))
	}
}
通过反射设置变量的值
package main

import (
	"fmt"
	"reflect"
)
func reflectValue2(x interface{}) {
	v := reflect.ValueOf(x)
	k := v.Elem().Kind() //值的类型种类
	fmt.Println(k == reflect.Int64)
	if k == reflect.Int64 {
		v.Elem().SetInt(200) // Elem进行地址取值,SetInt进行修改
	}
}

func main() {
	x2 := int64(100)
	reflectValue2(&x2) // 传指针
	fmt.Println(x2) 
}

isNil()
func (v Value)IsNil()bool

IsNi1()报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则isNil函
数会导致panic,

isValid()
func (v Value)IsValid()bool

IsValid()返回v是否持有一个值。如果v是Valuet零值会返回假,此时除了IsValid、.String、Kind之外的方法都会导致
panice

结构体反射

与结构体相关的方法

任意值通过reflect.TypeOf()获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象
(reflect.Type)的NumField()和Field()方法获得结构体成员的详细信息。

reflect.Type中与获取结构体成员相关的的方法如下表所示。

方法说明
Field(i int)StructField根据余引,返回宗引对应的结构体字段的信息,
NumField() int返回结构体成员字段数量。
FieldByName(name string)(StructField,bool)根据给定字符串返回字符串对应的结构体字段的信息,
FieldByIndex(index []int)StructField多层成员访问时,根据0it提供的每个结构体的字段索引,返回字段的信息,
FieldByNameFunc(match func(string)bool) (StruetField,bool)根据传入的匹配函数匹配需要的字段。
NumMethod()int返回该类型的方法集中方法的数目
Method(int)Method返回该类型方法集中的第个方法
MethodByName(string)(Method,bool)根据方法名返回该类型方法集中的方法
StructField类型

StructField类型用来描述结构体中的一个字段的信息。
StructField的定义如下:

type StructField struct{
//Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为"。
//http://golang.org/ref/spec#uniqueness_of_identifiers
Name string
PkgPath string
Type Type //字段的类型
Tag StructTag//字段的标简
offset uintptr  //字股在结枸裤中的字节偏移里
Index []int  //用于Type.FieldByIndex时的索引切片
Anonymous bool //是否匿名字段
}

package main

import (
	"fmt"
	"reflect"
)

type student struct {
	Name  string `json:"name"`
	Score int    `json:"score"`
}

func main() {
	stu1 := student{
		Name:  "小王子",
		Score: 90,
	}
	t := reflect.TypeOf(stu1)
	fmt.Println(t.Name(), t.Kind()) //student struct

	//通过fo循环遍历结构体的所有字段信息 NumField返回字段的数量
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
	}
	//通过字段名获取指定结构体字段信息
	if scoreField, ok := t.FieldByName("Score"); ok {
		fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
	}
}

练习:ini文件解析

package main

import (
	"errors"
	"fmt"
	"io/ioutil"
	"reflect"
	"strconv"
	"strings"
)

type Mysqlconfig struct {
	Address  string `ini:"address"`
	Port     int    `ini:"port"`
	Username string `ini:"username"`
	Password string `ini:"password"`
}

type RedisConfig struct {
	Host     string `ini:"host"`
	Port     int    `ini:"port"`
	Password string `ini:"password"`
	Database int    `ini:"database"`
	Test     bool   `ini:"test"`
}
type Config struct {
	Mysqlconfig `ini:"mysql"`
	RedisConfig `ini:"redis"`
}

func loadIni(fileName string, data interface{}) (err error) {
	// 0.参数的效验
	//   0.1 传进来的data参数必须是结构体类型指针(因为需要在函数中赋值)
	t := reflect.TypeOf(data)
	if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct {
		err = errors.New("类型不匹配") // 格式化输出后,返回一个error类型
		return err
	}
	// 1.读文件得到字节类型数据
	b, err := ioutil.ReadFile(fileName)
	if err != nil {
		return err
	}
	var structName string
	// 2.一行一行的读数据
	lineSlice := strings.Split(string(b), "\r\n")
	for idx, v := range lineSlice {
		// 2.1 如果是注释就跳过
		// 去掉两边空格
		v = strings.TrimSpace(v)
		// 空行直接跳过
		if len(v) == 0 {
			continue
		}
		if strings.HasPrefix(v, ";") || strings.HasPrefix(v, "#") {
			continue
		}
		//   2.2 如果是[开头的就表示是节(section)
		if strings.HasPrefix(v, "[") {
			if v[0] != '[' || v[len(v)-1] != ']' {
				return fmt.Errorf("1语法错误line:%d", idx+1)
			}
			sectionName := strings.TrimSpace(v[1 : len(v)-1])
			if len(sectionName) == 0 {
				return fmt.Errorf("2语法错误line:%d", idx+1)
			}
			// 根据字符串sectionName去data里面根据反射找到对应的结构体
			//v1 := reflect.ValueOf(data)
			for i := 0; i < t.Elem().NumField(); i++ {
				field := t.Elem().Field(i)
				if sectionName == field.Tag.Get("ini") {
					// 说明找到了对应的嵌套结构体,把字段名记录下来
					structName = field.Name
					//fmt.Println(structName)
					//fmt.Println(sectionName)

				}
			}
		} else {
			//   2.3 如果不是[开头就是=分割的键值对
			//1.以等号分割这一行,等号左边是key,等号右边是value
			if strings.Index(v, "=") == -1 || strings.HasPrefix(v, "=") {
				err = fmt.Errorf("3语法错误,line:%d", idx+1)
				return
			}
			index := strings.Index(v, "=")
			key := strings.TrimSpace(v[:index])
			value := strings.TrimSpace(v[index+1:])
			//2.根据structName去data里面把对应的嵌套结构体给取出来
			vl := reflect.ValueOf(data)
			sValue := vl.Elem().FieldByName(structName)
			sType := sValue.Type()
			if sType.Kind() != reflect.Struct {
				err = fmt.Errorf("data中%s应该是个结构体", structName)
				return err
			}
			var filedName string
			var fileType reflect.StructField
			//3.遍历嵌套结构体的每一个字段,判断tag是不是等于key
			for i := 0; i < sType.NumField(); i++ {
				filed := sType.Field(i)
				fileType = filed
				if filed.Tag.Get("ini") == key {
					filedName = filed.Name
					break
				}
			}
			if len(filedName) == 0 {
				continue
			}
			//4.如果key=tag,给这个字段赋值
			fileObj := sValue.FieldByName(filedName)
			fmt.Println(filedName, fileType.Type.Kind())
			switch fileType.Type.Kind() {
			case reflect.String:
				fileObj.SetString(value)
			case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int8, reflect.Int64:
				valueint, err := strconv.ParseInt(value, 10, 64)
				if err != nil {
					err = fmt.Errorf("语法错误line:%d", idx+1)
					return err
				}
				fileObj.SetInt(valueint)
			case reflect.Float32, reflect.Float64:
				valueFloat, err := strconv.ParseFloat(value, 64)
				if err != nil {
					err = fmt.Errorf("语法错误line:%d", idx+1)
					return err
				}
				fileObj.SetFloat(valueFloat)
			case reflect.Bool:
				valueBool, err := strconv.ParseBool(value)
				if err != nil {
					err = fmt.Errorf("语法错误line:%d", idx+1)
					return err
				}
				fileObj.SetBool(valueBool)
			}
		}

	}

	return nil
}
func main() {
	cfg := Config{}
	err := loadIni("goxuexi/lx41/conf.ini", &cfg)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("%#v\n", cfg)

}

Go语言中的并发编程

并发与并行

并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。
并行:同一时刻执行多个任务(你和你朋友都在用微信和女朋友聊天)。

Go语言的并发通过 goroutine实现。

goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。
goroutine是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。

Go语言还提供channel在多个goroutine间进行通信。
goroutine和channel是Go语言秉承的CSP(Communicating Sequential
Process)并发模式的重要实现基础。

goroutine

在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,
同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。那么能不能有一种机制,程
序员只需要定义很多个任务,让系统去帮助我们把这些任务分到CPU上实现并发执行呢?

Go语言中的goroutine就是这样一种机制,goroutine的慨念类似于线程,但goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将goroutine中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。

在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴。

goroutine与线程
可增长的栈

OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB)

goroutine的栈不是国定的,他可以按需增大和缩小goroutine的栈大小限制可以达到1GB,虽然极少会用到这么大。所以在Go语言中一次创建干万左右的goroutine也是可以的。

goroutine调度

GMP是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。

  • G很好理解,就是个goroutine的,里面除了存放本goroutine信息外还有与所在P的绑定等信息。
  • M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟,M与内核线程一般是——映射的关系,一个groutine最终是要放到M上执行的:
  • p管理着一组goroutine队列,P里面会存储当前goroutinei运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutinel队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。

P与M一般也是一一对应的。他们关系是:P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新
建一个M,阻塞G所在的P会把其他的G挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时回收旧的M。

P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1,5版本之后默认为物理线程数。在并发量大的时候会增加
一些P和M,但不会太多,切换太频繁的话得不偿失。

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。其一大特点是goroutine的调度是在用户态下完成的,不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护看一块大的内存池,不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上,再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

GOMAXPROCS

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核
心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。
Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。
Go1.5版本之前,默认使用的是单核心执行。G01.5版本之后,默认使用全部的CPU逻辑核心数。

我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里举个例子:

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var wg sync.WaitGroup

func f() {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		fmt.Printf("A:%d\n", i)
	}
}

func f1() {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		fmt.Printf("B:%d\n", i)
	}
}

// 程序启动之后会创建一个主goroutine去执行
func main() {
	runtime.GOMAXPROCS(2) // 多线程 AB 混起来,不设置默认满核,跑满CPU
	//runtime.GOMAXPROCS(1) // 单线程 A完了B,或者B完了A
	wg.Add(2)
	go f()
	go f1()
	wg.Wait() // 等待wg的计数器减为0
}

Go语言中的操作系统线程和goroutinef的关系:

  1. 一个操作系统线程对应用户态多个goroutime。
  2. go程序可以同时使用多个操作系统线程。
  3. goroutine和OS线程是多对多的关系,即m:n,
使用goroutine

Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine

一个goroutine必定对应一个函数,可以创建多个 goroutine去执行相同的函数。

启动单个goroutine
package main

import (
	"fmt"
	"time"
)

func hello(s string) {
	fmt.Println("hello", s)

}

// 程序启动之后会创建一个主goroutine去执行
func main() {
	go hello("go") // 开启一个单独的goroutine去执行hello
	hello("zc")
	fmt.Println("main")
	time.Sleep(time.Second) // main结束。那么main开启的goroutine也都会释放,所以要延迟介绍
	/*  结果:
	hello zc
	main
	hello go
	*/
}

启动多个goroutine
package main

import (
	"fmt"
	"time"
)

func hello(s interface{}) {
	fmt.Println("hello", s)

}

// 程序启动之后会创建一个主goroutine去执行
func main() {
	for i := 0; i < 100; i++ {
		go hello(i)
	}
	fmt.Println("main")
	time.Sleep(time.Second) // main结束。那么main开启的goroutine也都会释放,所以要延迟介绍

}

sync.WaitGroup 及 math/rand包
goroutine 什么时候结束

goroutine对应的函数结束了,goroutine结束了。
main函数执行完了,由main函数创捷的那些goroutine都结束了。

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func f() {
	rand.Seed(time.Now().UnixNano()) // 种子
	for i := 0; i < 5; i++ {
		r1 := rand.Int()    // int64
		r2 := rand.Intn(10) //0 <= x < 10
		fmt.Println(r1, r2)
	}
}

var wg sync.WaitGroup

func f1(i int) {
	defer wg.Done() // goroutine所对应的函数结束 wg-1
	time.Sleep(time.Second * time.Duration(rand.Intn(3)))
	fmt.Println(i)
}

// 程序启动之后会创建一个主goroutine去执行
func main() {
	for i := 0; i < 10; i++ {
		wg.Add(1) // 调用一次goroutine wg+1
		go f1(i)
	}
	wg.Wait() // 等待wg的计数器减为0
}

通道(channel)

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言的并发模型是CSP (Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。
channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Go语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channer的定义和初始化

通道是引用类型,通道类型的空值是nil
需要指定通道中元素的类型

package main

import "fmt"

var b chan int // 需要指定通道中元素的类型

func main() {
	b = make(chan int)// 不带缓冲区的初始化
	b = make(chan int, 16) // 带缓存区的的初始化
	fmt.Println(b)
}

通道的操作

<-
发送: b <- 20
接受: x := <-b
关闭: close(b)

package main

import (
	"fmt"
	"sync"
)

var b chan int // 需要指定通道中元素的类型
var wg sync.WaitGroup

func f() {
	fmt.Println(b)
	b = make(chan int)     // 不带缓冲区的初始化
	b = make(chan int, 16) // 带缓存区的的初始化
	wg.Add(1)
	go func() {
		defer wg.Done()
		s := <-b
		fmt.Println(s) // 接受
	}()
	b <- 10 // 发送,必须有接受,没有会卡死,报错
	fmt.Println(b)
	wg.Wait()
}
func f1() {
	fmt.Println(b)
	b = make(chan int, 2) // 带缓存区的的初始化
	b <- 10               // 发送,必须有接受,没有会卡死,报错
	fmt.Printf("10发送到通道b里去了\n")
	b <- 20
	fmt.Printf("20发送到通道b里去了\n")
	x := <-b
	fmt.Printf("通道b中的值为:%v\n", x)
	fmt.Printf("通道b中的值为:%v\n", x)
	close(b)
}
func main() {
	f1()
}

通道练习
package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup
var once sync.Once

func f1(ch1 chan int) {
	defer wg.Done()
	for i := 0; i < 100; i++ {
		ch1 <- i
	}
	close(ch1)
}
func f2(ch1, ch2 chan int) {
	defer wg.Done()
	for {
		x, ok := <-ch1
		if !ok {
			break
		}
		ch2 <- x * x
	}
	// 确保某个操作只执行1次
	once.Do(func() {
		close(ch2)
	})
}
func main() {
	a := make(chan int, 100)
	b := make(chan int, 100)
	wg.Add(3)
	go f1(a)
	go f2(a, b)
	go f2(a, b)
	wg.Wait()
	for ret := range b {
		fmt.Println(ret)
	}
}

单向通道

只可以发送:ch1 chan<- int

只可以接受:ch1 <- chan int

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup
var once sync.Once

func f1(ch1 chan<- int) {
	defer wg.Done()
	for i := 0; i < 100; i++ {
		ch1 <- i
	}
	// <-ch1 // 无法接受值,因为是单向通道
	close(ch1)
}
func f2(ch1 <-chan int, ch2 chan<- int) {
	defer wg.Done()
	for {
		x, ok := <-ch1
		if !ok {
			break
		}
		ch2 <- x * x
	}
	// 确保某个操作只执行1次
	once.Do(func() {
		close(ch2)
	})
}
func main() {
	a := make(chan int, 100)
	b := make(chan int, 100)
	wg.Add(3)
	go f1(a)
	go f2(a, b)
	go f2(a, b)
	wg.Wait()
	for ret := range b {
		fmt.Println(ret)
	}
}

通道总结

channel常见的异常总结,如下图:

channels异常情况总结
channel nil非空 空的 满了 没满
接收阻塞接收值阻塞接收值接收值
发送阻塞发送值发送值阻塞发送值
关闭panic关闭成功读完数据后返回零值关闭成功返回零值关闭成功读完数据后返回零值关闭成功读完数据后返回零值

关闭已经关闭的channel也会引发panic。

练习worker pool(goroutine池)
package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

/*
使用goroutine和channel实现一个计算int64随机数各位数和的程序。
1.开启一个goroutinet循环生成int64类型的随机数,发送到jobChan
2.开启24个goroutine,从jobChant中取出随机数计算各位数的和,将结果发送到resultChan
3.主goroutine,从resultChan取出结果并打印到终端输出
*/

type job struct {
	value int64
}

type result struct {
	job *job
	sum int64
}

var jobChan = make(chan *job, 100)
var resultChan = make(chan *result, 100)
var wg sync.WaitGroup

func f1(zl chan<- *job) {
	defer wg.Done()
	// 循环生成int64类型的随机数,发送到jobChan
	for {
		x := rand.Int63()
		newJob := &job{
			value: x,
		}
		zl <- newJob
		time.Sleep(time.Millisecond * 500)
	}

}

func f2(zl <-chan *job, resultChan chan<- *result) {
	defer wg.Done()
	//2.开启24个goroutine,从jobChant中取出随机数计算各位数的和,将结果发送到resultChan
	for {
		job := <-zl
		sum := int64(0)
		n := job.value
		for n > 0 {
			sum += n % 10
			n = n / 10
		}
		newResult := &result{
			job: job,
			sum: sum,
		}
		resultChan <- newResult
	}
}
func main() {
	// n := 123
	// for n > 0 {
	// 	fmt.Println(n % 10) // 3 2 1
	// 	n = n / 10          // 12 1 0
	// }
	wg.Add(1)
	go f1(jobChan)
	wg.Add(24)
	for i := 0; i < 24; i++ {
		go f2(jobChan, resultChan)
	}
	// 3.主goroutine,从resultChan取出结果并打印到终端输出
	for v := range resultChan {
		fmt.Printf("value:%d  sum:%d\n", v.job.value, v.sum)
	}
	wg.Wait()
}

select多路复用
	for {
		data, ok := <-ch1
		if !ok {
			fmt.Println("失败")
		}
		data2, ok := <-ch2
		...
	}

这种方式虽然可以实现从多个通道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了select关键
字,可以同时响应多个通道的操作。select的使用类似于switchi语句,它有一些列case分支和一个默认的分支。每个case会
对应一个通道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:


func f2(ch1, ch2 chan int) {
	var ch3 chan int = make(chan int, 1)
	defer wg.Done()
	select {
	case ch3 <- 12:
		fmt.Println("ch3:", <-ch3)
	case <-ch1:
		fmt.Println("zz")
	case data := <-ch2:
		fmt.Println("data:", data)
	default:
		fmt.Println("没有")
	}
}
	for i := 0; i < 10; i++ {
		select {
		case x := <-ch:
			fmt.Println(x) // 0 2 4 6 8
		case ch <- i:
		}
	}
练习:异步写日志

异步日志库git地址

主要代码:

type logMsg struct {
	Level     LogLevel
	msg       string
	funcName  string
	fileName  string
	timestamp string
	line      int
}
func (f *FileLogger) writeLogBackground() {
	for {
		if f.checkSize(f.fileObj) {
			newFile, err := f.splitFile(f.fileObj)
			if err != nil {
				return
			}
			f.fileObj = newFile
		}
		select {
		case logTmp := <-f.logChan:
			Level, _ := getLogString(logTmp.Level)
			fmt.Fprintf(f.fileObj, "[%s] [%s] [%s:%s:%d]:%s\n",
				logTmp.timestamp, Level, logTmp.fileName, logTmp.funcName, logTmp.line, logTmp.msg)
			if logTmp.Level >= ERROR {
				if f.checkSize(f.errFileObj) {
					newFile, err := f.splitFile(f.errFileObj)
					if err != nil {
						return
					}
					f.errFileObj = newFile
				}
				fmt.Fprintf(f.errFileObj, "[%s] [%s] [%s:%s:%d]:%s\n",
					logTmp.timestamp, Level, logTmp.fileName, logTmp.funcName, logTmp.line, logTmp.msg)
			}
		default:
			// 取不到日志,休息500毫秒
			time.Sleep(time.Millisecond * 500)
		}

	}
}

func (f *FileLogger) log(lv LogLevel, format string, a ...interface{}) {
	if f.enable(lv) {
		msg := fmt.Sprintf(format, a...)
		now := time.Now()
		funName, fileName, lineNo := gerInfo(3)
		funName = strings.Split(funName, ".")[1]
		// 先把日志发送到通道
		// 造一个logMsg对象
		logTmp := &logMsg{
			Level:     lv,
			msg:       msg,
			funcName:  funName,
			fileName:  fileName,
			timestamp: now.Format("2006-01-02 15:04:05"),
			line:      lineNo,
		}
		select {
		case f.logChan <- logTmp:
		default:
			// 把日志就丢掉保证不出现阻塞
		}

	}
}

sync包

互斥锁

package main

import (
	"fmt"
	"sync"
)

var x = 0
var wg sync.WaitGroup

func add() {
	for i := 0; i < 50000; i++ {
		x += 1
	}
	wg.Done()
}

func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

输出的内容理论上是100000,然而结果是错的,因为两个goroutine冲突

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。使用互斥锁来修复上面代码的问题:

var lock sync.Mutex
package main

import (
	"fmt"
	"sync"
)

var x = 0
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
	for i := 0; i < 50000; i++ {
		lock.Lock() // 上锁
		x += 1
		lock.Unlock() // 开锁
	}
	wg.Done()
}

func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。

读写互斥锁

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下便用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。
读写锁分为两种:读锁和写锁。当一个goroutine来获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待:当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

var rwLock sync.RWMutex
package main

import (
	"fmt"
	"sync"
	"time"
)

var x = 0

var wg sync.WaitGroup
var lock sync.Mutex     // 互斥锁 15.6903761s
var rwLock sync.RWMutex // 读写锁 426.9311ms

func read() {
	defer wg.Done()
	//lock.Lock()
	rwLock.RLock()
	fmt.Println(x)
	time.Sleep(time.Millisecond)
	//lock.Unlock()
	rwLock.RUnlock()
}
func write() {
	defer wg.Done()
	rwLock.Lock()
	// lock.Lock()
	x = x + 1
	time.Sleep(time.Millisecond * 5)
	rwLock.Unlock()
	// lock.Unlock()
}

func main() {
	start := time.Now()
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go write()
	}
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go read()
	}
	wg.Wait()
	fmt.Println(time.Now().Sub(start))
}

sync.Once

说在前面的话:这是一个进阶知识点。
在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。

Go语言中的sync包中提供了一个针对只执行一次场最的解决方案-sync.Once

sync.Once只有一个Do方法,其签名如下:

func (o *Once)Do(f func()){}

备注: 如果要执行的函数于需要传速参数就需要搭配闭包来使用。

var once sync.Once
	once.Do(func() {
		fmt.Println(666)
	})

sync.Map

Go语言中内置的map不是并发安全的。请看下面的示例:

var m = make(map[string]int)

func get(key string) int {
	return m[key]
}
func set(key string, value int) {
	m[key] = value
}
func main() {
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(n)
			set(key, n)
			fmt.Println(key, get(key))
			wg.Done()
		}(i)
	}
	wg.Wait()
}

上面的代码开启少量几个goroutine的时候可能没什么问题,当并发多了之后执行上面的代码就会报fatal error:concurrent map writes错误。
像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map-
sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如
Store写入值、Load取值、LoadOrStore读取或写入、Delete删除、Range遍历、等操作方法。

package main

import (
	"fmt"
	"strconv"
	"sync"
)

var wg sync.WaitGroup
var m = sync.Map{}

func main() {
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(n)
			m.Store(key, n)
			value, _ := m.Load(key)
			fmt.Println(key, value)
			wg.Done()
		}(i)
	}
	wg.Wait()
}

atomic包

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

var wg sync.WaitGroup
var lock sync.Mutex
var x int64

func add() {
	// lock.Lock()
	// x = x + 1
	// lock.Unlock() // 不使用锁
	atomic.AddInt64(&x, 1) // atomic包内置的int64的加法操作
	wg.Done()
}
func main() {
	wg.Add(100000)
	for i := 0; i < 100000; i++ {
		go add()
	}
	wg.Wait()
	fmt.Println(x)
	ok := atomic.CompareAndSwapInt64(&x, 100000, 10) // x如果是100000,那么返回真,将x改成10
	fmt.Println(ok, x)

}


网络编程

现在我们几乎每天都在使用互联网,我们前面已经学习了如何编写GO语言程序,但是如何才能让我们的程序通过网络互相通信呢?本章我们就一起来学习下GO语言中的网络编程。关于网络编程其实是一个很庞大的领域,本文只是简单的演示了如何使用net包进行TCPUDP通信。如需了解更详细的网络编程请自行检索和阅读专业资料。

互联网协议介绍

互联网的核心是一系列议,总称为”互联网协议”(Internet Protocol Suite),正是这一些协议规定了电脑如何连接和组网。我们理解了这些协议,就理解了互联网的原理。由于这些协议太过庞大和复杂,没有办法在这里一概而全,只能介绍一下我们日常开发中接触较多的几个协议。

互联网分层模型

互联网的逻辑实现被分为好几层。每一层都有自己的功能,就像建筑物一样,每一层都靠下一层支持。用户接触到的只是最上面的那一层,根本不会感觉到下面的几层。要理解互联网就需要自下而上理解每一层的实现的功能。
在这里插入图片描述

如上图所示,互联网按照不同的模型划分会有不用的分层,但是不论按照什么模型去划分,越往上的层越靠近用户,越往下的层越靠近硬件。在软件开发中我们使用最多的是上图中将互联网划分为五个分层的模型。接下来我们一层一层的自底向上介绍一下每一层。

物理层

我们的电脑要与外界互联网通信,需要先把电脑连接网络,我们可以用双绞线、光纤、无线电波等方式。这就叫做”实物理层”,它就是把电脑连接起来的物理手段。它主要规定了网络的一些电气特性,作用是负责传送0和1的电信号。

数据链路层

单纯的0和1没有任何意义,所以我们使用者会为其赋予一些特定的含义,规定解读电信号的方式:例如:多少个电信号算一组?每个信号位有何意义?这就是”数据链接层”的功能,它在”物理层”的上方,确定了物理层传输的o和1的分组方式及代表的意义。早期的时候,每家公司都有自己的电信号分组方式。逐渐地,一种叫做”以太网”(Et山hernet)的协议,占据了主导地位。

以太网规定,一组电信号构成一个数据包,叫做"帧(Frame)。每一帧分成两个部分:标头(Head)和数据(Data)。其中”标头”包含数据包的一些说明项,比如发送者、接受者、数据类型等等;”数据”则是数据包的具体内容。”标头”的长度,固定为18字节。”数据”的长度,最短为46字节,最长为1500字节。因此,整个”帧”最短为64字节,最长为1518字节。如果数据很长,就必须分割成多个进行发送。

那么,发送者和接受者是如何标识呢?以太网规定,连入网络的所有设备都必须具有网卡”接口。数据包必须是从一块网卡,传送到另一块网卡。网卡的地址,就是数据包的发送地址和接收地址,这叫做MAC地址。每块网卡出厂的时候,都有一个全世界独一无二的MAC地址,长度是48个二进制位,通常用12个十六进制数表示。前6个十六进制数是厂商编号,后6个是该厂商的网卡流水号。有了MAC地址,就可以定位网卡和数据包的路径了。

网络层

按照以太网协议的规则我们可以依靠MAC地址来向外发送数据。理论上依靠MAC地址,你电脑的网卡就可以找到身在世界另一个角落的某台电脑的网卡了,但是这种做法有一个重大缺陷就是以太网采用广播方式发送数据包,所有成员人手一”包”,不仅效率低,而且发送的数据只能局限在发送者所在的子网络。也就是说如果两台计算机不在同一个子网络,广播是传不过去的。这种设计是合理且必要的,因为如果互联网上每一台计算机都会收到互联网上收发的所有数据包,那是不现实的。

因此,必须找到一种方法区分野些MAC地址属于同一个子网络,哪些不是。如果是同一个子网络,就采用广播方式发送否则就采用”路由”方式发送。这就导致了”网络层”的诞生。它的作用是引进一套新的地址,使得我们能够区分不同的计算机是否属于同一个子网络。这套地址就叫做”网络地址”,简称”网址”。

“网络层出现以后,每台计算机有了两种地址,一种是LAC地址,另一种是网络地址。两种地址之间没有任何联系,MAC地址是绑定在网卡上的,网络地址则是网络管理员分配的。网络地址帮助我们确定计算机所在的子网络,MAC地址则将数据包送到该子网络中的目标网卡。因此,从逻辑上可以推断,必定是先处理网络地址,然后再处理MAC地址。

规定网络地址的协议,叫做IP协议。它所定义的地址,就被称为IP地址。目前,广泛采用的是IP协议第四版,简称IPv4。IPv4这个版本规定,网络地址由32个二进制位组成,我们通常习惯用分成四段的十进制数表示P地址,从0.0.0.0一直到255.255.255.255。

根据IP协议发送的数据,就叫做IP数据包。IP数据包也分为标头和数据两个部分:”标头”部分主要包括版本、长度、IP地址等信息,”数据部分则是IP数据包的具体内容。IP数据包的标头”部分的长度为20到6o字节,整个数据包的总长度最大为65535字节。

传输层

有了MAC地址和地址,我们已经可以在互联网上任意两台主机上建立通信。但问题是同一台主机上会有许多程序都需要用网络收发数据,比如QQ和浏览器这两个程序都需要连接互联网并收发数据,我们如何区分某个数据包到底是归哪个程序的呢?也就是说,我们还需要一个参数,表示这个数据包到底供哪个程序(进程)便用。这个参数就叫做”端口(port),它其实是每一个使用网卡的程序的编号。每个数据包都发到主机的特定端口,所以不同的程序就能取到自己所需要的数据。

“端口”是0到65535之间的一个整数,正好16个二进制位。0到1023的端口被系统占用,用户只能选用大于1023的端口。有了IP和端口我们就能实现唯一确定互联网上一个程序,进而实现网络间的程序通信。

我们必须在数据包中加入端口信息,这就需要新的协议。最简单的实现叫做UDP协议,它的格式几乎就是在数据前面,加上端口号。UDP数据包,也是由标头”和”数据”两部分组成:”标头”部分主要定义了发出端口和接收端口,”数据”部分就是具体的内容。UDP数据包非常简单,”标头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。

UDP协议的优点是比较简单,容易实现,但是缺点是可靠性较差,一且数据包发出,无法知道对方是否收到。为了解决这个问题,提高网络可靠性,TCP协议就诞生了。TCP协议能够确保数据不会遗失。它的缺点是过程复杂、实现困难、消耗较多的资源。TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过
IP数据包的长度,以确保单个TCP数据包不必再分割。

应用层

应用程序收到”传静层的数据,接下来就要对数据进行解包。由于互联网是开放架构,数据来源五花八门,必须事先规定好通信的数据格式,否则接收方根本无法获得真正发送的数据内容。”应用层的作用就是规定应用程序便用的数据格式,例如我们TCP协议之上常见的Email、HTTP、FTP等协议,这些协议就组成了互联网协议的应用层如下图所示,发送方的HTTP数据经过互联网的传输过程中会依次添加各层协议的标头信息,接收方收到数据包之后再依次根据协议解包得到数据。
在这里插入图片描述

socket编程

Socket:是BSD UNIX的进通信机制,通常也称作套接字”,用于描述IP地址和端口,是一个通信链的句柄。Socketi可以
理解为TCP/IP网络的API,它定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。电脑上运行
的应用程序通常通过”套接字”向网络发出请求或者应答网络请求。

socket图解

Socket是应用层与TCP/IP协议族通信的中间软件抽象层。在设计模式中,Socket其实就是一个门面模式,它把复
杂的TCP/IP协议族隐藏在Socket后面,对用户来说只需要调用Socketi规定的相关函数,让Socket去组织符合指定的
协议数据然后进行通信。
在这里插入图片描述

Go语言实现TCP通信

TCP协议

TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。

TCP服务端

一个T℃P服务端可以同时连接很多个客户端,例如世界各地的用户使用自己电脑上的浏览器访问淘宝网。因为Go语言中创建多个goroutine?实现并发非常方便和高效,所以我们可以每建立一次链接就创建一个goroutine去处理。

TCP服务端程序的处理流程:

  1. 监听端口
  2. 接收客户端请求建立链接
  3. 创建goroutines处理链接。
    我们使用Go语言的net包实现的TCP服务端代码如下:

sever服务端

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

func processConn(conn net.Conn) {
	// 3. 与客户通信
	var tmp [128]byte
	reader := bufio.NewReader(os.Stdin)
	for {
		n, err := conn.Read(tmp[:])
		if err != nil {
			fmt.Println("本地端口启动服务失败", err)
			return
		}
		fmt.Println(string(tmp[:n]))
		fmt.Print("请回复:")
		msg, _ := reader.ReadString('\n')
		msg = strings.TrimSpace(msg)
		if msg == "exit" {
			break
		}
		conn.Write([]byte(msg))
	}
}
func main() {
	// 1. 本地端口启动服务
	listener, err := net.Listen("tcp", "127.0.0.1:20000")
	if err != nil {
		fmt.Println("本地端口启动服务失败", err)
		return
	}
	for {
		// 2. 等待别人和我建立连接
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("本地端口启动服务失败", err)
			return
		}
		go processConn(conn)

	}
}

client客户端

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

func main() {
	// 1. 与server端建立连接
	conn, err := net.Dial("tcp", "127.0.0.1:20000")
	if err != nil {
		fmt.Println(err)
		return
	}
	// 2. 发送数据
	reader := bufio.NewReader(os.Stdin)
	for {
		fmt.Print("请说话:")
		msg, _ := reader.ReadString('\n')
		msg = strings.TrimSpace(msg)
		if msg == "exit" {
			break
		}
		conn.Write([]byte(msg))
	}
	conn.Close()

}

TCP粘包问题

sever:

package main

import (
	"bufio"
	"fmt"
	"io"
	"net"
)

func processConn(conn net.Conn) {
	defer conn.Close()
	// 3. 与客户通信
	var tmp [1024]byte
	reader := bufio.NewReader(conn)
	for {
		n, err := reader.Read(tmp[:])
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("失败", err)
			break
		}
		fmt.Println("收到client发来的数据", string(tmp[:n]))
	}
}
func main() {
	// 1. 本地端口启动服务
	listener, err := net.Listen("tcp", "127.0.0.1:20000")
	if err != nil {
		fmt.Println("本地端口启动服务失败", err)
		return
	}
	defer listener.Close()
	for {
		// 2. 等待别人和我建立连接
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("建立连接失败", err)
			continue
		}
		go processConn(conn)

	}
}

client:

package main

import (
	"fmt"
	"net"
)

func main() {
	// 1. 与server端建立连接
	conn, err := net.Dial("tcp", "127.0.0.1:20000")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer conn.Close()
	// 2. 发送数据
	for i := 0; i < 20; i++ {
		msg := "Hello,Hello,How are you"
		conn.Write([]byte(msg))
	}

}
/*
 Hello,Hello,How are youHello,Hello,How are youHello,Hello,How are youHello,Hello,How are youHello,Hello,How are you
收到client发来的数据 Hello,Hello,How are youHello,Hello,How are youHello,Hello,How are youHello,Hello,How are youHello,Hello,How are youHello,Hello,How are youHello,Hello,How are youHello,Hello,How are youHello,Hello,How are youHello,Hello,How are youHello,Hello,How are youHello,Hello,How are youHello,Hello,How are youHello,Hello,How are youHello,Hello,How are you

*/

注意: tcp发送的时候会等待一小会,看看后续是否还有数据,如果有,会一起发送

解决办法

出现”粘包的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。

封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内
容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度回定以及包头中含有包体长度的变量就能正确
的拆分出一个完整的数据包。

我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。

package protocol

import (
	"bufio"
	"bytes"
	"encoding/binary"
)

func Encode(message string) ([]byte, error) {
	//读取消,息的长度,转换成int32类型(占4个字节)
	var length = int32(len(message))
	var pkg = new(bytes.Buffer)
	//写入消息头
	err := binary.Write(pkg, binary.LittleEndian, length)
	if err != nil {
		return nil, err
	}
	//写入消息实体
	err = binary.Write(pkg, binary.LittleEndian, []byte(message))
	if err != nil {
		return nil, err
	}
	return pkg.Bytes(), nil
}

// Decode解码消息
func Decode(reader *bufio.Reader) (string, error) {
	//读取消息的长度
	lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据
	lengthBuff := bytes.NewBuffer(lengthByte)
	var length int32
	err := binary.Read(lengthBuff, binary.LittleEndian, &length)
	if err != nil {
		return "", err
	}
	//Buffered返回缓冲中现有的可读取的字节数。
	if int32(reader.Buffered()) < length+4 {
		return "", err
	}
	//读取真正的消息数据
	pack := make([]byte, int(4+length))
	_, err = reader.Read(pack)
	if err != nil {
		return "", err
	}
	return string(pack[4:]), nil
}

Go语言实现UTP通信

sever服务端

package main

import (
	"fmt"
	"net"
	"strings"
)

func main() {
	// 1. 本地端口启动服务
	conn, err := net.ListenUDP("udp", &net.UDPAddr{
		IP:   net.IPv4(127, 0, 0, 1),
		Port: 40000,
	})
	if err != nil {
		fmt.Println("本地端口启动服务失败", err)
		return
	}
	var data [1024]byte
	for {
		// 2. 不需要建立连接,直接收发数据
		n, addr, err := conn.ReadFromUDP(data[:])
		if err != nil {
			fmt.Println("收发出现错误!", err)
			return
		}
		fmt.Println(string(data[:n]))
		reply := strings.ToUpper(string(data[:n]))
		conn.WriteToUDP([]byte(reply), addr)

	}
}

服务端:

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

func main() {
	// 1. 与server端建立连接
	conn, err := net.DialUDP("udp", nil, &net.UDPAddr{
		IP:   net.IPv4(127, 0, 0, 1),
		Port: 40000,
	})
	if err != nil {
		fmt.Println(err)
		return
	}
	defer conn.Close()
	// 2. 不需要建立连接,直接发送数据
	reader := bufio.NewReader(os.Stdin)
	var reply [1024]byte
	for {
		fmt.Print("请说话:")
		msg, _ := reader.ReadString('\n')
		msg = strings.TrimSpace(msg)
		if msg == "exit" {
			break
		}
		conn.Write([]byte(msg))
		n, _, err := conn.ReadFromUDP(reply[:])
		if err != nil {
			fmt.Println("35", err)
			return
		}
		fmt.Println("收到的回复:", string(reply[:n]))
	}

}

HTTP 客户端和服务端

Go语言内置的net/http包提供了HTTP客户端和服务端的实现。

HTTP 协议

超文本传输协议(HTTP,HyperText Transfer
Protocol))是互联网上应用最为广泛的一种网络传输协议,所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。

HTTP 客户端

resp, err := http.Get("http://example.com/")

resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)

resp, err := http.PostForm("http://example.com/form",
	url.Values{"key": {"Value"}, "id": {"123"}})

程序在使用完response后必须关闭回复的主体。

resp, err := http.Get("http://example.com/")
if err != nil {
	// handle error
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)

GET请求示例

使用net/http包编写一个简单的发送HTTP请求的Sever端,代码如下:

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
)

func f1(w http.ResponseWriter, r *http.Request) {
	str, err := ioutil.ReadFile("sever/http/lx1/index.html")
	if err != nil {
		w.Write([]byte(fmt.Sprintf("%v", err)))
	}
	w.Write(str)
}
func main() {
	http.ListenAndServe("127.0.0.1:9000", nil)
	http.HandleFunc("/xx", f1)
}

Clienti端

	r, err := http.Get(`http://127.0.0.1:9000/xxx?name="xx"&age=12`)
	if err != nil {
		fmt.Println(err)
	}
	s, err := ioutil.ReadAll(r.Body)
	fmt.Println(string(s))
带参数的GET请求示例

关于GET请求的参数需要使用Go语言内置的net/url这个标准库来处理。

func main() {
	apiUrl := "http://127.0.0.1:9090/get"
	// URL param
	data := url.Values{}
	data.Set("name", "小王子")
	data.Set("age", "18")
	u, err := url.ParseRequestURI(apiUrl)
	if err != nil {
		fmt.Printf("parse url requestUrl failed, err:%v\n", err)
	}
	u.RawQuery = data.Encode() // URL encode
	fmt.Println(u.String())
	resp, err := http.Get(u.String())
	if err != nil {
		fmt.Printf("post failed, err:%v\n", err)
		return
	}
	defer resp.Body.Close()
	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Printf("get resp failed, err:%v\n", err)
		return
	}
	fmt.Println(string(b))
}

Server端

func getHandler(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	data := r.URL.Query()
	fmt.Println(data.Get("name"))
	fmt.Println(data.Get("age"))
	answer := `{"status": "ok"}`
	w.Write([]byte(answer))
}
Post请求示例
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
)

// net/http post demo

func main() {
	urls := "http://127.0.0.1:9000/xxxx"
	// 表单数据
	//contentType := "application/x-www-form-urlencoded"
	//data := "name=小王子&age=18"
	// json
	contentType := "application/json"
	data := `{"name":"小王子","age":18}`
	resp, err := http.Post(urls, contentType, strings.NewReader(data))
	if err != nil {
		fmt.Printf("post failed, err:%v\n", err)
		return
	}
	defer resp.Body.Close()
	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Printf("get resp failed, err:%v\n", err)
		return
	}
	fmt.Println(string(b))

	resp2, err := http.PostForm("http://127.0.0.1:9000/xxxx", url.Values{"name": {"Value"}, "age": {"123"}})
	if err != nil {
		fmt.Printf("post failed, err:%v\n", err)
		return
	}
	b, err = ioutil.ReadAll(resp2.Body)
	if err != nil {
		fmt.Printf("get resp failed, err:%v\n", err)
		return
	}
	fmt.Println(string(b))

}

sever端

func postHandler(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	// 1. 请求类型是application/x-www-form-urlencoded时解析form数据
	r.ParseForm()
	fmt.Println("30", r.PostForm) // 打印form数据
	fmt.Println(31, r.PostForm.Get("name"), r.PostForm.Get("age"))
	// 2. 请求类型是application/json时从r.Body读取数据
	b, err := ioutil.ReadAll(r.Body)
	if err != nil {
		fmt.Printf("read request.Body failed, err:%v\n", err)
		return
	}

	fmt.Println(39, string(b))

	answer := `{"status": "ok"}`
	w.Write([]byte(answer))
}
自定义Client

要管理HTTP客户端的头域、重定向策略和其他设置,创建一个Client:

client := &http.Client{
	CheckRedirect: redirectPolicyFunc,
}
resp, err := client.Get("http://example.com")
// ...
req, err := http.NewRequest("GET", "http://example.com", nil)
// ...
req.Header.Add("If-None-Match", `W/"wyzzy"`)
resp, err := client.Do(req)
自定义Transport

要管理代理、TLS配置、keep-alive、压缩和其他设置,创建一个Transport:

tr := &http.Transport{
	TLSClientConfig:    &tls.Config{RootCAs: pool},
	DisableCompression: true,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://example.com")
自定义server对象
package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"time"
)

type TestHandler struct {
	str string
}

// ServeHTTP方法,绑定TestHandler
func (th *TestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	log.Printf("Handle")
	w.Write([]byte(string("Handle")))
}
func f1(w http.ResponseWriter, r *http.Request) {
	str, err := ioutil.ReadFile("sever/http/lx1/index.html")
	if err != nil {
		w.Write([]byte(fmt.Sprintf("%v", err)))
	}
	w.Write(str)
}
func main() {
	s := &http.Server{
		Addr:         "127.0.0.1:7000",
		//Handler:        myHandler,
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 10 * time.Second,
	}
	mux := http.NewServeMux()
	mux.HandleFunc("/login", f1)
	mux.Handle("/", &TestHandler{"Hi"}) //根路由
	s.Handler = mux
	s.ListenAndServe()
}

单元测试

Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。

go test 命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。
*test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。

类型格试作用
测试函数函数名前缀为Test测试程序的一些逻辑行为是正确
基准函数函数名前缀为Benchmark测试函数的性能
示例函数函数名前缀为Example为文档提供示例文档

go test命令会遍历所有的*test.go文件中符合上述命名规侧则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

测试函数

每个测试函数必须导入testing包,测试函数的基本格式(签名)如下:

func TestName(t *testing.T){}

测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头,举几个例子:

func TestAdd(t *testing.T){...}

func Testsum(t "testing.T){...}

func TestLogit *testing.T){...}

其中参数t用于报告测试失败和附加的日志信息。testing.T的拥有的方法如下:

func (c *T)Error(args ...interface)
func (c *T)Errorf(format string,args ...interface{})
func (c "T)Fail()
func (c "T)FailNow()
func (c *T)Failed()bool
func (c *T)Fatal(args ...interface{})
func (c *T)Fatalf(format string,args ...interface{})
func (c "T)Log(args ...interface{))
func (c "T)Logf(format string,args ...interface{})
func (c *T)Name()string
func (t *T)Parallel()
func (t *T)Run(name string,f func(t *T))bool
func (c *T)Skip(args ...interface{})
func (c "T)SkipNow()
func (c *T)Skipf(format string,args ...interface{})
func (c *T)Skipped()bool
测试函数示例

接下来,我们定义一个split的包,包中定义了一个Split函数,具体实现如下:

package split_string

import (
	"reflect"
	"testing"
)

func TestSplit(t *testing.T) {
	got := Split("babcbef", "b")
	want := []string{"", "a", "c", "ef"}
	if reflect.DeepEqual(got, want) {
		t.Errorf("want:%v but got:%v\n", want, got)
	}

	// got2 := Split2("babcbef", "b")
	// want2 := []string{"", "a", "c", "ef"}
	// if reflect.DeepEqual(got2, want2) {
	// 	t.Error("want:%v but got:%v", got2, want2)
	// }
}
func TestSplit2(t *testing.T) {
	got := Split("a:b:c", ":")
	want := []string{"a", "b", "c"}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("want:%v but got:%v\n", want, got)
	}
}

测试组

我们现在还想要测试一下split函数对中文字符串的支持,这个时候我们可以再编写一个TestChineseSplit测试函数,但是我们也可以使用如下更友好的一种方式来添加更多的测试用例。

package split_string

import (
	"reflect"
	"testing"
)

func TestSplit(t *testing.T) {
	type testCase struct {
		str  string
		ser  string
		want []string
	}
	testGroup := []testCase{
		{"babcbef", "b", []string{"", "a", "c", "ef"}},
		{"a:b:c", ":", []string{"a", "b", "c"}},
		{"a,b,c,er", ",", []string{"a", "b", "c", "er"}},
		{"小明:你好", ":", []string{"小明", "你好"}},
	}
	for _, v := range testGroup {
		got := Split(v.str, v.ser)
		if !reflect.DeepEqual(got, v.want) {
			t.Errorf("want:%v but got:%v", got, v.want)
		}
	}
}

子测试
package split_string

import (
	"reflect"
	"testing"
)

func TestSplit(t *testing.T) {
	type testCase struct {
		str  string
		ser  string
		want []string
	}
	// 子测试
	testGroup := map[string]testCase{
		"case 1": {"babcbef", "b", []string{"", "a", "c", "ef"}},
		"case 2": {"a:b:c", ":", []string{"a", "b", "c"}},
		"case 3": {"a,b,c,er", ",", []string{"a", "b", "c", "er"}},
		"case 4": {"小明:你好", ":", []string{"小明", "你好"}},
	}
	for name, v := range testGroup {
		t.Run(name, func(t *testing.T) {
			got := Split(v.str, v.ser)
			if !reflect.DeepEqual(got, v.want) {
				t.Errorf("want:%v but got:%v", got, v.want)
			}
		})
	}
}

测试覆盖率

测试覆盖率是你的代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。

Go提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover来查看测试覆盖率。例如:
在这里插入图片描述

Go还提供了一个额外的coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件。例如:

go test -cover -coverprofile cover.out
生成一个cover.out文件

go tool cover -html cover.out
以html格式打开cover.out

性能基准测试

基准测试函数格式

基准侧试就是在一定的工作负载之下检测程序性能的一种方法。基准侧试的基本格式如下:

func BenchmarkName(b *testing.B){}

基准测试以Benchmark为前缀,需要一个*testing.B类型的参数b,基准测试必须要执行b.N次,这样的测试才有对照性,b.N的值是系统根据实际情况去调整的,从而保证测试的稳定性。testing.B拥有的方法如下:

func (c *B)Error(args ...interface{})
func (c *B)Errorf(format string,args ...interface{})
func (c *B)Fail()
func (c *B)FailNow()
func (c *B)Failed()bool
func (c *B)Fatal(args ...interface{})
func (c *B)Fatalf(format string,args ...interface{})
func (c *B)Log(args ...interface{})
func (c *B)Logf(format string,args ...interface)
func (c *B)Name()string
func (b *B)ReportAllocs()
func (b *B)ResetTimer()
func (b *B)Run(name string,f func(b *B))bool
func (b *B)RunParallel(body func(*PB))
func (b *B)SetBytes(n int64)
func (b *B)SetParallelism(p int)
func (c *B)Skip(args ...interface{})
func (c *B)SkipNow()
func (c *B)Skipf(format string,args ..interface{})
func (c *B)Skipped()bool
func (b *B)StartTimer()
func (b *B)StopTimer()
基准测试示例

我们为split包中的Split函数编写基准测试如下:


// 性能基准测试
func BenchmarkSplit(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Split("a:b:c", ":")
	}
}

在这里插入图片描述
其中BenchmarkSplit-8表示对Split函数进行基准测试,数字8表示GOMAXPROCS的值,这个对于并发基准测试很重要。
4481820和 270.0 ns/op表示每次调用Split函数耗时270ns,这个结果是
4481820次调用的平均值。

我们还可以为基准测试添加-benchmem参数,来获得内存分配的统计数据。
在这里插入图片描述

重置时间

b.ResetTimer之前的处理不会放到执行时间里,也不会输出到报告中,所以可以在之前做一些不计划作为则试报告的操作。例如:

func BenchmarkSplit2(b *testing.B) {
	time.Sleep(5 * time.Second) //假设需要做一些耗时的无关操作
	b.ResetTimer()              //重置计时器
	for i := 0; i < b.N; i++ {
		Split("a:b:c", ":")
	}
}
并行测试

func(b*B)RunParallel(body func(*PB))会以并行的方式执行给定的基准测试。
RunParallel会创建出多个goroutine,并将b.N分配给这些goroutine执行,其中goroutine数显的默认
值为GOMAXPROCS。用户如果想要增加非CPU受限(non-CPU-bound)基准测试的并行性,那么可以在RunParallel之前调用SetParallellism``RunParallel通常会与-cpu标志一同使用。

还可以通过在测试命令后添加-cpu参数如go test -bench = . -cpu 1 来指定使用的CPU数量.

示例函数

示例函数的格式

go test特殊对待的第三种函数就是示例函数,它们的函数名以Example为前缀。它们既没有参数也没有返回值。标准格式如下:

func ExampleName(){}

Go性能优化

Go语言项目中的性能优化主要有以下几个方面:

  • CPU prof1le:报告程序的CPU使用情况,按照一定频率去采集应用程序在CPU和寄存器上面的数据
  • Memory Profile(Heap Profile):报告程序的内存使用情况。
  • Block Profiling:报告goroutines不在运行状态的情况,可以用来分析和查找死锁等性能瓶颈
  • Goroutine Profiling:报告goroutines的使用情况,有哪些goroutine,它们的调用关系是怎样的

采集性能数据

Go语言内置了获取程序的运行数据的工具,包括以下两个标准库:

  • runtime/pprof:采集工具型应用运行数据进行分析
  • net/http/pprof:采集服务型应用运行时数据进行分析

ppro开启后,每隔一段时间(10ms)就会收集下当前的堆栈信息,获取格格函数占用的CPU以及内存资源:最后通过对这些采样数据进行分析,形成一个性能分析报告。

flag包

flag参数类型
flag包支持的命令行参数类型有bool、int、int64、uint、uint64、float、float64、string、duration

定义命令行flag参数

有以下两种常用的定义命令行flag参数的方法。
flag.Type()基本格式如下:

flag.Type(flag名,默认值,帮助信息)*Type

例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:

package main

import (
	"flag"
	"fmt"
)

func main() {
	// 创建一个标志位参数
	name := flag.String("name", "小飞", "请输入名字")
	age := flag.Int("age", 18, "请输入年龄")
	married := flag.Bool("married", false, "结婚了吗")
	// 使用flag
	flag.Parse()
	fmt.Println(*name)
	fmt.Println(*age)
	fmt.Println(*married)
}

在这里插入图片描述

flag.TypeVar()

基本格式如下:flag.TypeVar(Type指针,flag名,默认值,帮助信息)例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:

	// 创建一个标志位参数 flag.TypeVar()
	var name string
	var age int
	var married bool
	flag.StringVar(&name, "name", "小飞", "请输入名字")
	flag.IntVar(&age, "age", 18, "请输入年龄")
	flag.BoolVar(&married, "married", false, "结婚了吗")
	// 使用flag
	flag.Parse()
	fmt.Println(name)
	fmt.Println(age)
	fmt.Println(married)

在这里插入图片描述

flag.Parse()

通过以上两种方法定义好命令行flag参数后,需要通过调用flag.Parse()来对命令行参数进行解析。

支持的命令行参数格式有以下几种:

  • -flag xxx(使用空格,一个-符号)
  • –flag xxx(使用空格,两个-符号)
  • -flag=xxx:(使用等号,一个-符号)
  • –flag=xxx(使用等号,两个符号)

注意: 布尔值必须使用等于号

flag 其他函数
	flag.Args()  ///返回命令行参数后的其他参数,以[门string类型
	flag.NArg()  //返回命令行参数后的其他参数个数
	flag.NFlag() //返回使用的命令行参数个数
package main

import (
	"flag"
	"fmt"
)

func main() {
	// 创建一个标志位参数 flag.Type()
	// name := flag.String("name", "小飞", "请输入名字")
	// age := flag.Int("age", 18, "请输入年龄")
	// married := flag.Bool("married", false, "结婚了吗")
	// // 使用flag
	// flag.Parse()
	// fmt.Println(*name)
	// fmt.Println(*age)
	// fmt.Println(*married)

	// 创建一个标志位参数 flag.TypeVar()
	var name string
	var age int
	var married bool
	flag.StringVar(&name, "name", "小飞", "请输入名字")
	flag.IntVar(&age, "age", 18, "请输入年龄")
	flag.BoolVar(&married, "married", false, "结婚了吗")
	// 使用flag
	flag.Parse()
	fmt.Println(name)
	fmt.Println(age)
	fmt.Println(married)
	fmt.Println("Args", flag.Args())   //返回命令行参数后的其他参数,以[门string类型
	fmt.Println("NArg", flag.NArg())   //返回命令行参数后的其他参数个数
	fmt.Println("NFlag", flag.NFlag()) //返回使用的命令行参数个数
}

在这里插入图片描述

工具型应用

如果你的应用程序是运行一段时间就结束退出类型。那么最好的办法是在应用退出的时候把profiling的报告保存到文件中,进行分析。对于这种情况,可以便用runtime/pprof库。首先在代码中导入runtime/pprof工具:

import "runtime/pprof"
package main

import (
	"flag"
	"fmt"
	"os"
	"runtime/pprof"
	"time"
)

func logicCode() {
	var c chan int
	for {
		select {
		case v := <-c:
			fmt.Printf("recv from chan,value:%v\n", v)
		default:
		}
	}
}
func main() {
	var isCPUprof bool  // 是否开启 CPUprofile 的标志位
	var isMemPprof bool // 是否开启 MemPprofile 的标志位
	flag.BoolVar(&isCPUprof, "cpu", false, "turn cpu pprof on")
	flag.BoolVar(&isMemPprof, "mem", false, "turn cpu pprof on")
	flag.Parse()

	if isCPUprof {
		f1, err := os.Create("./cpu.pprof") // 在当前路径下创建一个cpu.pprof文件
		if err != nil {
			fmt.Println("cpu创建失败", err)
			return
		}
		pprof.StartCPUProfile(f1) // 往文件中记录CPU profile信息
		defer func() {
			pprof.StopCPUProfile()
			f1.Close()
		}()
	}
	for i := 0; i < 8; i++ {
		go logicCode()
	}
	time.Sleep(20 * time.Second)
	if isMemPprof {
		f2, err := os.Create("./mem.pprof")
		if err != nil {
			fmt.Println("mem创建失败", err)
			return
		}
		pprof.WriteHeapProfile(f2)
		f2.Close()
	}
}

go run main.go -cpu=true 20秒后生成一个cpu.pprof文件
go tool pprof cpu.pprof 读取cpu.pprof文件
在这里插入图片描述

  • flat:当前函数占用CPU的耗时
  • flat%::当前函数占用CPU的耗时百分比
  • sun%:函数占用CPU的耗时累计百分比
  • cum::当前函数力加上调用当前函数的函数占用CPU的总耗时
  • cum%:当前函数加上调用当前函数的函数占用CPU的总耗时百分比
  • 最后一列:函数名称

我们还可以使用list函数名命令查看具体的函数分析,例如执行list logicCode查看我们编写的函数的详细分析。
在这里插入图片描述

Go操作Mysql

Go语言中的database/sql包提供了保证SQL或类SQL数据库的泛用接口,并不提供具体的数据库驱动。使用database/.sql包时必须注以(至少)一个数据库驱动。

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

下载MySQL驱动

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

打开数据库

	dsn := "root:123456@tcp(127.0.0.1:3306)/goday"
	db, err := sql.Open("mysql", dsn)

连接数据库

err = db.Ping()
package main

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	// 数据库信息
	dsn := "root:123456@tcp(127.0.0.1:3306)/goday"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		fmt.Println("打开数据库失败!", err)
		return
	}
	defer db.Close()
	err = db.Ping()
	if err != nil {
		fmt.Println("数据库连接失败!", err)
		return
	}
	fmt.Println("连接数据库成功")

}


SetMaxOpenConns 设置最大连接数

func (db *DB)SetMaxOpenConns(n int)

SetMaxOpenConns设置与数据库建立连接的最大数目。如果n大于0且小于最大闲置连接数,会将最大闲置连接数减小到匹配最大开启连接数的限制。如果<=0,不会限制最大开启连接数,默认为0(无限制)。

SetMaxIdleConns

func (db "DB)SetMaxIdleconns(n int)

SetMaxIdleConns设量连接池中的最大闲置连接数。如果n大于最大开启连接数,则新的最大闲置连接数会减小到匹配最大开启连接数的限制。如果<=0,不会保留闲置连接。业务少的时候,可以适当关闭几个。

	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)

查询单条记录

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

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

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
)

var db *sql.DB

type user struct {
	id   int
	name string
	age  int
}

func initDB() (err error) {
	// 数据库信息
	dsn := "root:123456@tcp(127.0.0.1:3306)/goday"
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		return fmt.Errorf("连接数据库失败:%v\n!", err)
	}
	err = db.Ping()
	if err != nil {

		return fmt.Errorf("数据库Ping失败:%v\n!", err)
	}
	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)
	return nil
}
func queryOne(sqlStr string, i ...interface{}) (u1 user) {
	// 执行并拿到结果
	// 从连接池里拿一个连接出来去数据库查询单条记录
	// 必须对db.QueryRow的返回值调用Scan方法,因为该方法会释放数据库连接
	db.QueryRow(sqlStr, i...).Scan(&u1.id, &u1.name, &u1.age)
	return u1
}
func insert() {

}
func main() {
	var u1 user
	err := initDB()
	if err != nil {
		fmt.Println("数据库初始化失败:", err)
	}
	fmt.Println("连接数据库成功")
	// sql语句
	sqlStr := `select id,name,age from user where id=? AND AGE=?;`
	u1 = queryOne(sqlStr, 10, 827)
	// 打印结果
	fmt.Printf("%#v", u1)
}

查询多条记录

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

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

具体示例代码:

package main

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
)

var db *sql.DB

type user struct {
	id   int
	name string
	age  int
}

func initDB() (err error) {
	// 数据库信息
	dsn := "root:123456@tcp(127.0.0.1:3306)/goday"
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		return fmt.Errorf("连接数据库失败:%v\n!", err)
	}
	err = db.Ping()
	if err != nil {

		return fmt.Errorf("数据库Ping失败:%v\n!", err)
	}
	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)
	return nil
}

func queryMore(sqlStr string, i ...interface{}) (u []user) {
	// 执行并拿到结果
	// 从连接池里拿一个连接出来去数据库查询单条记录
	// 必须对db.QueryRow的返回值调用Scan方法,因为该方法会释放数据库连接
	rows, err := db.Query(sqlStr, i...)
	if err != nil {
		fmt.Errorf("错误%v!", err)
	}
	defer rows.Close()
	for rows.Next() {
		var u1 user
		err = rows.Scan(&u1.id, &u1.name, &u1.age)
		if err != nil {
			fmt.Errorf("错误%v!", err)
		}
		u = append(u, u1)
	}
	return u
}

func main() {
	var u1 user
	err := initDB()
	if err != nil {
		fmt.Println("数据库初始化失败:", err)
		return
	}
	fmt.Println("连接数据库成功")
	// sql语句
	sqlStr = `select id,name,age from user where id<?;`
	u2 := queryMore(sqlStr, 10)
	fmt.Printf("%#v", u2)

}

插入、更新和删除数据

插入、更新和删除操作都使用方法。 Exec(query string,args...interface{})(Result,error)
插入:Result.LastInsertId()(int64, error) 返回插入的id
更新/删除:Result.RowsAffected()(int64, error) 返回影响的行数

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

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

LastInsertId()具体插入数据示例代码如下:
package main

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
)

var db *sql.DB

type user struct {
	id   int
	name string
	age  int
}

func initDB() (err error) {
	// 数据库信息
	dsn := "root:123456@tcp(127.0.0.1:3306)/goday"
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		return fmt.Errorf("连接数据库失败:%v\n!", err)
	}
	err = db.Ping()
	if err != nil {

		return fmt.Errorf("数据库Ping失败:%v\n!", err)
	}
	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)
	return nil
}
func queryOne(sqlStr string, i ...interface{}) (u1 user) {
	// 执行并拿到结果
	// 从连接池里拿一个连接出来去数据库查询单条记录
	// 必须对db.QueryRow的返回值调用Scan方法,因为该方法会释放数据库连接
	db.QueryRow(sqlStr, i...).Scan(&u1.id, &u1.name, &u1.age)
	return u1
}

func insert(sqlStr string, i ...interface{}) int64 {
	ret, err := db.Exec(sqlStr, i...)
	if err != nil {
		fmt.Println(err)
	}
	//如果是插入数据的操作,能够拿到插入数据的id
	id, err := ret.LastInsertId()
	if err != nil {
		fmt.Println("get id failed,err:", err)
	}
	return id
}
func main() {
	var u1 user
	err := initDB()
	if err != nil {
		fmt.Println("数据库初始化失败:", err)
		return
	}
	fmt.Println("连接数据库成功")
	// sql语句
	sqlStr := `select id,name,age from user where id=? AND AGE=?;`
	u1 = queryOne(sqlStr, 10, 827)
	// 打印结果
	fmt.Printf("%#v\n", u1)

	sqlStr = `INSERT INTO user(name,age) VALUES("小飞",?)`
	d := insert(sqlStr, 13)
	fmt.Printf("%#v\n", d)
	// sql语句
	sqlStr = `select id,name,age from user where id=?;`
	u1 = queryOne(sqlStr, d)
	// 打印结果
	fmt.Printf("%#v\n", u1)
}

RowsAffected()具体更新数据示例代码如下:
package main

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
)

var db *sql.DB

type user struct {
	id   int
	name string
	age  int
}

func initDB() (err error) {
	// 数据库信息
	dsn := "root:123456@tcp(127.0.0.1:3306)/goday"
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		return fmt.Errorf("连接数据库失败:%v\n!", err)
	}
	err = db.Ping()
	if err != nil {

		return fmt.Errorf("数据库Ping失败:%v\n!", err)
	}
	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)
	return nil
}
func queryOne(sqlStr string, i ...interface{}) (u1 user) {
	// 执行并拿到结果
	// 从连接池里拿一个连接出来去数据库查询单条记录
	// 必须对db.QueryRow的返回值调用Scan方法,因为该方法会释放数据库连接
	db.QueryRow(sqlStr, i...).Scan(&u1.id, &u1.name, &u1.age)
	return u1
}
func updateRowDemo(sqlStr string, i ...interface{}) int64 {
	ret, err := db.Exec(sqlStr, i...)
	if err != nil {
		fmt.Println(err)
	}
	//如果是插入数据的操作,能够拿到插入数据的id
	id, err := ret.RowsAffected()
	if err != nil {
		fmt.Println("get id failed,err:", err)
	}
	return id
}
func main() {
	var u1 user
	err := initDB()
	if err != nil {
		fmt.Println("数据库初始化失败:", err)
		return
	}
	fmt.Println("连接数据库成功")
	// sql语句
	sqlStr := `UPDATE user SET age=? WHERE name=?;`
	d := updateRowDemo(sqlStr, 16, "xxx")
	fmt.Printf("%#v\n", d)
	sqlStr = `select id,name,age from user where name=?;`
	u1 := queryOne(sqlStr, "xxx")
	fmt.Printf("%#v\n", u1)

}

RowsAffected()具体删除数据示例代码如下:
package main

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
)

var db *sql.DB

type user struct {
	id   int
	name string
	age  int
}

func initDB() (err error) {
	// 数据库信息
	dsn := "root:123456@tcp(127.0.0.1:3306)/goday"
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		return fmt.Errorf("连接数据库失败:%v\n!", err)
	}
	err = db.Ping()
	if err != nil {

		return fmt.Errorf("数据库Ping失败:%v\n!", err)
	}
	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)
	return nil
}

func deleteRowDemo(sqlStr string, i ...interface{}) int64 {
	ret, err := db.Exec(sqlStr, i...)
	if err != nil {
		fmt.Println(err)
	}
	//如果是插入数据的操作,能够拿到插入数据的id
	id, err := ret.RowsAffected()
	if err != nil {
		fmt.Println("get id failed,err:", err)
	}
	return id
}
func main() {
	var u1 user
	err := initDB()
	if err != nil {
		fmt.Println("数据库初始化失败:", err)
		return
	}
	fmt.Println("连接数据库成功")
	// sql语句
	sqlStr := `DELETE FROM user WHERE id = ?;`
	d := deleteRowDemo(sqlStr, 2)
	fmt.Printf("%#v\n", d)

}

MySQL预处理

什么是预处理?

普通SQL语句执行过程:

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

预处理执行过程:

  1. 把$QL语句分成两部分,命令部分与数据部分
  2. 先把命令部分发送给MySQL服务端,MySQL服务端进行SQL预处理。
  3. 然后把数据部分发送给MySQL服务端,MySQL服务端对SQL语句进行占位符替换。
  4. MySQL服务端执行完整的SQ儿语句并将结果返回给客户端。
为什么要预处理?
  1. 优化MySQL服务器重复执行SQL的方法,可以提升服务器性能,提前让服务器编译,一次编译多次执行,节省后续编译的成本。
  2. 避免SQL注入问题。
Go实现MySQL预处理

Go中的

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

Prepare方法会先将sql语句发送给MySQL服务端,返回一个准备好的状态用于之后的查询和命令。返回值可以同时执行多个查询和命令。

查询操作的预处理示例代码如下:

package main

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
)

var db *sql.DB

type user struct {
	id   int
	name string
	age  int
}

func initDB() (err error) {
	// 数据库信息
	dsn := "root:123456@tcp(127.0.0.1:3306)/goday"
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		return fmt.Errorf("连接数据库失败:%v\n!", err)
	}
	err = db.Ping()
	if err != nil {

		return fmt.Errorf("数据库Ping失败:%v\n!", err)
	}
	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)
	return nil
}

func prepareQueryDemo() {
	var u1 user
	// sql语句
	sqlStr := `select id,name,age from user where id=?;`
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer stmt.Close()
	for i := 1; i < 10; i++ {
		err = stmt.QueryRow(i).Scan(&u1.id, &u1.name, &u1.age)
		if err != nil {
			fmt.Println(err)
			return
		}
		// 打印结果
		fmt.Printf("%#v\n", u1)
	}

	// sql语句
	sqlStr = `insert into user(name,age) VALUES(?,?);`
	stmt2, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer stmt2.Close()

	for i := 10; i < 20; i++ {
		res, err := stmt2.Exec("xx", i)
		if err != nil {
			fmt.Println(err)
			return
		}
		d, err := res.LastInsertId()
		if err != nil {
			fmt.Println(err)
			return
		}
		// 打印结果
		fmt.Printf("%#v\n", d)
	}

}
func main() {
	err := initDB()
	if err != nil {
		fmt.Println("数据库初始化失败:", err)
		return
	}
	fmt.Println("连接数据库成功")
	prepareQueryDemo()
}

Go实现MySQL事务操作

什么是事务?
事务:一个最小的不可再分的工作单元;通常一个事务对应一个完整的业务(例如银行账户转账业务,该业务就是一个最
小的工作单元),同时这个完整的业务需要执行多次的DMIL(insert、.update、delete)语句共同联合完成。A转账给B,这里
面就需要执行两次update操作。
在MySOL中只有使用了innodb数据库引擎的数据库或表才支持事务。事务处理可以用来维护数据库的完整性,保证成
批的SQL语句要么全部执行,要么全部不执行。

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

事务相关方法

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

开始事务

 func (db *DB)Begin()(*Tx,error)

提交事务

 func (tx *Tx)Commit()error

回滚事务

func (tx *Tx)Rollback()error
事务示例
package main

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
)

var db *sql.DB

type user struct {
	id   int
	name string
	age  int
}

func initDB() (err error) {
	// 数据库信息
	dsn := "root:123456@tcp(127.0.0.1:3306)/goday"
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		return fmt.Errorf("连接数据库失败:%v\n!", err)
	}
	err = db.Ping()
	if err != nil {

		return fmt.Errorf("数据库Ping失败:%v\n!", err)
	}
	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)
	return nil
}

// 事务操作示例
func transactionDemo() {
	tx, err := db.Begin() // 开启事务
	if err != nil {
		fmt.Println("begin err", err)
		return
	}
	sqlStr1 := `update user set age = age - 2 where id = 1`
	sqlStr2 := `update xx set age = age + 2 where id = 2` // 错误,执行此sql会报错回滚
	// 执行sql1
	_, err = tx.Exec(sqlStr1)
	if err != nil {
		err := tx.Rollback()
		if err != nil {
			fmt.Println("回滚失败")
			return
		}
		fmt.Println("sqlStr1失败")
		return
	}
	// 执行sql2
	_, err = tx.Exec(sqlStr2)
	if err != nil {
		err := tx.Rollback()
		if err != nil {
			fmt.Println("回滚失败")
			return
		}
		fmt.Println("sqlStr2失败")
		return
	}
	err = tx.Commit()
	if err != nil {
		err := tx.Rollback()
		if err != nil {
			fmt.Println("回滚失败")
			return
		}
		fmt.Println("提交失败")
		return
	}
	fmt.Println("事务执行成功 ")

}
func main() {
	err := initDB()
	if err != nil {
		fmt.Println("数据库初始化失败:", err)
		return
	}
	fmt.Println("连接数据库成功")
	transactionDemo()
}

sqlx的使用

go get github.com/jmoiron/sqlx

连接数据库
var db *sqlx.DB

func initDB() (err error) {
	// 数据库信息
	dsn := "root:123456@tcp(127.0.0.1:3306)/goday"
	db, err = sqlx.Connect("mysql", dsn)
	if err != nil {
		return fmt.Errorf("连接数据库失败:%v\n!", err)
	}
	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)
	return nil
}
查询单行
func GetOne() {
	var u user
	sqlStr := `select id,name,age from user where id=?;`
	err := db.Get(&u, sqlStr, 1)
	if err != nil {
		return
	}
	fmt.Printf("%#v\n", u)
}
查询多行
func GetList() {
	var userList []user
	sqlStr := `select id,name,age from user where id>? and id<?;`
	err := db.Select(&userList, sqlStr, 1, 4)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("%#v\n", userList)
}

sql注入

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

这里我们演示一个自行拼接SQL语句的示例,编写一个根据name字段查询user表的函数如下:

func Get() {
	var u []user
	xx := `xxxx" or 1=1;#`
	sqlStr := fmt.Sprintf(`select id,name,age from user where name="%s";`, xx)
	fmt.Println(sqlStr)
	err := db.Select(&u, sqlStr)
	if err != nil {
		return
	}
	fmt.Printf("%#v\n", u)
}

Go语言操作Redis

Redis介绍

Redis是一个开源的内存数据库,Redis提供了多种不同类型的数据结构,很多业务场景下的问题都可以很自然地映射到这些数据结构上。除此之外,通过复制、持久化和客户端分片等特性,我们可以很方便地将Redis扩展成一个能够包含数百GB数据、每秒处理上百万次请求的系统。

Redis支持的数据结构

Redis支持诸如字符串(strings)、哈希(hashes)、列表ists)集合[(sets)、带范围查询的排序集合(sortedsets)、位图(bitmaps)、hyperloglogs、.带半径查询和流的地理空间索引等数据结构(geospatial
indexes)。

Redis应用场景

  • 缓存系统,减轻主数据库(MySQL)的压力。
  • 计数场景,比如微博、抖音中的关注数和粉丝数。
  • 热门排行榜,需要排序的场景特别适合使用ZSET。
  • 利用LIST可以实现队列的功能。

Go操作Redis

安装

Go语言中使用第三方库https:/github.com/go-redis/redisi连接Redis数据库并进行操作。使用以下命令下载并安装:

 go get -u github.com/go-redis/redis
连接
package main

import (
	"fmt"

	"github.com/go-redis/redis"
)

var redisdb *redis.Client

func initRedis() (err error) {
	redisdb = redis.NewClient(&redis.Options{
		Addr:     "127.0.0.1:6379",
		Password: "",
		DB:       0,
	})
	_, err = redisdb.Ping().Result()
	if err != nil {
		return err
	}
	return
}
func main() {
	err := initRedis()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("redis连接成功")
}

基本使用set/get
func redisExample() {
	err := redisdb.Set("score", 100, 0).Err()
	if err != nil {
		fmt.Println("设置值失败")
		return
	}
	val, err := redisdb.Get("score").Result()
	if err != nil {
		fmt.Println("读取值失败")
		return
	}
	fmt.Println(val)

}

命令行:

  1. 保存: set dsb “zl” EX 5
  2. 取出: get dsb
zset示例
func redisExample2() {
	key := "language_rank"
	items := []redis.Z{
		{
			Score:  90.0,
			Member: "Golang",
		},
		{
			Score:  98.0,
			Member: "Java",
		},
		{
			Score:  95.0,
			Member: "Python",
		},
		{
			Score:  97.0,
			Member: "JavaScript",
		},
		{
			Score:  99.0,
			Member: "C/C++",
		},
	}
	// ZADD 添加到 redisdb
	n, err := redisdb.ZAdd(key, items...).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("n=", n)

	// Golang加10分
	newScore, err := redisdb.ZIncrBy(key, 10.0, "Golang").Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("newScore  =", newScore)
	// 取分数最高的3个
	ret, err := redisdb.ZRevRangeWithScores(key, 0, 2).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	for _, v := range ret {
		fmt.Println(v.Member, v.Score)
	}
	fmt.Println("取95-100分的")
	// 取95-100分的
	op := redis.ZRangeBy{
		Min: "95",
		Max: "100",
	}
	ret, err = redisdb.ZRangeByScoreWithScores(key, op).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	for _, v := range ret {
		fmt.Println(v.Member, v.Score)
	}
}

命令行:
循环: zrange language_rank 0 100
排序: zrevrange language_rank 0 2 取前两个

NSQ

NSQ是目前比较流行的一个分布式的消息队列,本文主要介绍了NSQ及Go语言如何操作NSQ。

NSQ介绍

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

  1. NSQ提倡分布式和分散的拓扑,没有单点故障,支持容措和高可用性,并提供可靠的消息交付保证
  2. NSQ支持横向扩展,没有任何集中式代理
  3. NSQ易于置和部零,并且内置了管理界面。

NSQ的应用场景

通常来说,消息队列都适用以下场景。

异步处理

参照下图利用消息队列把业务流程中的非关键流程异步化,从而显著降低业务请求的响应时间。
在这里插入图片描述

应用解耦

通过使用消息队列将不同的业务逻辑解耦,降低系统间的耦合,提高系统的健壮性。后续有其他业务要使用订单数据可直接订阅消息队列,提高系统的灵活性。
在这里插入图片描述

流量削峰

类似秒杀(大秒)等场景下,某一时间可能会产生大量的请求,使用消息队列能够为后端处理请求提供一定的缓冲区,保证后端服务的稳定性。
在这里插入图片描述

安装

官方下载页面根据自己的平台下载并解压即可。

NSQ组件

nsqd

nsqd是一个守护进程,它接收、排队并向客户端发送消息。

启动nsqd,指定-broadcast-address=127.0.0.1来配置广播地址

./nsqd -broadcast-address=127.0.0.1

如果是在搭配nsqlookupd使用的模式下需要还指定nsqlookupd地址:

./nsqd -broadcast-address=127.0.0.1 -lookupd-tcp-address=127.0.0.1:4160

如果是部署了多个nsqlookupd节点的集群,那还可以指定多个-lookupd-tcp-address。

nsqdq相关配置项如下:
-auth-http-address value
    <addr>:<port> to query auth server (may be given multiple times)
-broadcast-address string
    address that will be registered with lookupd (defaults to the OS hostname) (default "PROSNAKES.local")
-config string
    path to config file
-data-path string
    path to store disk-backed messages
-deflate
    enable deflate feature negotiation (client compression) (default true)
-e2e-processing-latency-percentile value
    message processing time percentiles (as float (0, 1.0]) to track (can be specified multiple times or comma separated '1.0,0.99,0.95', default none)
-e2e-processing-latency-window-time duration
    calculate end to end latency quantiles for this duration of time (ie: 60s would only show quantile calculations from the past 60 seconds) (default 10m0s)
-http-address string
    <addr>:<port> to listen on for HTTP clients (default "0.0.0.0:4151")
-http-client-connect-timeout duration
    timeout for HTTP connect (default 2s)
-http-client-request-timeout duration
    timeout for HTTP request (default 5s)
-https-address string
    <addr>:<port> to listen on for HTTPS clients (default "0.0.0.0:4152")
-log-prefix string
    log message prefix (default "[nsqd] ")
-lookupd-tcp-address value
    lookupd TCP address (may be given multiple times)
-max-body-size int
    maximum size of a single command body (default 5242880)
-max-bytes-per-file int
    number of bytes per diskqueue file before rolling (default 104857600)
-max-deflate-level int
    max deflate compression level a client can negotiate (> values == > nsqd CPU usage) (default 6)
-max-heartbeat-interval duration
    maximum client configurable duration of time between client heartbeats (default 1m0s)
-max-msg-size int
    maximum size of a single message in bytes (default 1048576)
-max-msg-timeout duration
    maximum duration before a message will timeout (default 15m0s)
-max-output-buffer-size int
    maximum client configurable size (in bytes) for a client output buffer (default 65536)
-max-output-buffer-timeout duration
    maximum client configurable duration of time between flushing to a client (default 1s)
-max-rdy-count int
    maximum RDY count for a client (default 2500)
-max-req-timeout duration
    maximum requeuing timeout for a message (default 1h0m0s)
-mem-queue-size int
    number of messages to keep in memory (per topic/channel) (default 10000)
-msg-timeout string
    duration to wait before auto-requeing a message (default "1m0s")
-node-id int
    unique part for message IDs, (int) in range [0,1024) (default is hash of hostname) (default 616)
-snappy
    enable snappy feature negotiation (client compression) (default true)
-statsd-address string
    UDP <addr>:<port> of a statsd daemon for pushing stats
-statsd-interval string
    duration between pushing to statsd (default "1m0s")
-statsd-mem-stats
    toggle sending memory and GC stats to statsd (default true)
-statsd-prefix string
    prefix used for keys sent to statsd (%s for host replacement) (default "nsq.%s")
-sync-every int
    number of messages per diskqueue fsync (default 2500)
-sync-timeout duration
    duration of time per diskqueue fsync (default 2s)
-tcp-address string
    <addr>:<port> to listen on for TCP clients (default "0.0.0.0:4150")
-tls-cert string
    path to certificate file
-tls-client-auth-policy string
    client certificate auth policy ('require' or 'require-verify')
-tls-key string
    path to key file
-tls-min-version value
    minimum SSL/TLS version acceptable ('ssl3.0', 'tls1.0', 'tls1.1', or 'tls1.2') (default 769)
-tls-required
    require TLS for client connections (true, false, tcp-https)
-tls-root-ca-file string
    path to certificate authority file
-verbose
    enable verbose logging
-version
    print version string
-worker-id
    do NOT use this, use --node-id
nsqlookupd

nsqlookupd是维护所有nsqd状态、提供服务发现的守护进程。它能为消费者查找特定topic下的nsqd提供了运行时的自动发现服务。
它不维持持久状态,也不需要与任何其他nsqlookupd实例协调以满足查询。因此根据你系统的冗余要求尽可能多地部署nsqlookupd节点。它们小豪的资源很少,可以与其他服务共存。我们的建议是为每个数据中心运行至少3个集群。

nsqlookupd相关配置项如下:
-broadcast-address string
    address of this lookupd node, (default to the OS hostname) (default "PROSNAKES.local")
-config string
    path to config file
-http-address string
    <addr>:<port> to listen on for HTTP clients (default "0.0.0.0:4161")
-inactive-producer-timeout duration
    duration of time a producer will remain in the active list since its last ping (default 5m0s)
-log-prefix string
    log message prefix (default "[nsqlookupd] ")
-tcp-address string
    <addr>:<port> to listen on for TCP clients (default "0.0.0.0:4160")
-tombstone-lifetime duration
    duration of time a producer will remain tombstoned if registration remains (default 45s)
-verbose
    enable verbose logging
-version
    print version string
nsqadmin

一个实时监控集群状态、执行各种管理任务的Web管理平台。 启动nsqadmin,指定nsqlookupd地址:

./nsqadmin -lookupd-http-address=127.0.0.1:4161

我们可以使用浏览器打开http://127.0.0.1:4171/访问如下管理界面。
在这里插入图片描述

nsqadmin相关的配置项如下:
-allow-config-from-cidr string
    A CIDR from which to allow HTTP requests to the /config endpoint (default "127.0.0.1/8")
-config string
    path to config file
-graphite-url string
    graphite HTTP address
-http-address string
    <addr>:<port> to listen on for HTTP clients (default "0.0.0.0:4171")
-http-client-connect-timeout duration
    timeout for HTTP connect (default 2s)
-http-client-request-timeout duration
    timeout for HTTP request (default 5s)
-http-client-tls-cert string
    path to certificate file for the HTTP client
-http-client-tls-insecure-skip-verify
    configure the HTTP client to skip verification of TLS certificates
-http-client-tls-key string
    path to key file for the HTTP client
-http-client-tls-root-ca-file string
    path to CA file for the HTTP client
-log-prefix string
    log message prefix (default "[nsqadmin] ")
-lookupd-http-address value
    lookupd HTTP address (may be given multiple times)
-notification-http-endpoint string
    HTTP endpoint (fully qualified) to which POST notifications of admin actions will be sent
-nsqd-http-address value
    nsqd HTTP address (may be given multiple times)
-proxy-graphite
    proxy HTTP requests to graphite
-statsd-counter-format string
    The counter stats key formatting applied by the implementation of statsd. If no formatting is desired, set this to an empty string. (default "stats.counters.%s.count")
-statsd-gauge-format string
    The gauge stats key formatting applied by the implementation of statsd. If no formatting is desired, set this to an empty string. (default "stats.gauges.%s")
-statsd-interval duration
    time interval nsqd is configured to push to statsd (must match nsqd) (default 1m0s)
-statsd-prefix string
    prefix used for keys sent to statsd (%s for host replacement, must match nsqd) (default "nsq.%s")
-version
    print version string

NSQ架构

在这里插入图片描述

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的一部分消息)。

NSQ接收和发送消息流程

在这里插入图片描述

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

Go操作NSQ

官方提供了Go语言版的客户端:go-nsq,更多客户端支持请查看CLIENT LIBRARIES

安装
go get -u github.com/nsqio/go-nsq
生产者

一个简单的生产者示例代码如下:

// nsq_producer/main.go
package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"

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

// NSQ Producer Demo

var producer *nsq.Producer

// 初始化生产者
func initProducer(str string) (err error) {
	config := nsq.NewConfig()
	producer, err = nsq.NewProducer(str, config)
	if err != nil {
		fmt.Printf("create producer failed, err:%v\n", err)
		return err
	}
	return nil
}

func main() {
	nsqAddress := "127.0.0.1:4150"
	err := initProducer(nsqAddress)
	if err != nil {
		fmt.Printf("init producer failed, err:%v\n", err)
		return
	}

	reader := bufio.NewReader(os.Stdin) // 从标准输入读取
	for {
		data, err := reader.ReadString('\n')
		if err != nil {
			fmt.Printf("read string from stdin failed, err:%v\n", err)
			continue
		}
		data = strings.TrimSpace(data)
		if strings.ToUpper(data) == "Q" { // 输入Q退出
			break
		}
		// 向 'topic_demo' publish 数据
		err = producer.Publish("topic_demo", []byte(data))
		if err != nil {
			fmt.Printf("publish msg to nsq failed, err:%v\n", err)
			continue
		}
	}
}

将上面的代码编译执行,然后在终端输入两条数据123和456:

$ ./nsq_producer 
123
2018/10/22 18:41:20 INF    1 (127.0.0.1:4150) connecting to nsqd
456

使用浏览器打开http://127.0.0.1:4171/可以查看到类似下面的页面: 在下面这个页面能看到当前的topic信息:
在这里插入图片描述
点击页面上的topic_demo就能进入一个展示更多详细信息的页面,在这个页面上我们可以查看和管理topic,同时能够看到目前在LWZMBP:4151 (127.0.01:4151)这个nsqd上有2条message。又因为没有消费者接入所以暂时没有创建channel。nsqadmin界面2
在这里插入图片描述

在/nodes这个页面我们能够很方便的查看当前接入lookupd的nsqd节点。nsqadmin界面3
在这里插入图片描述

这个/counter页面显示了处理的消息数量,因为我们没有接入消费者,所以处理的消息数量为0。nsqadmin界面4
在这里插入图片描述

在/lookup界面支持创建topic和channel。nsqadmin界面5
在这里插入图片描述

消费者

一个简单的消费者示例代码如下:

// nsq_consumer/main.go
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: "沙河1号",
	}
	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", "127.0.0.1:4161")
	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                              // 阻塞
}

将上面的代码保存之后编译执行,就能够获取之前我们publish的两条消息了:

$ ./nsq_consumer 
2018/10/22 18:49:06 INF    1 [topic_demo/first] querying nsqlookupd http://127.0.0.1:4161/lookup?topic=topic_demo
2018/10/22 18:49:06 INF    1 [topic_demo/first] (127.0.0.1:4150) connecting to nsqd
沙河1号 recv from 127.0.0.1:4150, msg:123
沙河1号 recv from 127.0.0.1:4150, msg:456

同时在nsqadmin的/counter页面查看到处理的数据数量为2。
在这里插入图片描述
关于go-nsq的更多内容请阅读go-nsq的官方文档。

go module

go module是Go1.11版本之后官方推出的版本管理工具,并且从Go1.13版本开始,go
module将是Go语言默认的依赖管理工具。

GO111MODULE

要启用go module支持首先要设置环境变量GO111MODULE,通过它可以开启或关闭模块支持,它有三个可选值:off、on、auto,默认值是auto。

  • GO111MODULE=off禁用模块支持,编译时会从GOPATH和vendor文件夹中查找包。
  • GO111MODULE=on启用模块支持,编译时会忽略GOPATH和vendor文件夹,只根据 go.mod下载依赖。
  • GO111MODULE=auto,当项目在$GOPATH/src外且项目根目录有go.mod文件时,开启模块支持。

简单来说,设置GO111MODULE=on之后就可以使用go module了,以后就没有必要在GOPATH中创建项目了,并且还能够很好的管理项目依赖的第三方包信息。

使用 go module 管理依赖后会在项目根目录下生成两个文件go.mod和go.sum。

GOPROXY

Go1.11之后设置GOPROXY命令为:

export GOPROXY=https://goproxy.cn

Go1.13之后GOPROXY默认值为https://proxy.golang.org,在国内是无法访问的,所以十分建议大家设置GOPROXY,这里我推荐使用goproxy.cn。

go env -w GOPROXY=https://goproxy.cn,direct

go mod命令

常用的go mod命令如下:

go mod download    下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)
go mod edit        编辑go.mod文件
go mod graph       打印模块依赖图
go mod init        初始化当前文件夹, 创建go.mod文件
go mod tidy        增加缺少的module,删除无用的module
go mod vendor      将依赖复制到vendor下
go mod verify      校验依赖
go mod why         解释为什么需要依赖

go.mod

go.mod文件记录了项目所有的依赖信息,其结构大致如下:

module github.com/Q1mi/studygo/blogger

go 1.12

require (
	github.com/DeanThompson/ginpprof v0.0.0-20190408063150-3be636683586
	github.com/gin-gonic/gin v1.4.0
	github.com/go-sql-driver/mysql v1.4.1
	github.com/jmoiron/sqlx v1.2.0
	github.com/satori/go.uuid v1.2.0
	google.golang.org/appengine v1.6.1 // indirect
)

其中

  • module用来定义包名
  • require用来定义依赖包及版本
  • indirect表示间接引用
依赖的版本

go mod支持语义化版本号,比如go get foo@v1.2.3,也可以跟git的分支或tag,比如go get
foo@master,当然也可以跟git提交哈希,比如go get foo@e3702bed2。关于依赖的版本支持以下几种格式:

gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7
gopkg.in/vmihailenco/msgpack.v2 v2.9.1
gopkg.in/yaml.v2 <=v2.2.1
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
latest
replace

在国内访问golang.org/x的各个包都需要翻墙,你可以在go.mod中使用replace替换成github上对应的库。

replace (
	golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac => github.com/golang/crypto v0.0.0-20180820150726-614d502a4dac
	golang.org/x/net v0.0.0-20180821023952-922f4815f713 => github.com/golang/net v0.0.0-20180826012351-8a410e7b638d
	golang.org/x/text v0.3.0 => github.com/golang/text v0.3.0
)

go get

在项目中执行go get命令可以下载依赖包,并且还可以指定下载的版本。

  • 运行go get -u将会升级到最新的次要版本或者修订版本(x.y.z, z是修订版本号, y是次要版本号)
  • 运行go get -u=patch将会升级到最新的修订版本
  • 运行go get package@version将会升级到指定的版本号version
  • 如果下载所有依赖可以使用go mod download命令。

整理依赖

我们在代码中删除依赖代码后,相关的依赖库并不会在go.mod文件中自动移除。这种情况下我们可以使用go mod tidy命令更新go.mod中的依赖关系。

go mod edit

因为我们可以手动修改go.mod文件,所以有些时候需要格式化该文件。Go提供了一下命令:

  • 格式化: go mod edit -fmt
  • 添加依赖项: go mod edit -require=golang.org/x/text

如果只是想修改go.mod文件中的内容,那么可以运行go mod edit -droprequire=package path,比如要在go.mod中移除golang.org/x/text包,可以使用如下命令:

  • 移除依赖项: go mod edit -droprequire=golang.org/x/text

关于go mod edit的更多用法可以通过go help mod edit查看。

在项目中使用go module

既有项目

如果需要对一个已经存在的项目启用go module,可以按照以下步骤操作:

  1. 在项目目录下执行go mod init,生成一个go.mod文件。
  2. 执行go get,查找并记录当前项目的依赖,同时生成一个go.sum记录每个依赖库的版本和哈希值。
新项目

对于一个新创建的项目,我们可以在项目文件夹下按照以下步骤操作:

  1. 执行go mod init 项目名命令,在当前项目文件夹下创建一个go.mod文件。
  2. 手动编辑go.mod中的require依赖项或执行go get自动发现、维护依赖。

Context

如何优雅的控制子goroutine退出

简单示例

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func f1(ctx context.Context) {
	defer wg.Done()
	go f2(ctx)
FORLOOP:
	for {
		fmt.Println("xxx")
		time.Sleep(time.Millisecond * 500)
		select {
		case <-ctx.Done():
			break FORLOOP
		default:
		}
	}
}
func f2(ctx context.Context) {
	defer wg.Done()
FORLOOP:
	for {
		fmt.Println("yyy")
		time.Sleep(time.Millisecond * 500)
		select {
		case <-ctx.Done():
			break FORLOOP
		default:
		}
	}
}
func main() {
	ctx, cancle := context.WithCancel(context.Background())
	wg.Add(1)
	go f1(ctx)
	time.Sleep(time.Second * 5)
	cancle() // 通知子goroutine结束
	wg.Wait()
}

Context初识

Go1.7加入了一个新的标准库context,它定义了Context类型,专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。

对服务器传入的请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传递上下文,或者可以使用WithCancelWithDeadlineWithTimeoutWithValue创建的派生上下文。当一个上下文被取消时,它派生的所有上下文也被取消。

Context接口

context.Context是一个接口,该接口定义了四个需要实现的方法。具体签名如下:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

其中:

  • Deadline方法需要返回当前Context被取消的时间,也就是完成工作的截止时间(deadline);
  • Done方法需要返回一个Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done方法会返回同一个Channel;
  • Err方法会返回当前Context结束的原因,它只会在Done返回的Channel被关闭时才会返回非空的值;
    • 如果当前Context被取消就会返回Canceled错误;
    • 如果当前Context超时就会返回DeadlineExceeded错误;
  • Value方法会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value 并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据;

Background()和TODO()

Go内置两个函数:Background()TODO(),这两个函数分别返回一个实现了Context接口的backgroundtodo。我们代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context,衍生出更多的子上下文对象。

Background()主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。

TODO(),它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。

backgroundtodo本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

With系列函数

此外,context包中还定义了四个With系列函数。

WithCancel

WithCancel的函数签名如下:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel返回带有新Done通道的父节点的副本。当调用返回的cancel函数或当关闭父上下文的Done通道时,将关闭返回上下文的Done通道,无论先发生什么情况。

取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。

func gen(ctx context.Context) <-chan int {
		dst := make(chan int)
		n := 1
		go func() {
			for {
				select {
				case <-ctx.Done():
					return // return结束该goroutine,防止泄露
				case dst <- n:
					n++
				}
			}
		}()
		return dst
	}
func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // 当我们取完需要的整数后调用cancel

	for n := range gen(ctx) {
		fmt.Println(n)
		if n == 5 {
			break
		}
	}
}

上面的示例代码中,gen函数在单独的goroutine中生成整数并将它们发送到返回的通道。gen的调用者在使用生成的整数之后需要取消上下文,以免gen启动的内部goroutine发生泄漏。

WithDeadline

WithDeadline的函数签名如下:

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

返回父上下文的副本,并将deadline调整为不迟于d。如果父上下文的deadline已经早于d,则WithDeadline(parent, d)在语义上等同于父上下文。当截止日过期时或当调用返回的cancel函数时,或者当父上下文的Done通道关闭时,返回上下文的Done通道将被关闭,以最先发生的情况为准。

取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。

func main() {
	d := time.Now().Add(50 * time.Millisecond)
	ctx, cancel := context.WithDeadline(context.Background(), d)

	// 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
	// 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
	defer cancel()

	select {
	case <-time.After(1 * time.Second):
		fmt.Println("overslept")
	case <-ctx.Done():
		fmt.Println(ctx.Err())
	}
}

上面的代码中,定义了一个50毫秒之后过期的deadline,然后我们调用context.WithDeadline(context.Background(), d)得到一个上下文(ctx)和一个取消函数(cancel),然后使用一个select让主程序陷入等待:等待1秒后打印overslept退出或者等待ctx过期后退出。

在上面的示例代码中,因为ctx 50毫秒后就会过期,所以ctx.Done()会先接收到context到期通知,并且会打印ctx.Err()的内容。

WithTimeout

WithTimeout的函数签名如下:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithTimeout返回WithDeadline(parent, time.Now().Add(timeout))

取消此上下文将释放与其相关的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel,通常用于数据库或者网络连接的超时控制。具体示例如下:

package main

import (
	"context"
	"fmt"
	"sync"

	"time"
)

// context.WithTimeout

var wg sync.WaitGroup

func worker(ctx context.Context) {
LOOP:
	for {
		fmt.Println("db connecting ...")
		time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
		select {
		case <-ctx.Done(): // 50毫秒后自动调用
			break LOOP
		default:
		}
	}
	fmt.Println("worker done!")
	wg.Done()
}

func main() {
	// 设置一个50毫秒的超时
	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 5)
	cancel() // 通知子goroutine结束
	wg.Wait()
	fmt.Println("over")
}
WithValue

WithValue函数能够将请求作用域的数据与 Context 对象建立关系。声明如下:

func WithValue(parent Context, key, val interface{}) Context

WithValue返回父节点的副本,其中与key关联的值为val。

仅对API和进程间传递请求域的数据使用上下文值,而不是使用它来传递可选参数给函数。

所提供的键必须是可比较的,并且不应该是string类型或任何其他内置类型,以避免使用上下文在包之间发生冲突。WithValue的用户应该为键定义自己的类型。为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}。或者,导出的上下文关键变量的静态类型应该是指针或接口。

package main

import (
	"context"
	"fmt"
	"sync"

	"time"
)

// context.WithValue

type TraceCode string

var wg sync.WaitGroup

func worker(ctx context.Context) {
	key := TraceCode("TRACE_CODE")
	traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取trace code
	if !ok {
		fmt.Println("invalid trace code")
	}
LOOP:
	for {
		fmt.Printf("worker, trace code:%s\n", traceCode)
		time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
		select {
		case <-ctx.Done(): // 50毫秒后自动调用
			break LOOP
		default:
		}
	}
	fmt.Println("worker done!")
	wg.Done()
}

func main() {
	// 设置一个50毫秒的超时
	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
	// 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合
	ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234")
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 5)
	cancel() // 通知子goroutine结束
	wg.Wait()
	fmt.Println("over")
}
使用Context的注意事项
  • 推荐以参数的方式显示传递Context
  • 以Context作为参数的函数方法,应该把Context作为第一个参数。
  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()
  • Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
  • Context是线程安全的,可以放心的在多个goroutine中传递

kafka

Kafka集群的架构

在这里插入图片描述

  1. broker:服务器节点
  2. topic:消息的主题,可以理解为消息的分类
  3. partition:分区,把同一个topic分成不同的分区,提高负载
    1. leader:分区的主节点(老大)
    2. follower:分区的从节点(小弟)
  4. Consumer Group:消费组
生产者往Kafka发送数据的流程(6步)

在这里插入图片描述

  1. 获取kafka里的leader
  2. 发送信息到leader
  3. 落盘
  4. follower主动拉取
  5. 成功返回ack
  6. 返回ack到sc生产者
kafka选择分区的模式(3种)
  1. 指定往哪个分区写
  2. 指定key,kafka根据key做hash然后决定写那个分区
  3. 轮询方式
生产者往kafka发送数据的模式(3种)
  1. 0:把数据发给leader就成功,效率最高、安全性最低。
  2. 1:把数据发送给leader,等待leader回ACK
  3. alI:把数据发给leader,确保follower从leader拉取数据回复ack给leader,leader再回复ACK(安全性最高)
分区存储文件的原理

Partition在服务器上的表现形式就是一个一个的文件夹,每个partition的文件夹下面会有多组segment文件,每组segment文件又包含.index文件、.log文件、.timeindex文件三个文件,其中.log文件就是实际存储messagel的地方,而.index.timeindex文件为索引文件,用于检索消息。

为什么kafka快

在每个partition中,每条消息都会被分配一个顺序的唯一标识,把

消费组组消费数据的原理

在这里插入图片描述
举个例子,如上图所示一个两个节点的Kafka集群上拥有一个四个partition(P0-P3)的topic。有两个消费者组都在消费这个topic中的数据,消费者组A有两个消费者实例,消费者组B有四个消费者实例。

从图中我们可以看到,在同一个消费者组中,每个消费者实例可以消费多个分区,但是每个分区最多只能被消费者组中的一个实例消费

kafka使用

下载

下载地址,下载 kafka_2.12-3.4.1

启动
  1. 安装包解压
  2. 打开config/zookeeper.properties
  3. dataDir=D:/tmp/zookeeper 改数据存储路径
  4. clientPort=2181 节点
  5. win10需要在管理员模式下启动
cd /d E:\Desktop\kafka_2.12-3.4.1

bin\windows\zookeeper-server-start.bat config\zookeeper.properties
  1. 打开config/server.properties
  2. log.dirs=D:/tmp/kafka-logs
cd /d E:\Desktop\kafka_2.12-3.4.1

bin\windows\kafka-server-start.bat config\server.properties

练习:日志收集

在这里插入图片描述
注:橙色是需要实现的部分

log Agent的工作流程

  1. 读日志 – tail第三方
    1. 安装:go get github.com/hpcloud/tail
    2. 使用
package main

import (
	"fmt"
	"time"

	"github.com/hpcloud/tail"
)

// tailf的用法示例

func main() {
	fileName := "./my.log"
	config := tail.Config{
		ReOpen:    true,                                 // 重新打开
		Follow:    true,                                 // 是否跟随
		Location:  &tail.SeekInfo{Offset: 0, Whence: 2}, // 从文件的哪个地方开始读
		MustExist: false,                                // 文件不存在不报错
		Poll:      true,
	}
	tails, err := tail.TailFile(fileName, config)
	if err != nil {
		fmt.Println("tail file failed, err:", err)
		return
	}
	var (
		line *tail.Line
		ok   bool
	)
	for {
		line, ok = <-tails.Lines //遍历chan,读取日志内容
		if !ok {
			fmt.Printf("tail file close reopen, filename:%s\n", tails.Filename)
			time.Sleep(time.Second)
			continue
		}
		fmt.Println("line:", line.Text)
	}
}

  1. 往kafka写日志 – sarama第三方库
    1. 安装:go get github.com/Shopify/sarama
    2. 使用:
package main

import (
	"fmt"

	"github.com/Shopify/sarama"
)

func main() {
	config := sarama.NewConfig()
	// tailf包的使用
	config.Producer.RequiredAcks = sarama.WaitForAll          // 发送数据需要leader和follow都确认
	config.Producer.Partitioner = sarama.NewRandomPartitioner // 新选出一个分区
	config.Producer.Return.Successes = true                   // 成功交付的信息将在success_channel返回
	// 构造一个消息
	msg := &sarama.ProducerMessage{}
	msg.Topic = "web_log"
	msg.Value = sarama.StringEncoder("this is a test log")
	// 连接kafka
	client, err := sarama.NewSyncProducer([]string{"127.0.0.1:9092"}, config)
	if err != nil {
		fmt.Println("producer error: ", err)
	}
	fmt.Println("连接成功")
	defer client.Close()
	// 发送消息
	pid, offset, err := client.SendMessage(msg)
	if err != nil {
		fmt.Println("send message failed: ", err)
		return
	}
	fmt.Printf("pid: %v offset: %v\n", pid, offset)
}

log Agent的实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值