Go重新学习记录

T

视频连接:2020年最新 Go语言线上VIP就业班全套(价值5K)/golang开发工程师
视频配套博客:Go语言学习之路
文档:Golang标准库文档

第1天

P03~P07

安装GO语言

下载地址:GO
下载安装完成后,使用cmd ,输入go version查看版本号测试是否安装完成。
在这里插入图片描述

配置GO环境变量

GOROOTGOPATH都是环境变量,其中GOROOT是我们安装go开发包的路径,而从Go 1.8版本开始,Go开发包在安装完成后会为GOPATH设置一个默认目录,
你也可以修改自己想要的目录。配置如下
在这里插入图片描述
cmd输入go env可以查看go的一些配置
在这里插入图片描述

GO的目录结构

GO项目一般的目录结构如图
在这里插入图片描述
根据图中创建bin/pkg/src/目录。

GO的IDE

Go采用的是UTF-8编码的文本文件存放源代码,理论上使用任何一款文本编辑器都可以做Go语言开发,这里推荐使用VS Code和Goland。 VS Code是微软开源的编辑器,而Goland是jetbrains出品的付费IDE。
我们这里使用VS Code 加插件做为go语言的开发工具。

VSCode安装GO语言扩展如图:
在这里插入图片描述
VSCode安装Go语言开发工具包:
首先修改Go的代理GOPROXY,使用国内代理可以快速下载,打开终端执行以下命令:

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

然后在VSCode中使用快捷键Ctrl+Shift+P,输入go Install/Update Tools,然后全选,安装。

执行第一个Go程序

在src目录中创建test.com域名文件,再创建一个main.go文件,输入以下代码:

package main  // 声明 main 包,表明当前是一个可执行程序
import "fmt"  // 导入内置 fmt 包

func main(){  // main函数,是程序执行的入口
	fmt.Println("Hello World!")  // 在终端打印 Hello World!
}

在终端中执行go run ./src/test.com/main.go
在这里插入图片描述

第2天

P08~P18

变量

变量必须声明之后才能使用,关键字var
Go语言中非全局变量声明后必须使用(不包括初始化),否则编译失败。
Go语言的变量声明格式如下:

var 变量名 变量类型
var 变量名 类型 = 表达式
eg:
var name string
var isOk bool
var age int = 18//声明的时候初始化
var age1 = 18//类型推导的方式
var name1, age2 = "Q1mi", 20//一次初始化多个变量
var (//批量声明的方法,只能全局下用。
    a string
    b int
    c bool
)
m := 200//简短变量声明,只能在函数里面使用

在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量_。匿名变量用一个下划线_表示。匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。 (在Lua等编程语言里,匿名变量也被叫做哑元变量。)

常量

关键字const

const pi = 3.1415
const e = 2.7182
const (//const同时声明多个常量时,如果省略了值则表示和上面一行的值相同。
    n1 = 100
    n2
    n3
)//常量n1、n2、n3的值都是100。

iota

iota是go语言的常量计数器,只能在常量的表达式中使用。
iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数加一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。

//例子1
const (
		n1 = iota //0
		n2        //1
		n3        //2
		n4        //3
)
const n5 = iota //0
//例子2,使用_跳过某些值
const (
		n1 = iota //0
		n2        //1
		_
		n4        //3
)
//例子3,iota声明中间插队
const (
		n1 = iota //0
		n2 = 100  //100,iota=1
		n3 = iota //2
		n4        //3
)
//例子4,<<表示左移操作。
const (
		_  = iota
		KB = 1 << (10 * iota)//1<<10表示将1的二进制表示向左移10位,也就是由1变成了10000000000,也就是十进制的1024。
		MB = 1 << (10 * iota)
		GB = 1 << (10 * iota)
		TB = 1 << (10 * iota)
		PB = 1 << (10 * iota)
)
//例子5,多个iota定义在一行
const (
		a, b = iota + 1, iota + 2 //1,2
		c, d                      //2,3
		e, f                      //3,4
)

基本数据类型

整型

整型分为以下两个大类: 按长度分为:int8、int16、int32、int64 对应的无符号整型:uint8、uint16、uint32、uint64
其中,uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型。

类型描述
uint8无符号 8位整型 (0 到 255)
uint16无符号 16位整型 (0 到 65535)
uint32无符号 32位整型 (0 到 4294967295)
uint64无符号 64位整型 (0 到 18446744073709551615)
int8有符号 8位整型 (-128 到 127)
int16有符号 16位整型 (-32768 到 32767)
int32有符号 32位整型 (-2147483648 到 2147483647)
int64有符号 64位整型 (-9223372036854775808 到 9223372036854775807)

特殊整型:

类型描述
uint32位操作系统上就是uint32,64位操作系统上就是uint64
int32位操作系统上就是uint32,64位操作系统上就是uint64
uintptr无符号整型,用于存放一个指针

注意事项:
获取对象的长度的内建len()函数返回的长度可以根据不同平台的字节长度进行变化。实际使用中,切片或 map 的元素数量等都可以用int来表示。在涉及到二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用intuint

//类型推导一般是int类型。
i := 11
fmt.Printf("%T", i)//int

浮点型

Go语言支持两种浮点型数:float32float64

复数

两种类型:complex64complex128
复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部和虚部为64位。

布尔值

bool

字符串

Go 语言里的字符串的内部实现使用UTF-8编码。
1个字符‘A’=1个字节
1个utf-8编码的汉字‘我’=3个字节。

//定义多行的字符串,内容不转义
s1 := `第一行
第二行
第三行
`
fmt.Println(s1)

字符串常用的方法:

方法描述
len(str)求长度
+或fmt.Sprintf拼接字符串
strings.Split分割
strings.contains判断是否包含
strings.HasPrefix,strings.HasSuffix前缀/后缀判断
strings.Index(),strings.LastIndex()子串出现的位置
strings.Join(a[]string, sep string)join操作

fmt使用总结:

func main() { // main函数,是程序执行的入口
	i := 11
	fmt.Printf("%T
", i) //类型int
	fmt.Printf("%v
", i) //11
	fmt.Printf("%b
", i) //二进制1011
	fmt.Printf("%d
", i) //十进制11
	fmt.Printf("%o
", i) //八进制13
	fmt.Printf("%x
", i) //十六进制b
	s := "hello"
	fmt.Printf("%s
", s) //输出字符串。
}

byte和rune类型

Go 语言的字符有以下两种类型:
byte 型,或者叫uint8类型 ,代表了ASCII码的一个字符。
rune类型,代表一个 UTF-8字符。
当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32。

// 遍历字符串
func main() {
	s := "hello沙河"
	for i := 0; i < len(s); i++ { //byte
		fmt.Printf("%v(%c) ", s[i], s[i])
	}
	fmt.Println()
	for _, r := range s { //rune
		fmt.Printf("%v(%c) ", r, r)
	}
	fmt.Println()
}
//输出结果,所以有中文的字符串用rune类型切割才不会乱码。
//104(h) 101(e) 108(l) 108(l) 111(o) 230(æ) 178(²) 153() 230(æ) 178(²) 179(³)
//104(h) 101(e) 108(l) 108(l) 111(o) 27801(沙) 27827(河)

修改字符串
要修改字符串,需要先将其转换成[]rune或[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。

func changeString() {
	s1 := "big"
	// 强制类型转换
	byteS1 := []byte(s1)
	byteS1[0] = 'p'
	fmt.Println(string(byteS1))

	s2 := "白萝卜"
	runeS2 := []rune(s2)
	runeS2[0] = '红'
	fmt.Println(string(runeS2))
}

流程控制

if

if格式如下:

if 表达式1 {
    分支1
} else if 表达式2 {
    分支2
} else{
    分支3
}

for

for格式如下:

for 初始语句;条件表达式;结束语句{
    循环体语句
}
for {//死循环
    循环体语句
}

for range

for range也叫键值循环
Go语言中可以使用for range遍历数组、切片、字符串、map 及通道(channel)。 通过for range遍历的返回值有以下规律:

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

switch case

//例子1
func testSwitch3() {
	switch n := 7; n {
	case 1, 3, 5, 7, 9:
		fmt.Println("奇数")
	case 2, 4, 6, 8:
		fmt.Println("偶数")
	default:
		fmt.Println(n)
	}
}
//例子2
func switchDemo4() {
	age := 30
	switch {
	case age < 25:
		fmt.Println("好好学习吧")
	case age > 25 && age < 35:
		fmt.Println("好好工作吧")
	case age > 60:
		fmt.Println("好好享受吧")
	default:
		fmt.Println("活着真好")
	}
}
//例子3
func switchDemo5() {
	s := "a"
	switch {
	case s == "a":
		fmt.Println("a")
		fallthrough//fallthrough语法可以执行满足条件的case的下一个case,是为了兼容C语言中的case设计的。不推荐使用
	case s == "b":
		fmt.Println("b")
	case s == "c":
		fmt.Println("c")
	default:
		fmt.Println("...")
	}
}
//输出结果:
//a
//b

goto

goto语句通过标签进行代码间的无条件跳转。goto语句可以在快速跳出循环、避免重复退出上有一定的帮助。(不推荐使用,了解就行了)

func gotoDemo2() {
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			if j == 2 {
				// 设置退出标签
				goto breakTag//跳出两层循环。
			}
			fmt.Printf("%v-%v
", i, j)
		}
	}
	return
	// 标签
breakTag:
	fmt.Println("结束for循环")
}

break

常规

continue

常规

第3天

P19~P23

数组

语法:

// 定义一个长度为3元素类型为int的数组a
var 数组变量名 [元素数量]T
//例子
var a [3]int
var numArray = [3]int{1, 2}//使用指定的初始值完成初始化
var cityArray = [...]string{"北京", "上海", "深圳"} //让编译器根据初始值的个数自行推断数组的长度
a := [...]int{1: 1, 3: 5}//指定索引值的方式来初始化数组,结果:[0 1 0 5]
a := [...][2]string{//二维数组的遍历,多维数组只有第一层可以使用...来让编译器推导数组长度
		{"北京", "上海"},
		{"广州", "深圳"},
		{"成都", "重庆"},
	}

数组的遍历

func main() {
	var a = [...]string{"北京", "上海", "深圳"}
	// 方法1:for循环遍历
	for i := 0; i < len(a); i++ {
		fmt.Println(a[i])
	}
	// 方法2:for range遍历
	for index, value := range a {
		fmt.Println(index, value)
	}
}

切片

切片(Slice)是一个拥有相同类型元素的可变长度的序列。
切片是一个引用类型,它的内部结构包含地址长度容量底层数组的值发生改变,那么这个切片也随之改变。
语法如下:

var name []T
make([]T, size, cap)//make函数创建切片
//例子
var a []string              //声明一个字符串切片
var b = []int{}             //声明一个整型切片并初始化
var c = []bool{false, true} //声明一个布尔切片并初始化
var d = make([]int, 5, 10)  //声明一个长度5,容量10的切片。
var e = make([]int, 5)      //声明一个长度5,容量5的切片。

切片的本质就是对底层数组的封装,所以我们可以切割数组来得到切片。切片包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。内置的len()函数求长度。内置的cap()函数求切片的容量。
切片的长度就是它元素的个数。
切片的容量就是底层数组从切片的第一个元素到最后一个元素的数量。
例子图解:
在这里插入图片描述

a := [...]int{1, 3, 5, 7, 9, 11, 13}
a1 := a[2:4]//[5,7]基于数组切割,左闭右开。
a2 := a[:4]//=[0:4] [1,3,5,7],长度是4,容量是7
a3 := a[3:]//=[3:len(a)] [7,9,11,13],长度是4,容量是4,
a4 := a[:]//=[0,len(a)]

第4天

P24~P41

指针

区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。
记住两个符号:&(取地址)*(根据地址取值)

func main() {
	//指针取值
	a := 10
	b := &a // 取变量a的地址,将指针保存到b中
	fmt.Printf("type of b:%T
", b)
	c := *b // 指针取值(根据指针去内存取值)
	fmt.Printf("type of c:%T
", c)
	fmt.Printf("value of c:%v
", c)
}
输出:
type of b:*int
type of c:int
value of c:10

map

有点类似C#的字典。
Go语言中提供的映射关系容器为map,其内部使用散列表(hash)实现。
map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。
语法:

map[KeyType]ValueType
make(map[KeyType]ValueType, [cap])//map类型的变量默认初始值为nil,需要使用make()函数来分配内存。预估好cap,避免再次扩容。
例子:
func main() {
	scoreMap := make(map[string]int, 8)
	scoreMap["张三"] = 90
	scoreMap["小明"] = 100
	fmt.Println(scoreMap)
	fmt.Println(scoreMap["小明"])
	fmt.Printf("type of a:%T
", scoreMap)
	
	//map也支持在声明的时候填充元素,例如:
	userInfo := map[string]string{
		"username": "沙河小王子",
		"password": "123456",
	}
}
输出:
map[小明:100 张三:90]
100
type of a:map[string]int

判断map中键是否存在,语法例子如下:

value, ok := map[key]
例子:
func main() {
	scoreMap := make(map[string]int)
	scoreMap["张三"] = 90
	scoreMap["小明"] = 100
	// 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
	v, ok := scoreMap["张三"]
	if ok {
		fmt.Println(v)
	} else {
		fmt.Println("查无此人")
	}
}

map的遍历
for range 遍历map。

func main() {
	scoreMap := make(map[string]int)
	scoreMap["张三"] = 90
	scoreMap["小明"] = 100
	scoreMap["娜扎"] = 60
	for k, v := range scoreMap {
		fmt.Println(k, v)
	}
}

删除map
使用delete()内建函数从map中删除一组键值对,delete()函数的格式如下:

delete(map, key)
例子:
func main(){
	scoreMap := make(map[string]int)
	scoreMap["张三"] = 90
	scoreMap["小明"] = 100
	scoreMap["娜扎"] = 60
	delete(scoreMap, "小明")//将小明:100从map中删除
	for k,v := range scoreMap{
		fmt.Println(k, v)
	}
}

函数

语法:

func 函数名(参数 类型)(返回值){
    函数体
}
例子:
//有参数有返回值
//命名的返回值就相当于在函数中声明了一个变量。
func sum(x int,y int)(ret int){
	ret = x + y
	return//使用命名返回值可以省略。
}
//没有返回值
func f1(x int,y int){
	fmt.Println(x + y)
}
//没有参数但有返回值
func f2() int{
	return 3
}
//没有参数没有返回值
func f3(){
}
//参数类型简写
//当参数中连续多个参数的类型一致时,前面一致的都可以忽略。
func f4(x, y int, m, n string){
}
//可变长度参数,必须在参数最后
func f5(x string, y ...int)//y类型是切片 []int
{
}
//调用
f5("xxx", 1, 2, 3)

函数作为参数

func add(x, y int) int {
	return x + y
}
func calc(x, y int, op func(int, int) int) int {
	return op(x, y)
}
func main() {
	ret2 := calc(10, 20, add)
	fmt.Println(ret2) //30
}

函数作为返回值

func do(s string) (func(int, int) int, error) {
	switch s {
	case "+":
		return add, nil
	case "-":
		return sub, nil
	default:
		err := errors.New("无法识别的操作符")
		return nil, err
	}
}

匿名函数

函数内部不能定义函数但可以定义匿名函数。匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数

func(参数)(返回值){
    函数体
}
例子
func main() {
	// 将匿名函数保存到变量
	add := func(x, y int) {
		fmt.Println(x + y)
	}
	add(10, 20) // 通过变量调用匿名函数

	//自执行函数:匿名函数定义完加()直接执行
	func(x, y int) {
		fmt.Println(x + y)
	}(10, 20)
}

闭包

闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境

//例子1:
func adder2(x int) func(int) int {
	return func(y int) int {
		x += y
		return x
	}
}
func main() {
	//变量f是一个函数并且它引用了其外部作用域中的x变量,此时f就是一个闭包。 在f的生命周期内,变量x也一直有效。
	var f = adder2(10)
	fmt.Println(f(10)) //20
	fmt.Println(f(20)) //40
	fmt.Println(f(30)) //70

	f1 := adder2(20)
	fmt.Println(f1(40)) //60
	fmt.Println(f1(50)) //110
}
//例子2:
func makeSuffixFunc(suffix string) func(string) string {
	return func(name string) string {
		if !strings.HasSuffix(name, suffix) {
			return name + suffix
		}
		return name
	}
}
func main() {
	jpgFunc := makeSuffixFunc(".jpg")
	txtFunc := makeSuffixFunc(".txt")
	fmt.Println(jpgFunc("test")) //test.jpg
	fmt.Println(txtFunc("test")) //test.txt
}
//例子3:
func calc(base int) (func(int) int, func(int) int) {
	add := func(i int) int {
		base += i
		return base
	}
	sub := func(i int) int {
		base -= i
		return base
	}
	return add, sub
}
func main() {
	f1, f2 := calc(10)
	fmt.Println(f1(1), f2(2)) //11 9
	fmt.Println(f1(3), f2(4)) //12 8
	fmt.Println(f1(5), f2(6)) //13 7
}

defer

Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行
Go语言中函数的return不是原子操作,底层分两步执行。第一步将结果赋值给返回值,第二步真正的return返回值。而defer的执行在这两者之间

func f1() int {
	x := 5
	defer func() {
		x++  //修改的是x而不是返回值
	}()
	return x
}
结果:5
func f2() (x int) {//x就是返回值
	defer func() {
		x++
	}()
	return 5
}
结果:6

内置函数

一些go自带的函数

append()

用来追加元素到数组、slice中。
Go语言的内建函数append()可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。
append()函数将元素追加到切片的最后并返回该切片。当追加后的长度大于原来底层数组的容量时,则该切片会进行扩容,扩容的规则如下

  • 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
  • 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
  • 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
  • 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
func main(){
	var s []int
	s = append(s, 1)        // [1]
	s = append(s, 2, 3, 4)  // [1 2 3 4]
	s2 := []int{5, 6, 7}  
	s = append(s, s2...)    // [1 2 3 4 5 6 7](...表示将s2拆开)
}

copy()

Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中。

func main() {
	// copy()复制切片
	a := []int{1, 2, 3, 4, 5}
	c := make([]int, 5, 5)
	copy(c, a)     //使用copy()函数将切片a中的元素复制到切片c
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(c) //[1 2 3 4 5]
	c[0] = 1000
	fmt.Println(a) //[1 2 3 4 5]
	fmt.Println(c) //[1000 2 3 4 5]
}

new()

用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针。
项目中用的比较少

func main() {
	a := new(int)
	b := new(bool)
	fmt.Printf("%T
", a) // *int
	fmt.Printf("%T
", b) // *bool
	fmt.Println(*a)       // 0
	fmt.Println(*b)       // false
}

make()

用来分配内存,主要用来分配引用类型,比如chan、map、slice。
new和make的区别:

  • 二者都是用来做内存分配的。
  • make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
  • 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

panic和recover

用来做错误处理。
recover()必须搭配defer使用。
defer一定要在可能引发panic的语句之前定义。

func funcA() {
	fmt.Println("func A")
}

func funcB() {
	defer func() {
		err := recover()
		//如果程序出出现了panic错误,可以通过recover恢复过来
		if err != nil {
			fmt.Println("recover in B")
		}
	}()
	panic("panic in B")
}
func funcC() {
	fmt.Println("func C")
}
func main() {
	funcA()
	funcB()
	funcC()
}

第5天

P42~60

标准库

fmt

标准库fmt提供了以下几种输出相关函数。
Print

func Print(a ...interface{}) (n int, err error)//普通输出
func Printf(format string, a ...interface{}) (n int, err error)//占位符
func Println(a ...interface{}) (n int, err error)//换行

Scan
输入,跟Print相反

func Scan(a ...interface{}) (n int, err error)
func Scanf(format string, a ...interface{}) (n int, err error)
func Scanln(a ...interface{}) (n int, err error)

Sprint
传入的数据生成并返回一个字符串。

func Sprint(a ...interface{}) string
func Sprintf(format string, a ...interface{}) string
func Sprintln(a ...interface{}) string
例子:
s2 := fmt.Sprintf("name:%s,age:%d", name, age)
s3 := fmt.Sprintln("沙河小王子")

Errorf
根据format参数生成格式化字符串并返回一个包含该字符串的错误。

func Errorf(format string, a ...interface{}) error
例子:
e := errors.New("原始错误e")
w := fmt.Errorf("Wrap了一个错误%w", e)

os

主要用于文件操作
打开文件的模式有以下几种:

模式含义
os.O_WRONLY只写
os.O_CREATE创建文件
os.O_RDONLY只读
os.O_RDWR读写
os.O_TRUNC清空
os.O_APPEND追加
//引入
import (
	"os"
	"io"
	"io/ioutil"
)
//读取操作
file, err := os.Open("./main.go")//打开文件,只能读
var tmp = make([]byte, 128)
n, err := file.Read(tmp)
//写入操作
file, err := os.OpenFile("xx.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) //打开文件,能读能写
file.Write([]byte(str))       //写入字节切片数据
file.WriteString("hello 小王子") //直接写入字符串数据

//bufio是在file的基础上封装了一层API,支持更多的功能。
//bufio读取操作
file, err := os.Open("./xx.txt")
reader := bufio.NewReader(file)
//bufio写入操作
file, err := os.OpenFile("xx.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
writer := bufio.NewWriter(file)
writer.WriteString("hello沙河
") //将数据先写入缓存
writer.Flush() //将缓存中的内容写入文件

//io/ioutil包的ReadFile方法能够读取完整的文件,只需要将文件名作为参数传入。
//ioutil读取操作
content, err := ioutil.ReadFile("./main.go")
//ioutil写入操作
str := "hello 沙河"
err := ioutil.WriteFile("./xx.txt", []byte(str), 0666)

//文件操作完成后记得关闭文件。通常使用defer注册文件关闭语句。
defer file.Close()

time

time.Duration是time包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单位。time.Duration表示一段时间间隔,可表示的最长时间段大约290年。
//时间内置的一些常量。
const (
    Nanosecond  Duration = 1
    Microsecond          = 1000 * Nanosecond
    Millisecond          = 1000 * Microsecond
    Second               = 1000 * Millisecond
    Minute               = 60 * Second
    Hour                 = 60 * Minute
)
例如:
time.Second表示1s
time.Duration表示1纳秒
引入
import (
	"time"
)
now := time.Now() //获取当前时间
year := now.Year()     //年
month := now.Month()   //月
day := now.Day()       //日
hour := now.Hour()     //小时
minute := now.Minute() //分钟
second := now.Second() //秒
timestamp1 := now.Unix()     //时间戳
timestamp2 := now.UnixNano() //纳秒时间戳
timeObj := time.Unix(timestamp1, 0) //将时间戳转为时间格式
later := now.Add(24*time.Hour) // 当前时间加24小时后的时间
d := now.Sub(later)//now减later的时间间隔,返回的是Duration类型。
now.Format("2006-01-02 15:04:05.000 Mon Jan")//24小时制格式化。格式化的模板为Go的出生时间2006年1月2号15点04分 Mon Jan
now.Format("2006-01-02 03:04:05.000 PM Mon Jan")//指定PM表示12小时制格式化。
newTime,err := time.Parse("2006-01-02","2020-08-03")//字符串转时间。
// 加载时区
loc, err := time.LoadLocation("Asia/Shanghai")//上海时区
// 按照指定时区和指定格式解析字符串时间
timeObj, err := time.ParseInLocation("2006/01/02 15:04:05", "2019/08/04 14:15:20", loc)

//定时器time.Tick(时间间隔)
func tickDemo() {
	ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器
	for i := range ticker {
		fmt.Println(i)//每秒都会执行的任务
	}
}

strconv

strconv包实现了基本数据类型和其字符串表示的相互转换。

//Atoi()函数用于将字符串类型的整数转换为int类型
s1 := "100"
i1, err := strconv.Atoi(s1)
if err != nil {
	fmt.Println("can't convert to int")
} else {
	fmt.Printf("type:%T value:%#v
", i1, i1) //type:int value:100
}
//Itoa()函数用于将int类型数据转换为对应的字符串表示
i2 := 200
s2 := strconv.Itoa(i2)
fmt.Printf("type:%T value:%#v
", s2, s2) //type:string value:"200"


//以下方法将字符串转化为bool,float,int,uint类型
//这些函数都有两个返回值,第一个返回值是转换后的值,第二个返回值为转化失败的错误信息。
b, err := strconv.ParseBool("true")
f, err := strconv.ParseFloat("3.1415", 64)
i, err := strconv.ParseInt("-2", 10, 64)//后两个参数表示10进制64位
u, err := strconv.ParseUint("2", 10, 64)

类型别名和自定义类型

Go语言中可以使用type关键字来定义自定义类型。

//将MyInt定义为int类型
type MyInt int
//类型别名
type TypeAlias = Type

结构体

Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
类似C#的结构体,也是值类型。

type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    …
}
例子:
type person struct {
	name string
	city string
	age  int8
}
func main() {
	var p1 person
	p1.name = "沙河娜扎"
	p1.city = "北京"
	p1.age = 18
	fmt.Printf("p1=%v
", p1)  //p1={沙河娜扎 北京 18}
	fmt.Printf("p1=%#v
", p1) //p1=main.person{name:"沙河娜扎", city:"北京", age:18}
}

匿名结构体

package main
     
import (
    "fmt"
)  
func main() {
    var user struct{Name string; Age int}
    user.Name = "小王子"
    user.Age = 18
    fmt.Printf("%#v
", user)
}

new初始化结构体

var p2 = new(person)
p2.name = "小王子"
p2.age = 28
p2.city = "上海"
fmt.Printf("p2=%#v
", p2) //p2=&main.person{name:"小王子", city:"上海", age:28}

取结构体的地址实例化

使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。

p3 := &person{}
fmt.Printf("%T
", p3)     //*main.person
fmt.Printf("p3=%#v
", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "七米"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v
", p3) //p3=&main.person{name:"七米", city:"成都", age:30}

使用键值对初始化

p5 := person{
	name: "小王子",
	city: "北京",
	age:  18,
}
fmt.Printf("p5=%#v
", p5) //p5=main.person{name:"小王子", city:"北京", age:18}
p6 := &person{
	name: "小王子",
	city: "北京",
	age:  18,
}
fmt.Printf("p6=%#v
", p6) //p6=&main.person{name:"小王子", city:"北京", age:18}

构造函数

type person struct {
	name string
	city string
	age  int8
}
//构造函数:约定俗成用new开头
//但结构体比较大的时候最好使用结构体指针,减少程序内存开销
func newPerson(name, city string, age int8) *person {
	return &person{
		name: name,
		city: city,
		age:  age,
	}
}

方法和接收者

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self。

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
}
例子:
//Person 结构体
type Person struct {
	name string
	age  int8
}

//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
	return &Person{
		name: name,
		age:  age,
	}
}

//只有Person类型能调用。值接受者:传拷贝进去
func (p Person) Dream() {
	fmt.Printf("%s的梦想是学好Go语言!
", p.name)
}
// SetAge 设置p的年龄。指针接受者:传内存进去
func (p *Person) SetAge(newAge int8) {
	p.age = newAge
}

func main() {
	p1 := NewPerson("小王子", 25)
	p1.Dream()
	p1.SetAge(22)
}

结构体的匿名字段

不常用,了解下

//Person 结构体Person类型
type Person struct {
	string//匿名字段,唯一
	int
}

func main() {
	p1 := Person{
		"小王子",
		18,
	}
	fmt.Printf("%#v
", p1)        //main.Person{string:"北京", int:18}
	fmt.Println(p1.string, p1.int) //北京 18
}

匿名嵌套结构体

//Address 地址结构体
type Address struct {
	Province string
	City     string
}
//User 用户结构体
type User struct {
	Name    string
	Gender  string
	Address //匿名结构体
}
func main() {
	var user2 User
	user2.Name = "小王子"
	user2.Gender = "男"
	user2.Address.Province = "山东"    //通过匿名结构体.字段名访问
	user2.City = "威海"                //直接访问匿名结构体的字段名
	fmt.Printf("user2=%#v
", user2) //user2=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
}

结构体的“继承”

利用匿名嵌套结构体实现继承

//Animal 动物
type Animal struct {
	name string
}
func (a *Animal) move() {
	fmt.Printf("%s会动!
", a.name)
}

//Dog 狗
type Dog struct {
	Feet    int8
	*Animal //通过嵌套匿名结构体实现继承
}
func (d *Dog) wang() {
	fmt.Printf("%s会汪汪汪~
", d.name)
}

func main() {
	d1 := &Dog{
		Feet: 4,
		Animal: &Animal{ //注意嵌套的是结构体指针
			name: "乐乐",
		},
	}
	d1.wang() //乐乐会汪汪汪~
	d1.move() //乐乐会动!
}

结构体与JSON序列化

//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)//data是序列化后的json,err是报错信息,c是结构体
//JSON反序列化:JSON格式的字符串-->结构体
err = json.Unmarshal([]byte(str), c)//err是报错信息,str是json,c是结构体。要一一对应

结构体标签

反引号包裹起来

//Student 学生
type Student struct {
	ID     int    `json:"id"` //通过指定tag实现json序列化该字段时的key
	Gender string //json序列化是默认使用字段名作为key
	name   string //私有不能被json包访问
}

第6天

P61~P80

接口

在Go语言中接口(interface)是一种类型,一种抽象的类型。
如果一个变量实现了接口中的所有方法,那么这个变量就可以称为这个接口的变量。

type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2}
例子:多个类型实现同一接口,一个类型实现多个接口。
// Sayer 接口
type Sayer interface {
	say()
}
type Mover interface{
	move()
}
//定义两个结构体
type dog struct {}
type cat struct {}
//两个结构体实现Sayer接口的方法(多个类型实现同一接口)
// cat实现了Sayer接口
func (c cat) say() {
	fmt.Println("喵喵喵")
}
// dog实现了Sayer接口
func (d dog) say() {
	fmt.Println("汪汪汪")
}
// dog还实现Mover接口(一个类型实现多个接口)
func (d dog) move() {
	fmt.Printf("会动")
}
//调用
func main() {
	var x Sayer // 声明一个Sayer类型的变量x
	var y Mover // 声明一个Mover类型的变量y
	a := cat{}  // 实例化一个cat
	b := dog{}  // 实例化一个dog
	x = a       // 可以把cat实例直接赋值给x
	x.say()     // 喵喵喵,此时x的类型为cat
	x = b       // 可以把dog实例直接赋值给x
	x.say()     // 汪汪汪,此时x的类型为dog
	y = b       // 可以把dog实例直接赋值给y
	y.move()    // 会动,此时y的类型为dog
}

使用值接收者实现接口与使用指针接收者实现接口的区别?

  • 前者,结构体类型和结构体指针类型的变量都能接收。
  • 后者,只能接收结构体指针类型的变量。

接口嵌套

// Sayer 接口
type Sayer interface {
	say()
}
// Mover 接口
type Mover interface {
	move()
}
// 接口嵌套,嵌套了Sayer和Mover的接口
type animal interface {
	Sayer
	Mover
}
type cat struct {
	name string
}
func (c cat) say() {
	fmt.Println("喵喵喵")
}
func (c cat) move() {
	fmt.Println("猫会动")
}
func main() {
	var x animal
	x = cat{name: "花花"}
	x.move()
	x.say()
}

空接口

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量。

func main() {
	// 定义一个空接口x
	var x interface{}
	s := "Hello 沙河"
	x = s
	fmt.Printf("type:%T value:%v
", x, x)
	i := 100
	x = i
	fmt.Printf("type:%T value:%v
", x, x)
	b := true
	x = b
	fmt.Printf("type:%T value:%v
", x, x)
}
// 空接口作为函数参数
func justifyType(x interface{}) {
	switch v := x.(type) {//类型断言,可以得到空接口接收的值的具体类型。
	case string:
		fmt.Printf("x is a string,value is %v
", v)
	case int:
		fmt.Printf("x is a int is %v
", v)
	case bool:
		fmt.Printf("x is a bool is %v
", v)
	default:
		fmt.Println("unsupport type!")
	}
}
// 空接口作为map值
func main() {
	var studentInfo = make(map[string]interface{})
	studentInfo["name"] = "沙河娜扎"
	studentInfo["age"] = 18
	studentInfo["married"] = false
	fmt.Println(studentInfo)
}

在Go语言中只需要将标识符的首字母大写就可以让标识符对外可见了。
语法:

//定义包
package 包名
//包的导入
import "包的路径"
//或
import 别名 "包的路径"

在Go语言程序执行时导入包语句会自动触发包内部init()函数的调用。需要注意的是:init()函数没有参数也没有返回值。 init()函数在程序运行时自动被调用执行,不能在代码中主动调用它。
在运行时,被最后导入的包会最先初始化并调用其init()函数

第7天

P81~P110

反射

reflect包提供了reflect.TypeOfreflect.ValueOf两个函数来获取任意对象的ValueType

通过放射获取类型

package main
import (
	"fmt"
	"reflect"
)
func reflectType(x interface{}) {
	v := reflect.TypeOf(x)
	fmt.Printf("type:%v
", v)
}
func main() {
	var a float32 = 3.14
	reflectType(a) // type:float32
	var b int64 = 100
	reflectType(b) // type:int64
}

在反射中关于类型还划分为两种:类型(Type)种类(Kind)。种类(Kind)是指底层的类型。

//例子:我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。
package main
import (
	"fmt"
	"reflect"
)
type myInt int64
func reflectType(x interface{}) {
	t := reflect.TypeOf(x)
	//Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回空。
	fmt.Printf("type:%v kind:%v
", t.Name(), t.Kind())
}
func main() {
	var a *float32 // 指针
	var b myInt    // 自定义类型
	var c rune     // 类型别名
	reflectType(a) // type: kind:ptr
	reflectType(b) // type:myInt kind:int64
	reflectType(c) // type:int32 kind:int32
	type person struct {
		name string
		age  int
	}
	type book struct{ title string }
	var d = person{
		name: "沙河小王子",
		age:  18,
	}
	var e = book{title: "《跟小王子学Go语言》"}
	reflectType(d) // type:person kind:struct
	reflectType(e) // type:book kind:struct
}

通过放射获取值

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
", int64(v.Int()))
	case reflect.Float32:
		// v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换
		fmt.Printf("type is float32, value is %f
", float32(v.Float()))
	case reflect.Float64:
		// v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
		fmt.Printf("type is float64, value is %f
", float64(v.Float()))
	}
}
func main() {
	var a float32 = 3.14
	var b int64 = 100
	reflectValue(a) // type is float32, value is 3.140000
	reflectValue(b) // type is int64, value is 100
	// 将int类型的原始值转换为reflect.Value类型
	c := reflect.ValueOf(10)
	fmt.Printf("type c :%T
", c) // type c :reflect.Value
}

通过反射设置变量的值

反射中使用专有的Elem()方法来获取指针对应的值。SetInt()来设置值。

package main
import (
	"fmt"
	"reflect"
)
func reflectSetValue1(x interface{}) {
	v := reflect.ValueOf(x)
	if v.Kind() == reflect.Int64 {
		v.SetInt(200) //修改的是副本,reflect包会引发panic
	}
}
func reflectSetValue2(x interface{}) {
	v := reflect.ValueOf(x)
	// 反射中使用 Elem()方法获取指针对应的值
	if v.Elem().Kind() == reflect.Int64 {
		v.Elem().SetInt(200)
	}
}
func main() {
	var a int64 = 100
	// reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
	reflectSetValue2(&a)
	fmt.Println(a)
}

isNil()和isValid()

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

func main() {
	// *int类型空指针
	var a *int
	fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil())
	// nil值
	fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid())
	// 实例化一个匿名结构体
	b := struct{}{}
	// 尝试从结构体中查找"abc"字段
	fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid())
	// 尝试从结构体中查找"abc"方法
	fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid())
	// map
	c := map[string]int{}
	// 尝试从map中查找一个不存在的键
	fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid())
}

结构体反射

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

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
	// 通过for循环遍历结构体的所有字段信息
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		fmt.Printf("name:%s index:%d type:%v json tag:%v
", 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
", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
	}
}

并发

并发:同一时间段内执行多个任务。
并行:同一时刻执行多个任务。
Go语言的并发通过goroutine实现。goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。

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

Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度上下文切换的机制。

Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。

package main
import {
	"fmt"
}
var wg sync.WaitGroup//sync.WaitGroup来实现goroutine的同步

func hello(i int) {
	defer wg.Done() // goroutine结束就登记-1
	fmt.Println("Hello Goroutine!", i)
}
//程序启动后会创建一个主goroutine去执行。
func main() {
	for i := 0; i < 10; i++ {
		wg.Add(1) // 启动一个goroutine就登记+1
		go hello(i)//单独开启一个goroutine去执行函数
	}
	wg.Wait() // 等待所有登记的goroutine都结束,确保所有线程执行完后退出。
	//mian函数结果了,由mian函数启动的线程也就结束了。
}

可增长的栈

OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的goroutine也是可以的。

goroutine调度

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

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

GOMAXPROCS

Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。

Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。
M:N表示把M个goroutine分配给N个操作系统线程去执行。(GOMAXPROCS是m:n调度中的n)

func a() {
	for i := 1; i < 10; i++ {
		fmt.Println("A:", i)
	}
}
func b() {
	for i := 1; i < 10; i++ {
		fmt.Println("B:", i)
	}
}
func main() {
	runtime.GOMAXPROCS(1)
	go a()
	go b()
	time.Sleep(time.Second)
}

channel

Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信
channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
Go 语言中的通道(channel)是一种特殊的类型,一种引用类型。遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

var 变量 chan 元素类型
//例子:
var ch1 chan int   // 声明一个传递整型的通道
var ch2 chan bool  // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道

//通道的操作
//定义和初始化通道
ch := make(chan int10)//通道必须使用make函数初始化才能使用,否则为nil。指定容量10。
//有指定容量的称为缓冲区通道,没有制动容量称为无缓存区通道。
//使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。

//发送
ch <- 10 // 把10发送到ch中
//接收
x := <- ch // 从ch中接收值并赋值给变量x
<-ch       // 从ch中接收值,忽略结果
//关闭
close(ch)

单向通道

  • chan<- int是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作;
  • <-chan int是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作。

select

select即通道多路复用。满足同时从多个通道接收数据。
select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。

func main() {
	ch := make(chan int, 1)
	for i := 0; i < 10; i++ {
		select {
		case x := <-ch:
			fmt.Println(x)
		case ch <- i:
		}
	}
}

使用select语句能提高代码的可读性。

  • 可处理一个或多个channel的发送/接收操作。
  • 如果多个case同时满足,select会随机选择一个。
  • 对于没有case的select{}会一直等待,可用于阻塞main函数。

互斥锁

sync包的Mutex类型来实现互斥锁。

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

func add() {
	for i := 0; i < 5000; i++ {
		lock.Lock() // 加锁
		x = x + 1
		lock.Unlock() // 解锁
	}
	wg.Done()
}
func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

读写互斥锁

读写锁在Go语言中使用sync包中的RWMutex类型。
读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

var (
	x      int64
	wg     sync.WaitGroup
	lock   sync.Mutex
	rwlock sync.RWMutex
)
func write() {
	// lock.Lock()   // 加互斥锁
	rwlock.Lock() // 加写锁
	x = x + 1
	time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
	rwlock.Unlock()                   // 解写锁
	// lock.Unlock()                     // 解互斥锁
	wg.Done()
}
func read() {
	// lock.Lock()                  // 加互斥锁
	rwlock.RLock()               // 加读锁
	time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
	rwlock.RUnlock()             // 解读锁
	// lock.Unlock()                // 解互斥锁
	wg.Done()
}
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()
	end := time.Now()
	fmt.Println(end.Sub(start))
}

WaitGroup

Go语言中可以使用sync.WaitGroup来实现并发任务的同步。

var wg sync.WaitGroup

func hello() {
	defer wg.Done()
	fmt.Println("Hello Goroutine!")
}
func main() {
	wg.Add(1)
	go hello() // 启动另外一个goroutine去执行hello函数
	fmt.Println("main goroutine done!")
	wg.Wait()
}

Once

Once可以确保多线程场景下某些操作在高并发的场景下只执行一次。
sync.Once只有一个Do方法,其签名如下:

func (o *Once) Do(f func()) {}
//例子:实现并发安全的单例模式:
package singleton
import (
    "sync"
)
type singleton struct {}
var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}

Map

Go语言中内置的map不是并发安全的。
而Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如StoreLoadLoadOrStoreDeleteRange等操作方法。

var m = sync.Map{}//并发安全的

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(n)
			m.Store(key, n)//必须使用sync.Map内置的Store方法设置键值对
			value, _ := m.Load(key)//必须使用sync.Map内置的Load方法根据key取值。
			fmt.Printf("k=:%v,v:=%v
", key, value)
			wg.Done()
		}(i)
	}
	wg.Wait()
}

atomic

代码中的加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。针对基本数据类型我们还可以使用原子操作来保证并发安全。Go语言中原子操作由内置的标准库sync/atomic提供。
以下是atomic包提供的一些方法:
读取操作

  • func LoadInt32(addr *int32) (val int32)
    func LoadInt64(addr *int64) (val int64)
    func LoadUint32(addr *uint32) (val uint32)
    func LoadUint64(addr *uint64) (val uint64)
    func LoadUintptr(addr *uintptr) (val uintptr)
    func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)

写入操作

  • func StoreInt32(addr *int32, val int32)
    func StoreInt64(addr *int64, val int64)
    func StoreUint32(addr *uint32, val uint32)
    func StoreUint64(addr *uint64, val uint64)
    func StoreUintptr(addr *uintptr, val uintptr)
    func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

修改操作

  • func AddInt32(addr *int32, delta int32) (new int32)
    func AddInt64(addr *int64, delta int64) (new int64)
    func AddUint32(addr *uint32, delta uint32) (new uint32)
    func AddUint64(addr *uint64, delta uint64) (new uint64)
    func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

交换操作

  • func SwapInt32(addr *int32, new int32) (old int32)
    func SwapInt64(addr *int64, new int64) (old int64)
    func SwapUint32(addr *uint32, new uint32) (old uint32)
    func SwapUint64(addr *uint64, new uint64) (old uint64)
    func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
    func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)

比较并交换操作

  • func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
    func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
    func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
    func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
    func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
    func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
package main
import "sync"
//
var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add(){
	// lock.Lock()
	// x++
	// lock.Unlock()
	
	atomic.AddInt64(&x,1)//该语句就相当于上面三句注释的代码
	wg.Done()
}
func main(){
	wg.Add(1000)
	for i:=0;i<1000;i++{
		go add()
	}
	wg.Wait()
	fmt.Println(x)
}

第8天

P111~P129

TCP通信

服务端

// tcp/server/main.go
// TCP server端
// 处理函数
func process(conn net.Conn) {
	defer conn.Close() // 关闭连接
	for {
		reader := bufio.NewReader(conn)
		var buf [128]byte
		n, err := reader.Read(buf[:]) // 读取数据
		if err != nil {
			fmt.Println("read from client failed, err:", err)
			break
		}
		recvStr := string(buf[:n])
		fmt.Println("收到client端发来的数据:", recvStr)
		conn.Write([]byte(recvStr)) // 发送数据
	}
}

func main() {
	listen, err := net.Listen("tcp", "127.0.0.1:20000")//监听
	if err != nil {
		fmt.Println("listen failed, err:", err)
		return
	}
	for {
		conn, err := listen.Accept() // 建立连接
		if err != nil {
			fmt.Println("accept failed, err:", err)
			continue
		}
		go process(conn) // 启动一个goroutine处理连接
	}
}

客户端

// tcp/client/main.go
// 客户端
func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:20000")//拨号
	if err != nil {
		fmt.Println("err :", err)
		return
	}
	defer conn.Close() // 关闭连接
	inputReader := bufio.NewReader(os.Stdin)
	for {
		input, _ := inputReader.ReadString('
') // 读取用户输入
		inputInfo := strings.Trim(input, "
")
		if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出
			return
		}
		_, err = conn.Write([]byte(inputInfo)) // 发送数据
		if err != nil {
			return
		}
		buf := [512]byte{}
		n, err := conn.Read(buf[:])
		if err != nil {
			fmt.Println("recv failed, err:", err)
			return
		}
		fmt.Println(string(buf[:n]))
	}
}

UDP通信

UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)参考模型中一种无连接的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。

服务端

// UDP/server/main.go
// UDP server端
func main() {
	listen, err := net.ListenUDP("udp", &net.UDPAddr{
		IP:   net.IPv4(0, 0, 0, 0),
		Port: 30000,
	})
	if err != nil {
		fmt.Println("listen failed, err:", err)
		return
	}
	defer listen.Close()
	for {
		var data [1024]byte
		n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据
		if err != nil {
			fmt.Println("read udp failed, err:", err)
			continue
		}
		fmt.Printf("data:%v addr:%v count:%v
", string(data[:n]), addr, n)
		_, err = listen.WriteToUDP(data[:n], addr) // 发送数据
		if err != nil {
			fmt.Println("write to udp failed, err:", err)
			continue
		}
	}
}

客户端

// UDP 客户端
func main() {
	socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
		IP:   net.IPv4(0, 0, 0, 0),
		Port: 30000,
	})
	if err != nil {
		fmt.Println("连接服务端失败,err:", err)
		return
	}
	defer socket.Close()
	sendData := []byte("Hello server")
	_, err = socket.Write(sendData) // 发送数据
	if err != nil {
		fmt.Println("发送数据失败,err:", err)
		return
	}
	data := make([]byte, 4096)
	n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据
	if err != nil {
		fmt.Println("接收数据失败,err:", err)
		return
	}
	fmt.Printf("recv:%v addr:%v count:%v
", string(data[:n]), remoteAddr, n)
}

HTTP通信

net/http包提供了HTTP客户端和服务端的实现。
服务端实现:

// http server

func sayHello(w http.ResponseWriter, r *http.Request) {
	str := '<h1>hello</h1>'
	w.Write([]byte(str))//响应
}

func main() {
	//HandleFunc函数可以向DefaultServeMux添加处理器。
	http.HandleFunc("/api/test", sayHello)
	//ListenAndServe使用指定的监听地址和处理器启动一个HTTP服务端。处理器参数通常是nil,这表示采用包变量DefaultServeMux作为处理器。
	err := http.ListenAndServe("127.0.0.1:9090", nil)
	if err != nil {
		fmt.Printf("http server failed, err:%v
", err)
		return
	}
}

客户端:GET方式和POST方式
GET方式:

//GET方式
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
", err)
	}
	u.RawQuery = data.Encode() // URL encode
	fmt.Println(u.String())
	resp, err := http.Get(u.String())//发送请求
	if err != nil {
		fmt.Println("post failed, err:%v
", err)
		return
	}
	defer resp.Body.Close()//使用完response后必须关闭回复的主体。
	b, err := ioutil.ReadAll(resp.Body)//响应内容
	if err != nil {
		fmt.Println("get resp failed,err:%v
", err)
		return
	}
	fmt.Println(string(b))
}

POST方式

package main

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

// net/http post demo

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

Server
//对应的Server端HandlerFunc如下:
func postHandler(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	// 1. 请求类型是application/x-www-form-urlencoded时解析form数据
	r.ParseForm()
	fmt.Println(r.PostForm) // 打印form数据
	fmt.Println(r.PostForm.Get("name"), r.PostForm.Get("age"))
	// 2. 请求类型是application/json时从r.Body读取数据
	b, err := ioutil.ReadAll(r.Body)
	if err != nil {
		fmt.Println("read request.Body failed, err:%v
", err)
		return
	}
	fmt.Println(string(b))
	answer := `{"status": "ok"}`
	w.Write([]byte(answer))
}

单元测试

性能测试

单元,性能测试

第9天

P130~P139

MySQL

存储引擎

mysql支持插件式的存储引擎
常见的存储引擎:MyISAM和InnoDB。
MyISAM:

  • 查询适度快
  • 只支持表锁
  • 不支持事务
    InnoDB:
  • 整体速度快
  • 支持表锁和行锁
  • 支持事务

事务

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

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

索引

MySQL索引原理:B树和B+树

Go操作MySQL

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

//下载驱动Go-MySQL-Driver
go get -u github.com/go-sql-driver/mysql

操作数据库

// 定义一个全局对象db
var db *sql.DB

// 定义一个初始化数据库的函数
func initDB() (err error) {
	// DSN:Data Source Name
	dsn := "user:password@tcp(127.0.0.1:3306)/test"	
	// 注意!!!这里不要使用:=,我们是给全局变量赋值,然后在main函数中使用全局变量db
	db, err = sql.Open("mysql", dsn)// 不会校验账号密码是否正确,只会校验连接字符串的格式是否准确。
	if err != nil {
		return err
	}	
	err = db.Ping()// 尝试与数据库建立连接(校验账号密码是否正确)
	if err != nil {
		return err
	}
	db.SetMaxOpenConns(10)//设置与数据库建立连接的最大数目。一般很少设置。
	db.SetMaxIdleConns(10)//设置连接池中的最大闲置连接数。一般很少设置。
	return nil
}

其中sql.DB是一个数据库(操作)句柄,代表一个具有零到多个底层连接的连接池。它可以安全地被多个goroutine同时使用。database/sql包会自动创建和释放连接;它也会维护一个闲置连接的连接池。

单行查询

// 查询单条数据示例
func queryRowDemo() {
	sqlStr := "select id, name, age from user where id=?"//Go语言中mysql语句的占位符是"?"
	var u user
	// 确保QueryRow之后调用Scan方法,因为Scan方法内有个close动作会释放mysql连接。
	err := db.QueryRow(sqlStr, 2).Scan(&u.id, &u.name, &u.age)//QueryRow参数2是传递占位符的参数
	if err != nil {
		fmt.Printf("scan failed, err:%v
", err)
		return
	}
	fmt.Printf("id:%d name:%s age:%d
", u.id, u.name, u.age)
}

多行查询

// 查询多条数据示例
func queryMultiRowDemo() {
	sqlStr := "select id, name, age from user where id > ?"
	rows, err := db.Query(sqlStr, 0)
	if err != nil {
		fmt.Printf("query failed, err:%v
", err)
		return
	}
	// 非常重要:关闭rows释放持有的数据库链接
	defer rows.Close()
	// 循环读取结果集中的数据
	for rows.Next() {
		var u user
		err := rows.Scan(&u.id, &u.name, &u.age)
		if err != nil {
			fmt.Printf("scan failed, err:%v
", err)
			return
		}
		fmt.Printf("id:%d name:%s age:%d
", u.id, u.name, u.age)
	}
}

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

插入

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

更新

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

删除

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

预处理

// 预处理查询示例
func prepareQueryDemo() {
	sqlStr := "select id, name, age from user where id > ?"
	//Prepare方法会先将sql语句发送给MySQL服务端,返回一个准备好的状态用于之后的查询和命令。
	//返回值可以同时执行多个查询和命令。
	//好处是
	//1.提高性能,服务器编译一次就可以多次运行
	//2.防止SQL注入
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%v
", err)
		return
	}
	defer stmt.Close()
	rows, err := stmt.Query(0)
	if err != nil {
		fmt.Printf("query failed, err:%v
", err)
		return
	}
	defer rows.Close()
	// 循环读取结果集中的数据
	for rows.Next() {
		var u user
		err := rows.Scan(&u.id, &u.name, &u.age)
		if err != nil {
			fmt.Printf("scan failed, err:%v
", err)
			return
		}
		fmt.Printf("id:%d name:%s age:%d
", u.id, u.name, u.age)
	}
}

事务

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

第10天

P140~P145

Redis

应用场景

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

go-redis库操作redis

go-redis支持连接哨兵及集群模式的Redis。

安装
go get -u github.com/go-redis/redis
连接

普通连接

// 声明一个全局的rdb变量
var rdb *redis.Client

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

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

连接Redis哨兵模式

func initClient()(err error){
	rdb := redis.NewFailoverClient(&redis.FailoverOptions{
		MasterName:    "master",
		SentinelAddrs: []string{"x.x.x.x:26379", "xx.xx.xx.xx:26379", "xxx.xxx.xxx.xxx:26379"},
	})
	_, err = rdb.Ping().Result()
	if err != nil {
		return err
	}
	return nil
}

连接Redis集群

func initClient()(err error){
	rdb := redis.NewClusterClient(&redis.ClusterOptions{
		Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"},
	})
	_, err = rdb.Ping().Result()
	if err != nil {
		return err
	}
	return nil
}
基本操作

set/get示例

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

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

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

zset示例

func redisExample2() {
	zsetKey := "language_rank"
	languages := []*redis.Z{
		&redis.Z{Score: 90.0, Member: "Golang"},
		&redis.Z{Score: 98.0, Member: "Java"},
		&redis.Z{Score: 95.0, Member: "Python"},
		&redis.Z{Score: 97.0, Member: "JavaScript"},
		&redis.Z{Score: 99.0, Member: "C/C++"},
	}
	// ZADD
	num, err := rdb.ZAdd(zsetKey, languages...).Result()
	if err != nil {
		fmt.Printf("zadd failed, err:%v
", err)
		return
	}
	fmt.Printf("zadd %d succ.
", num)

	// 把Golang的分数加10
	newScore, err := rdb.ZIncrBy(zsetKey, 10.0, "Golang").Result()
	if err != nil {
		fmt.Printf("zincrby failed, err:%v
", err)
		return
	}
	fmt.Printf("Golang's score is %f now.
", newScore)

	// 取分数最高的3个
	ret, err := rdb.ZRevRangeWithScores(zsetKey, 0, 2).Result()
	if err != nil {
		fmt.Printf("zrevrange failed, err:%v
", err)
		return
	}
	for _, z := range ret {
		fmt.Println(z.Member, z.Score)
	}

	// 取95~100分的
	op := &redis.ZRangeBy{
		Min: "95",
		Max: "100",
	}
	ret, err = rdb.ZRangeByScoreWithScores(zsetKey, op).Result()
	if err != nil {
		fmt.Printf("zrangebyscore failed, err:%v
", err)
		return
	}
	for _, z := range ret {
		fmt.Println(z.Member, z.Score)
	}
}
//输出结果如下:
$ ./06redis_demo 
zadd 0 succ.
Golang's score is 100.000000 now.
Golang 100
C/C++ 99
Java 98
JavaScript 97
Java 98
C/C++ 99
Golang 100

NSQ消息队列

第11天

P146~P159

Go module

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

GO111MODULE

要启用go module支持首先要设置环境变量GO111MODULE,通过它可以开启或关闭模块支持,它有三个可选值:off、on、auto,默认值是auto。
使用 go module 管理依赖后会在项目根目录下生成两个文件go.modgo.sum

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 tidy命令更新go.mod中的依赖关系。

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

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表示间接引用

Context

两个默认值

context.Background()
context.TODO()

四个方法

context.withCancel(context.Background())
context.withDeadline(context.Background(),time.Time)
context.withTimeout(context.Background(),tiem.Duration)
context.withValue(context.Background(),key,value)

kafka

Go语言中连接kafka使用第三方库sarama:github.com/Shopify/sarama。
下载安装:

go get github.com/Shopify/sarama

发送消息

package main
import (
	"fmt"

	"github.com/Shopify/sarama"
)
// 基于sarama第三方库开发的kafka client
func main() {
	config := sarama.NewConfig()
	config.Producer.RequiredAcks = sarama.WaitForAll          // 发送完数据需要leader和follow都确认
	config.Producer.Partitioner = sarama.NewRandomPartitioner // 新选出一个partition
	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{"192.168.1.7:9092"}, config)
	if err != nil {
		fmt.Println("producer closed, err:", err)
		return
	}
	defer client.Close()
	// 发送消息
	pid, offset, err := client.SendMessage(msg)
	if err != nil {
		fmt.Println("send msg failed, err:", err)
		return
	}
	fmt.Printf("pid:%v offset:%v
", pid, offset)
}

消费消息

package main
import (
	"fmt"

	"github.com/Shopify/sarama"
)
// kafka consumer
func main() {
	consumer, err := sarama.NewConsumer([]string{"127.0.0.1:9092"}, nil)
	if err != nil {
		fmt.Printf("fail to start consumer, err:%v
", err)
		return
	}
	partitionList, err := consumer.Partitions("web_log") // 根据topic取到所有的分区
	if err != nil {
		fmt.Printf("fail to get list of partition:err%v
", err)
		return
	}
	fmt.Println(partitionList)
	for partition := range partitionList { // 遍历所有的分区
		// 针对每个分区创建一个对应的分区消费者
		pc, err := consumer.ConsumePartition("web_log", int32(partition), sarama.OffsetNewest)
		if err != nil {
			fmt.Printf("failed to start consumer for partition %d,err:%v
", partition, err)
			return
		}
		defer pc.AsyncClose()
		// 异步从每个分区消费信息
		go func(sarama.PartitionConsumer) {
			for msg := range pc.Messages() {
				fmt.Printf("Partition:%d Offset:%d Key:%v Value:%v", msg.Partition, msg.Offset, msg.Key, msg.Value)
			}
		}(pc)
	}
}

第12天

P160~P178

etcd

etcd是使用Go语言开发的一个开源的、高可用的分布式key-value存储系统,可以用于配置共享和服务的注册和发现。
类似项目有zookeeper和consul。
etcd具有以下特点:

  • 完全复制:集群中的每个节点都可以使用完整的存档
  • 高可用性:Etcd可用于避免硬件的单点故障或网络问题
  • 一致性:每次读取都会返回跨多主机的最新写入
  • 简单:包括一个定义良好、面向用户的API(gRPC)
  • 安全:实现了带有可选的客户端证书身份验证的自动化TLS
  • 快速:每秒10000次写入的基准速度
  • 可靠:使用Raft算法实现了强一致、高可用的服务存储目录

ES

Elasticsearch(ES)是一个基于Lucene构建的开源、分布式、RESTful接口的全文搜索引擎。Elasticsearch还是一个分布式文档数据库,其中每个字段均可被索引,而且每个字段的数据均可被搜索,ES能够横向扩展至数以百计的服务器存储以及处理PB级的数据。可以在极短的时间内存储、搜索和分析大量的数据。通常作为具有复杂搜索场景情况下的核心发动机。

Kibana

第13天

P179~P189

Gin框架

下载安装

go get -u github.com/gin-gonic/gin

请求方式示例

Get方式示例:

package main
import (
	"github.com/gin-gonic/gin"
)
func main() {
	// 创建一个默认的路由引擎
	r := gin.Default()
	// GET:请求方式;/hello:请求的路径
	// 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数
	r.GET("/hello", func(c *gin.Context) {
		//c.Param:获取GET方式传递的name参数的值
		name := c.Param("name")
		//c.DefaultQuery:获取GET方式传递的name参数的值,第二个参数是默认值
		name := c.DefaultQuery("name","Jack")
		// c.JSON:返回JSON格式的数据
		c.JSON(200, gin.H{
			"message": "Hello world!",
		})
	})
	// 启动HTTP服务,默认在0.0.0.0:8080启动服务
	r.Run()
}

POST方式示例:

package main
import (
	"github.com/gin-gonic/gin"
)
func main() {
	// 创建一个默认的路由引擎
	r := gin.Default()
	// Post:请求方式;/hello:请求的路径
	// 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数
	r.POST("/hello", func(c *gin.Context) {
		//c.PostForm:获取POST方式传递的name参数的值
		name := c.PostForm("name")
		//c.DefaultPostForm:获取POST方式传递的name参数的值,第二个参数是默认值
		name := c.DefaultPostForm("name","Jack")
		//c.PostFormArray:数组的形式接收
		key := c.PostFormArray("key")
		//c.FormFile:获取上传的文件
		file,_ := c.FormFile("file")
		// c.JSON:返回JSON格式的数据
		c.JSON(200, gin.H{
			"message": "Hello world!",
		})
	})
	// 启动HTTP服务,默认在0.0.0.0:8080启动服务
	r.Run()
}

多文件上传:

func main() {
	router := gin.Default()
	// 处理multipart forms提交文件时默认的内存限制是32 MiB
	// 可以通过下面的方式修改
	// router.MaxMultipartMemory = 8 << 20  // 8 MiB
	router.POST("/upload", func(c *gin.Context) {
		// Multipart form
		form, _ := c.MultipartForm()
		files := form.File["file"]

		for index, file := range files {
			log.Println(file.Filename)
			dst := fmt.Sprintf("C:/tmp/%s_%d", file.Filename, index)
			// 上传文件到指定的目录
			c.SaveUploadedFile(file, dst)
		}
		c.JSON(http.StatusOK, gin.H{
			"message": fmt.Sprintf("%d files uploaded!", len(files)),
		})
	})
	router.Run()
}

路由组

Gin框架中的路由使用的是httprouter这个库。
其基本原理就是构造一个路由地址的前缀树。

func main() {
	r := gin.Default()
	userGroup := r.Group("/user")
	{
		userGroup.GET("/index", func(c *gin.Context) {...})
		userGroup.GET("/login", func(c *gin.Context) {...})
		userGroup.POST("/login", func(c *gin.Context) {...})

	}
	//路由组也是支持嵌套的
	shopGroup := r.Group("/shop")
	{
		shopGroup.GET("/index", func(c *gin.Context) {...})
		shopGroup.GET("/cart", func(c *gin.Context) {...})
		shopGroup.POST("/checkout", func(c *gin.Context) {...})
		// 嵌套路由组
		xx := shopGroup.Group("xx")
		xx.GET("/oo", func(c *gin.Context) {...})
	}
	r.Run()
}

中间件

Gin中的中间件必须是一个gin.HandlerFunc类型
定义一个中间件例子:

// StatCost 是一个统计耗时请求耗时的中间件
func StatCost() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		c.Set("name", "小王子") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
		// 调用该请求的剩余处理程序
		//c.Next() 之前的操作是在 Handler 执行之前就执行;
		c.Next()
		//c.Next() 之后的操作是在 Handler 执行之后再执行;
		// 不调用该请求的剩余处理程序
		// c.Abort()
		// 计算耗时
		cost := time.Since(start)
		log.Println(cost)
	}
}

注册路由

//为全局路由注册
func main() {
	// 新建一个没有任何默认中间件的路由
	r := gin.New()
	// 注册一个全局中间件
	r.Use(StatCost())
	
	r.GET("/test", func(c *gin.Context) {
		name := c.MustGet("name").(string) // 从上下文取值
		log.Println(name)
		c.JSON(http.StatusOK, gin.H{
			"message": "Hello world!",
		})
	})
	r.Run()
}
// 给/test2路由单独注册中间件(可注册多个)
r.GET("/test2", StatCost(), func(c *gin.Context) {
	name := c.MustGet("name").(string) // 从上下文取值
	log.Println(name)
	c.JSON(http.StatusOK, gin.H{
		"message": "Hello world!",
	})
})
// 为路由组注册中间件
shopGroup := r.Group("/shop", StatCost())
{
    shopGroup.GET("/index", func(c *gin.Context) {...})
    ...
}

gin默认中间件:
gin.Default()默认使用了LoggerRecovery中间件,其中:
Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。
Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。
如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。

gin中间件中使用goroutine:
当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。

原文地址:https://www.cnblogs.com/zt102545/p/13940213.html