Golang Devops项目开发

1.1 GO语言基础

1 初识Go语言

1.1.1 开发环境搭建

参考文档:《Windows Go语言环境搭建》

1.2.1 Go语言特性-垃圾回收

a. 内存自动回收,再也不需要开发人员管理内存

b. 开发人员专注业务实现,降低了心智负担

c. 只需要new分配内存,不需要释放

d. gc 垃圾回收

1.2.2 Go语言特性-天然并发

a. 从语言层面支持并发,非常简单

b. goroutine,轻量级线程,创建成千上万个goroutine成为可能

c. 基于CSP(Communicating Sequential Process)模型实现

package main

import(
	"time"
)

func main() {

	for i := 0; i < 100; i++ {
		go test_goroute(i)
	}

	time.Sleep(time.Second)
}

进一步阅读:《 GO Channel并发、死锁问题》 https://www.cnblogs.com/show58/p/12699083.html

《 Go的CSP并发模型实现:M, P, G 》https://www.cnblogs.com/sunsky303/p/9115530.html

1.2.3 Go语言特性-channel

a. 管道,类似unix/linux中的pipe

b. 多个goroutine之间通过channel进行通信

c. 支持任何类型

func main() {

    pipe := make(chan int, 3)

    pipe <- 1

    pipe <- 2

}
package main

import "fmt"

func test_pipe() {
	pipe := make(chan int, 3)
	pipe <- 1
	pipe <- 2
	pipe <- 3
	var t1 int
	t1 = <-pipe
	fmt.Println("t1: ", t1)

}

func sum(s []int, c chan int) {
	test_pipe()
	sum := 0
	for _, v := range s {
		sum += v
	}
	fmt.Println("sum:", sum)
	c <- sum // send sum to c
}

func main() {
	s := []int{7, 2, 8, -9, 4, 0}
	c := make(chan int)
	go sum(s[:len(s)/2], c) // 7+2+8 = 17, -9 + 4+0 = -5
	go sum(s[len(s)/2:], c)
	// x, y := <-c, <-c // receive from c
	x := <-c
	y := <-c
	fmt.Println(x, y, x+y)
}

1.2.4 Go语言特性-多返回值

一个函数返回多个值

func calc(a int, b int) (int, int) {
	sum := a+b
	avg := (a+b)/2
	return sum, avg
}

1.4.1包的概念

1. 和python一样,把相同功能的代码放到一个目录,称之为包

2. 包可以被其他包引用

3. main包是用来生成可执行文件,每个程序只有一个main包

4. 包的主要用途是提高代码的可复用性

2. Go语言基础

2 基本数据类型和操作符

1. 文件名&关键字&标识符

2. Go程序基本结构

3. 常量和变量

4. 数据类型和操作符

5. 字符串类型

2.1文件名&关键字&标识符

1. 所有go源码以.go结尾

2. 标识符以字母或下划线开头,大小写敏感,比如:

a. boy

b. Boy

c. a+b

d. 0boy

e. _boy

f. =_boy

g. _

3. _是特殊标识符,用来忽略结果

4. 保留关键字

2.1文件名&关键字&标识符-关键字1

2.1文件名&关键字&标识符-关键字2

◼ var和const :变量和常量的声明

var varName type 或者 varName : = value

◼ package and import: 包和导入

◼ func: 用于定义函数和方法

◼ return :用于从函数返回

◼ defer someCode :在函数退出之前执行

◼ go : 用于并行

◼ select 用于选择不同类型的通讯

◼ interface 用于定义接口

◼ struct 用于定义抽象数据类型

2.1文件名&关键字&标识符-关键字3

◼ break、case、continue、for、fallthrough、else、if、switch、goto、default 流程控制

◼ fallthrough的用法注意总结 [推荐阅读

https://blog.csdn.net/ajdfhajdkfakr/article/details/79086125]

◼ 1.加了fallthrough后,会直接运行【紧跟的后一个】case或default语句,不论条件是否满

足都会执行

◼ 2.加了fallthrough语句后,【紧跟的后一个】case条件不能定义常量和变量

◼ 3.执行完fallthrough后直接跳到下一个条件语句,本条件执行语句后面的语句不执行

◼ chan用于channel通讯

◼ type用于声明自定义类型

◼ map用于声明map类型数据

◼ range用于读取slice、map、channel数据

2.2 Go程序的基本结构1

1. 任何一个代码文件隶属于一个包

2. import 关键字,引用其他包:

2.2 Go程序的基本结构2

3. golang可执行程序,package main,

并且有且只有一个main入口函数

4. 包中函数调用:

a. 同一个包中函数,直接调用

b. 不同包中函数,通过包名+点+

函数名进行调用

5. 包访问控制规则:

a. 大写意味着这个函数/变量是可导出的

b. 小写意味着这个函数/变量是私有的,

包外部不能访问

2.4 常量1

1. 常量使用const 修饰,代表永远是只读的,不能修改。

2. const 只能修饰boolean,number(int相关类型、浮点类型、complex)和string。

3. 语法:const identifier [type] = value,其中type可以省略。

举例: const b string = “hello world”

const b = “hello world”

const Pi = 3.1414926

const a = 9/3

const c = getValue()

package main

import (
	"fmt"
	"time"
)

const (
	Man    = 1
	Female = 2
)

func main() {
	for {
		second := time.Now().Unix()
		if second%Female == 0 {
			fmt.Println("female")
		} else {
			fmt.Println("man")
		}
		time.Sleep(1000 * time.Millisecond)
	}
}

2.4 常量2

4. 比较优雅的写法:

尽量减少我们写代码 const (

a = 0

b = 1

c = 2

)

5. 更加专业的写法:

const (

a = iota

b //1

c //2

)

2.5 变量1

1. 语法:var identifier type

2.5 变量2

Var (

    a int //默认为0

    b string //默认为””

    c bool //默认为false

    d = 8

    e = “hello world”

    )

练习

写一个程序获取当前运行的操作系统名称和PATH环境环境变量的值,并打印在终端

package main

import (
	"fmt"
	"os"
)

func main() {
	var goos string = os.Getenv("GOOS")
	fmt.Printf("The operating system is: %s\n", goos)
	path := os.Getenv("Path")
	fmt.Printf("Path is %s\n", path)
}

2.6 值类型和引用类型

1. 值类型:变量直接存储值,内存通常在栈中分配。

2. 引用类型:变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配。通过GC回收。

2.6值类型和引用类型

1. 值类型:基本数据类型int、float、bool、string以及数组和struct。

2. 引用类型:指针、slice、map、chan等都是引用类型。

练习:写一个程序,交换两个整数的值。比如: a=3; b=4; 交换之后:a=4;b=3

代码:2-6-swap.go

package main

import "fmt"

func swap(a *int, b *int) {
	tmp := *a
	*a = *b
	*b = tmp
	return
}

func swap1(a int, b int) (int, int) {
	return b, a
}

func test() {
	var a = 100
	fmt.Println(a)
	//var b int
	for i := 0; i < 100; i++ {
		var b = i * 2
		fmt.Println(b)
	}

	//fmt.Println(c)
	//fmt.Println(b)
}

func test2() {
	var a int8 = 100
	var b int16 = int16(a)

	fmt.Printf("a=%d b=%d\n", a, b)
}

func main() {
	first := 100
	second := 200
	//swap(&first, &second)
	//first, second = swap1(first, second)
	first, second = second, first
	fmt.Println("first=", first)
	fmt.Println("second=", second)

	test()

	test2()
}

练习:写一个程序用来打印值类型和引用类型变量到终端,并观察输出结果。

代码: 2-6-value_quote.go

package main

import (
	"fmt"
)

func modify(a int) {
	a = 10
	return
}

func modify1(a *int) {
	*a = 10
}

func main() {
	a := 5
	b := make(chan int, 1)
	

	fmt.Println("a=", a)
	fmt.Println("b=", b)

	modify(a)
	fmt.Println("a=", a)
	modify1(&a)
	fmt.Println("a=", a)
}

2.7 变量的作用域

1. 在函数内部声明的变量叫做局部变量,生命周期仅限于函数内部。

2.在函数外部声明的变量叫做全局变量,生命周期作用于整个包,如果是大写的,

则作用于整个程序。

请指出下面程序的输出是什么
package main
var a = "G"
func main() {
n() // G
m() // O
n() //O
} 
func n() { 
fmt.Println(a) 
}
func m() {
a = "O"
fmt.Println(a) 
}

2.8 数据类型和操作符1

1. bool 类型,只能存 true false
2. 相关操作符, !、 && ||

2.8 数据类型和操作符2

3. 数字类型,主要有int、int8、int16、int32、int64、uint8、uint16、uint32、uint64、

float32、float64

4. 类型转换,type(variable),比如:var a int=8; var b int32=int32(a)

package main
func main() {
var a int
var b int32
a = 15
b = a + a // compiler error cannot use a + a (value of type int) as int32 value in assignment
b = b + 5 // ok: 5 is a constant
}
package main

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

func init() {
	rand.Seed(time.Now().UnixNano())
}

func main() {

	for i := 0; i < 10; i++ {
		a := rand.Int()
		fmt.Println(a)
	}

	for i := 0; i < 10; i++ {
		a := rand.Intn(100)
		fmt.Println(a)
	}

	for i := 0; i < 10; i++ {
		a := rand.Float32()
		fmt.Println(a)
	}

}

2.8 数据类型和操作符3

5. 字符类型:var a byte

var a byte = ‘c’

6. 字符串类型: var str string

练习:请指出下面程序的输出是什么?

package main

import "fmt"

func main() {

    var n int16 = 34

    var m int32

    m = n

    m = int32(n)

    fmt.Printf("32 bit int is: %d\n", m)

    fmt.Printf("16 bit int is: %d\n", n)

}

PS D:\Workspace\Go\src\projects\demo> go run main.go
# command-line-arguments
.\main.go:8:6: cannot use n (variable of type int16) as type int32 in assignment

练习:使用 math/rand 生成 10 个随机整数, 10 个小于 100
随机整数以及 10 个随机浮点数 代码:

2.9数据类型和操作符1

1. 逻辑操作符: == 、!=、

<

<=

>和 >=

2.9数据类型和操作符2

2. 数学操作符:+、-、

*

、/等等

package main

import (
	"fmt"
	"strings"
	"unsafe"
	_ "unsafe"
)

func test1() {
	bytes := []byte("I am byte array !")
	str := string(bytes)
	bytes[0] = 'i' //注意这一行,bytes在这里修改了数据,但是str打印出来的依然没变化,
	fmt.Println(str)
}
func test2() {
	bytes := []byte("I am byte array !")
	str := (*string)(unsafe.Pointer(&bytes))
	bytes[0] = 'i'
	fmt.Println(*str)
}
func test3() {
	var data [10]byte
	data[0] = 'T'
	data[1] = 'E'
	var str string = string(data[:])
	fmt.Println(str)
}

func str2bytes(s string) []byte {
	x := (*[2]uintptr)(unsafe.Pointer(&s))
	h := [3]uintptr{x[0], x[1], x[1]}
	return *(*[]byte)(unsafe.Pointer(&h))
}

func bytes2str(b []byte) string {
	return *(*string)(unsafe.Pointer(&b))
}
func test4() {
	s := strings.Repeat("abc", 3)
	fmt.Println("str2bytes")
	b := str2bytes(s)
	fmt.Println("bytes2str")
	s2 := bytes2str(b)
	fmt.Println(b, s2)
}

func main() {
	test1()
	test2()
	test3()
	test4()
}

2.9 数据类型和操作符3

字符串表示两种方式: 1)双引号 2)`` (反引号)

package main

import "fmt"

func main() {
	var str = "hello world\n\n"
	var str2 = `hello \n \n \n
	this is a test string
	This is a test string too·`
	fmt.Println("str=", str)
	fmt.Println("str2=", str2)
}
package main

import (
	"fmt"
)

func test_switch1() {
	a := 2
	switch a {
	case 1:
		fmt.Println("a=1")
	case 2:
		fmt.Println("a=2")
	case 3:
		fmt.Println("a=3")
	case 4:
		fmt.Println("a=4")
	default:
		fmt.Println("default")
	}
}

func test_switch2() {
	a := 2
	switch a {
	case 1:
		fmt.Println("a=1")
	case 2:
		fmt.Println("a=2")
		fallthrough
	case 3:
		fmt.Println("a=3")
	case 4:
		fmt.Println("a=4")
	default:
		fmt.Println("default")
	}
}

func main() {
	fmt.Printf("执行test_switch%d\n", 1)
	test_switch1()
	fmt.Printf("执行test_switch%d\n", 2)
	test_switch2()
}

3. Go函数

3 流程控制

for range 语句

str := "hello world,中国"

for i, v := range str {

fmt.Printf("index[%d] val[%c]\n", i, v)

}

用来遍历数组、slice、map、chan。

package main

import "fmt"

func modify(p *int) {

	fmt.Println(p)
	*p = 1000900
	return
}

func main() {

	var a int = 10
	fmt.Println(&a)

	var p *int
	p = &a

	fmt.Println("the address of p:", &p)
	fmt.Println("the value of p:", p)
	fmt.Println("the value of p point to variable:", *p)

	fmt.Println(*p)
	*p = 100
	fmt.Println(a)

	var b int = 999
	p = &b
	*p = 5

	fmt.Println(a)
	fmt.Println(b)

	modify(&a)
	fmt.Println(a)
}

3 函数1

1. 声明语法:func 函数名 (参数列表) [(返回值列表)] {}

func add()

{

}

3 函数2

2. golang函数特点:

a. 不支持重载,一个包不能有两个名字一样的函数

b. 函数是一等公民,函数也是一种类型,一个函数可以赋值给变量

c. 匿名函数

d. 多返回值

3 函数2

2. golang函数特点:

package main

import (
	"fmt"
	"reflect"
)

func add(a, b int) int {
	return a + b
}
func main() {
	c := add
	fmt.Println(c)
	sum := c(10, 20)
	fmt.Println(sum)
	sf1 := reflect.ValueOf(c)
	sf2 := reflect.ValueOf(add)
	if sf1 == sf2 {
		fmt.Println("c equal add")
	}

}
package main

import (
	"fmt"
)

type add_func func(int, int) int

func add(a, b int) int {
	return a + b
}
func operator(op add_func, a int, b int) int {
	return op(a, b)
}
func main() {
	c := add
	fmt.Println(c)
	sum := operator(c, 100, 200)
	fmt.Println(sum)
}

3 函数3

3. 函数参数传递方式:

1). 值传递

2). 引用传递

注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值

传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址

拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。

3 函数3

3. 函数参数传递方式: 1). 值传递

2). 引用传递

注意2:map、slice、chan、指针、interface默认以引用的方式传递,有待考证?

go只有值传递、浅拷贝

package main

import "fmt"

func modify(a int) {
	a = 100
}
func main() {
	a := 8
	fmt.Println(a)
	modify(a)
	fmt.Println(a)
}

3 函数4

4. 命名返回值的名字

func add(a, b int) (c int) {

    c = a + b

    return

}

func calc(a, b int) (sum int, avg int) {

    sum = a + b

    avg = (a + b) / 2

    return

}

3 函数5

5. _标识符,用来忽略返回值:

func calc(a, b int) (sum int, avg int) {

sum = a + b

avg = (a +b)/2

return

}

func main() {

sum, _ := calc(100, 200)

}

3 函数6

6. 可变参数: func add(arg…int) int {

}

0个或多个参数

func add(a int, arg…int) int {

}

1个或多个参数

func add(a int, b int, arg…int) int {

}

2个或多个参数

注意:其中arg是一个slice,我们可以通过arg[index]依次访问所有参数

通过len(arg)来判断传递参数的个数

package main

import "fmt"

func add(a int, arg ...int) int {
	var sum int = a
	for i := 0; i < len(arg); i++ {
		sum += arg[i]
	}

	return sum
}

func concat(a string, arg ...string) (result string) {

	result = a
	for i := 0; i < len(arg); i++ {
		result += arg[i]
	}

	return
}

func main() {
	sum := add(10, 3, 3, 3, 3)
	fmt.Println(sum)

	res := concat("hello", " ", "world")
	fmt.Println(res)
}

3 函数7

7. defer用途:

1. 当函数返回时,执行defer语句。因此,可以用来做资源清理

2. 多个defer语句,按先进后出的方式执行

3. defer语句中的变量,在defer声明时就决定了。

3 函数7 defer用途

package main

import "fmt"

func main() {

    i := 0

    defer fmt.Println(i)

    i++

    return

}

打印是多少?

PS D:\Workspace\Go\src\projects\demo> go run main.go
0

package main

import "fmt"

func main() {

    for i := 0; i < 5; i++ {

        defer fmt.Printf("%d", i)

    }

}

PS D:\Workspace\Go\src\projects\demo> go run main.go
43210

3 函数7 defer用途

1 关闭文件句柄

func read() {

    file := open(filename)

    defer file.Close()

    //文件操作

}

3 函数7 defer用途

2. 锁资源释放

func read() {

mc.Lock()

defer mc.Unlock()

//其他操作

}

3 函数7 defer用途

3. 数据库连接释放

func read() {

conn := openDatabase()

defer conn.Close()

//其他操作

}

4. Go数组和切片

4 常用结构

1. 内置函数、闭包

2. 数组与切片

3. map数据结构

4. package介绍

4.1 内置函数

1. close:主要用来关闭channel

2. len:用来求长度,比如string、array、slice、map、channel

3. new:用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针

4. make:用来分配内存,主要用来分配引用类型,比如chan、map、slice

5. append:用来追加元素到数组、slice中

6. panic和recover:用来做错误处理

7. new和make的区别

package main

import (
	"fmt"
	"strings"
)

// 缩小变量作用域,减少对全局变量的污染。下面的累加如果用全局变量进行实现,全局变量容易被其他人污染。
// 同时,所有我要实现n个累加器,那么每次需要n个全局变量。利用闭包,
// 每个生成的累加器myAdder1, myAdder2 := adder(), adder()有自己独立的sum,sum可以看作为myAdder1.sum与myAdder2.sum。
func Adder() func(int) int {
	var x int
	f := func(d int) int {
		x += d
		return x
	}
	return f
}

func makeSuffix(suffix string) func(string) string {
	f := func(name string) string {

		if strings.HasSuffix(name, suffix) == false {
			return name + suffix
		}
		return name
	}

	return f
}

func main() {

	f := Adder()
	fmt.Println(f(1))
	fmt.Println(f(100))
	// fmt.Println(f(1000))
	/*
		f1 := makeSuffix(".bmp")
		fmt.Println(f1("test"))
		fmt.Println(f1("pic"))

		f2 := makeSuffix(".jpg")
		fmt.Println(f2("test"))
		fmt.Println(f2("pic"))
	*/
}

4.2闭包

1. 闭包:一个函数和与其相关的引用环境组合而成的实体

package main

import “fmt”

func main() {

var f = Adder()

fmt.Print(f(1),” - “)

fmt.Print(f(20),” - “)

fmt.Print(f(300))

}

func Adder() func(int) int {

var x int

return func(delta int) int {

x += delta

return x

}

}

4.2闭包 例子

package main

import (

"fmt"

"strings"

)

func makeSuffixFunc(suffix string) func(string) string {

return func(name string) string {

if !strings.HasSuffix(name, suffix) {

return name + suffix

}

return name

}

}

func main() {

func1 := makeSuffixFunc(".bmp")

func2 := makeSuffixFunc(".jpg")

fmt.Println(func1("test"))

fmt.Println(func2("test"))

}

4.3数组与切片

1. 数组:是同一种数据类型的固定长度的序列。

2. 数组定义:var a [len]int,比如:var a[5]int 一旦定义,长度不能变

3. 长度是数组类型的一部分,因此,var a[5] int和var a[10]int是不同的类型

4. 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1

for i := 0; i < len(a); i++ {

}

5. 访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic

6. 数组是值类型,因此改变副本的值,不会改变本身的值

arr2 := arr1

arr2[2] = 100

package main

import "fmt"

func test1() {
	var a [10]int

	//j := 10
	a[0] = 100
	//a[j] = 200

	fmt.Println(a)

	for i := 0; i < len(a); i++ {
		fmt.Println(a[i])
	}

	for index, val := range a {
		fmt.Printf("a[%d]=%d\n", index, val)
	}
}

func test3(arr *[5]int) {
	(*arr)[0] = 1000
}

func test2() {
	var a [10]int
	b := a

	b[0] = 100
	fmt.Println(a)
}

func main() {

	//test1()
	test2()
	var a [5]int
	test3(&a)
	fmt.Println(a)
}

4.3数组与切片-案例

package main

import (

"fmt"

)

func modify(arr [5]int) {

arr[0] = 100

return

}

func main() {

var a [5]int //数组大小是固定的

modify(a)

for i := 0; i < len(a); i++ {

fmt.Println(a[i])

}

}

package main

import (

"fmt"

)

func modify(arr *[5]int) {

(*arr)[0] = 100

return

}

func main() {

var a [5]int

modify(&a)

for i := 0; i < len(a); i++ {

fmt.Println(a[i])

}

}

4.3 数组与切片-数组

1. 数组初始化 对于数组 []里面肯定要有东西

b. var age1 = [5]int{1,2,3,4,5}

c. var age2 = […]int{1,2,3,4,5,6}

a. var age0 [5]int = [5]int{1,2,3}

d. var str = [5]string{3:”hello world”, 4:”tom”}

2. 多维数组

a. var age [5][3]int

b. var f [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}

3. 多维数组遍历

package main

import (

"fmt"

)

func main() {

var f [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}

for k1, v1 := range f {

for k2, v2 := range v1 {

fmt.Printf("(%d,%d)=%d ", k1, k2, v2)

}

fmt.Println()

}

}

4.3 数组与切片-切片定义

1. 切片:切片是数组的一个引用,因此切片是引用类型

2. 切片的长度可以改变,因此,切片是一个可变的数组

3. 切片遍历方式和数组一样,可以用len()求长度

4. cap可以求出slice最大的容量,0 <= len(slice) <= (array),其中array是slice引用的数组

5. 切片的定义:var 变量名 []类型,比如 var str []string var arr []int

4.3 数组与切片-切片初始化

1. 切片初始化:var slice []int = arr[start:end]

包含start到end之间的元素,但不包含end

2. Var slice []int = arr[0:end]可以简写为 var slice []int=arr[:end]

3. Var slice []int = arr[start:len(arr)] 可以简写为 var slice[]int = arr[start:]

4. Var slice []int = arr[0, len(arr)] 可以简写为 var slice[]int = arr[:]

5. 如果要切片最后一个元素去掉,可以这么写:

Slice = slice[:len(slice)-1]

package main

import "fmt"

type slice struct {
	ptr *[100]int
	len int
	cap int
}

func make1(s slice, cap int) slice {
	s.ptr = new([100]int)
	s.cap = cap
	s.len = 0
	return s
}

func modify(s slice) {
	s.ptr[1] = 1000
}

func testSlice2() {
	var s1 slice
	s1 = make1(s1, 10)

	s1.ptr[0] = 100
	modify(s1)

	fmt.Println(s1.ptr)
}

func testSlice() {
	var slice []int
	var arr [5]int = [...]int{1, 2, 3, 4, 5}

	slice = arr[:]
	slice = slice[1:]
	slice = slice[:len(slice)-1]
	fmt.Println(slice)
	fmt.Println(len(slice))
	fmt.Println(cap(slice))

	slice = slice[0:1]
	fmt.Println(len(slice))
	fmt.Println(cap(slice))

}

func modify1(a []int) {
	a[1] = 1000
}

func testSlice3() {
	var b []int = []int{1, 2, 3, 4}
	modify1(b)
	fmt.Println(b)
}

func testSlice4() {
	var a = [10]int{1, 2, 3, 4}

	b := a[1:5]
	fmt.Printf("%p\n", b)
	fmt.Printf("%p\n", &a[1])
}

func main() {
	//testSlice()
	//testSlice2()
	//testSlice3()
	testSlice4()
}

4.3 数组与切片-切片实战1

1. 练习:写一个程序,演示切片的各个用法

代码:4-3-slice1.go

package main

import "fmt"

func testSlice() {
	var a [5]int = [...]int{1, 2, 3, 4, 5}
	s := a[1:]
	fmt.Printf("before len[%d] cap[%d]\n", len(s), cap(s))
	s[1] = 100
	fmt.Printf("s=%p a[1]=%p\n", s, &a[1])
	fmt.Println("before a:", a)

	s = append(s, 10)
	s = append(s, 10)
	fmt.Printf("after len[%d] cap[%d]\n", len(s), cap(s))
	s = append(s, 10)
	s = append(s, 10)
	s = append(s, 10)

	s[1] = 1000
	fmt.Println("after a:", a)
	fmt.Println(s)
	fmt.Printf("s=%p a[1]=%p\n", s, &a[1])
}

func testCopy() {

	var a []int = []int{1, 2, 3, 4, 5, 6}
	b := make([]int, 1)

	copy(b, a)
	fmt.Printf("copy len[%d] cap[%d]\n", len(b), cap(b))
	fmt.Println(b)
}

func testString() {
	s := "hello world"
	s1 := s[0:5]
	s2 := s[6:]

	fmt.Println(s1)
	fmt.Println(s2)

}

func testModifyString() {
	s := "我hello world"
	s1 := []rune(s)

	s1[0] = 200
	s1[1] = 128
	s1[2] = 64
	str := string(s1)
	fmt.Println(str)
}

func main() {
	//testSlice()
	//testCopy()
	//testString()
	testModifyString()
}

4.3 数组与切片-切片实战2

2. 切片的内存布局,类似C++ vector:

2. 练习,写一个程序,演示切片的内存布局

4.3 数组与切片-切片实战3

3. 通过make来创建切片

var slice []type = make([]type, len)

slice := make([]type, len)

slice := make([]type, len, cap)

4.3 数组与切片-切片实战4

4. 用append内置函数操作切片

slice = append(slice, 10)

var a = []int{1,2,3}

var b = []int{4,5,6}

a = append(a, b…)

5. For range 遍历切片

for index, val := range slice {

}

6. 切片resize

var a = []int {1,3,4,5}

b := a[1:2]

b = b[0:3]

7. 切片拷贝

s1 := []int{1,2,3,4,5}

s2 := make([]int, 10)

copy(s2, s1)

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

s3 = append(s3, s2…)

s3 = append(s3, 4,5,6)

4-3-slice-make.go

4.3 数组与切片-切片实战5

8. string与slice

string底层就是一个byte的数组,因此,也

可以进行切片操作

str := “hello world”

s1 := str[0:5]

fmt.Println(s1)

s2 := str[5:]

fmt.Println(s2)

9. string 的底层布局

4.3 数组与切片-切片实战6

10. 如何改变string中的字符值?

string本身是不可变的,因此要改变string中字符,需要如下操作:

str := “hello world”

s := []byte(str)

s[0] = ‘o’

str = string(s)

4.4 数组与切片的区别1

它们的定义:

 数组:类型 [n]T 表示拥有 n 个 T 类型的值的数组。

 切片:类型 []T 表示一个元素类型为 T 的切片。

数组的例子

var x[3]int = [3]int{1,2,3}

var y[3]int = x

fmt.Println(x,y)

y[0]=999

fmt.Println(x,y)

切片的例子

var x[]int = []int{1,2,3}

var y[]int = x

fmt.Println(x,y)

y[0]=999

fmt.Println(x,y)

4.4 数组与切片的区别2

它们的定义:

 数组:类型 [n]T 表示拥有 n 个 T 类型的值的数组。

 切片:类型 []T 表示一个元素类型为 T 的切片。

数组是需要指定个数的,而切片则不需要。数组赋值也可是使用如下方式,忽略元素个数,使用

“...”代替

x:= [...]int{1,2,3}
y := x
fmt.Println(x,y)
y[0]=999
fmt.Println(x,y)

4.5 new和make的区别

new

func main() {

var i *int

i=new(int)

*i=10

fmt.Println(*i)

}

 make

func make(t Type, size ...IntegerType) Type func new(Type) *Type

make也是用于内存分配的,但是和new不同,它只用于

chan、map以及切片的内存创建,而且它返回的类型就是这

三个类型本身,而不是他们的指针类型,因为这三种类型

就是引用类型,所以就没有必要返回他们的指针了。

5. Go test方法

5 Go test

前置条件:

1、文件名须以"_test.go"结尾

2、方法名须以"Test"打头,并且形参为 (t *testing.T)

5 Go test 举例

举例:gotest.go

package mytest

import (

"errors"

)

func Division(a, b float64) (float64, error) {

if b == 0 {

return 0, errors.New("除数不能为0")

}

return a / b, nil

}

gotest_test.go

package mytest

import (

"testing"

)

func Test_Division_1(t *testing.T) {

if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function

t.Error("除法函数测试没通过") // 如果不是如预期的那么就报错

} else {

t.Log("第一个测试通过了") //记录一些你期望记录的信息

}

}

func Test_Division_2(t *testing.T) {

if _, e := Division(6, 0); e == nil { //try a unit test on function

t.Error("Division did not work as expected.") // 如果不是如预期的那么就

报错

} else {

t.Log("one test passed.", e) //记录一些你期望记录的信息

}

}

5 Go test 测试

1. 在目录下执行 go test 是测试目录所有以XXX_test.go 结尾的文件。

2.测试单个方法

go test -v -run="Test_Division_1" -count 5

3.查看帮助 go help test

package mytest

import (
	"testing"
)

func Test_Division_1(t *testing.T) {
	if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function
		t.Error("除法函数测试没通过") // 如果不是如预期的那么就报错
	} else {
		t.Log("第一个测试通过了") //记录一些你期望记录的信息
	}
}

func Test_Division_2(t *testing.T) {
	if _, e := Division(6, 0); e == nil { //try a unit test on function
		t.Error("Division did not work as expected.") // 如果不是如预期的那么就报错
	} else {
		t.Log("one test passed.", e) //记录一些你期望记录的信息
	}
}

5 Go test 命令介绍1

通过go help test可以看到go test的使用说明:

格式形如:

go test [-c] [-i] [build flags] [packages] [flags for test binary]

参数解读:

-c : 编译go test成为可执行的二进制文件,但是不运行测试。

-i : 安装测试包依赖的package,但是不运行测试。

关于build flags,调用go help build,这些是编译运行过程中需要使用到的参数,一般设置为空

关于packages,调用go help packages,这些是关于包的管理,一般设置为空

关于flags for test binary,调用go help testflag,这些是go test过程中经常使用到的参数

-test.v : 是否输出全部的单元测试用例(不管成功或者失败),默认没有加上,所以只输出失败的单元测试用例。

-test.run pattern: 只跑哪些单元测试用例

-test.bench patten: 只跑那些性能测试用例

-test.benchmem : 是否在性能测试的时候输出内存情况

-test.benchtime t : 性能测试运行的时间,默认是1s

-test.cpuprofile cpu.out : 是否输出cpu性能分析文件

-test.memprofile mem.out : 是否输出内存性能分析文件

5 Go test 命令介绍2-续

-test.blockprofile block.out : 是否输出内部goroutine阻塞的性能分析文件

-test.memprofilerate n : 内存性能分析的时候有一个分配了多少的时候才打点记录的问题。这个参数就是设置打

点的内存分配间隔,也就是profile中一个sample代表的内存大小。默认是设置为512 * 1024的。如果你将它设置

为1,则每分配一个内存块就会在profile中有个打点,那么生成的profile的sample就会非常多。如果你设置为0,

那就是不做打点了。

你可以通过设置memprofilerate=1和GOGC=off来关闭内存回收,并且对每个内存块的分配进行观察。

-test.blockprofilerate n: 基本同上,控制的是goroutine阻塞时候打点的纳秒数。默认不设置就相当于-

test.blockprofilerate=1,每一纳秒都打点记录一下

-test.parallel n : 性能测试的程序并行cpu数,默认等于GOMAXPROCS。

-test.timeout t : 如果测试用例运行时间超过t,则抛出panic

-test.cpu 1,2,4 : 程序运行在哪些CPU上面,使用二进制的1所在位代表,和nginx的nginx_worker_cpu_affinity是一个道理

-test.short : 将那些运行时间较长的测试用例运行时间缩短

1.2 Go语言接口与反射

Go语言接口与反射

1. 结构

2. 接口

3. 反射

1. 结构

1.1 struct简介

◼ Go通过结构体struct和interface实现oop(面向对象编程)

◼ struct的成员(也叫属性或字段)可以是任何类型,如普通类型、复合类型、函数、map、interface、

struct等

1.2 struct详解-struct定义

1.2 struct详解-声明与初始化

声明与初始化

var stu1 Student

var stu2 *Student= &Student{} //简写stu2 := &Student{}

var stu3 *Student = new(Student) //简写stu3 := new(Student)

1.2 struct详解-struct使用

访问其成员都使用 ".“
struct 分配内存使用 new ,返回的是指针
struct 没有构造函数,但是我们可以自己定义“构造函数”
struct 是我们自己定义的类型,不能和其他类型进行强制转换

type Student struct {

    name string

    age int

    Class string

    }

var stu1 Student
stu1.age = 34
stu1.name = "dar"
stu1.Class = "class1"
fmt.Println(stu1.name) //dar
var stu2 *Student = new(Student)
stu2.name = "ki"
stu2.age = 33
fmt.Println(stu2.name, (*stu2).name) //ki
var stu3 *Student = &Student{name: "rose", age: 18, Class: "class3"}
fmt.Println(stu3.name, (*stu3).name) //rose rose
package main

import (
	"fmt"
	"unsafe"
)

type Student struct {
	name  string
	age   int32  // 小写 私密 只能在自己的包里面用
	Class string // 大写 公开 类似C++  public
}

func main() {
	// 1 值形式
	var stu1 Student // 里面的变量全是零 栈上的
	fmt.Println("stu1:", stu1)
	stu1.age = 34
	stu1.name = "dar"
	stu1.Class = "class1"
	fmt.Println(stu1.name) //dar

	// 2 new 函数创建
	var stu2 *Student = new(Student) // new出来的是堆上
	stu2.name = "king"
	stu2.age = 33
	fmt.Println(stu2.name, (*stu2).name) //king

	// &形式创建
	var stu3 *Student = &Student{
		name:  "rose",
		age:   18,
		Class: "class3", // 如果分行的时候每行都要,
	}
	// var stu3 *Student = &Student{name: "rose", age: 18, Class: "class3"}
	fmt.Println(stu3.name, (*stu3).name) //rose  rose
	fmt.Printf("addr: %p, %p, %p\n", &stu1, stu2, stu3)

	// 值 初始化
	var stu4 Student = Student{ // KV 形式初始化值
		name:  "老师",
		age:   18,
		Class: "Go", // 注意这里的逗号不能少
	}
	fmt.Println("stu4:", stu4) // stu4: {柚子老师 18 }

	// 值顺序初始化
	var stu5 Student = Student{ // 顺序形式 形式初始化值
		"1",
		18,
		"音视频", // 注意这里的逗号不能少
	}
	fmt.Println("stu5:", stu5)

	// nil结构体
	var stu6 *Student = nil
	fmt.Println("stu6:", stu6)

	// 结构体大小
	fmt.Println("unsafe.Sizeof(stu5):", unsafe.Sizeof(stu5))
	fmt.Println("unsafe.Sizeof(stu6):", unsafe.Sizeof(stu6))
	// fmt.Println("unsafe.Sizeof(string):", unsafe.Sizeof(string))
	// fmt.Println("unsafe.Sizeof(int):", unsafe.Sizeof(int))
}

1.2 struct详解-自定义构造函数

◼ 通过工厂模式自定义构造函数方法

func Newstu(name1 string,age1 int,class1 string) *Student {

    return &Student{name:name1,age:age1,Class:class1}

    }

    func main() {

    stu1 := Newstu(“dar",34,"math")

    fmt.Println(stu1.name) // dar

    }

package main

import "fmt"

type Student struct {
	name  string
	age   int
	Class string
}

func Newstu(name1 string, age1 int, class1 string) *Student {
	return &Student{name: name1, age: age1, Class: class1}
}
func main() {
	stu1 := Newstu("dar", 34, "math")
	fmt.Println(stu1.name) // dar
}

1.3 struct tag

◼ tag可以为结构体的成员添加说明或者标签便于使用,这些说明可以通过反射获取到。

◼ 结构体中的成员首字母小写对外不可见,但是我们把成员定义为首字母大写这样与外界进行数据

交互会带来极大的不便,此时tag带来了解决方法

type Student struct {

    Name string "the name of student"

    Age int "the age of student"

    Class string "the class of student"

   }

1.3 struct tag –应用场景json示例

应用场景示例,json序列化操作(序列化和反序列化演示)

type Student struct {
	Name string `json:"name"`
	Age int `json:"age"`
	}
	func main() {
	var stu = Student{Name: "dar", Age: 34}
	data, err := json.Marshal(stu)
	if err != nil {
	fmt.Println("json encode failed err:", err)
	return
	}
	fmt.Println(string(data)) //{"name":"dar","age":34}
	}
package main

import (
	"encoding/json"
	"fmt"
)

// stu:  序列化后:{"name":"dar","age":34}
// 				 {"name1":"dar","age2":34}
type Student struct {
	Name string `json:"name1"`
	Age  int    `json:"age2"`
}

func main() {
	var stu = Student{Name: "dar", Age: 34}
	data, err := json.Marshal(stu) //   {"name1":"dar","age2":34}
	if err != nil {
		fmt.Println("json encode failed err:", err)
		return
	}
	fmt.Println("stu: ", string(data)) //{"name":"dar","age":34}

	var stu2 Student
	err = json.Unmarshal(data, &stu2) // 反序列化 
	fmt.Println("stu2: ", stu2)       // {dar 34}
}

1.4 struct匿名成员(字段、属性)

◼ 结构体中,每个成员不一定都有名称,也允许字段没有名字,即匿名成员。

◼ 匿名成员的一个重要作用,可以用来实现oop中的继承。

◼ 同一种类型匿名成员只允许最多存在一个。

◼ 当匿名成员是结构体时,且两个结构体中都存在相同字段时,优先选择最近的字段。

type Person struct {

    Name string

    Age int

    }

    type Student struct {

    score string

    Age int

    Person // 匿名内嵌结构体

    }

func main() {
	var stu = new(Student)
	stu.Age = 34                         //优先选择Student中的Age
	fmt.Println(stu.Person.Age, stu.Age) // 0,34
}
// 1.4 struct匿名成员(字段、属性)

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}
type Student struct {
	score  string
	Age    int
	Person // 匿名内嵌结构体
}

func main() {
	var stu = new(Student)
	stu.Age = 22                         //优先选择Student中的Age
	fmt.Println(stu.Person.Age, stu.Age) // 0,22

	var stu2 = Student{
		score: "100",
		Age:   20,
		Person: Person{
			Name: "柚子老师",
			Age:  18,
		},
	}
	fmt.Println("stu2: ", stu2)
}

1.5 struct-继承、多继承

◼ 当结构体中的成员也是结构体时,该结构体就继承了这个结构体,继承了其所有的方法与属性,当然有多个

结构体成员也就是多继承。

◼ 访问父结构中属性也使用“.”,但是当子结构体中存在和父结构中的字段相同时候,只能使用:"子结构体.父

结构体.字段"访问父结构体中的属性,如上面示例的stu.Person.Age

◼ 继承结构体可以使用别名,访问的时候通过别名访问,如下面示例man1.job.Salary:

type Person struct {

    Name string

    Age int

    }

    type Teacher struct {

    Salary int

    Classes string

   }

type man struct {
	sex string
	job Teacher //别名,继承Teacher 这个时候就不是匿名了
	Person //继承Person
	}
	func main() {
	var man1 = new(man)
	man1.Age = 34
	man1.Name = "dar"
	man1.job.Salary = 100000
	fmt.Println(man1, man1.job.Salary) //&{ {8500 } {dar 34}} 8500
	}
// 1.5 struct-继承、多继承
package main

import "fmt"

type Person struct {
	Name string
	Age  int
}
type Teacher struct {
	Salary int
	Class  string
}

type Man struct {
	sex    string
	job    Teacher //别名,继承Teacher
	Person         //继承Person
}

func main() {
	var man1 = new(Man)
	man1.Age = 34
	man1.Name = "dar"
	man1.job.Salary = 100000
	fmt.Println("man1:", man1, man1.job.Salary) //&{ {8500 } {dar 34}} 8500

	var man2 = Man{
		sex: "女",
		job: Teacher{
			Salary: 8000,
			Class:  "班班",
		},
		Person: Person{ // 匿名初始化方式
			Name: "老师",
			Age:  18,
		},
	}
	fmt.Println("man2", man2)
}

1.6 struct-结构体中的方法

方法是什么

◼ Go方法是作用在接受者(个人理解成作用对象)上的一个函数,接受者是某种类型的变量,因此方法是一种特殊

类型的函数。

◼ 接收者可以是任何类型,不仅仅是结构体,Go中的基本类型(int,string,bool等)也是可以,或者说数组的别名

类型,甚至可以是函数类型。但是,接受者不能是一个接口类型,因为接口是一个抽象的定义,方法是一个具体

实现。

◼ 一个类型加上它的方法等价于面向对象中的一个类。一个重要的区别是:在 Go 中,类型的代码和绑定在它上面的

方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。

◼ 类型 T(或 *T)上的所有方法的集合叫做类型 T(或 *T)的方法集。

◼ 因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。但是如果基于接

收者类型,是有重载的:具有同样名字的方法可以在 2 个或多个不同的接收者类型上存在,比如在同一个包里这么

做是允许的

◼ 别名类型不能有它原始类型上已经定义过的方法(因为别名类型和原始类型底层是一样的)。

定义方法的格式

func(recv recevier_type(结构体))methodName(parameter_list)(return_value_list){}

1.6 struct-结构体中的方法-示例

type Person struct {

    Name string

    Age int

    }

    func (p Person) Getname() string { //p代表结构体本身的实列,类似python中的self,这里p可以写为self

    fmt.Println(p.Name)

    return p.Name

    }

    func main() {

    var person1 = new(Person)

    person1.Age = 34

    person1.Name = "dar"

    person1.Getname() // dar

    }

结构体的指针方法

package main

import (
	"fmt"
	"math"
)

type Circle struct {
	x      int
	y      int
	Radius int
}

// 面积
func (c Circle) Area() float64 {
	return math.Pi * float64(c.Radius) * float64(c.Radius)
}

// 周长
func (c Circle) Circumference() float64 {
	return 2 * math.Pi * float64(c.Radius)
}

func (c Circle) expand() {
	c.Radius *= 2
}

func (c *Circle) expand2() {
	c.Radius *= 2
}
func main() {
	var c = Circle{Radius: 50}
	fmt.Println(c.Area(), c.Circumference())
	// 指针变量调用方法形式上是一样的
	var pc = &c
	pc.expand2()
	fmt.Println(pc.Area(), pc.Circumference())

}
type Person struct {
Name string
Age int
}
func (p Person) Getname() string { //p代表结构体本身的实列,类似python中的self,这里p可以写为self
fmt.Println(p.Name)
return p.Name
}
func main() {
var person1 = new(Person)
person1.Age = 34
person1.Name = "dar"
person1.Getname() // dar
}

1.6 struct-结构体中的方法-方法和函数的区别

◼ 方法只能被其接受者调用

◼ 接收者是指针时,方法可以改变接受者的值(或状态),函数也能做到

◼ 接受者和方法必须在同一个包内

1.6 struct-结构体中的方法-方法的接受者是值或者指针的区别

◼ 当接受者是一个值的时候,这个值是该类型实例的拷贝

◼ 如果想要方法改变接受者的数据,就在接受者的指针类型上定义该方法。否则,就在普

通的值类型上定义方法。

总结:指针方法和值方法都可以在指针或者非指针上被调用。也就是说,方法接收者是

指针类型时,指针类型的值也是调用这个方法,反之亦然。

1.7 struct-内存分布

go中的结构体内存布局和c结构体布局类似,每个成员的内存分布是连续的

type Student struct {
Name string
Age int64
wight int64
high int64
score int64
}

package main

import (
	"fmt"
	"reflect"
)

// type Student struct {
// 	Name  string // 16
// 	Age   int64  // 8
// 	wight int64  // 8
// 	high  int64  // 8
// 	score int64  // 8
// }
type Student struct {
	Name  string // 16  有两个变量: 指针, length
	Age   int8   //
	wight int64  // 8
	high  int8   // 8
	score int64  // 8
}

func main() {
	var stu1 = new(Student)
	stu1.Name = "只为你"
	fmt.Printf("地址分布:")
	fmt.Printf("%p\n", &stu1.Name)
	fmt.Printf("%p\n", &stu1.Age)
	fmt.Printf("%p\n", &stu1.wight)
	fmt.Printf("%p\n", &stu1.high)
	fmt.Printf("%p\n", &stu1.score)
	typ := reflect.TypeOf(Student{})
	fmt.Printf("Struct is %d bytes long\n", typ.Size())
	// We can run through the fields in the structure in order
	n := typ.NumField()
	for i := 0; i < n; i++ {
		field := typ.Field(i) // 反射出filed
		fmt.Printf("%s at offset %v, size=%d, align=%d\n",
			field.Name, field.Offset, field.Type.Size(),
			field.Type.Align())
	}
}

2. 接口

2.1 interface简介

interface(接口)是golang最重要的特性之一,Interface类型可以定义一组方法,但是这些不需要实

现。并且interface不能包含任何变量。

◼ interface 是方法的集合

◼ interface是一种类型,并且是指针类型

◼ interface的 更重要的作用在于多态实现

◼ interface 不能包含任何变量

2.2 interface定义

type 接口名称 interface {

    method1 (参数列表) 返回值列表

    method2 (参数列表) 返回值列表

    ...

    }

2.3 interface使用

◼ 接口的使用不仅仅针对结构体,自定义类型、变量等等都可以实现接口。

◼ 如果一个接口没有任何方法,我们称为空接口,由于空接口没有方法,任意结构体都

隐式地实现了空接口。

◼ 要实现一个接口,必须实现该接口里面的所有方法。

// 2.3 interface使用

package main

import "fmt"

//定义接口
type Skills interface {
	Running()
	Getname() string
}

type Student struct {
	Name string
	Age  int
}

type Teacher struct {
	Name string
	Age  int
}

// 实现接口
func (p Student) Getname() string { //实现Getname方法
	fmt.Println(p.Name)
	return p.Name
}

func (p Student) Running() { // 实现 Running方法
	fmt.Printf("%s running\n", p.Name)
}

func (p Teacher) Getname() string { //实现Getname方法
	fmt.Println(p.Name)
	return p.Name
}

func (p Teacher) Running() { // 实现 Running方法
	fmt.Printf("%s running\n", p.Name)
}

func (p Teacher) Running2() { // 实现 Running方法
	fmt.Printf("%s running\n", p.Name)
}

// 想用接口,那就要实现对应接口的所有方法
func main() {
	var skill Skills // 一个接口变量
	var stu1 Student // 结构体变量
	stu1.Name = "dar"
	stu1.Age = 34
	skill = stu1
	skill.Running() //调用接口

	var teacher Teacher = Teacher{"老师", 18}
	skill = teacher
	skill.Running() //调用接口
	teacher.Running()
}
// 空接口
package main

import "fmt"

// Go 语言为了避免用户重复定义很多空接口,它自己内置了一个,这个空接口的名字特别奇怪,叫 interface{}
/*
空接口里面没有方法,所以它也不具有任何能力,其作用相当于 Java 的 Object 类型,可以容纳任意对象,
它是一个万能容器。比如一个字典的 key 是字符串,但是希望 value 可以容纳任意类型的对象,
类似于 Java 语言的 Map 类型,这时候就可以使用空接口类型 interface{}。*/
// 空接口 map 里面用
func main() {
	// map  k-v  一个map里面是有key都是同一类型,value也是同一类型
	// map[string]int   k-string  v-int
	// 连续两个大括号,是不是看起来很别扭
	// 代码中 user 字典变量的类型是 map[string]interface{},
	// 从这个字典中直接读取得到的 value 类型是 interface{},需要通过类型转换才能得到期望的变量。
	var user = map[string]interface{}{
		"age":     30,
		"address": "Beijing",
		"married": true,
	}
	fmt.Println(user)
	// 类型转换语法来了
	var age = user["age"].(int)
	var address = user["address"].(string)
	var married = user["married"].(bool)
	fmt.Println(age, address, married)

	user["price"] = 5.5
	var price = user["price"].(float64) // ?报错?
	fmt.Println("user: ", user, price)

	fmt.Println("user2: ")
	var user2 = map[interface{}]interface{}{
		111:        30,
		"address2": "Beijing",
		1.2:        true,
	}
	fmt.Println("user2: ", user2)
}

2.4 interface多态

◼ go语言中interface是实现多态的一种形式,所谓多态,就是一种事物的多种形态

◼ 同一个interface,不同的类型实现,都可以进行调用,它们都按照统一接口进行操作

// 2.4 interface多态

package main

import "fmt"

type Skills interface {
	Running()
	Getname() string
}

type Student struct {
	Name string
	Age  int
}

type Teacher struct {
	Name   string
	Salary int
}

func (p Student) Getname() string { //实现Getname方法
	fmt.Println(p.Name)
	return p.Name
}

func (p Student) Running() { // 实现 Running方法
	fmt.Printf("%s running", p.Name)
}

func (p Teacher) Getname() string { //实现Getname方法
	fmt.Println(p.Name)
	return p.Name
}

func (p Teacher) Running() { // 实现 Running方法
	fmt.Printf("\n%s running", p.Name)
}
func main() {
	var skill Skills
	var stu1 Student
	var t1 Teacher
	t1.Name = "ki"
	stu1.Name = "dar"
	stu1.Age = 22
	skill = stu1
	skill.Running()
	skill = t1
	t1.Running()
}

2.5 interface接口嵌套

◼ go语言中的接口可以嵌套,可以理解为继承,子接口拥有父接口的所有方法

◼ 如果使用该子接口,必须将父接口和子接口的所有方法都实现

type Skills interface {
	Running()
	Getname() string
	}
	type Test interface {
	sleeping()
	Skills //继承Skills
	}
// 2.5 interface接口嵌套

package main

import "fmt"

type Skills interface {
	Running()
	// Running(is int)		// 函数名是唯一的
	Getname() string
}

type Test interface {
	Sleeping()
	Skills //继承Skills
}
type Student struct {
	Name string
	Age  int
}

type Teacher struct {
	skill  Skills // skill也只能当变量去用
	Name   string
	Salary int
}

func (p Student) Getname() string { //实现Getname方法
	fmt.Println(p.Name)
	return p.Name
}

func (p Student) Running() { // 实现 Running方法
	fmt.Printf("%s running", p.Name)
}

func (p Teacher) Getname() string { //实现Getname方法
	fmt.Println(p.Name)
	return p.Name
}

func (p Teacher) Running() { // 实现 Running方法
	fmt.Printf("\n%s running", p.Name)
}

func (p Teacher) Sleeping() { // 实现 Running方法
	fmt.Printf("\n%s Sleeping", p.Name)
}
func main() {
	var skill Skills
	var stu1 Student
	var t1 Teacher
	t1.Name = "ki"
	stu1.Name = "dar"
	stu1.Age = 22
	skill = stu1
	skill.Running()
	skill = t1
	t1.Running()

	var test Test
	test = t1
	test.Sleeping()

	// test = stu1
}

2.6 interface接口组合

◼ 接口的定义也支持组合继承

// 2.6 接口的组合继承
package main

import "fmt"

// 可以闻
type Smellable interface {
	smell()
}

// 可以吃
type Eatable interface {
	eat()
}

type Fruitable interface {
	Smellable
	Eatable
}

// 苹果既可能闻又能吃
type Apple struct{}

func (a Apple) smell() {
	fmt.Println("apple can smell")
}

func (a Apple) eat() {
	fmt.Println("apple can eat")
}

// 花只可以闻
type Flower struct{}

func (f Flower) smell() {
	fmt.Println("flower can smell")
}

// func TestType(items ...interface{}) {
// 	for k, v := range items {
// 		switch v.(type) {
// 		case string:
// 			fmt.Printf("type is string, %d[%v]\n", k, v)
// 		case bool:
// 			fmt.Printf("type is bool, %d[%v]\n", k, v)
// 		case int:
// 			fmt.Printf("type is int, %d[%v]\n", k, v)
// 		case float32, float64:
// 			fmt.Printf("type is float, %d[%v]\n", k, v)
// 		case Smellable:
// 			fmt.Printf("type is Smellable, %d[%v]\n", k, v)
// 		case *Smellable:
// 			fmt.Printf("type is *Smellable, %d[%p]\n", k, v)
// 		case Eatable:
// 			fmt.Printf("type is Eatable, %d[%v]\n", k, v)
// 		case *Eatable:
// 			fmt.Printf("type is Eatable, %d[%p]\n", k, v)
// 		case Fruitable:
// 			fmt.Printf("type is Fruitable, %d[%v]\n", k, v)
// 		case *Fruitable:
// 			fmt.Printf("type is Fruitable, %d[%p]\n", k, v)
// 		}
// 	}
// }
func main() {
	var s1 Smellable
	var s2 Eatable
	var apple = Apple{}
	var flower = Flower{}
	s1 = apple
	s1.smell()
	s1 = flower
	s1.smell()
	s2 = apple
	s2.eat()

	fmt.Println("\n组合继承")
	var s3 Fruitable
	s3 = apple
	s3.smell()
	s3.eat()

	// TestType(s1, s2, s3, apple, flower)
}

2.7 interface类型转换

由于接口是一般类型,当我们使用接口时候可能不知道它是那个类型实现的,

基本数据类型我们有对应的方法进行类型转换,当然接口类型也有类型转换。

var s int

var x interface

x = s

y , ok := x.(int) //将interface 转为int,ok可省略 但是省略以后转换失败会报错,

true转换成功,false转换失败, 并采用默认值

// 2.7 interface类型转换

package main

import "fmt"

func main() {
	var x interface{}

	s := "dar"
	x = s // 为什么能赋值到空接口, 每种类型都已经隐藏实现了空接口
	y, ok := x.(int)
	z, ok1 := x.(string)
	fmt.Println(y, ok)
	fmt.Println(z, ok1)
}

//0 false
//dar true

2.8 interface类型判断

func TestType(items ...interface{}) {

for k, v := range items {

switch v.(type) {

case string:

fmt.Printf("type is string, %d[%v]\n", k, v)

case bool:

fmt.Printf("type is bool, %d[%v]\n", k, v)

case int:

fmt.Printf("type is int, %d[%v]\n", k, v)

case float32, float64:

fmt.Printf("type is float, %d[%v]\n", k, v)

case Student:

fmt.Printf("type is Student, %d[%v]\n", k, v)

case *Student:

fmt.Printf("type is Student, %d[%p]\n", k, v)

}

}

}

package main

import "fmt"

type Student struct {
	Name string
}

func TestType(items ...interface{}) {
	for k, v := range items {
		switch v.(type) {
		case string:
			fmt.Printf("type is string, %d[%v]\n", k, v)
		case bool:
			fmt.Printf("type is bool, %d[%v]\n", k, v)
		case int:
			fmt.Printf("type is int, %d[%v]\n", k, v)
		case float32, float64:
			fmt.Printf("type is float, %d[%v]\n", k, v)
		case Student:
			fmt.Printf("type is Student, %d[%v]\n", k, v)
		case *Student:
			fmt.Printf("type is Student, %d[%p]\n", k, v)
		}
	}
}

func main() {
	var stu Student
	TestType("dar", 100, stu, 3.3)
}

//type is string, 0[dar]
//type is int, 1[100]
//type is Student, 2[{}]
//type is float, 3[3.3]

2.9 指向指针的接口变量

3 reflect反射是什么,为什么需要反射

反射定义:在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问、检测和修

改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修

改自己的行为。

GO 反射的意义:Go 语言的 ORM 库离不开它,Go 语言的 json 序列化库离不开它, fmt包字

符串格式化离不开它,Go 语言的运行时更是离不开它。

反射的目标:

1. 获取变量的类型信息,例如这个类型的名称、占用字节数、所有的方法列表、所有的内部字段结

构、它的底层存储类型等等。

2. 动态的修改变量内部字段值。比如 json 的反序列化,你有的是对象内部字段的名称和相应的值,

你需要把这些字段的值循环填充到对象相应的字段里

package main

import "fmt"

type Rect struct {
	Width  int
	Height int
}

func main() {
	var a interface{}
	var r = Rect{50, 50}
	a = &r // 指向了结构体指针

	var rx = a.(*Rect) // 转换成指针类型
	r.Width = 100
	r.Height = 100
	fmt.Println("r:", r)
	fmt.Println("rx:", rx)
	fmt.Printf("rx:%p, r:%p\n", rx, &r)
}

3 reflect反射

◼ go语言中的反射通过refect包实现,reflect包实现了运行时反射,允许程序操作任意类型的对象

◼ reflect包中的两个关键数据类Type和Value

func TypeOf(v interface{}) Type // 返回类型 实际是接口

func ValueOf(v interface{}) Value // 返回值 结构体

/*
但是有时你希望在运行时使用变量的在编写程序时还不存在的信息。
比如你正在尝试将文件或网络请求中的数据映射到变量中。或者你想构建一个适用于不同类型的工具。
在这种情况下,你需要使用反射。反射使您能够在运行时检查类型。
它还允许您在运行时检查,修改和创建变量,函数和结构体。
*/
package main

import (
	"fmt"
	"reflect"
	"strings"
)

type Foo struct {
	A int `tag1:"First Tag" tag2:"Second Tag"`
	B string
}

func main() {
	sl := []int{1, 2, 3}
	greeting := "hello"
	greetingPtr := &greeting
	f := Foo{A: 10, B: "Salutations"}
	fp := &f

	slType := reflect.TypeOf(sl)
	gType := reflect.TypeOf(greeting)
	grpType := reflect.TypeOf(greetingPtr)
	fType := reflect.TypeOf(f)
	fpType := reflect.TypeOf(fp)

	examiner(slType, 0)
	examiner(gType, 0)
	examiner(grpType, 0)
	examiner(fType, 0)
	examiner(fpType, 0)
}

func examiner(t reflect.Type, depth int) {
	fmt.Println(strings.Repeat("\t", depth), "Type is", t.Name(), "and kind is", t.Kind())
	switch t.Kind() {
	case reflect.Array, reflect.Chan, reflect.Map, reflect.Ptr, reflect.Slice:
		fmt.Println(strings.Repeat("\t", depth+1), "Contained type:")
		examiner(t.Elem(), depth+1)
	case reflect.Struct:
		for i := 0; i < t.NumField(); i++ {
			f := t.Field(i)
			fmt.Println(strings.Repeat("\t", depth+1), "Field", i+1, "name is", f.Name, "type is", f.Type.Name(), "and kind is", f.Type.Kind())
			if f.Tag != "" {
				fmt.Println(strings.Repeat("\t", depth+2), "Tag is", f.Tag)
				fmt.Println(strings.Repeat("\t", depth+2), "tag1 is", f.Tag.Get("tag1"), "tag2 is", f.Tag.Get("tag2"))
			}
		}
	}
}

PS D:\Workspace\Go\src\projects\demo> go run main.go
 Type is  and kind is slice
         Contained type:
         Type is int and kind is int
 Type is string and kind is string
 Type is  and kind is ptr
         Contained type:
         Type is string and kind is string
 Type is Foo and kind is struct
         Field 1 name is A type is int and kind is int
                 Tag is tag1:"First Tag" tag2:"Second Tag"
                 tag1 is First Tag tag2 is Second Tag
         Field 2 name is B type is string and kind is string
 Type is  and kind is ptr
         Contained type:
         Type is Foo and kind is struct
                 Field 1 name is A type is int and kind is int
                         Tag is tag1:"First Tag" tag2:"Second Tag"
                         tag1 is First Tag tag2 is Second Tag
                 Field 2 name is B type is string and kind is string

// 使用反射创建新实例
/*
除了检查变量的类型外,还可以使用反射来读取,设置或创建值。
首先,需要使用refVal := reflect.ValueOf(var)为变量创建一个reflect.Value实例。
如果希望能够使用反射来修改值,则必须使用refPtrVal := reflect.ValueOf(&var);
获得指向变量的指针。如果不这样做,则可以使用反射来读取该值,但不能对其进行修改。

一旦有了reflect.Value实例就可以使用Type()方法获取变量的reflect.Type。

如果要修改值,请记住它必须是一个指针,并且必须首先对其进行解引用。
使用refPtrVal.Elem().Set(newRefVal)来修改值,并且传递给Set()的值也必须是reflect.Value。
*/

package main

import (
	"fmt"
	"reflect"
)

type Foo struct {
	A int 
	B string
}

// 使用反射创建新实例
func main() {
	greeting := "hello"
	f := Foo{A: 10, B: "Salutations"}

	gVal := reflect.ValueOf(greeting)
	// not a pointer so all we can do is read it
	fmt.Println(gVal.Interface()) // hello

	gpVal := reflect.ValueOf(&greeting)
	// it’s a pointer, so we can change it, and it changes the underlying variable
	gpVal.Elem().SetString("goodbye")
	fmt.Println(greeting) // 修改成了goodbye

	fType := reflect.TypeOf(f)
	fVal := reflect.New(fType)
	fVal.Elem().Field(0).SetInt(20)
	fVal.Elem().Field(1).SetString("Greetings")
	f2 := fVal.Elem().Interface().(Foo) // 调用Interface()方法从reflect.Value回到普通变量值
	fmt.Printf("f2: %+v, %d, %s\n", f2, f2.A, f2.B)
	fmt.Println("f2:", f2)
	fmt.Println("f:", f)
}

PS D:\Workspace\Go\src\projects\demo> go run main.go
hello
goodbye
f2: {A:20 B:Greetings}, 20, Greetings
f2: {20 Greetings}
f: {10 Salutations}

// 反射创建引用类型的实例
/*
使用反射来生成通常需要make函数的实例。可以使用reflect.MakeSlice,
reflect.MakeMap和reflect.MakeChan函数制作切片,Map或通道。
在所有情况下,都提供一个reflect.Type,然后获取一个reflect.Value,
可以使用反射对其进行操作,或者可以将其分配回一个标准变量。
*/
package main

import (
	"fmt"
	"reflect"
)

func main() {
	// 定义变量
	intSlice := make([]int, 0)
	mapStringInt := make(map[string]int)

	// 获取变量的 reflect.Type
	sliceType := reflect.TypeOf(intSlice)
	mapType := reflect.TypeOf(mapStringInt)

	// 使用反射创建类型的新实例
	intSliceReflect := reflect.MakeSlice(sliceType, 0, 0)
	mapReflect := reflect.MakeMap(mapType)

	// 将创建的新实例分配回一个标准变量
	v := 10
	rv := reflect.ValueOf(v)
	intSliceReflect = reflect.Append(intSliceReflect, rv)
	intSlice2 := intSliceReflect.Interface().([]int)
	fmt.Println("intSlice2: ", intSlice2)
	fmt.Println("intSlice : ", intSlice)

	k := "hello"
	rk := reflect.ValueOf(k)
	mapReflect.SetMapIndex(rk, rv)
	mapStringInt2 := mapReflect.Interface().(map[string]int)
	fmt.Println("mapStringInt2: ", mapStringInt2)
	fmt.Println("mapStringInt : ", mapStringInt)
}

PS D:\Workspace\Go\src\projects\demo> go run main.go
intSlice2:  [10]
intSlice :  []
mapStringInt2:  map[hello:10]
mapStringInt :  map[

/*
使用反射创建函数
反射不仅仅可以为存储数据创造新的地方。还可以使用reflect.MakeFunc函数使用reflect来创建新函数。
该函数期望我们要创建的函数的reflect.Type,以及一个闭包,其输入参数为[]reflect.Value类型,
其返回类型也为[] reflect.Value类型。下面是一个简单的示例,它为传递给它的任何函数创建一个定时包装器
*/
package main

import (
	"fmt"
	"reflect"
	"runtime"
	"time"
)

func MakeTimedFunction(f interface{}) interface{} {
	rf := reflect.TypeOf(f)
	fmt.Println("rf: ", rf)
	if rf.Kind() != reflect.Func {
		panic("expects a function")
	}
	vf := reflect.ValueOf(f)
	fmt.Println("vf: ", vf)
	wrapperF := reflect.MakeFunc(rf, func(in []reflect.Value) []reflect.Value {
		start := time.Now()
		out := vf.Call(in)
		end := time.Now()
		fmt.Printf("calling %s took %v\n", runtime.FuncForPC(vf.Pointer()).Name(), end.Sub(start))
		return out
	})
	return wrapperF.Interface()
}

func timeMe() {
	fmt.Println("starting")
	time.Sleep(1 * time.Second)
	fmt.Println("ending")
}

func timeMeToo(a int) int {
	fmt.Println("starting")
	time.Sleep(time.Duration(a) * time.Second)
	result := a * 2
	fmt.Println("ending")
	return result
}

func main() {

	fmt.Println("MakeTimedFunction1: ")
	timed := MakeTimedFunction(timeMe).(func())
	fmt.Println("转成普通函数1: ")
	timed()

	fmt.Println("\n\nMakeTimedFunction2: ")
	timedToo := MakeTimedFunction(timeMeToo).(func(int) int)
	fmt.Println("转成普通函数2: ")
	fmt.Println(timedToo(5))
}

PS D:\Workspace\Go\src\projects\demo> go run main.go
MakeTimedFunction1: 
rf:  func()
vf:  0x733a80
转成普通函数1:
starting
ending
calling main.timeMe took 1.0150935s


MakeTimedFunction2:
rf:  func(int) int
vf:  0x733b40
转成普通函数2:
starting
ending
calling main.timeMeToo took 5.0133999s
10

3 reflect反射- Type和Value

TypeOf() 方法返回变量的类型信息得到的是一个类型为 reflect.Type 的变量,

ValueOf() 方法返回变量的值信息得到的是一个类型为 reflect.Value 的变量。

package main

import (
	"fmt"
	"reflect"
)

type Skills interface {
	reading()
	running()
}

type Student struct {
	Age  int
	Name string
}

func (self Student) runing() {
	fmt.Printf("%s is running\n", self.Name)
}
func (self Student) reading() {
	fmt.Printf("%s is reading\n", self.Name)
}
func main() {
	stu1 := Student{Name: "dar", Age: 34}
	inf := new(Skills)
	stu_type := reflect.TypeOf(stu1)
	inf_type := reflect.TypeOf(inf).Elem() //  获取指针所指的对象类型
	fmt.Println("类型stu_type:", stu_type)
	fmt.Println(stu_type.String())  //main.Student
	fmt.Println(stu_type.Name())    //Student
	fmt.Println(stu_type.PkgPath()) //main
	fmt.Println(stu_type.Kind())    //struct
	fmt.Println(stu_type.Size())    //24

	fmt.Println("\n类型inf_type:", inf_type)
	fmt.Println(inf_type.NumMethod())                        //2
	fmt.Println(inf_type.Method(0), inf_type.Method(0).Name) // {reading main func() <invalid Value> 0} reading
	fmt.Println(inf_type.MethodByName("reading"))            //{reading main func() <invalid Value> 0} true

}

PS D:\Workspace\Go\src\projects\demo> go run main.go
类型stu_type: main.Student
main.Student
Student
main
struct
24

类型inf_type: main.Skills
2
{reading main func() <invalid Value> 0} reading
{reading main func() <invalid Value> 0} true

package main

import (
	"fmt"
	"reflect"
)

type Skills interface {
	reading()
	running()
}

type Student struct {
	Name string "json:name"
	Age  int    "json:age"
}

func (self Student) runing() {
	fmt.Printf("%s is running\n", self.Name)
}
func (self Student) reading() {
	fmt.Printf("%s is reading\n", self.Name)
}
func main() {
	stu1 := Student{Name: "dar", Age: 34}
	stu_type := reflect.TypeOf(stu1)
	fmt.Println(stu_type.NumField())          //2
	fmt.Println(stu_type.Field(0))            //{Name  string  0 [0] false}
	fmt.Println(stu_type.FieldByName("Name")) //{{Age  int  16 [1] false} true
	fmt.Println(stu_type.Field(1))            //{Name  string  0 [0] false}
	fmt.Println(stu_type.FieldByName("Age"))  //{{Age  int  16 [1] false} true
}
package main

import (
	"fmt"
	"reflect"
)

func main() {
	str := "dar"
	val := reflect.ValueOf(str).Kind()
	fmt.Println(val) //string
}

3 reflect反射-利弊

反射的好处

1. 为了降低多写代码造成的bug率,做更好的归约和抽象

2. 为了灵活、好用、方便,做动态解析、调用和处理

3. 为了代码好看、易读、提高开发效率,补足与动态语言之间的一些差别

反射的弊端

1. 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。

2. Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。

3. 反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。

3.1 reflect反射-Type

Type:Type类型用来表示一个go类型。

不是所有go类型的Type值都能使用所有方法。请参见每个方法的文档获取使用限制。在调用有分

类限定的方法时,应先使用Kind方法获知类型的分类。调用该分类不支持的方法会导致运行时的

panic。

获取Type对象的方法:

func TypeOf(i interface{}) Type

str := "dar"
res_type := reflect.TypeOf(str)
fmt.Println(res_type) //string
int1 := 1
res_type2 := reflect.TypeOf(int1)
fmt.Println(res_type2) //int

3.1 reflect反射-Type续-reflect.Type通用方法

func (t *rtype) String() string // 获取 t 类型的字符串描述,不要通过 String 来判断两种类型是否一致。
func (t *rtype) Name() string // 获取 t 类型在其包中定义的名称,未命名类型则返回空字符串。
func (t *rtype) PkgPath() string // 获取 t 类型所在包的名称,未命名类型则返回空字符串。
func (t *rtype) Kind() reflect.Kind // 获取 t 类型的类别。
func (t *rtype) Size() uintptr // 获取 t 类型的值在分配内存时的大小,功能和 unsafe.SizeOf 一样。
func (t *rtype) Align() int // 获取 t 类型的值在分配内存时的字节对齐值。
func (t *rtype) FieldAlign() int // 获取 t 类型的值作为结构体字段时的字节对齐值。
func (t *rtype) NumMethod() int // 获取 t 类型的方法数量。
func (t *rtype) Method() reflect.Method // 根据索引获取 t 类型的方法,如果方法不存在,则 panic。
// 如果 t 是一个实际的类型,则返回值的 Type 和 Func 字段会列出接收者。
// 如果 t 只是一个接口,则返回值的 Type 不列出接收者,Func 为空值。
func (t *rtype) MethodByName(string) (reflect.Method, bool) // 根据名称获取 t 类型的方法。
func (t *rtype) Implements(u reflect.Type) bool // 判断 t 类型是否实现了 u 接口。
func (t *rtype) ConvertibleTo(u reflect.Type) bool // 判断 t 类型的值可否转换为 u 类型。
func (t *rtype) AssignableTo(u reflect.Type) bool // 判断 t 类型的值可否赋值给 u 类型。
func (t *rtype) Comparable() bool // 判断 t 类型的值可否进行比较操作
//注意对于:数组、切片、映射、通道、指针、接口
func (t *rtype) Elem() reflect.Type // 获取元素类型、获取指针所指对象类型,获取接口的动态类型

3.1 reflect反射-Type续-reflect.Type其他方法

// 数值

func (t *rtype) Bits() int // 获取数值类型的位宽,t 必须是整型、浮点型、复数型

// 数组

func (t *rtype) Len() int // 获取数组的元素个数

// 映射

func (t *rtype) Key() reflect.Type // 获取映射的键类型

// 通道

func (t *rtype) ChanDir() reflect.ChanDir // 获取通道的方向

// 结构体

func (t *rtype) NumField() int // 获取字段数量

func (t *rtype) Field(int) reflect.StructField // 根据索引获取字段

func (t *rtype) FieldByName(string) (reflect.StructField, bool) // 根据名称获取字段

func (t *rtype) FieldByNameFunc(match func(string) bool) (reflect.StructField, bool) // 根据指定的匹配函数 math 获取字段

func (t *rtype) FieldByIndex(index []int) reflect.StructField // 根据索引链获取嵌套字段

// 函数

func (t *rtype) NumIn() int // 获取函数的参数数量

func (t *rtype) In(int) reflect.Type // 根据索引获取函数的参数信息

func (t *rtype) NumOut() int // 获取函数的返回值数量

func (t *rtype) Out(int) reflect.Type // 根据索引获取函数的返回值信息

func (t *rtype) IsVariadic() bool // 判断函数是否具有可变参数

3.1 reflect反射-Type结构

,rtype 实现了 Type 接口的所有方法。剩下的不同的部分信

息各种特殊类型结构体都不一样。可以将 rtype 理解成父类,

特殊类型的结构体是子类,会有一些不一样的字段信息。

// 基础类型 rtype 实现了 Type 接口
type rtype struct {
size uintptr // 占用字节数
ptrdata uintptr
hash uint32 // 类型的hash值
...
kind uint8 // 元类型
...
}
// 切片类型
type sliceType struct {
rtype
elem *rtype // 元素类型
}
// 结构体类型
type structType struct {
rtype
pkgPath name // 所在包名
fields []structField // 字段列表
}
// 获取和设置普通类型的值
package main

import (
	"fmt"
	"reflect"
)

func main() {
	str := "dar"
	age := 11
	fmt.Println(reflect.ValueOf(str).String()) //获取str的值,结果dar
	fmt.Println(reflect.ValueOf(age).Int())    //获取age的值,结果age
	str2 := reflect.ValueOf(&str)              //获取Value类型
	str2.Elem().SetString("ki")              //设置值
	fmt.Println(str2.Elem(), age)              //ki 11

	age2 := reflect.ValueOf(&age) //获取Value类型
	fmt.Println("age2:", age2)

	age2.Elem().SetInt(40) //设置值
	fmt.Println("age:", age)
	fmt.Println("reflect.ValueOf(age):", reflect.ValueOf(age))
}

3.1 reflect反射- reflect.Value方法

reflect.Value.Kind():获取变量类别,返回常量
const (
Invalid Kind = iota //不存在的类型
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr // 指针的整数类型
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Ptr
Slice
String
Struct
UnsafePointer
)

str := "dar"
val := reflect.ValueOf(str).Kind()
fmt.Println(val) //string

3.2 reflect反射- reflect.Value方法

获取值方法:

func (v Value) Int() int64 // 获取int类型值,如果 v 值不是有符号整型,则 panic。

func (v Value) Uint() uint64 // 获取unit类型的值,如果 v 值不是无符号整型(包括 uintptr),则 panic。

func (v Value) Float() float64 // 获取float类型的值,如果 v 值不是浮点型,则 panic。

func (v Value) Complex() complex128 // 获取复数类型的值,如果 v 值不是复数型,则 panic。

func (v Value) Bool() bool // 获取布尔类型的值,如果 v 值不是布尔型,则 panic。

func (v Value) Len() int // 获取 v 值的长度,v 值必须是字符串、数组、切片、映射、通道。

func (v Value) Cap() int // 获取 v 值的容量,v 值必须是数值、切片、通道。

func (v Value) Index(i int) reflect.Value // 获取 v 值的第 i 个元素,v 值必须是字符串、数组、切片,i 不能超出范围。

func (v Value) Bytes() []byte // 获取字节类型的值,如果 v 值不是字节切片,则 panic。

func (v Value) Slice(i, j int) reflect.Value // 获取 v 值的切片,切片长度 = j - i,切片容量 = v.Cap() - i。

// v 必须是字符串、数值、切片,如果是数组则必须可寻址。i 不能超出范围。

func (v Value) Slice3(i, j, k int) reflect.Value // 获取 v 值的切片,切片长度 = j - i,切片容量 = k - i。

// i、j、k 不能超出 v 的容量。i <= j <= k。

// v 必须是字符串、数值、切片,如果是数组则必须可寻址。i 不能超出范围。

func (v Value) MapIndex(key Value) reflect.Value // 根据 key 键获取 v 值的内容,v 值必须是映射。

// 如果指定的元素不存在,或 v 值是未初始化的映射,则返回零值(reflect.ValueOf(nil))

func (v Value) MapKeys() []reflect.Value // 获取 v 值的所有键的无序列表,v 值必须是映射。

// 如果 v 值是未初始化的映射,则返回空列表。

func (v Value) OverflowInt(x int64) bool // 判断 x 是否超出 v 值的取值范围,v 值必须是有符号整型。

func (v Value) OverflowUint(x uint64) bool // 判断 x 是否超出 v 值的取值范围,v 值必须是无符号整型。

func (v Value) OverflowFloat(x float64) bool // 判断 x 是否超出 v 值的取值范围,v 值必须是浮点型。

func (v Value) OverflowComplex(x complex128) bool // 判断 x 是否超出 v 值的取值范围,v 值必须是复数型。

//简单结构体操作
package main

import (
	"fmt"
	"reflect"
)

type Skills interface {
	reading()
	running()
}

type Student struct {
	Name string
	Age  int
}

func (self Student) runing() {
	fmt.Printf("%s is running\n", self.Name)
}
func (self Student) reading() {
	fmt.Printf("%s is reading\n", self.Name)
}
func main() {
	stu1 := Student{Name: "dar", Age: 18}
	stu_val := reflect.ValueOf(stu1)                //获取Value类型
	fmt.Println(stu_val.NumField())                 //2
	fmt.Println(stu_val.Field(0), stu_val.Field(1)) //dar 18
	fmt.Println(stu_val.FieldByName("Age"))         //18

	stu_val2 := reflect.ValueOf(&stu1).Elem() // 要修改传引用或者指针
	stu_val2.FieldByName("Age").SetInt(33)    //设置字段值 ,结果33
	fmt.Println(stu1.Age)

}

3.2 reflect反射- reflect.Value方法

设置值方法:

func (v Value) SetInt(x int64) //设置int类型的值

func (v Value) SetUint(x uint64) // 设置无符号整型的值

func (v Value) SetFloat(x float64) // 设置浮点类型的值

func (v Value) SetComplex(x complex128) //设置复数类型的值

func (v Value) SetBool(x bool) //设置布尔类型的值

func (v Value) SetString(x string) //设置字符串类型的值

func (v Value) SetLen(n int) // 设置切片的长度,n 不能超出范围,不能为负数。

func (v Value) SetCap(n int) //设置切片的容量

func (v Value) SetBytes(x []byte) //设置字节类型的值

func (v Value) SetMapIndex(key, val reflect.Value) //设置map的key和value,前提必须是初始化以后,存在覆盖、不存在

添加

//通过反射调用结构体中的方法,通过reflect.Value.Method(i int).Call()
//或者reflect.Value.MethodByName(name string).Call()实现
package main

import (
	"fmt"
	"reflect"
)

type Student struct {
	Name string
	Age  int
}

func (this *Student) SetName(name string) {
	this.Name = name
	fmt.Printf("set name %s\n", this.Name)
}

func (this *Student) SetAge(age int) {
	this.Age = age
	fmt.Printf("set age %d\n", age)
}

func (this *Student) String() string {
	fmt.Printf("this is %s\n", this.Name)
	return this.Name
}

func (this *Student) SetAgeAndName(age int, name string) {
	this.Age = age
	fmt.Printf("set age %d, name:%s\n", age, name)
}

func main() {
	stu1 := &Student{Name: "dar", Age: 18}
	val := reflect.ValueOf(stu1)         //获取Value类型,也可以使用reflect.ValueOf(&stu1).Elem()
	val.MethodByName("String").Call(nil) //调用String方法

	params := make([]reflect.Value, 1)
	params[0] = reflect.ValueOf(18)
	val.MethodByName("SetAge").Call(params) //通过名称调用方法

	params[0] = reflect.ValueOf("ki")
	// val.Method(1).Call(params) //通过方法索引调用
	val.Method(2).Call(params) //通过方法索引调用	通过索引的方式拿到函数不安全
	fmt.Println(stu1.Name, stu1.Age)

	params = make([]reflect.Value, 2)
	params[0] = reflect.ValueOf(18)
	params[1] = reflect.ValueOf("老师")
	val.MethodByName("SetAgeAndName").Call(params)
}

//this is dar
//set age 18
//set name ki
//ki 18

3.2 reflect反射- reflect.Value方法

其他方法:

//结构体相关:

func (v Value) NumField() int // 获取结构体字段(成员)数量

func (v Value) Field(i int) reflect.Value //根据索引获取结构体字段

func (v Value) FieldByIndex(index []int) reflect.Value // 根据索引链获取结构体嵌套字段

func (v Value) FieldByName(string) reflect.Value // 根据名称获取结构体的字段,不存在返回reflect.ValueOf(nil)

func (v Value) FieldByNameFunc(match func(string) bool) Value // 根据匹配函数 match 获取字段,如果没有匹配的字段,则返回

零值(reflect.ValueOf(nil))

//通道相关:

func (v Value) Send(x reflect.Value)// 发送数据(会阻塞),v 值必须是可写通道。

func (v Value) Recv() (x reflect.Value, ok bool) // 接收数据(会阻塞),v 值必须是可读通道。

func (v Value) TrySend(x reflect.Value) bool // 尝试发送数据(不会阻塞),v 值必须是可写通道。

func (v Value) TryRecv() (x reflect.Value, ok bool) // 尝试接收数据(不会阻塞),v 值必须是可读通道。

func (v Value) Close() // 关闭通道

//函数相关

func (v Value) Call(in []Value) (r []Value) // 通过参数列表 in 调用 v 值所代表的函数(或方法)。函数的返回值存入 r 中返回。

// 要传入多少参数就在 in 中存入多少元素。

// Call 即可以调用定参函数(参数数量固定),也可以调用变参函数(参数数量可变)。

func (v Value) CallSlice(in []Value) []Value // 调用变参函数

3.2 reflect反射- reflect.Value 结构体

type Value struct {

    typ *rtype // 变量的类型结构体

    ptr unsafe.Pointer // 数据指针

    flag uintptr // 标志位

    }

3.3 Go 语言官方的反射三大定律1

官方对 Go 语言的反射功能做了一个抽象的描述,总结出了三大定律

1. Reflection goes from interface value to reflection object.

2. Reflection goes from reflection object to interface value.

3. To modify a reflection object, the value must be settable.

第一个定律的意思是反射将接口变量转换成反射对象 Type 和 Value

func TypeOf(v interface{}) Type

func ValueOf(v interface{}) Value

第二个定律的意思是反射可以通过反射对象 Value 还原成原先的接口变量,这个指的就是 Value

结构体提供的 Interface() 方法。

func (v Value) Interface() interface{}

第三个定律的功能不是很好理解,它的意思是想用反射功能来修改一个变量的值,前提是这个

值可以被修改。

package main

import (
	"fmt"
	"reflect"
)

func test1() {
	var s int = 42
	var v = reflect.ValueOf(s)
	v.SetInt(43)
	fmt.Println(s)
}
func test2() {
	var s int = 42
	// 反射指针类型
	var v = reflect.ValueOf(&s)
	// 要拿出指针指向的元素进行修改
	v.Elem().SetInt(43)
	fmt.Println(s)
}
func main() {
	test1()
}

3.3 Go 语言官方的反射三大定律2

值类型的变量是不可以通过反射来修改,因为在反射之前,传参的时候需要将值变量转

换成接口变量,值内容会被浅拷贝,反射对象 Value 指向的数据内存地址不是原变量的内

存地址,而是拷贝后的内存地址。这意味着如果值类型变量可以通过反射功能来修改,

那么修改操作根本不会影响到原变量的值,那就白白修改了。所以 reflect 包就直接禁止

了通过反射来修改值类型的变量。

package main

import (
	"fmt"
	"reflect"
)

type Rect struct {
	Width  int
	Height int
	Name   string
}

// 通过统一的接口去实现属性设置的
func SetRectAttr(r *Rect, name string, value int) {
	var v = reflect.ValueOf(r)
	var field = v.Elem().FieldByName(name)
	field.SetInt(int64(value))
}

// 结构体也是值类型,也必须通过指针类型来修改。
func main() {
	var r = Rect{50, 100}
	SetRectAttr(&r, "Width", 100) // 修改属性的接口
	SetRectAttr(&r, "Height", 200)
	SetRectAttr(&r, "Name", "正方形")
	fmt.Println(r)
}
package main

import (
	"fmt"
	"reflect"
	"time"
)

const N = 1000000

func test1() {
	var sum = 0
	t0 := time.Now()

	for i := 0; i < (N); i++ {
		var s int = 42
		// 反射指针类型
		var v = reflect.ValueOf(&s)
		// 要拿出指针指向的元素进行修改
		v.Elem().SetInt(43)
		sum += s
	}

	elapsed := time.Since(t0)
	fmt.Println("反射赋值 ", "N:", N, "time:", elapsed, ", sum:", sum)
}

func test2() {
	var sum = 0
	t0 := time.Now()

	for i := 0; i < (N); i++ {
		var s int = 42
		s = 43
		sum += s
	}

	elapsed := time.Since(t0)
	fmt.Println("直接赋值 ", "N:", N, "time:", elapsed, ", sum:", sum)
}
func main() {
	test1()
	test2()
}
// 3 reflect反射 基础
package main

import (
	"fmt"
)

func main() {
	newValue := make(map[interface{}]interface{}, 0)
	newValue[11] = "dar"
	newValue["age"] = 18
	fmt.Println(newValue)
}

4 Go map实战

◼ go中的map是hash表的一个引用,类型写为:map[key]value,其中的key, value分别对

应一种数据类型,如map[string]string

◼ 要求所有的key的数据类型相同,所有value数据类型相同(注:key与value可以有不同的

数据类型,如果想不同则使用interface作为value)

map中的key的数据类型

◼ map中的每个key在keys的集合中是唯一的,而且需要支持 == or != 操作

◼ key的常用类型:int, rune, string, 结构体(每个元素需要支持 == or != 操作), 指针, 基于这

些类型自定义的类型

float32/64 类型从语法上可以作为key类型,但是实际一般不作为key,因为其类型有误差

4.1 Go map实战-key的几种数据类型举例

package main

import "fmt"

func main() {
	// m0 可以, key类型为string, 支持 == 比较操作
	{
		fmt.Println("---- m0 ----")
		var m0 map[string]string // 定义map类型变量m0,key的类型为string,value的类型string
		fmt.Println(m0)
	}

	// m1 不可以, []byte是slice,不支持 == != 操作,不可以作为map key的数据类型
	{
		// fmt.Println("---- m1 ----");
		//var m1 map[[]byte]string // 报错: invalid map key type []byte
		//fmt.Println(m1)

		// 准确说slice类型只能与nil比较,其他的都不可以,可以通过如下测试:
		// var b1,b2 []byte
		// fmt.Println(b1==b2) // 报错: invalid operation: b1 == b2 (slice can only be compared to nil)
	}

	// m2 可以, interface{}类型可以作为key,但是需要加入的key的类型是可以比较的
	{
		fmt.Println("---- m2 ----")
		var m2 map[interface{}]string
		m2 = make(map[interface{}]string)
		//m2[[]byte("k2")]="v2" // panic: runtime error: hash of unhashable type []uint8
		m2[123] = "123"
		m2[12.3] = "123"
		fmt.Println(m2)

		var str string = m2[12.3]
		fmt.Println(str)
	}

	// m3 可以, 数组支持比较
	{
		fmt.Println("---- m3 ----")
		a3 := [3]int{1, 2, 3}
		var m3 map[[3]int]string
		m3 = make(map[[3]int]string)
		m3[a3] = "m3"
		fmt.Println(m3)
	}

	// m4 可以,book1里面的元素都是支持== !=
	{
		fmt.Println("---- m4 ----")
		type book1 struct {
			name string
		}
		var m4 map[book1]string
		fmt.Println(m4)
	}

	// m5 不可以, text元素类型为[]byte, 不满足key的要求
	{
		fmt.Println("---- m5 ----")
		// type book2 struct {
		// 	name string
		// 	text []byte //没有这个就可以
		// }
		//var m5 map[book2]string //invalid map key type book2
		//fmt.Println(m5)
	}
}

4.2 map基本操作

map创建

两种创建的方式:一是通过字面值;二是通过make函数

map增删改查

map遍历

•遍历的顺序是随机的

•使用for range遍历的时候,k,v使用的同一块内存,这也是容易出现错误的地

增加,修改: m["c"] = "11"

查: v1 := m["x"]

v2, ok2 := m["x"]

删: delete(m, "x")

for k, v := range m { fmt.Printf("k:[%v].v:[%v]\n", k, v) //

输出k,v值 }

5 Go string字符串

字符串通常有两种设计,一种是「字符」串,一种是「字节」串。「字符」串中的每个

字都是定长的,而「字节」串中每个字是不定长的。Go 语言里的字符串是「字节」串,

英文字符占用 1 个字节,非英文字符占多个字节。

其中 codepoint 是每个「字」的其实偏移量。 Go 语言的字符串采用 utf8 编码,中文汉字通常需要
占用 3 个字节,英文只需要 1 个字节。 len() 函数得到的是字节的数量,通过下标来访问字符串得
到的是「字节」。
// 4-2 map基本操作
package main

import "fmt"

func create() {
	fmt.Println("map创建方式:")
	// 1 字面值
	{
		m1 := map[string]string{
			"m1": "v1", // 定义时指定的初始key/value, 后面可以继续添加
		}
		_ = m1

	}

	// 2 使用make函数
	{
		m2 := make(map[string]string) // 创建时,里面不含元素,元素都需要后续添加
		m2["m2"] = "v2"               // 添加元素
		_ = m2

	}

	// 定义一个空的map
	{
		m3 := map[string]string{}
		m4 := make(map[string]string)
		_ = m3
		_ = m4
	}
}

func curd() {
	fmt.Println("map增删改查:")
	// 创建
	fmt.Println("建:")
	m := map[string]string{
		"a": "va",
		"b": "vb",
	}
	fmt.Println(len(m)) // len(m) 获得m中key/value对的个数

	// 增加,修改
	fmt.Println("增改:")
	{
		// k不存在为增加,k存在为修改
		m["c"] = ""
		m["c"] = "11"                      // 重复增加(key相同),使用新的值覆盖
		fmt.Printf("%#v %#v\n", m, len(m)) // map[string]string{"a":"va", "b":"vb", "c":"11"} 3
	}

	// 查
	fmt.Println("查:")
	{
		// v := m[k] // 从m中取键k对应的值给v,如果k在m中不存在,则将value类型的零值赋值给v
		// v, ok := m[k] // 从m中取键k对应的值给v,如果k存在,ok=true,如果k不存在,将value类型的零值赋值给v同时ok=false
		// 查1 - 元素不存在
		v1 := m["x"] //
		v2, ok2 := m["x"]
		fmt.Printf("%#v %#v %#v\n", v1, v2, ok2) // "" "" false

		// 查2 - 元素存在
		v3 := m["a"]
		v4, ok4 := m["a"]
		fmt.Printf("%#v %#v %#v\n", v3, v4, ok4) //"va" "va" true
	}
	fmt.Println("删:")
	// 删, 使用内置函数删除k/v对
	{
		// delete(m, k) 将k以及k对应的v从m中删掉;如果k不在m中,不执行任何操作
		delete(m, "x")                     // 删除不存在的key,原m不影响
		delete(m, "a")                     // 删除存在的key
		fmt.Printf("%#v %#v\n", m, len(m)) // map[string]string{"b":"vb", "c":"11"} 2
		delete(m, "a")                     // 重复删除不报错,m无影响
		fmt.Printf("%#v %#v\n", m, len(m)) /// map[string]string{"b":"vb", "c":"11"} 2
	}
}

func travel() {
	fmt.Println("map遍历:")
	m := map[string]int{
		"a": 1,
		"b": 2,
	}
	for k, v := range m {
		fmt.Printf("k:[%v].v:[%v]\n", k, v) // 输出k,v值
	}

	var fruits = map[string]int{
		"apple":  2,
		"banana": 5,
		"orange": 8,
	}

	for name, score := range fruits {
		fmt.Println(name, score)
	}

	for name := range fruits {
		fmt.Println(name)
	}
}

func main() {
	create()
	curd()
	travel()
}

5.1 Go string字符串-遍历

package main

import "fmt"

func main() {

    var s = "嘻哈china"

    for i := 0; i < len(s); i++ {

        fmt.Printf("%x ", s[i])

    }

}

e5 98 bb e5 93 88 63 68 69 6e 61 

func main() {

    var s = "嘻哈china"

    for codepoint, runeValue := range s {

        fmt.Printf("[%d]: %x", codepoint, int32(runeValue))

    }

}

PS D:\Workspace\Go\src\projects\demo> go run main.go
[0]: 563b[3]: 54c8[6]: 63[7]: 68[8]: 69[9]: 6e[10]: 61 

5-2 Go string字节串的内存表示和操作

// 按字符 rune 遍历
package main

import "fmt"

func splice() {
	var s1 = "hello" // 静态字面量
	var s2 = ""
	for i := 0; i < 10; i++ {
		s2 += s1 // 动态构造
	}
	fmt.Println(len(s1))
	fmt.Println(len(s2))
}

// 字符串是只读的
func onlyread() {
	var s = "hello"
	s[0] = 'H'
}

// 切割切割
func cut() {
	var s1 = "hello world"
	var s2 = s1[3:8]
	fmt.Println(s2)
}

// 字节切片和字符串的相互转换
func string2bytes() {
	var s1 = "hello world"
	var b = []byte(s1) // 字符串转字节切片
	var s2 = string(b) // 字节切片转字符串
	fmt.Println(b)
	fmt.Println(s2)
}
func main() {
	splice()
	onlyread()
	cut()
	string2bytes()
}

1.3 Go语言并发编程

1. Goroutine

1 Go协程 Goroutine
1.1 Goroutine 使用
1.2 Goroutine 原理

1.1 如何使用Goroutine

在函数或方法调用前面加上关键字go,您将会同时运行一个新的Goroutine

1.2 子协程异常退出的影响

在使用子协程时一定要特别注意保护好每个子协程,确保它们正常安全的运行。因为子协程

的异常退出会将异常传播到主协程,直接会导致主协程也跟着挂掉,然后整个程序就崩溃了。

1.3 协程异常处理-recover

recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅

在延迟函数 defer 中有效。

如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的

执行。

// 1.3 协程异常处理-recover

package main

import (
	"fmt"
	"runtime"
)

// 崩溃时需要传递的上下文信息
type panicContext struct {
	function string // 所在函数
}

// 保护方式允许一个函数
func ProtectRun(entry func()) {
	// 延迟处理的函数
	defer func() {
		// 发生宕机时,获取panic传递的上下文并打印
		err := recover()
		switch err.(type) {
		case runtime.Error: // 运行时错误
			fmt.Println("runtime error:", err)
		default: // 非运行时错误
			fmt.Println("error:", err)
		}
	}()
	entry()
}
func main() {
	fmt.Println("运行前")
	// 允许一段手动触发的错误
	ProtectRun(func() {
		fmt.Println("手动宕机前")
		// 使用panic传递上下文
		panic(&panicContext{
			"手动触发panic",
		})
		fmt.Println("手动宕机后")
	})
	// 故意造成空指针访问错误
	ProtectRun(func() {
		fmt.Println("赋值宕机前")
		var a *int
		*a = 1
		fmt.Println("赋值宕机后")
	})
	fmt.Println("运行后")
}
// 1.3 协程异常处理-recover

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("run in main goroutine")
	go func() {
		fmt.Println("run in child goroutine")
		defer func() { // 要在对应的协程里执行
			fmt.Println("执行defer:")
			if err := recover(); err != nil {
				fmt.Println("捕获error:", err)
			}
		}()
		fmt.Println("run in grand grand child goroutine")
		var ptr *int
		*ptr = 0x12345 // 故意制造崩溃  ,该协程运行到这里结束
		go func() {
			fmt.Println("子子run in grand child goroutine")	// 这里也不会运行
			go func() {
				
			}()
		}()
		// time.Sleep(time.Second * 1)
		fmt.Println("离开: run in child goroutine leave") // 这里能否执行到
	}()
	time.Sleep(2 * time.Second)
	fmt.Println("main goroutine will quit")
}

1-4 启动百万协程

Go 语言能同时管理上百万的协程

// 1-4 启动百万协程

package main

import (
	"fmt"
	"runtime"
	"time"
)

const N = 1000000

func main() {
	fmt.Println("run in main goroutine")
	i := 1
	for {
		go func() {
			for {
				time.Sleep(time.Second)
			}
		}()
		if i%10000 == 0 {
			fmt.Printf("%d goroutine started\n", i)
		}
		i++
		if i == N {
			break
		}
	}
	fmt.Println("NumGoroutine:", runtime.NumGoroutine())
	time.Sleep(time.Second * 15)
}

1-5 死循环

如果有个别协程死循环了会导致其它协程饥饿得到不运行么?

package main

import (
	"fmt"
	"runtime"
	"syscall"
	"time"
)
// 获取的是线程ID,不是协程ID
func GetCurrentThreadId() int {
	var user32 *syscall.DLL
	var GetCurrentThreadId *syscall.Proc
	var err error

	user32, err = syscall.LoadDLL("Kernel32.dll")	// Windows用的
	if err != nil {
		fmt.Printf("syscall.LoadDLL fail: %v\n", err.Error())
		return 0
	}
	GetCurrentThreadId, err = user32.FindProc("GetCurrentThreadId")
	if err != nil {
		fmt.Printf("user32.FindProc fail: %v\n", err.Error())
		return 0
	}

	var pid uintptr
	pid, _, err = GetCurrentThreadId.Call()

	return int(pid)
}

func main() {
	// runtime.GOMAXPROCS(1)		
	// 读取当前的线程数
	fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // 本身电脑物理核心是4核 支持超线程 8核
	fmt.Println("run in main goroutine")
	n := 5
	for i := 0; i < n; i++ {
		go func() {
			fmt.Println("dead loop goroutine start, threadId:", GetCurrentThreadId())
			for {
			} // 死循环
			fmt.Println("dead loop goroutine stop")
		}()
	}

	go func() {
		var count = 0
		for {
			time.Sleep(time.Second)
			count++
			fmt.Println("for goroutine running:", count, "threadId:", GetCurrentThreadId())
		}
	}()
	fmt.Println("NumGoroutine: ", runtime.NumGoroutine())
	var count = 0
	for {
		time.Sleep(time.Second)
		count++
		fmt.Println("main goroutine running:", count, "threadId:", GetCurrentThreadId())
	}
}

1.6 设置线程数

Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执

行Go的代码。其默认的值是运行机器上的CPU的核心数,所以在一个有8个核心的机器上

时,调度器一次会在8个OS线程上去调度GO代码。

package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Println("runtime.NumCPU():", runtime.NumCPU())
	// 读取默认的线程数
	fmt.Println(runtime.GOMAXPROCS(0))
	// 设置线程数为 10
	runtime.GOMAXPROCS(10)
	// 读取当前的线程数
	fmt.Println(runtime.GOMAXPROCS(0))
}

1-7 G-P-M模型-为什么引入协程?

核心原因为goroutine的轻量级,无论是从进程到线程,还是从线程到协程,其核心都是为了使得

我们的调度单元更加轻量级。可以轻易得创建几万几十万的goroutine而不用担心内存耗尽等问题。

1-7 G-P-M模型-系统调用

调用system call陷入内核没有返回之前,为保证调度的并发性,golang 调度器在进入系统调用之前

从线程池拿一个线程或者新建一个线程,当前P交给新的线程M1执行。

G0 返回之后,需要找一个可用的 P 继续运行,
如果没有则将其放在全局队列等待调度。 M0
G0 返回后退出或放回线程池。

1-7 G-P-M模型-工作流窃取

在P队列上的goroutine全部调度完了之后,对应的M首先会尝试从global runqueue中获取

goroutine进行调度。如果golbal runqueue中没有goroutine,当前M会从别的M对应P的local

runqueue中抢一半的goroutine放入自己的P中进行调度。具体要看C代码去了。

2. Channel

2 通道channel

如果说goroutine是Go语音程序的并发体的话,那么channels它们之间的通信机制。

作为协程的输出,通道是一个容器,它可以容纳数据。

作为协程的输入,通道是一个生产者,它可以向协程提供数据。

通道作为容器是有限定大小的,满了就写不进去,空了就读不出来。

通道有它自己的类型,它可以限定进入通道的数据的类型。

2.1 创建通道

创建通道只有一种语法,使用make 函数

有两种通道类型:

「缓冲型通道」 var bufferedChannel = make(chan int(这里是类型,什么类型都

行), 1024)

「非缓冲型通道」 var unbufferedChannel = make(chan int)

两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相通的对象,那

么比较的结果为真。一个channel也可以和nil进行比较。

package main

import "fmt"

func send(ch chan int) {
	i := 0
	for {
		i++
		ch <- i
	}
}

func recv(ch chan int) {
	value := <-ch
	fmt.Println(value)
	value = <-ch
	fmt.Println(value)
	close(ch)
}

// 向一个已经关闭的通道执行写操作会抛出异常,这意味着我们在写通道时一定要确保通道没有被关闭。
func main() {
	var ch = make(chan int, 4)
	go recv(ch)
	send(ch)
}

2.2 读写通道

Go 语言为通道的读写设计了特殊的箭头语法糖 <-,让我们使用通道时非常方便。把箭头写在

通道变量的右边就是写通道,把箭头写在通道的左边就是读通道。一次只能读写一个元素

// 2.2 读写通道

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan float32, 4)
	for i := 0; i < cap(ch); i++ {
		ch <- 1.0 // 写通道
	}
	for len(ch) > 0 {
		value := <-ch // 读通道
		fmt.Println(value)
	}

	// ch1 := make(chan int, 1) // 这里是缓存 有一个1元素
	ch1 := make(chan int) // 非缓存的,实际是0个,并不是1个

	go func() {
		time.Sleep(1 * time.Second)
		ch1 <- 1
		ch1 <- 1             // 这里已经阻塞
		fmt.Println("写入ch1") //这里没打印
	}()

	value1 := <-ch1
	value1 = <-ch1
	time.Sleep(5 * time.Second)
	fmt.Println("退出, value1:", value1)
}

通道作为容器,它可以像切片一样,使用 cap() 和 len() 全局函数获得通道的容量和当前内

部的元素个数。

2-3 读写阻塞

通道满了,写操作就会阻塞,协程就会进入休眠,直到有其它协程读通道挪出了空间,协程

才会被唤醒。如果有多个协程的写操作都阻塞了,一个读操作只会唤醒一个协程。

// 2-3 读写阻塞
package main

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

func send(ch chan int) {
	for {
		var value = rand.Intn(100)
		ch <- value
		fmt.Printf("send %d\n", value) // 这里没有延时
	}
}

func recv(ch chan int) {
	for {
		value := <-ch
		fmt.Printf("recv %d\n", value)
		time.Sleep(time.Second)
	}
}

func main() {
	var ch = make(chan int, 1)
	// 子协程循环读
	go recv(ch)
	// 主协程循环写
	send(ch)
}

2.4 关闭通道

Go 语言的通道有点像文件,不但支持读写操作, 还支持关闭。读取一个已经关闭的通道会立

即返回通道类型的「零值」,而写一个已经关闭的通道会抛异常。如果通道里的元素是整型的,

读操作是不能通过返回值来确定通道是否关闭的。

// 2.4 关闭通道
/*
Go 语言的通道有点像文件,不但支持读写操作, 还支持关闭。
读取一个已经关闭的通道会立即返回通道类型的「零值」,而写一个已经关闭的通道会抛异常。
如果通道里的元素是整型的,读操作是不能通过返回值来确定通道是否关闭的。
*/
package main

import "fmt"

func main() {
	var ch = make(chan int, 4)
	ch <- 1
	ch <- 2

	fmt.Println("len(ch):", len(ch), "cap(ch):", cap(ch))
	close(ch)

	value := <-ch
	fmt.Println(value)
	value = <-ch
	fmt.Println(value)
	value = <-ch
	fmt.Println(value)
	value = <-ch
	fmt.Println(value)
	ch <- 3
}

2-5 通道写安全

向一个已经关闭的通道执行写操作会抛出异常,这意味着我们在写通道时一定要确保通道没

有被关闭

 多人写入怎么办?

// 2-5 通道写安全
package main

import "fmt"

func send(ch chan int) { // 在写入端关闭, 没有太多的通用性
	ch <- 1
	ch <- 2
	ch <- 3
	ch <- 4
	close(ch)
}

func recv(ch chan int) {
	for v := range ch {
		fmt.Println(v)
	}
	value := <-ch // 判别不了是否已经读取完毕
	fmt.Println("value:", value)
}

// 确保通道写安全的最好方式是由负责写通道的协程自己来关闭通道,读通道的协程不要去关闭通道。
func main() {
	var ch = make(chan int, 1)
	go send(ch)
	recv(ch)
}

2-6 WaitGroup

在写端关闭channel对单写的程序有效,但是多写的时候呢?

使用到内置 sync 包提供的 WaitGroup 对象,它使用计数来等待指定事件完成。

// 2-6 WaitGroup 在写端关闭channel对单写的程序有效,但是多写的时候呢?

package main

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

func send(ch chan int, wg *sync.WaitGroup) {
	defer wg.Done() // 计数值减一
	i := 0
	for i < 4 {
		i++
		ch <- i
	}
}

func recv(ch chan int) {
	for v := range ch {
		fmt.Println(v)
	}
}
// 只要一个值能做界定符 比如nil, 比如0xfffe
func main() {
	var ch = make(chan int, 4)
	var wg = new(sync.WaitGroup)
	wg.Add(2)       // 增加计数值
	go send(ch, wg) // 写
	go send(ch, wg) // 写
	go recv(ch)
	// Wait() 阻塞等待所有的写通道协程结束
	// 待计数值变成零,Wait() 才会返回
	wg.Wait()
	// 关闭通道
	close(ch)
	time.Sleep(time.Second)
}

2-7 多路通道

在真实的世界中,还有一种消息传递场景,那就是消费者有多个消费来源,只要有一个

来源生产了数据,消费者就可以读这个数据进行消费。这时候可以将多个来源通道的数

据汇聚到目标通道,然后统一在目标通道进行消费

/*
2-7 多路通道
在真实的世界中,还有一种消息传递场景,那就是消费者有多个消费来源,只要有一个来源生产了数据,
消费者就可以读这个数据进行消费。这时候可以将多个来源通道的数据汇聚到目标通道,然后统一在目标通道进行消费。

*/
package main

import (
	"fmt"
	"time"
)

// 每隔一会生产一个数
func send(ch chan int, gap time.Duration) {
	i := 0
	for {
		i++
		ch <- i
		time.Sleep(gap)
	}
}

// 将多个原通道内容拷贝到单一的目标通道
func collect(source chan int, target chan int) {
	for v := range source {
		target <- v // ch3 <- ch2 ; ch3 <- ch1
	}
}

func collect2(ch1 chan int, ch2 chan int, target chan int) {
	for {
		select {
		case v := <-ch1:
			target <- v
		case v := <-ch2:
			target <- v
		default: // 非阻塞
			fmt.Println("collect2")
		}
	}
}

// 从目标通道消费数据
func recv(ch chan int) {
	for v := range ch {
		fmt.Printf("receive %d\n", v)
	}
}

func main() {
	var ch1 = make(chan int)
	var ch2 = make(chan int)
	var ch3 = make(chan int)
	go send(ch1, time.Second)
	go send(ch2, 2*time.Second)
	// go collect(ch1, ch3)
	// go collect(ch2, ch3)
	go collect2(ch1, ch2, ch3)
	recv(ch3)
}

2-8 多路复用select

// 2-8 多路复用select
package main

import (
	"fmt"
	"time"
)

func send(ch chan int, gap time.Duration) {
	i := 0
	for {
		i++
		ch <- i
		time.Sleep(gap)
	}
}

func recv(ch1 chan int, ch2 chan int) {
	for {
		select {
		case v := <-ch1:
			fmt.Printf("recv %d from ch1\n", v)
		case v := <-ch2:
			fmt.Printf("recv %d from ch2\n", v)
		}
	}
}

func main() {
	var ch1 = make(chan int)
	var ch2 = make(chan int)
	go send(ch1, time.Second)
	go send(ch2, 2*time.Second)
	recv(ch1, ch2)
}

2-9 非阻塞读写

通道的非阻塞读写。当通道空时,读操作不会阻塞,当通道满时,写操作也不会阻塞。非

阻塞读写需要依靠 select 语句的 default 分支。当 select 语句所有通道都不可读写时,如果

定义了 default 分支,那就会执行 default 分支逻辑,这样就起到了不阻塞的效果。

// 2-9 非阻塞读写
package main

import (
	"fmt"
	"time"
)

func send(ch1 chan int, ch2 chan int) {
	i := 0
	for {
		i++
		select {
		case ch1 <- i:
			fmt.Printf("send ch1 %d\n", i)
		case ch2 <- i:
			fmt.Printf("send ch2 %d\n", i)
		default:
			fmt.Printf("ch block\n")
			time.Sleep(2 * time.Second) // 这里只是为了演示
		}
	}
}

func recv(ch chan int, gap time.Duration, name string) {
	for v := range ch {
		fmt.Printf("receive %s %d\n", name, v)
		time.Sleep(gap)
	}
}

func main() {
	// 无缓冲通道
	var ch1 = make(chan int)
	var ch2 = make(chan int)
	// 两个消费者的休眠时间不一样,名称不一样
	go recv(ch1, time.Second, "ch1")
	go recv(ch2, 2*time.Second, "ch2")
	send(ch1, ch2)
}

2-10 生产者、消费者模型

生产者消费模型

// 2-10 生产者、消费者模型
package main

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

// 生产者
func Producer(factor int, out chan<- int) {
	for i := 0; ; i++ {
		out <- i * factor
		time.Sleep(5 * time.Second)
	}
}

// 消费者
func Consumer(in <-chan int) {
	for v := range in {
		fmt.Println(v)
	}
}

func main() {
	ch := make(chan int, 64)
	go Producer(3, ch) // 生成3的倍数序列
	go Producer(5, ch) // 生成5的倍数序列

	go Consumer(ch)

	//Ctrl +C 退出
	sig := make(chan os.Signal, 1)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
	fmt.Printf("wait Ctrl +C")
	fmt.Printf("quit (%v)\n", <-sig)
}

3. 线程安全

3-1 线程安全-互斥锁

竞态检查工具是基于运行时代码检查,而不是通过代码静态分析来完成的。这意味着那些没

有机会运行到的代码逻辑中如果存在安全隐患,它是检查不出来的。

需要加上-race 执行

package main

import "fmt"
// go多协程 是有竞态,不像以前的ntyco,libco没有竞态的
func write(d map[string]int) {
	d["fruit"] = 2
}

func read(d map[string]int) {
	fmt.Println(d["fruit"])
}

// go run -race 3-1-unsafe.go
func main() {
	d := map[string]int{}
	go read(d)
	write(d)
}

3-2 避免锁复制

sync.Mutex 是一个结构体对象,这个对象在使用的过程中要避免被复制 —— 浅拷贝。复制会导致

锁被「分裂」了,也就起不到保护的作用。所以在平时的使用中要尽量使用它的指针类型。读者可

以尝试将上面的类型换成非指针类型,然后运行一下竞态检查工具,会看到警告信息再次布满整个

屏幕。锁复制存在于结构体变量的赋值、函数参数传递、方法参数传递中,都需要注意。

package main

import (
	"fmt"
	"sync"
)

type SafeDict struct {
	data  map[string]int
	mutex *sync.Mutex
}

func NewSafeDict(data map[string]int) *SafeDict {
	return &SafeDict{
		data:  data,
		mutex: &sync.Mutex{},
	}
}

// defer 语句总是要推迟到函数尾部运行,所以如果函数逻辑运行时间比较长,
// 这会导致锁持有的时间较长,这时使用 defer 语句来释放锁未必是一个好注意。
func (d *SafeDict) Len() int {
	d.mutex.Lock()
	defer d.mutex.Unlock()
	return len(d.data)
}

// func (d *SafeDict) Test() int {
// 	d.mutex.Lock()
// 	length := len(d.data)
// 	d.mutex.Unlock() // 手动解锁 减少粒度	// 这种情况就不要用 defer d.mutex.Unlock()
// 	fmt.Println("length: ", length)
// 	// 这里还有耗时处理 耗时1000ms
// }

func (d *SafeDict) Put(key string, value int) (int, bool) {
	d.mutex.Lock()
	defer d.mutex.Unlock()
	old_value, ok := d.data[key]
	d.data[key] = value
	return old_value, ok
}

func (d *SafeDict) Get(key string) (int, bool) {
	d.mutex.Lock()
	defer d.mutex.Unlock()
	old_value, ok := d.data[key]
	return old_value, ok
}

func (d *SafeDict) Delete(key string) (int, bool) {
	d.mutex.Lock()
	defer d.mutex.Unlock()
	old_value, ok := d.data[key]
	if ok {
		delete(d.data, key)
	}
	return old_value, ok
}

func write(d *SafeDict) {
	d.Put("banana", 5)
}

func read(d *SafeDict) {
	fmt.Println(d.Get("banana"))
}

// go run -race 3-2-lock.go
func main() {
	d := NewSafeDict(map[string]int{
		"apple": 2,
		"pear":  3,
	})
	go read(d)
	write(d)
}

3-3 使用匿名锁字段

在结构体章节,我们知道外部结构体可以自动继承匿名内部结构体的所有方法。如果将上面的

SafeDict 结构体进行改造,将锁字段匿名,就可以稍微简化一下代码。

package main

import (
	"fmt"
	"sync"
)

type SafeDict struct {
	data map[string]int
	*sync.Mutex
}

func NewSafeDict(data map[string]int) *SafeDict {
	return &SafeDict{
		data,
		&sync.Mutex{}, // 一样是要初始化的
	}
}

func (d *SafeDict) Len() int {
	d.Lock()
	defer d.Unlock()
	return len(d.data)
}

func (d *SafeDict) Put(key string, value int) (int, bool) {
	d.Lock()
	defer d.Unlock()
	old_value, ok := d.data[key]
	d.data[key] = value
	return old_value, ok
}

func (d *SafeDict) Get(key string) (int, bool) {
	d.Lock()
	defer d.Unlock()
	old_value, ok := d.data[key]
	return old_value, ok
}

func (d *SafeDict) Delete(key string) (int, bool) {
	d.Lock()
	defer d.Unlock()
	old_value, ok := d.data[key]
	if ok {
		delete(d.data, key)
	}
	return old_value, ok
}

func write(d *SafeDict) {
	d.Put("banana", 5)
}

func read(d *SafeDict) {
	fmt.Println(d.Get("banana"))
}

func main() {
	d := NewSafeDict(map[string]int{
		"apple": 2,
		"pear":  3,
	})
	go read(d)
	write(d)
}

3-4 使用读写锁

日常应用中,大多数并发数据结构都是读多写少的,对于读多写少的场合,可以将互斥锁换

成读写锁,可以有效提升性能。sync 包也提供了读写锁对象 RWMutex,不同于互斥锁只有两

个常用方法 Lock() 和 Unlock(),读写锁提供了四个常用方法,分别是写加锁 Lock()、写释放锁

Unlock()、读加锁 RLock() 和读释放锁 RUnlock()。写锁是排他锁,加写锁时会阻塞其它协程再

加读锁和写锁,读锁是共享锁,加读锁还可以允许其它协程再加读锁,但是会阻塞加写锁。

// 3-4 使用读写锁
package main

import (
	"fmt"
	"sync"
)

type SafeDict struct {
	data map[string]int
	*sync.RWMutex	//  sync.Mutex API也有点不一样
}

func NewSafeDict(data map[string]int) *SafeDict {
	return &SafeDict{data, &sync.RWMutex{}}
}

func (d *SafeDict) Len() int {
	d.RLock()
	defer d.RUnlock()
	return len(d.data)
}

func (d *SafeDict) Put(key string, value int) (int, bool) {
	d.Lock()
	defer d.Unlock()
	old_value, ok := d.data[key]
	d.data[key] = value
	return old_value, ok
}

func (d *SafeDict) Get(key string) (int, bool) {
	d.RLock()
	defer d.RUnlock()
	old_value, ok := d.data[key]
	return old_value, ok
}

func (d *SafeDict) Delete(key string) (int, bool) {
	d.Lock()
	defer d.Unlock()
	old_value, ok := d.data[key]
	if ok {
		delete(d.data, key)
	}
	return old_value, ok
}

func write(d *SafeDict) {
	d.Put("banana", 5)
}

func read(d *SafeDict) {
	fmt.Println(d.Get("banana"))
}

func main() {
	d := NewSafeDict(map[string]int{
		"apple": 2,
		"pear":  3,
	})
	go read(d)
	write(d)
}

3.5 发布订阅模型

综合前面学的

支持过滤器设置主题

// 3.5 发布订阅模型
package main

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

type (
	subscriber chan interface{}         // 订阅者为一个通道
	topicFunc  func(v interface{}) bool // 主题为一个过滤器
)

// 发布者对象
type Publisher struct {
	m           sync.RWMutex             //读写锁
	buffer      int                      // 订阅队列的缓存大小
	timeout     time.Duration            // 发布超时时间
	subscribers map[subscriber]topicFunc // 订阅者信息
}

// 构建一个发布者对象,可以设置发布超时时间和缓存队列的长度
func NewPublisher(publishTimeout time.Duration, buffer int) *Publisher {
	return &Publisher{
		buffer:      buffer,
		timeout:     publishTimeout,
		subscribers: make(map[subscriber]topicFunc),
	}

}

// 关闭发布者对象,同时关闭所有的订阅通道
func (p *Publisher) Close() {
	p.m.Lock()
	defer p.m.Unlock()
	for sub := range p.subscribers {
		delete(p.subscribers, sub)
		close(sub)
	}
}

// 添加一个新的订阅者,订阅过滤器筛选后的主题
func (p *Publisher) SubscribeTopic(topic topicFunc) chan interface{} {
	ch := make(chan interface{}, p.buffer)
	p.m.Lock()
	p.subscribers[ch] = topic
	p.m.Unlock()
	return ch
}

// 添加一个新的订阅者,订阅全部主题
func (p *Publisher) Subscribe() chan interface{} {
	return p.SubscribeTopic(nil)
}

// 退出订阅
func (p *Publisher) Evict(sub chan interface{}) {
	p.m.Lock()
	defer p.m.Unlock()
	delete(p.subscribers, sub)
	close(sub)
}

// 发送主题,可以容忍一定的超时
func (p *Publisher) sendTopic(
	sub subscriber, topic topicFunc, v interface{}, wg *sync.WaitGroup,
) {
	defer wg.Done()
	if topic != nil && !topic(v) { // 过滤信息
		return
	}
	select {
	case sub <- v:
	case <-time.After(p.timeout): // 超时
	}
}

// 发布一个主题
func (p *Publisher) Publish(v interface{}) {
	p.m.Lock()
	defer p.m.Unlock()
	var wg sync.WaitGroup
	for sub, topic := range p.subscribers {
		wg.Add(1)
		go p.sendTopic(sub, topic, v, &wg)
	}
	wg.Wait()
}

func main() {
	p := NewPublisher(100*time.Millisecond, 10)
	defer p.Close()

	all := p.Subscribe()

	golang := p.SubscribeTopic(func(v interface{}) bool {
		if s, ok := v.(string); ok {
			return strings.Contains(s, "golang")
		}
		return false
	})

	p.Publish("hello world")
	p.Publish("hello, golang")
	go func() {
		for msg := range all {
			fmt.Println("all:", msg)
		}
	}()
	go func() {
		for msg := range golang {
			fmt.Println("golang:", msg)
		}
	}()

	// 运行一段时间后退出
	time.Sleep(3 * time.Second)
}

3.6 sync.Once初始化

sync.Once.Do(f func())是一个挺有趣的东西,能保证once只执行一次,无论你是否更换once.Do(xx)

这里的方法,这个sync.Once块只会执行一次。

package main

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

var once sync.Once

func main() {
	for i, v := range make([]string, 10) {
		once.Do(onces)
		fmt.Println("count:", v, "---", i)
	}
	for i := 0; i < 5; i++ {

		go func() {
			once.Do(onced)
			fmt.Println("213")
		}()
	}
	time.Sleep(4000)
}
func onces() {
	fmt.Println("执行onces")
}
func onced() {
	fmt.Println("执行onced")
}

4. context

4 Go语言Context

为什么需要 Context

•每一个处理都应该有个超时限制

•需要在调用中传递这个超时

• 比如开始处理请求的时候我们说是 3 秒钟超时

• 那么在函数调用中间,这个超时还剩多少时间了?

• 需要在什么地方存储这个信息,这样请求处理中间

可以停止

Context是协程安全的。代码中可以将单个Context传递给任意数量的goroutine,并在取

消该Context时可以将信号传递给所有的goroutine。

4.1 Context接口

type Context interface {

Deadline() (deadline time.Time, ok bool)

Done() <-chan struct{}

Err() error

Value(key interface{}) interface{}

}

◼ Deadline方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context

会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消

◼ Done方法返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以

读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该

做清理操作,然后退出goroutine,释放资源

◼ Err方法返回取消的错误原因,因为什么Context被取消。

◼ Value方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的

package main

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

func main() {
	ctx, cancel1 := context.WithCancel(context.Background())
	go func(ctx context.Context) {
		for {
			select {
			case v := <-ctx.Done():
				fmt.Println("监控退出,停止了..., v: ", v, ", err:", ctx.Err())
				return
			default:
				time.Sleep(2 * time.Second)
				fmt.Println("goroutine监控中...")
				// time.Sleep(2 * time.Second)
			}
		}
	}(ctx)

	time.Sleep(5 * time.Second)
	fmt.Println("可以了,通知监控停止")
	cancel1()

	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)
}

4.1 Background()和TODO()

◼ Go语言内置两个函数:Background() 和 TODO(),这两个函数分别返回一个实现了 Context 接口的background 和 todo。

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

Context,也就是根 Context。

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

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

package main

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

func main() {
	// ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(3*time.Second))
	// var wg sync.WaitGroup
	go func(ctx context.Context) {
		// wg.Add(1)
		// defer wg.Done()
		for {
			select {
			case <-ctx.Done():
				fmt.Println("监控退出,停止了..., err:", ctx.Err())
				return
			default:
				time.Sleep(2 * time.Second)
				fmt.Println("goroutine监控中...")
				// time.Sleep(2 * time.Second)
			}
		}
	}(ctx)
	// cancel()
	time.Sleep(5 * time.Second)
	fmt.Println("可以了,通知监控停止")

	cancel()
	// wg.Wait() // 等待协程退出
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)
}

4.2 Context的继承衍生

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

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

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

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

四个With函数,接收的都有一个partent参数,就是父Context,我们要基于这个父Context创建出子

Context的意思

◼ WithCancel函数,传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context

◼ WithDeadline函数,和WithCancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消

Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消

◼ WithTimeout和WithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思,只是传参数不一样。

◼ WithValue函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过Context.Value方法访问到

package main

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

func work(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	for {
		select {
		case <-ctx.Done():
			fmt.Println("监控退出,停止了...")
			return
		default:
			fmt.Println("hello")
			time.Sleep(time.Second)
		}
	}
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel() // 在建立之后,立即 defer cancel() 是一个好习惯。
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go work(ctx, &wg)
	}
	time.Sleep(time.Second)

	wg.Wait()
}

4.3 Context使用原则

◼ 不要把Context放在结构体中,要以参数的方式进行传递

◼ 以 Context 作为参数的函数方法,应该把 Context 作为第一个参数

◼ 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用

context.TODO

◼ Context 的 Value 相关方法应该传递请求域的必要数据,不应该用于传递可选参数;

◼ Context 是线程安全的,可以放心的在多个 Goroutine 中传递。

package main

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

var key string = "name"
var key2 string = "name1"

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	//附加值
	valueCtx := context.WithValue(ctx, key, "key【监控1】") // 是否可以有多个key
	valueCtx2 := context.WithValue(valueCtx, key2, "key【监控2】")

	go watch(valueCtx2)
	time.Sleep(5 * time.Second)
	fmt.Println("可以了,通知监控停止")
	cancel()
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)
}

func watch(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			//取出值
			fmt.Println(ctx.Value(key), "监控退出,停止了...")
			fmt.Println(ctx.Value(key2), "监控退出,停止了...")
			return
		default:
			//取出值
			fmt.Println(ctx.Value(key), "goroutine监控中...")
			time.Sleep(2 * time.Second)
		}
	}
}

4.4 Derived contexts派生上下文

Context包提供了从现有Context值派生新Context值的函数。这些值形成一个树:当一个

Context被取消时,从它派生的所有Context也被取消。

package main

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

func work(ctx context.Context, str string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("退出 ", str)
			return
		}
	}
}

func main() {
	ctx1 := context.Background()
	ctx2, cancel2 := context.WithCancel(ctx1)
	ctx3, cancel3 := context.WithTimeout(ctx2, time.Second*5)
	ctx4, cancel4 := context.WithTimeout(ctx3, time.Second*3)
	ctx5, cancel5 := context.WithTimeout(ctx4, time.Second*6)
	ctx6 := context.WithValue(ctx5, "userID", 12)
	go work(ctx1, "ctx1")
	go work(ctx2, "ctx2")
	go work(ctx3, "ctx3")
	go work(ctx4, "ctx4")
	go work(ctx5, "ctx5")
	go work(ctx6, "ctx6")

	time.Sleep(1 * time.Second)
	cancel5()
	time.Sleep(5 * time.Second)
	cancel3()
	cancel4()
	cancel5()
	cancel2()
}

推荐教程

https://geektutu.com/post/geecache-day1.html

Golang 如何正确使用 Context

https://studygolang.com/articles/23247?fr=sidebar 

cgo go和c混编

#include <stdio.h>  
#include <string.h>
char *fun(char *p1, char *p2)
{
    int i = 0;
    i = strcmp(p1, p2);
    if (0 == i)
    {
        return (p1);
    }
    else
    {
        return (p2);
    }
}
int main()
{
    char *(*pf)(char *p1, char *p2);
    pf = &fun;
    (*pf)("aa", "bb");
    return (0);
}

2.1 Go语言网络编程和Redis实战

Go语言网络编程和常用库使用

1. 网络编程

目前主流服务器一般均采用的都是”Non-Block + I/O多路复用”(有的也结合了多线

程、多进程)。不过I/O多路复用也给使用者带来了不小的复杂度,以至于后续出

现了许多高性能的I/O多路复用框架, 比如libevent、libev、libuv等,以帮助开发者

简化开发复杂性,降低心智负担。不过Go的设计者似乎认为I/O多路复用的这种通

过回调机制割裂控制流 的方式依旧复杂,且有悖于“一般逻辑”设计,为此Go语言

将该“复杂性”隐藏在Runtime中了:Go开发者无需关注socket是否是 non-block的,

也无需亲自注册文件描述符的回调,只需在每个连接对应的goroutine中以“block

I/O”的方式对待socket处理即可

package main

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

func main() {
	//设置连接模式 , ip和端口号
	conn, err := net.Dial("tcp", "127.0.0.1:8888")
	if err != nil {
		fmt.Println("client dial err=", err)
		return
	}
	defer conn.Close()
	// 在命令行输入单行数据
	reader := bufio.NewReader(os.Stdin)
	for {
		//从终端读取一行用户的输入,并发给服务器
		line, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("readString err=", err)
		}
		//去掉输入后的换行符
		line = strings.Trim(line, "\r\n")
		//如果是exit,则退出客户端
		if line == "exit" {
			fmt.Println("客户端退出了")
			break
		}
		//将line发送给服务器
		n, e := conn.Write([]byte(line))
		if e != nil {
			fmt.Println("conn.write err=", e)
		}
		fmt.Printf("客户端发送了%d字节的数据\n", n)
	}
}
package main

import (
	"fmt"
	"net"
	_ "time"
)

func process(conn net.Conn) {
	//这里接受客户端的数据
	defer conn.Close()
	for {
		//创建一个新的切片
		buf := make([]byte, 1024)
		//等待客户端发送信息,如果客户端没发送,协程就阻塞在这
		// fmt.Printf("服务器在等待客户端%v的输入\n", conn.RemoteAddr().String())
		// conn.SetReadDeadline(time.Now().Add(time.Duration(1) * time.Second))
		n, err := conn.Read(buf) // 默认是阻塞的
		if err != nil {
			fmt.Println("服务器read err=", err)
			fmt.Println("客户端退出了")
			return
		}
		//显示客户端发送内容到服务器的终端
		fmt.Print(string(buf[:n]) + "\n")

	}
}

func main() {
	fmt.Println("服务器开始监听...")
	//协议、端口
	listen, err := net.Listen("tcp", "0.0.0.0:8888")
	if err != nil {
		fmt.Println("监听失败,err=", err)
		return
	}
	//延时关闭
	defer listen.Close() // 函数退出的时候调用
	for {
		//循环等待客户端连接
		fmt.Println("等待客户端连接...")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("Accept() err=", err)
		} else {
			fmt.Printf("Accept() suc con=%v,客户端Ip=%v\n", conn, conn.RemoteAddr().String())
		}
		//这里准备起个协程为客户端服务
		go process(conn)
	}
	//fmt.Printf("监听成功,suv=%v\n", listen)
}

1.0 TCP socket api

•Read(): 从连接上读取数据。

•Write(): 向连接上写入数据。

•Close(): 关闭连接。

•LocalAddr(): 返回本地网络地址。

•RemoteAddr(): 返回远程网络地址。

•SetDeadline(): 设置连接相关的读写最后期限。等价于同时

调用SetReadDeadline()和SetWriteDeadline()。

•SetReadDeadline(): 设置将来的读调用和当前阻塞的读调用

的超时最后期限。

•SetWriteDeadline(): 设置将来写调用以及当前阻塞的写调用

的超时最后期限。

1.1 TCP连接的建立

服务端是一个标准的Listen + Accept的结构(可参考上面的代码),而在客户端Go语言使用net.Dial()或net.DialTimeout()进行连接建立。

服务端

参考上一页

客户端

阻塞Dial: 超时机制的Dial:

1.2 客户端连接异常情况分析

1、网络不可达或对方服务未启动

2、对方服务的listen backlog满

3、网络延迟较大,Dial阻塞并超时

1.2.1 客户端连接异常-网络不可达或对方服务未启动

如果传给Dial的Addr是可以立即判断出网络不可达,或者Addr中端口对应的服务没有启动,

端口未被监听,Dial会几乎立即返回错误,比如:

package main

import (
	"log"
	"net"
)

func main() {
	log.Println("begin dial...")
	conn, err := net.Dial("tcp", ":8888")
	if err != nil {
		log.Println("dial error:", err)
		return
	}
	defer conn.Close()
	log.Println("dial ok")
}

1.2.2 客户端连接异常-对方服务的listen backlog满

对方服务器很忙,瞬间有大量client端连接尝试向server建立,server端的listen backlog队列满,

server accept不及时((即便不accept,那么在backlog数量范畴里面,connect都会是成功的,因

为new conn已经加入到server side的listen queue中了,accept只是从queue中取出一个conn而

已),这将导致client端Dial阻塞。

package main

import (
	"log"
	"net"
	"time"
)

func establishConn(i int) net.Conn {
	conn, err := net.Dial("tcp", ":8888")
	if err != nil {
		log.Printf("%d: dial error: %s", i, err)
		return nil
	}
	log.Println(i, ":connect to server ok")
	return conn
}

func main() {
	var sl []net.Conn
	for i := 1; i < 1000; i++ {
		conn := establishConn(i)
		if conn != nil {
			sl = append(sl, conn)
		}
	}

	time.Sleep(time.Second * 10000)
}

1.2.3 客户端连接异常-网络延迟较大,Dial阻塞并超时

如果网络延迟较大,TCP握手过程将更加艰难坎坷(各种丢包),时间消耗的自然也会更长。Dial这

时会阻塞,如果长时间依旧无法建立连接,则Dial也会返回“ getsockopt: operation timed out”错误

在连接建立阶段,多数情况下,Dial是可以满足需求的,即便阻塞一小会儿。但对于某些程序而言,

需要有严格的连接时间限定,如果一定时间内没能成功建立连接,程序可能会需要执行一段“异常”处

理逻辑,为此我们就需要DialTimeout了。

执行结果如下,需要模拟一个网络延迟大的环境

package main

import (
	"log"
	"net"
	"time"
)

func main() {
	log.Println("begin dial...")
	conn, err := net.DialTimeout("tcp", "192.168.204.130:8888", 2*time.Second)
	if err != nil {
		log.Println("dial error:", err)
		return
	}
	defer conn.Close()
	log.Println("dial ok")
}

 1.3 Socket读写

Dial成功后,方法返回一个net.Conn接口类型变量值,这个接口变量的动态类型为一个

*TCPConn:

1.3.1 conn.Read的行为特点

1 Socket中无数据

连接建立后,如果对方未发送数据到socket,接收方(Server)会阻塞在Read操作上,这和前面提到的“模型”原理是一致的。

执行该Read操作的goroutine也会被挂起。runtime会监视该socket,直到其有数据才会重新

调度该socket对应的Goroutine完成read。

2 Socket中有部分数据

如果socket中有部分数据,且长度小于一次Read操作所期望读出的数据长度,那么Read将会成功读出这部分数据并返回,而不是等

待所有期望数据全部读取后再返回。

3 Socket中有足够数据

如果socket中有数据,且长度大于等于一次Read操作所期望读出的数据长度,那么Read将会成功读出这部分数据并返回。这个

情景是最符合我们对Read的期待的了:Read将用Socket中的数据将我们传入的slice填满后返回:n = 10, err = nil

4 Socket关闭

有数据关闭是指在client关闭时,socket中还有server端未读取的数据。当client端close socket退出后,server依旧没有开始Read,

10s后第一次Read成功读出了所有的数据,当第二次Read时,由于client端 socket关闭,Read返回EOF error

无数据关闭情形下的结果,那就是Read直接返回EOF error

5 读取操作超时

有些场合对Read的阻塞时间有严格限制,在这种情况下,Read的行为到底是什么样的呢?在返回超时错误时,是否也同时Read了

一部分数据了呢?

不会出现“读出部分数据且返回超时错误”的情况

1.3.2 conn.Write的行为特点

1 成功写

前面例子着重于Read,client端在Write时并未判断Write的返回值。所谓“成功写”指的就是Write调用返回的n

与预期要写入的数据长度相等,且error = nil。这是我们在调用Write时遇到的最常见的情形,这里不再举例了

2 写阻塞

TCP连接通信两端的OS都会为该连接保留数据缓冲,一端调用Write后,实际上数据是写入到OS的协议栈的

数据缓冲的。TCP是全双工通信,因此每个方向都有独立的数据缓冲。当发送方将对方的接收缓冲区以及自

身的发送缓冲区写满后,Write就会阻塞

3 写入部分数据

Write操作存在写入部分数据的情况。没有按照预期的写入所有数据。这时候循环写入便是

综上例子,虽然Go给我们提供了阻塞I/O的便利,但在调用Read和Write时依旧要综合需要方法返回的n和err

的结果,以做出正确处理。net.conn实现了io.Reader和io.Writer接口,因此可以试用一些wrapper包进行

socket读写,比如bufio包下面的Writer和Reader、io/ioutil下的函数等

1.4 Goroutine safe

基于goroutine的网络架构模型,存在在不同goroutine间共享conn的情况,那么conn的读写是

否是goroutine safe的呢。

Write

Read 内部是goroutine安全的,内部都有Lock保护

1.5 Socket属性

SetKeepAlive

SetKeepAlivePeriod

SetLinger

SetNoDelay (默认no delay)

SetWriteBuffer

SetReadBuffer

要使用上面的Method的,需要type assertion

tcpConn, ok := conn.(*TCPConn)

if !ok { //error handle }

tcpConn.SetNoDelay(true)

1.6 关闭连接

socket是全双工的,client和server端在己方已关闭的socket和对方关闭的socket上操作的

结果有不同。

package main

import (
	"log"
	"net"
	"time"
)

func main() {
	log.Println("begin dial...")
	conn, err := net.Dial("tcp", ":8888")
	if err != nil {
		log.Println("dial error:", err)
		return
	}
	conn.Close()
	log.Println("close ok")

	var buf = make([]byte, 32)
	n, err := conn.Read(buf)
	if err != nil {
		log.Println("read error:", err)
	} else {
		log.Printf("read % bytes, content is %s\n", n, string(buf[:n]))
	}

	n, err = conn.Write(buf)
	if err != nil {
		log.Println("write error:", err)
	} else {
		log.Printf("write % bytes, content is %s\n", n, string(buf[:n]))
	}

	time.Sleep(time.Second * 1000)
}
package main

import (
	"fmt"
	"log"
	"net"
)

func handleConn(c net.Conn) {
	defer c.Close()

	// read from the connection
	var buf = make([]byte, 10)
	log.Println("start to read from conn")
	n, err := c.Read(buf)
	if err != nil {
		log.Println("conn read error:", err)
	} else {
		log.Printf("read %d bytes, content is %s\n", n, string(buf[:n]))
	}

	n, err = c.Write(buf)
	if err != nil {
		log.Println("conn write error:", err)
	} else {
		log.Printf("write %d bytes, content is %s\n", n, string(buf[:n]))
	}
}

func main() {
	listen, err := net.Listen("tcp", ":8888")
	if err != nil {
		fmt.Println("listen error: ", err)
		return
	}

	for {
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("accept error: ", err)
			break
		}

		// start a new goroutine to handle the new connection
		go handleConn(conn)
	}
}

1-7 读写超时

SetDeadline(t time.Time) error 设置读写超时

SetReadDeadline(t time.Time) error 设置读超时

SetWriteDeadline(t time.Time) error 设置写超时

package main

import (
	"log"
	"net"
	"os"
	"time"
)

func main() {
	connTimeout := 3 * time.Second
	conn, err := net.DialTimeout("tcp", "127.0.0.1:8080", connTimeout) // 3s timeout
	if err != nil {
		log.Println("dial failed:", err)
		os.Exit(1)
	}
	defer conn.Close()

	readTimeout := 2 * time.Second

	buffer := make([]byte, 512)

	for {
		err = conn.SetReadDeadline(time.Now().Add(readTimeout)) // timeout
		if err != nil {
			log.Println("setReadDeadline failed:", err)
		}

		n, err := conn.Read(buffer)
		if err != nil {
			log.Println("Read failed:", err)
			//break
		}

		log.Println("count:", n, "msg:", string(buffer))
	}

}
package main

import (
	"log"
	"net"
	"time"
)

func main() {
	addr := "0.0.0.0:8080"

	tcpAddr, err := net.ResolveTCPAddr("tcp", addr)

	if err != nil {
		log.Fatalf("net.ResovleTCPAddr fail:%s", addr)
	}

	listener, err := net.ListenTCP("tcp", tcpAddr)
	if err != nil {
		log.Fatalf("listen %s fail: %s", addr, err)
	} else {
		log.Println("listening", addr)
	}

	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Println("listener.Accept error:", err)
			continue
		}

		go handleConnection(conn)
	}

}

func handleConnection(conn net.Conn) {
	defer conn.Close()

	var buffer []byte = []byte("You are welcome. I'm server.")

	for {
		time.Sleep(3 * time.Second) // sleep 3s
		n, err := conn.Write(buffer)
		if err != nil {
			log.Println("Write error:", err)
			break
		}
		log.Println("send:", n)
	}

	log.Println("connetion end")

}

2. Redis库redigo

2 redis

参考文档: github.com/garyburd/redigo/redis

https://pkg.go.dev/github.com/garyburd/redigo/redis#pkg-index

使用第三方开源的redis库: github.com/gomodule/redigo/redis

import(

“go get github.com/gomodule/redigo/redis”

)

go get github.com/gomodule/redigo/redis

2.1 连接redis

package main

import (
	"fmt"
	// "time"

	"github.com/gomodule/redigo/redis"
)

func main() {
	c, err := redis.Dial("tcp", "192.168.204.132:6379")
	c, err := redis.Dial("tcp", "192.168.204.132:6379", 
		redis.DialConnectTimeout(time.Duration(1) * time.Second),
		redis.DialPassword("111"),
		redis.DialDatabase(1))
	if err != nil {
		fmt.Println("conn redis failed,", err)
		return
	}

	defer c.Close()
}

2.2 redis set操作

package main

import (
	"fmt"

	"github.com/gomodule/redigo/redis"
)

func main() {
	c, err := redis.Dial("tcp", "192.168.204.132:6379")
	if err != nil {
		fmt.Println("conn redis failed,", err)
		return
	}

	defer c.Close()
	_, err = c.Do("Set", "count", 100)
	if err != nil {
		fmt.Println(err)
		return
	}

	r, err := redis.Int(c.Do("Get", "count"))
	if err != nil {
		fmt.Println("get count failed,", err)
		return
	}

	fmt.Println(r)
}

2.3 redis Hash操作

package main

import (
	"fmt"

	"github.com/gomodule/redigo/redis"
)

func main() {
	c, err := redis.Dial("tcp", "192.168.204.132:6379")
	if err != nil {
		fmt.Println("conn redis failed,", err)
		return
	}

	defer c.Close()
	_, err = c.Do("HSet", "books", "count", 100)
	if err != nil {
		fmt.Println(err)
		return
	}

	r, err := redis.Int(c.Do("HGet", "books", "count"))
	if err != nil {
		fmt.Println("get count failed,", err)
		return
	}

	fmt.Println(r)
}

2.4 redis mset操作

package main

import (
	"fmt"

	"github.com/gomodule/redigo/redis"
)

func main() {
	c, err := redis.Dial("tcp", "192.168.204.132:6379")
	if err != nil {
		fmt.Println("conn redis failed,", err)
		return
	}

	defer c.Close()
	_, err = c.Do("MSet", "count", 100, "efg", 300)
	if err != nil {
		fmt.Println(err)
		return
	}

	r, err := redis.Ints(c.Do("MGet", "count", "efg"))
	if err != nil {
		fmt.Println("get count failed,", err)
		return
	}

	for _, v := range r {
		fmt.Println(v)
	}
}

2.5 redis expire操作

2.6 redis list操作

2-7 subpub操作 

package main

import (
	"fmt"
	"time"

	red "github.com/gomodule/redigo/redis"
)

type Redis struct {
	pool *red.Pool
}

var redis *Redis

func initRedis() {
	redis = new(Redis)
	redis.pool = &red.Pool{
		MaxIdle:     256,
		MaxActive:   0,
		IdleTimeout: time.Duration(120),
		Dial: func() (red.Conn, error) {
			return red.Dial(
				"tcp",
				"127.0.0.1:6379",
				red.DialReadTimeout(time.Duration(1000)*time.Millisecond),
				red.DialWriteTimeout(time.Duration(1000)*time.Millisecond),
				red.DialConnectTimeout(time.Duration(1000)*time.Millisecond),
				red.DialDatabase(0),
				//red.DialPassword(""),
			)
		},
	}
}

func Exec(cmd string, key interface{}, args ...interface{}) (interface{}, error) {
	con := redis.pool.Get()
	if err := con.Err(); err != nil {
		return nil, err
	}
	defer con.Close()
	parmas := make([]interface{}, 0)
	parmas = append(parmas, key)

	if len(args) > 0 {
		for _, v := range args {
			parmas = append(parmas, v)
		}
	}
	return con.Do(cmd, parmas...)
}

func main() {
	initRedis()

	Exec("set", "hello", "world")
	fmt.Print(2)
	result, err := Exec("get", "hello")
	if err != nil {
		fmt.Print(err.Error())
	}
	str, _ := red.String(result, err)
	fmt.Print(str)
}
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/gomodule/redigo/redis"
)

// listenPubSubChannels listens for messages on Redis pubsub channels. The
// onStart function is called after the channels are subscribed. The onMessage
// function is called for each message.
func listenPubSubChannels(ctx context.Context, redisServerAddr string,
	onStart func() error,
	onMessage func(channel string, data []byte) error,
	channels ...string) error {
	// A ping is set to the server with this period to test for the health of
	// the connection and server.
	const healthCheckPeriod = time.Minute

	c, err := redis.Dial("tcp", redisServerAddr,
		// Read timeout on server should be greater than ping period.
		redis.DialReadTimeout(healthCheckPeriod+10*time.Second),
		redis.DialWriteTimeout(10*time.Second))
	if err != nil {
		return err
	}
	defer c.Close()

	psc := redis.PubSubConn{Conn: c}

	if err := psc.Subscribe(redis.Args{}.AddFlat(channels)...); err != nil {
		return err
	}

	done := make(chan error, 1)

	// Start a goroutine to receive notifications from the server.
	go func() {
		for {
			switch n := psc.Receive().(type) {
			case error:
				done <- n
				return
			case redis.Message:
				if err := onMessage(n.Channel, n.Data); err != nil {
					done <- err
					return
				}
			case redis.Subscription:
				switch n.Count {
				case len(channels):
					// Notify application when all channels are subscribed.
					if err := onStart(); err != nil {
						done <- err
						return
					}
				case 0:
					// Return from the goroutine when all channels are unsubscribed.
					done <- nil
					return
				}
			}
		}
	}()

	ticker := time.NewTicker(healthCheckPeriod)
	defer ticker.Stop()
loop:
	for err == nil {
		select {
		case <-ticker.C:
			// Send ping to test health of connection and server. If
			// corresponding pong is not received, then receive on the
			// connection will timeout and the receive goroutine will exit.
			if err = psc.Ping(""); err != nil {
				break loop
			}
		case <-ctx.Done():
			break loop
		case err := <-done:
			// Return error from the receive goroutine.
			return err
		}
	}

	// Signal the receiving goroutine to exit by unsubscribing from all channels.
	psc.Unsubscribe()

	// Wait for goroutine to complete.
	return <-done
}

func publish() {
	c, err := dial()
	if err != nil {
		fmt.Println(err)
		return
	}
	defer c.Close()

	c.Do("PUBLISH", "c1", "hello")
	c.Do("PUBLISH", "c2", "world")
	c.Do("PUBLISH", "c1", "goodbye")
}

// This example shows how receive pubsub notifications with cancelation and
// health checks.
func main() {
	redisServerAddr := "192.168.204.132:6379"

	ctx, cancel := context.WithCancel(context.Background())

	err := listenPubSubChannels(ctx,
		redisServerAddr,
		func() error {
			// The start callback is a good place to backfill missed
			// notifications. For the purpose of this example, a goroutine is
			// started to send notifications.
			go publish()
			return nil
		},
		func(channel string, message []byte) error {
			fmt.Printf("channel: %s, message: %s\n", channel, message)

			// For the purpose of this example, cancel the listener's context
			// after receiving last message sent by publish().
			if string(message) == "goodbye" {
				cancel()
			}
			return nil
		},
		"c1", "c2")

	if err != nil {
		fmt.Println(err)
		return
	}

}

2-8 连接池

MaxIdle:最大的空闲连接数,表示即使没有redis连接时依然可以保持N个空闲的连接,而不

被清除,随时处于待命状态。

MaxActive:最大的连接数,表示同时最多有N个连接。0表示不限制。

IdleTimeout:最大的空闲连接等待时间,超过此时间后,空闲连接将被关闭。如果设置成0,

空闲连接将不会被关闭。应该设置一个比redis服务端超时时间更短的时间。

DialConnectTimeout:连接Redis超时时间。

DialReadTimeout:从Redis读取数据超时时间。

DialWriteTimeout:向Redis写入数据超时时间。

连接流程大概是这样的

1.尝试从空闲列表MaxIdle中,获得一个可用连接;如果成功直接返回,失败则尝试步骤2

2.如果当前的MaxIdle < 连接数 < MaxActive,则尝试创建一个新连接,失败则尝试步骤3

3. 如果连接数 > MaxActive就等待,直到满足步骤2的条件,重复步骤2

2-8 redis连接池的坑

package main

import (
	"fmt"
	"time"

	red "github.com/gomodule/redigo/redis"
)

type Redis struct {
	pool *red.Pool
}

var redis *Redis

func initRedis() {
	redis = new(Redis)
	redis.pool = &red.Pool{
		MaxIdle:     256,
		MaxActive:   0,
		IdleTimeout: time.Duration(120),
		Dial: func() (red.Conn, error) {
			return red.Dial(
				"tcp",
				"127.0.0.1:6379",
				red.DialReadTimeout(time.Duration(1000)*time.Millisecond),
				red.DialWriteTimeout(time.Duration(1000)*time.Millisecond),
				red.DialConnectTimeout(time.Duration(1000)*time.Millisecond),
				red.DialDatabase(0),
				//red.DialPassword(""),
			)
		},
	}
}

func Exec(cmd string, key interface{}, args ...interface{}) (interface{}, error) {
	con := redis.pool.Get()
	if err := con.Err(); err != nil {
		return nil, err
	}
	defer con.Close()
	parmas := make([]interface{}, 0)
	parmas = append(parmas, key)

	if len(args) > 0 {
		for _, v := range args {
			parmas = append(parmas, v)
		}
	}
	return con.Do(cmd, parmas...)
}

func main() {
	initRedis()

	Exec("set", "hello", "world")
	fmt.Print(2)
	result, err := Exec("get", "hello")
	if err != nil {
		fmt.Print(err.Error())
	}
	str, _ := red.String(result, err)
	fmt.Print(str)
}

遇到过的问题

目前为止,连接池的问题只遇到过一次问题,而且是在测试环境的,当时的配置是

DialConnectTimeout:time.Duration(200)*time.Millisecond

DialReadTimeout:time.Duration(200)*time.Millisecond

DialWriteTimeout:time.Duration(200)*time.Millisecond

配置的都是200毫秒。有一次使用hgetall的时候,就一直报错,大概类似下面的提示

read tcp 127.0.0.1:6379: i/o timeout

字面意思就是 read tcp 超时,可能某些写入大点数据的时候也会报,write tcp

timeout。

后来将读写超时时间都改为1000毫秒,就再也没有出现过类似的报错。

想了解更多的Redis使用,可以看下官方的文档:

github.com/gomodule/redigo/redis

3. 临时对象池sync.Pool

3 临时对象池

sync.Pool类型值作为存放临时值的容器。此类容器是自动伸缩的,高效的,同时也是并发

安全的。

sync.Pool类型只有两个方法:

◼ Put,用于在当前的池中存放临时对象,它接受一个空接口类型的值

◼ Get,用于从当前的池中获取临时对象,它返回一个空接口类型的值

New字段

sync.Pool类型的New字段是一个创建临时对象的函数。它的类型是没有参数但是会返回一个空接口

类型的函数。即:func() interface{}。

这个函数是Get方法最后的获取到临时对象的手段。函数的结果不会被存入当前的临时对象池中,

而是直接返回给Get方法的调用方。

这里的New字段的实际值需要在初始化临时对象池的时候就给定。否则,在Get方法调用它的时候

就会得到nil。

package main

import (
	"bytes"
	"fmt"
	"io"
	"sync"
)

// 存放数据块缓冲区的临时对象
var bufPool sync.Pool

// 预定义定界符
const delimiter = '\n'

// 一个简易的数据库缓冲区的接口
type Buffer interface {
	Delimiter() byte                    // 获取数据块之间的定界符
	Write(contents string) (err error)  // 写入一个数据块
	Read() (contents string, err error) // 读取一个数据块
	Free()                              // 释放当前的缓冲区
}

// 实现一个上面定义的接口
type myBuffer struct {
	buf       bytes.Buffer
	delimiter byte
}

func (b *myBuffer) Delimiter() byte {
	return b.delimiter
}

func (b *myBuffer) Write(contents string) (err error) {
	if _, err = b.buf.WriteString(contents); err != nil {
		return
	}
	return b.buf.WriteByte(b.delimiter)
}

func (b *myBuffer) Read() (contents string, err error) {
	return b.buf.ReadString(b.delimiter)
}

func (b *myBuffer) Free() {
	bufPool.Put(b)
}

func init() {
	bufPool = sync.Pool{
		New: func() interface{} {
			return &myBuffer{delimiter: delimiter}
		},
	}
}

// 获取一个数据库缓冲区
func GetBuffer() Buffer {
	return bufPool.Get().(Buffer) // 做类型转换
}

func main() {
	buf := GetBuffer()
	defer buf.Free()
	buf.Write("写入第一行,")	// 写入数据
	buf.Write("接着写第二行。")	// 写入数据
	fmt.Println("数据已经写入,准备把数据读出")
	for {
		block, err := buf.Read()
		if err != nil {
			if err == io.EOF {
				break
			}
			panic(fmt.Errorf("读取缓冲区时ERROR: %s", err))
		}
		fmt.Print(block)
	}
}
package main

import (
	"fmt"
	_ "runtime"
	"runtime/debug"
	"sync"
	"sync/atomic"
)

func main() {
	// 禁用GC,并保证在main函数执行结束前恢复GC
	defer debug.SetGCPercent(debug.SetGCPercent(-1))
	var count int32

	// 实现一个函数 ,生成新对象
	newFunc := func() interface{} {
		fmt.Println("newFunc:", count)
		return atomic.AddInt32(&count, 1)
	}
	pool := sync.Pool{New: newFunc}	// 传入生成对象的函数....

	// New 字段值的作用
	v1 := pool.Get()	// 调用GET接口去取
	fmt.Printf("v1: %v\n", v1)
	pool.Put(v1)		// 放回去
	// 临时对象池的存取
	pool.Put(newFunc())
	// pool.Put(newFunc())
	// pool.Put(newFunc())
	v2 := pool.Get()
	// pool.Put(v2)
	fmt.Printf("v2: %v\n", v2)		// 这个时候v1和v2应该是一样

	// 垃圾回收对临时对象池的影响
	// debug.SetGCPercent(100)
	// runtime.GC()
	v3 := pool.Get()
	fmt.Printf("v3: %v\n", v3)
	pool.New = nil
	v4 := pool.Get()
	fmt.Printf("v4: %v\n", v4)
}

3.1 Get

Pool 会为每个 P 维护一个本地池,P 的本地池分为 私有池 private和共享池 shared。私有池

中的元素只能本地 P 使用,共享池中的元素可能会被其他 P 偷走,所以使用私有池 private

时不用加锁,而使用共享池 shared 时需加锁。

Get 会优先查找本地 private,再查找本地 shared,最后查找其他 P 的shared,如果以上全部

没有可用元素,最后会调用 New 函数获取新元素

3.2 Put

Put 优先把元素放在 private 池中;如果 private 不为空,则放在 shared 池中。有趣

的是,在入池之前,该元素有 1/4 可能被丢掉。

 

4. 配置文件读取goconfig

4 配置文件解析器goconfig的使用

ini配置文件读写 conf.ini

;redis cache
USER_LIST = USER:LIST
MAX_COUNT = 50
MAX_PRICE = 123456
IS_SHOW = true

[test]
dbdns = root:@tcp(127.0.0.1:3306)

[prod]
dbdns = root:@tcp(172.168.1.1:3306)

4-1-config-ini.go

package main

import (
	"fmt"
	"log"

	"github.com/Unknwon/goconfig"
)

func main() {
	cfg, err := goconfig.LoadConfigFile("./conf.ini")	// 读取后文件关闭了
	if err != nil {
		log.Fatalf("无法加载配置文件:%s", err)
	}
	userListKey, err := cfg.GetValue("", "USER_LIST")
	if err != nil {
		fmt.Println(err.Error())
	}
	fmt.Println(userListKey)
	userListKey2, _ := cfg.GetValue(goconfig.DEFAULT_SECTION, "USER_LIST")
	fmt.Println(userListKey2)
	maxCount := cfg.MustInt("", "MAX_COUNT")
	fmt.Println(maxCount)
	maxPrice := cfg.MustFloat64("", "MAX_PRICE")
	fmt.Println(maxPrice)
	isShow := cfg.MustBool("", "IS_SHOW")
	fmt.Println(isShow)

	db := cfg.MustValue("test", "dbdns")
	fmt.Println(db)

	dbProd := cfg.MustValue("prod", "dbdns")
	fmt.Println("dbProd: ",dbProd)

	//set 值
	cfg.SetValue("", "MAX_NEW", "100")
	maxNew := cfg.MustInt("", "MAX_NEW")
	fmt.Println(maxNew)

	maxNew1, err := cfg.Int("", "MAX_NEW")
	if err != nil {
		fmt.Println(err.Error())
	}
	fmt.Println(maxNew1)
	cfg.AppendFiles("conf1.ini")
	// cfg.DeleteKey("", "MAX_NEW")
}

5. 解析命令行flag

5 命令行解析Go flag

cmd -flag // 只支持bool类型

cmd -flag=xxx

cmd -flag xxx // 只支持非bool类型

1. 定义flag参数

参数有三个:第一个为 参数名称,第二个为 默认值,第三个是 使用说明

(1)通过 flag.String(),Bool(),Int() 等 flag.Xxx() 方法,该种方式返回一个相应的指针

var ip = flag.Int("flagname", 1234, "help message for flagname")

(2)通过 flag.XxxVar() 方法将 flag 绑定到一个变量,该种方式返回 值类型

var flagvar int

flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname")

(3)通过 flag.Var() 绑定自定义类型,自定义类型需要实现 Value 接口 (Receiver 必须为指针)

fmt.Println("flagvar has value ", flagvar)

5-1-cli-flag.go

package main

import (
	"flag"
	"fmt"
	//    "os"
)

// go run 4-1-cli-flag.go -ok -id 11111 -port 8899 -name TestUser very goo
func main() {
	//    fmt.Println(os.Args)
	ok := flag.Bool("ok", false, "is ok")		// 不设置ok 则为false
	id := flag.Int("id", 0, "id")
	port := flag.String("port", ":8080", "http listen port")
	var name string
	flag.StringVar(&name, "name", "Jack", "name")

	flag.Parse()
	//    flag.Usage()
	others := flag.Args()

	fmt.Println("ok:", *ok)
	fmt.Println("id:", *id)
	fmt.Println("port:", *port)
	fmt.Println("name:", name)
	fmt.Println("other:", others)
}

5-2-self-flag.go

package main

import (
	"flag"
	"fmt"
)

type FlagSet struct {
	Usage func()
}

var myFlagSet = flag.NewFlagSet("myflagset", flag.ExitOnError)
var stringFlag = myFlagSet.String("abc", "default value", "help mesage")

func main() {
	myFlagSet.Parse([]string{"-abc", "def", "ghi", "123"})
	args := myFlagSet.Args()
	for i := range args {
		fmt.Println(i, myFlagSet.Arg(i))
	}
}

6. uuid生成方案

6 uuid

雪花算法 Snowflake & Sonyflake https://www.cnblogs.com/li-peng/p/12124249.html

常用uuid性能测试

snowflake算法使用的一个64 bit的整型数据,被划分为四部分。

1.不含开头的第一个bit,因为是符号位;

2.41bit来表示收到请求时的时间戳,精确到1毫秒;

3.5bit表示数据中心的id, 5bit表示机器实例id

4.共计10bit的机器位,因此能部署在1024台机器节点上生

成ID;

5.12bit循环自增序列号,增至最大后归0,1毫秒最大生成

唯一ID的数量是4096个。

snowflake

sonyflake 这里时间戳用39位精确到10ms,所以可以达到174年,比

snowflake的长很久。

8bit 做为序列号,每10毫最大生成256个,1秒最多生成

25600个,比原生的Snowflake少好多,如果感觉不够用,

目前的解决方案是跑多个实例生成同一业务的ID来弥补。

16bit 做为机器号,默认的是当前机器的私有IP的最后两位。

sonyflake对于snowflake的改进是用空间换时间,时间戳位数减少,以从69年升至174年。

但是1秒最多生成的ID从409.6w降至2.56w条。

  

6-1-1-uuid.go

package main

import (
	"fmt"
	"log"
	"math/rand"
	"reflect"
	"time"

	"gitee.com/GuaikOrg/go-snowflake/snowflake"
	"github.com/chilts/sid"
	"github.com/kjk/betterguid"
	"github.com/oklog/ulid"
	"github.com/rs/xid"
	uuid "github.com/satori/go.uuid"
	"github.com/segmentio/ksuid"
	"github.com/sony/sonyflake"
)

const FOR_LOOP = 100000

func genXid() {
	id := xid.New()
	fmt.Printf("github.com/rs/xid:           %s, len:%d\n", id.String(), len(id.String()))
}

func genKsuid() {
	id := ksuid.New()
	fmt.Printf("github.com/segmentio/ksuid:  %s, len:%d\n", id.String(), len(id.String()))
}

func genBetterGUID() {
	id := betterguid.New()
	fmt.Printf("github.com/kjk/betterguid:   %s, len:%d\n", id, len(id))
}

func genUlid() {
	t := time.Now().UTC()
	entropy := rand.New(rand.NewSource(t.UnixNano()))
	id := ulid.MustNew(ulid.Timestamp(t), entropy)
	fmt.Printf("github.com/oklog/ulid:       %s, len:%d\n", id.String(), len(id.String()))
}

// https://gitee.com/GuaikOrg/go-snowflake
func genSnowflake() {
	flake, err := snowflake.NewSnowflake(int64(0), int64(0))
	if err != nil {
		log.Fatalf("snowflake.NewSnowflake failed with %s\n", err)
	}
	id := flake.NextVal()
	fmt.Printf("gitee.com/GuaikOrg/go-snowflake:%x, type:%s\n", id, reflect.TypeOf(id))
}

func genSonyflake() {
	flake := sonyflake.NewSonyflake(sonyflake.Settings{})
	id, err := flake.NextID()
	if err != nil {
		log.Fatalf("flake.NextID() failed with %s\n", err)
	}
	fmt.Printf("github.com/sony/sonyflake:   %x, type:%s\n", id, reflect.TypeOf(id))
}

func genSid() {
	id := sid.Id()
	fmt.Printf("github.com/chilts/sid:       %s, len:%d\n", id, len(id))
}

func genUUIDv4() {
	id, err := uuid.NewV4()
	if err != nil {
		fmt.Printf("get uuid error [%s]", err)
	}
	fmt.Printf("github.com/satori/go.uuid:   %s, len:%d\n", id, len(id))
}

func testGenXid(n int) {
	t0 := time.Now()
	for i := 0; i < n; i++ {
		_ = xid.New()
	}
	elapsed := time.Since(t0)
	fmt.Println("github.com/rs/xid          n:", n, "time:", elapsed)
}

func testGenKsuid(n int) {
	t0 := time.Now()
	for i := 0; i < n; i++ {
		_ = ksuid.New()
	}
	elapsed := time.Since(t0)
	fmt.Println("github.com/segmentio/ksuid n:", n, "time:", elapsed)
}

func testGenBetterguid(n int) {
	t0 := time.Now()
	for i := 0; i < n; i++ {
		_ = betterguid.New()
	}
	elapsed := time.Since(t0)
	fmt.Println("github.com/kjk/betterguid  n:", n, "time:", elapsed)
}

func testGenUlid(n int) {
	t0 := time.Now()
	for i := 0; i < n; i++ {
		t := time.Now().UTC()
		entropy := rand.New(rand.NewSource(t.UnixNano()))
		_ = ulid.MustNew(ulid.Timestamp(t), entropy)
	}
	elapsed := time.Since(t0)
	fmt.Println("github.com/oklog/ulid      n:", n, "time:", elapsed)
}

func testGenSnowflake(n int) {
	t0 := time.Now()
	flake, err := snowflake.NewSnowflake(int64(0), int64(0))
	if err != nil {
		log.Fatalf("snowflake.NewSnowflake failed with %s\n", err)
	}
	for i := 0; i < n; i++ {
		_ = flake.NextVal()
	}
	elapsed := time.Since(t0)
	fmt.Println("gitee.com/GuaikOrg/go-snowflake n:", n, "time:", elapsed)
}
func testGenSonyflake(n int) {
	t0 := time.Now()
	flake := sonyflake.NewSonyflake(sonyflake.Settings{}) // 注意这一行的位置
	for i := 0; i < n; i++ {
		_, err := flake.NextID()
		if err != nil {
			log.Fatalf("flake.NextID() failed with %s\n", err)
		}
	}
	elapsed := time.Since(t0)
	fmt.Println("github.com/sony/sonyflake  n:", n, "time:", elapsed)
}

func testGenSid(n int) {
	t0 := time.Now()
	for i := 0; i < n; i++ {
		_ = sid.Id()
	}
	elapsed := time.Since(t0)
	fmt.Println("github.com/chilts/sid      n:", n, "time:", elapsed)
}

func testGenUUIDv4(n int) {
	t0 := time.Now()
	for i := 0; i < n; i++ {
		_, err := uuid.NewV4()
		if err != nil {
			fmt.Printf("get uuid error [%s]", err)
		}
	}
	elapsed := time.Since(t0)
	fmt.Println("github.com/satori/go.uuid  n:", n, "time:", elapsed)
}

func main() {
	fmt.Printf("效果展示...\n")
	genXid()
	genXid()
	genXid()
	genKsuid()
	genBetterGUID()
	genUlid()
	genSnowflake()
	genSonyflake()
	genSid()
	genUUIDv4()
	fmt.Printf("性能测试...\n")
	testGenXid(FOR_LOOP)
	testGenKsuid(FOR_LOOP)
	testGenBetterguid(FOR_LOOP)
	testGenUlid(FOR_LOOP)
	testGenSnowflake(FOR_LOOP)
	testGenSonyflake(FOR_LOOP)
	testGenSid(FOR_LOOP)
	testGenUUIDv4(FOR_LOOP)
}

// github.com/rs/xid          n: 1000000 time: 29.2665ms
// github.com/segmentio/ksuid n: 1000000 time: 311.4816ms
// github.com/kjk/betterguid  n: 1000000 time: 89.2803ms
// github.com/oklog/ulid   n: 1000000 time: 11.746259s
// github.com/sony/sonyflake   n: 1000000 time: 39.0713342s
// thub.com/chilts/sid        n: 1000000 time: 254.9442ms
// github.com/satori/go.uuid     n: 1000000 time: 270.3201ms

2.2 Go语言Web开发与数据库实战

1 HTTP编程

a. Go原生支持http,import(“net/http”)

b. Go的http服务性能和nginx比较接近

c. 几行代码就可以实现一个web服务

1.1 HTTP常见请求方法

5. http常见请求方法

1)Get请求

2)Post请求

3)Put请求

4)Delete请求

5)Head请求

1.2 http 常见状态码

http 常见状态码

http.StatusContinue = 100

http.StatusOK = 200

http.StatusFound = 302

http.StatusBadRequest = 400

http.StatusUnauthorized = 401

http.StatusForbidden = 403

http.StatusNotFound = 404

http.StatusInternalServerError = 500

2 Client客户端

http包提供了很多访问Web服务器的函数,比如http.Get()、http.Post()、http.Head()等,

读到的响应报文数据被保存在 Response 结构体中。

Response结构体的定义

type Response struct {

Status string // e.g. "200 OK"

StatusCode int // e.g. 200

Proto string // e.g. "HTTP/1.0"

ProtoMajor int // e.g. 1

ProtoMinor int // e.g. 0

Header Header

Body io.ReadCloser

//...

}

服务器发送的响应包体被保存在Body中。可以使用它提供的Read方法来获取数据内容。保存至切片

缓冲区中,拼接成一个完整的字符串来查看。

结束的时候,需要调用Body中的Close()方法关闭io。

2.1基本的HTTP/HTTPS请求

Get、Head、Post和PostForm函数发出HTTP/HTTPS请求

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)

// ...

2.2.1 不带参数的Get方法示例

2-2-1-http-get-client.go

package main

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

func main() {
	// resp, err := http.Get("http://127.0.0.1:9000")
	resp, err := http.Get("https://www.baidu.com/")
	if err != nil {
		fmt.Println("get err:", err)
		return
	}
	defer resp.Body.Close() // 做关闭
	// data byte
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("get data err:", err)
		return
	}

	fmt.Println("body:", string(data))
	fmt.Println("resp:", resp)
}

2.2.2 带参数的Get方法示例

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

2-2-2-http-get-client.go

package main

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

func main() {

	//1.处理请求参数
	params := url.Values{}
	params.Set("name", "dar")
	params.Set("hobby", "足球")

	//2.设置请求URL
	rawUrl := "http://127.0.0.1:9000"
	reqURL, err := url.ParseRequestURI(rawUrl)
	if err != nil {
		fmt.Printf("url.ParseRequestURI()函数执行错误,错误为:%v\n", err)
		return
	}

	//3.整合请求URL和参数
	//Encode方法将请求参数编码为url编码格式("bar=baz&foo=quux"),编码时会以键进行排序。
	reqURL.RawQuery = params.Encode()

	//4.发送HTTP请求
	//说明: reqURL.String() String将URL重构为一个合法URL字符串。
	fmt.Println("Get url:", reqURL.String())
	resp, err := http.Get(reqURL.String())
	if err != nil {
		fmt.Printf("http.Get()函数执行错误,错误为:%v\n", err)
		return
	}
	defer resp.Body.Close()

	//5.一次性读取响应的所有内容
	body, err := ioutil.ReadAll(resp.Body)

	if err != nil {
		fmt.Printf("ioutil.ReadAll()函数执行出错,错误为:%v\n", err)
		return
	}

	fmt.Println("Response: ", string(body))
}

2-2-2-http-get-server.go

package main

import (
	"fmt"
	"net/http"
)
// 响应: http.ResponseWriter
// 请求:http.Request 
func myHandler(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	params := r.URL.Query()
	fmt.Println("r.URL: ", r.URL)
	fmt.Fprintln(w, "name:", params.Get("name"), "hobby:", params.Get("hobby")) // 回写数据
}
func main() {

	http.HandleFunc("/", myHandler)
	err := http.ListenAndServe("127.0.0.1:9000", nil)
	if err != nil {
		fmt.Printf("http.ListenAndServe()函数执行错误,错误为:%v\n", err)
		return
	}
}

2.3.1 post方法

发送POST请求的示例代码

2-3-1-http-post-client.go

package main

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

// net/http post demo

func main() {
	url := "http://127.0.0.1:9000/post"
	contentType := "application/json"
	data := `{"name":"darren","age":18}`
	resp, err := http.Post(url, contentType, strings.NewReader(data))
	if err != nil {
		fmt.Println("post failed, err:%v\n", err)
		return
	}
	defer resp.Body.Close()
	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("get resp failed,err:%v\n", err)
		return
	}
	fmt.Println("StatusCode:", resp.StatusCode)
	fmt.Println(string(b))
}

2-3-1-http-post-server.go

package main

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

func postHandler(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	fmt.Println("Method ", r.Method)
	if r.Method == "POST" {
		// 1. 请求类型是application/json时从r.Body读取数据
		b, err := ioutil.ReadAll(r.Body)
		if err != nil {
			fmt.Println("read request.Body failed, err:%v\n", err)
			return
		}
		fmt.Println(string(b))
		answer := `{"status": "ok"}`
		w.Write([]byte(answer))
	} else {
		fmt.Println("can't handle ", r.Method)
		w.WriteHeader(http.StatusBadRequest)
	}

}
func main() {

	http.HandleFunc("/post", postHandler)
	err := http.ListenAndServe("0.0.0.0:9000", nil)
	if err != nil {
		fmt.Printf("http.ListenAndServe()函数执行错误,错误为:%v\n", err)
		return
	}
}

2.4 head方法-client

HEAD请求常常被忽略,但是能提供很多有

用的信息,特别是在有限的速度和带宽下。

主要有以下特点:

1、只请求资源的首部;

2、检查超链接的有效性;

3、检查网页是否被修改;

4、多用于自动搜索机器人获取网页的标志

信息,获取rss种子信息,或者传递安全认证

信息等

package main

import (
	"fmt"
	"net"
	"net/http"
	"time"
)

func main() {

	url := "http://www.baidu1.com"
	c := http.Client{
		Transport: &http.Transport{
			Dial: func(network, addr string) (net.Conn, error) {
				timeout := time.Second * 2
				return net.DialTimeout(network, addr, timeout)
			},
		},
	}
	resp, err := c.Head(url)
	if err != nil {
		fmt.Printf("head %s failed, err:%v\n", url, err)
	} else {
		fmt.Printf("%s head succ, status:%v\n", url, resp.Status)
	}

}

2.5 表单处理

package main

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

const form = `<html><body><form action="#" method="post" name="bar">
                    <input type="text" name="in"/>
                    <input type="text" name="in"/>
                     <input type="submit" value="Submit"/>
             </form></html></body>`

func HomeServer(w http.ResponseWriter, request *http.Request) {
	io.WriteString(w, "/test1 或者/test2")
	// io.WriteString(w, "<h1>/test1 或者/test2</h1>")
}

func SimpleServer(w http.ResponseWriter, request *http.Request) {
	io.WriteString(w, "<h1>hello, world</h1>")
}

func FormServer(w http.ResponseWriter, request *http.Request) {
	w.Header().Set("Content-Type", "text/html")
	switch request.Method {
	case "GET":
		io.WriteString(w, form)
	case "POST":
		request.ParseForm()
		fmt.Println("request.Form[in]:", request.Form["in"])
		io.WriteString(w, request.Form["in"][0])
		io.WriteString(w, "\n")
		io.WriteString(w, request.Form["in"][1])	// go web开发
		// var ptr *int
		// *ptr = 0x123445 // 模拟异常
	}
}
func main() {
	http.HandleFunc("/", HomeServer)
	http.HandleFunc("/test1", SimpleServer)
	http.HandleFunc("/test2", FormServer)
	err := http.ListenAndServe(":9000", nil)
	if err != nil {
		fmt.Printf("http.ListenAndServe()函数执行错误,错误为:%v\n", err)
		return
	}
}

2.6 panic处理

2-6-panic-server.go

package main

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

const form = `<html><body><form action="#" method="post" name="bar">
                    <input type="text" name="in"/>
                    <input type="text" name="in"/>
                     <input type="submit" value="Submit"/>
             </form></html></body>`

func HomeServer(w http.ResponseWriter, request *http.Request) {
	io.WriteString(w, "<h1>/test1 或者/test2</h1>")
}

func SimpleServer(w http.ResponseWriter, request *http.Request) {
	io.WriteString(w, "<h1>hello, world</h1>")
}

func FormServer(w http.ResponseWriter, request *http.Request) {
	w.Header().Set("Content-Type", "text/html")
	switch request.Method {
	case "GET":
		io.WriteString(w, form)
	case "POST":
		request.ParseForm()
		fmt.Println("request.Form[in]:", request.Form["in"])
		io.WriteString(w, request.Form["in"][0])
		io.WriteString(w, "\n")
		io.WriteString(w, request.Form["in"][1])
		// var ptr *int
		// *ptr = 0x123445 // 模拟异常  注意协程的异常处理
		var ptr *int
		var a int
		ptr = &a
		*ptr = 0x123445 // 也是可以取地址写入的
	}
}
func main() {
	http.HandleFunc("/", HomeServer)
	http.HandleFunc("/test1", logPanics(SimpleServer))
	http.HandleFunc("/test2", logPanics(FormServer))
	err := http.ListenAndServe(":9000", nil)
	if err != nil {
		fmt.Printf("http.ListenAndServe()函数执行错误,错误为:%v\n", err)
		return
	}
}

func logPanics(handle http.HandlerFunc) http.HandlerFunc {
	return func(writer http.ResponseWriter, request *http.Request) {
		defer func() {
			if x := recover(); x != nil {
				log.Printf("[%v] caught panic: %v", request.RemoteAddr, x)
			}
		}()
		handle(writer, request)
	}
}

3 模板

3 模板

1)替换 {{.字段名}}

3-1-template.go

package main

import (
	"fmt"
	"html/template"
	"io"
	"net/http"
)

var myTemplate *template.Template

type Result struct {
	output string
}

func (p *Result) Write(b []byte) (n int, err error) {
	fmt.Println("called by template")
	p.output += string(b)
	return len(b), nil
}

type Person struct {
	Name  string
	Title string
	Age   int
}

func userInfo(w http.ResponseWriter, r *http.Request) {
	fmt.Println("handle hello")
	//fmt.Fprintf(w, "hello ")
	var arr []Person
	p := Person{Name: "Dar", Age: 18, Title: "个人网站"}
	p1 := Person{Name: "Ki", Age: 19, Title: "个人网站"}
	p2 := Person{Name: "子", Age: 20, Title: "个人网站"}
	arr = append(arr, p)
	arr = append(arr, p1)
	arr = append(arr, p2)

	fmt.Println("arr:", arr)

	resultWriter := &Result{}
	io.WriteString(resultWriter, "hello 模板")
	err := myTemplate.Execute(w, arr) // 模板替换, 执行完后, html模板和参数arr就写入 w http.ResponseWriter
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println("template render data:", resultWriter.output)
	//myTemplate.Execute(w, p)
	//myTemplate.Execute(os.Stdout, p)
	//file, err := os.OpenFile("C:/test.log", os.O_CREATE|os.O_WRONLY, 0755)
	//if err != nil {
	//	fmt.Println("open failed err:", err)
	//	return
	//}

}

func initTemplate(filename string) (err error) {
	myTemplate, err = template.ParseFiles(filename)
	if err != nil {
		fmt.Println("parse file err:", err)
		return
	}
	return
}

func main() {
	initTemplate("./index.html")
	http.HandleFunc("/user/info", userInfo)
	err := http.ListenAndServe("0.0.0.0:9000", nil)
	if err != nil {
		fmt.Println("http listen failed")
	}
}

3.1 模板-替换 {{.字段名}}

4 Mysql

建库建表

在MySQL中创建一个名为go_test的数据库

CREATE DATABASE go_test;

进入该数据库:

use go_test;

创建一张用于测试的数据表:

CREATE TABLE `user` (

`id` BIGINT(20) NOT NULL AUTO_INCREMENT,

`name` VARCHAR(20) DEFAULT '',

`age` INT(11) DEFAULT '0',

PRIMARY KEY(`id`)

)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT

CHARSET=utf8mb4;

4.0 连接mysql

Open函数:

db, err := sql.Open("mysql", "用户名:密码@tcp(IP:端口)/数据库?charset=utf8")

例如:db, err := sql.Open("mysql", "root:1234@tcp(127.0.0.1:3306)/test?charset=utf8")

package main

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql" // 注释掉后异常 _ 调用初始化函数
)

// https://github.com/go-sql-driver/mysql#usage
func main() {
	db, err := sql.Open("mysql", "root:1234@tcp(127.0.0.1:3306)/go_test?charset=utf8mb4")
	fmt.Println("err:", err) // err: <nil>
	if db == nil {
		fmt.Println("db open failed:", err)
	}

	err = db.Ping() //Ping verifies a connection to the database is still alive, establishing a connection if necessary
	if err != nil {
		fmt.Println("数据库链接失败", err)
	}
	defer db.Close()
}

4-1 mysql插入数据

4-1-mysql.go

package main

import (
	"database/sql"
	"fmt"

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

// 插入数据
func insertRowDemo(db *sql.DB) {
	sqlStr := "insert into user(name, age) values (?,?)"
	ret, err := db.Exec(sqlStr, "darren", 18)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	theID, err := ret.LastInsertId() // 新插入数据的id
	if err != nil {
		fmt.Printf("get lastinsert ID failed, err:%v\n", err)
		return
	}
	fmt.Printf("insert success, the id is %d.\n", theID)
}
func main() {
	db, err := sql.Open("mysql", "root:1234@tcp(127.0.0.1:3306)/go_test?charset=utf8mb4")
	fmt.Println("err:", err)
	err = db.Ping()
	if err != nil {
		fmt.Println("数据库链接失败", err)
		return
	}
	insertRowDemo(db)
	defer db.Close()
}

4-2 mysql查询-单行查询

单行查询

单行查询db.QueryRow()执行一次查询,并期望返回最多一行结果(即Row)。QueryRow总是返回非nil的

值,直到返回值的Scan方法被调用时,才会返回被延迟的错误。(如:未找到结果)

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

4-2-mysql-query copy.go

package main

import (
	"database/sql"
	"fmt"

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

type user struct {
	id   int
	name string
	age  int
}

// 查询单条数据示例
func queryRowDemo(db *sql.DB) {
	sqlStr := "select id, name, age from user where id=?"
	var u user
	// 非常重要:确保QueryRow之后调用Scan方法,否则持有的数据库链接不会被释放
	err := db.QueryRow(sqlStr, 3).Scan(&u.id, &u.name, &u.age)
	if err != nil {
		fmt.Printf("scan failed, err:%v\n", err)
		return
	}
	fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
}
func main() {
	db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/go_test?charset=utf8mb4")
	fmt.Println("err:", err)
	err = db.Ping()
	if err != nil {
		fmt.Println("数据库链接失败", err)
		return
	}
	queryRowDemo(db)
	defer db.Close()
}

4-2 mysql查询-多行查询

多行查询db.Query()执行一次查询,返回多行结果(即Rows),一般用于执行select命令。参数args表

示query中的占位参数。

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

4-2-mysql-multi-query.go

package main

import (
	"database/sql"
	"fmt"

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

type user struct {
	id   int
	name string
	age  int
}

// 查询多条数据示例
func queryMultiRowDemo(db *sql.DB) {
	sqlStr := "select id, name, age from user where id > ?"
	rows, err := db.Query(sqlStr, 0)
	if err != nil {
		fmt.Printf("query failed, err:%v\n", err)
		return
	}
	// 非常重要:关闭rows释放持有的数据库链接
	defer rows.Close()

	// 循环读取结果集中的数据
	for rows.Next() {
		var u user
		err := rows.Scan(&u.id, &u.name, &u.age) // 通过SCAN读取出来
		if err != nil {
			fmt.Printf("scan failed, err:%v\n", err)
			return
		}
		fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
	}
}

func main() {
	db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/go_test?charset=utf8mb4")
	fmt.Println("err:", err)
	err = db.Ping()
	if err != nil {
		fmt.Println("数据库链接失败", err)
		return
	}
	queryMultiRowDemo(db)
	defer db.Close()
}

4-3 mysql更新

4-3-mysql-update.go

package main

import (
	"database/sql"
	"fmt"

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

type user struct {
	id   int
	name string
	age  int
}

// 更新数据
func updateRowDemo(db *sql.DB) {
	sqlStr := "update user set age=? where id = ?"
	ret, err := db.Exec(sqlStr, 20, 2)
	if err != nil {
		fmt.Printf("update failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 操作影响的行数
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("update success, affected rows:%d\n", n)
}
func main() {
	db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/go_test?charset=utf8mb4")
	// fmt.Println("err:", err)
	err = db.Ping()
	if err != nil {
		fmt.Println("数据库链接失败", err)
		return
	}
	updateRowDemo(db)
	defer db.Close()
}

4-4 mysql删除

4-4-mysql-delete.go

package main

import (
	"database/sql"
	"fmt"

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

type user struct {
	id   int
	name string
	age  int
}

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

func main() {
	db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/go_test?charset=utf8mb4")
	// fmt.Println("err:", err)
	err = db.Ping()
	if err != nil {
		fmt.Println("数据库链接失败", err)
		return
	}
	deleteRowDemo(db)
	defer db.Close()
}

5 MySQL预处理

什么是预处理?

普通SQL语句执行过程:

1.客户端对SQL语句进行占位符替换得到完整的SQL语句。

2.客户端发送完整SQL语句到MySQL服务端

3.MySQL服务端执行完整的SQL语句并将结果返回给客户端。

预处理执行过程:

1.把SQL语句分成两部分,命令部分与数据部分。

2.先把命令部分发送给MySQL服务端,MySQL服务端进行SQL预处理。

3.然后把数据部分发送给MySQL服务端,MySQL服务端对SQL语句进行占位符替换。

4.MySQL服务端执行完整的SQL语句并将结果返回给客户端。

为什么要预处理?

1.优化MySQL服务器重复执行SQL的方法,可以提升服务器性能,提前让服务器编译,一次编译多次

执行,节省后续编译的成本。

2.避免SQL注入问题。

5.1 Go实现MySQL预处理

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

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

返回值可以同时执行多个查询和命令。 4-5-mysql-prepare.go

package main

import (
	"database/sql"
	"fmt"

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

type user struct {
	id   int
	name string
	age  int
}

// 预处理查询示例
func prepareQueryDemo(db *sql.DB) {
	sqlStr := "select id, name, age from user where id > ?"
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%v\n", err)
		return
	}
	defer stmt.Close()
	rows, err := stmt.Query(0)
	if err != nil {
		fmt.Printf("query failed, err:%v\n", 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\n", err)
			return
		}
		fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
	}
}

// 预处理插入示例
// 插入、更新和删除操作的预处理十分类似
func prepareInsertDemo(db *sql.DB) {
	sqlStr := "insert into user(name, age) values (?,?)"
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%v\n", err)
		return
	}
	defer stmt.Close()
	_, err = stmt.Exec("darren", 18)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	_, err = stmt.Exec("柚子老师", 18)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	fmt.Println("insert success.")
}

func main() {
	db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/go_test?charset=utf8mb4")
	// fmt.Println("err:", err)
	err = db.Ping()
	if err != nil {
		fmt.Println("数据库链接失败", err)
		return
	}
	prepareInsertDemo(db)
	prepareQueryDemo(db)
	defer db.Close()
}

6 Go实现MySQL事务

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

开始事务: func (db *DB) Begin() (*Tx, error)

提交事务: func (tx *Tx) Commit() error

回滚事务: func (tx *Tx) Rollback() error

6-mysql-transaction.go

package main

import (
	"database/sql"
	"fmt"

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

type user struct {
	id   int
	name string
	age  int
}

// 预处理查询示例
func prepareQueryDemo(db *sql.DB) {
	sqlStr := "select id, name, age from user where id > ?"
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%v\n", err)
		return
	}
	defer stmt.Close()
	rows, err := stmt.Query(0)
	if err != nil {
		fmt.Printf("query failed, err:%v\n", 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\n", err)
			return
		}
		fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
	}
}

// 预处理插入示例
// 插入、更新和删除操作的预处理十分类似
func prepareInsertDemo(db *sql.DB) {
	sqlStr := "insert into user(name, age) values (?,?)"
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%v\n", err)
		return
	}
	defer stmt.Close()
	_, err = stmt.Exec("darren", 18)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	_, err = stmt.Exec("柚子老师", 18)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	fmt.Println("insert success.")
}

// 事务操作示例
func transactionDemo(db *sql.DB) {
	tx, err := db.Begin() // 开启事务
	if err != nil {
		if tx != nil {
			tx.Rollback() // 回滚
		}
		fmt.Printf("begin trans failed, err:%v\n", err)
		return
	}
	sqlStr1 := "Update user set age=30 where id=?"
	_, err = tx.Exec(sqlStr1, 2)
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec sql1 failed, err:%v\n", err)
		return
	}
	sqlStr2 := "Update user set age=40 where id=?"
	_, err = tx.Exec(sqlStr2, 4)
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec sql2 failed, err:%v\n", err)
		return
	}
	err = tx.Commit() // 提交事务
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("commit failed, err:%v\n", err)
		return
	}
	fmt.Println("exec trans success!")
}
func main() {
	db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/go_test?charset=utf8mb4")
	if db != nil {
		defer db.Close() // 健壮的写法
	}
	// fmt.Println("err:", err)
	err = db.Ping()
	if err != nil {
		fmt.Println("数据库链接失败", err)
		return
	}
	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)
	stats := db.Stats()
	fmt.Println("stats1:", stats)
	prepareInsertDemo(db)
	prepareQueryDemo(db)
	stats = db.Stats()
	fmt.Println("stats2:", stats)
}

7 sqlx使用

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

安装

go get github.com/jmoiron/sqlx

7-mysql-sqlx.go

package main

import (
	"fmt"

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

type user struct {
	ID   int    `json:"id" db:"id"`
	Name string `json:"name" db:"name"`
	Age  int    `json:"age" db:"age"`
}

var db *sqlx.DB

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

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

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

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

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

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

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

// Get、QueryRowx: 查询一条数据
// QueryRowx可以指定到不同的数据类型中
func getNum() {
	var num int
	_ = db.Get(&num, "select count(*) from user")
	fmt.Printf("数据库一共有:%d 个用户\n", num)
	var u user
	_ = db.Get(&u, "select name, id, age from user where id = ?", 1)
	fmt.Printf("查找用户id==1的用户:%v \n", u)
}
func main() {
	err := initDB()
	if err != nil {
		fmt.Println("数据库链接失败", err)
		return
	}
	insertRowDemo()
	queryRowDemo()
	getNum()
	queryMultiRowDemo()
	// defer db.Close()
}

7-mysql-sqlx-2.go

package main

// 数据库连接初始化
import (
	"fmt"

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

// DB 数据库模型
var DB *sqlx.DB

const dsn = "root:123456@tcp(127.0.0.1:3306)/go_test?charset=utf8mb4"

type user struct {
	ID   int    `json:"id" db:"id"`
	Name string `json:"name" db:"name"`
	Age  int    `json:"age" db:"age"`
}

// connect 1.连接数据库
func connect() (db *sqlx.DB, err error) {
	db, err = sqlx.Connect("mysql", dsn)
	db.SetMaxOpenConns(100) // 设置连接池最大连接数
	db.SetMaxIdleConns(20)  // 设置连接池最大空闲连接数
	DB = db
	if err != nil {
		fmt.Println("数据库连接失败==>", err)
	}
	fmt.Println("数据库已连接!")
	return
}

// 添加数据 Exec、MustExec
// MustExec遇到错误的时候直接抛出一个panic错误,程序就退出了;
// Exec是将错误和执行结果一起返回,由我们自己处理错误。 推荐使用!
func createUser() {
	// 创建表
	sql := `
        CREATE TABLE user  (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            name varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '',
            age int(11) NULL DEFAULT 0,
            PRIMARY KEY (id) USING BTREE
        ) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact
    `
	_, err := DB.Exec(sql)
	fmt.Println(err)
}

// 添加数据
func insertUser() {
	sql := `insert into user (name, age) values ("lgx",18)`
	res := DB.MustExec(sql)
	fmt.Println(res.LastInsertId)
	fmt.Println(res.RowsAffected)
}

// 更新数据
func updateUser() {
	sql := `update user set name = ?, age = ? where id = ?`
	res, err := DB.Exec(sql, "LGX", 28, 20)
	fmt.Println(err, res)
}

// Get、QueryRowx: 查询一条数据
// QueryRowx可以指定到不同的数据类型中
func getNum() {
	var num int
	_ = DB.Get(&num, "select count(*) from user")
	fmt.Printf("数据库一共有:%d 个用户\n", num)
	var u user
	_ = DB.Get(&u, "select name, id, age from user where id = ?", 2)
	fmt.Printf("查找用户id==1的用户:%v \n", u)
}

// Select、Queryx:查询多条数据
// Queryx可以指定到不同的数据类型中
func getAll() {
	sql := `select id, name ,age from user where id > 1`
	var us []user
	err := DB.Select(&us, sql)
	fmt.Println(err, us)
}

// 删除
func deleteUser() {
	sql := `delete from user where id = 20`
	_, _ = DB.Exec(sql)
}

// 事务处理
func events() {
	tx, _ := DB.Beginx()
	_, err1 := tx.Exec("update user set age = 10 where id = 20")
	_, err2 := tx.Exec("update user set age = 10 where id = 21")
	fmt.Println(err1, err2)
	if err1 != nil || err2 != nil {
		tx.Rollback()
	}
	tx.Commit()
}

func main() {
	db, _ := connect()
	defer db.Close()
	// 建表
	// createUser()
	// 添加数据
	insertUser()
	// 修改数据
	updateUser()
	// 查数据-Get
	getNum()
	// 查数据-Select
	getAll()
	// 事务
	// events()
}

8 gin + mysql restfull api

代码仓库

github.com/yunixiangfeng/devops/tree/main/gin_restful

 api\users.go

package api

import (
	"fmt"
	. "gin_restful/models"
	"net/http"
	"strconv"

	"github.com/gin-gonic/gin"
)

//index
func IndexUsers(c *gin.Context) {
	c.String(http.StatusOK, "It works")
}

//增加一条记录
func AddUsers(c *gin.Context) {
	name := c.Request.FormValue("name")
	telephone := c.Request.FormValue("telephone")
	fmt.Println("name:", name)
	fmt.Println("telephone:", telephone)
	if name == "" {
		msg := fmt.Sprintf("name字段错误")
		c.JSON(http.StatusBadRequest, gin.H{
			"msg": msg,
		})
		return
	}
	person := Person{
		Name:      name,
		Telephone: telephone,
	}
	id := person.Create()
	msg := fmt.Sprintf("insert 成功 %d", id)
	c.JSON(http.StatusOK, gin.H{
		"msg": msg,
	})
}

//获得一条记录
func GetOne(c *gin.Context) {
	ids := c.Param("id")
	id, _ := strconv.Atoi(ids)
	p := Person{
		Id: id,
	}
	rs, _ := p.GetRow()
	c.JSON(http.StatusOK, gin.H{
		"result": rs,
	})
}

//获得所有记录
func GetAll(c *gin.Context) {
	p := Person{}
	rs, _ := p.GetRows()
	c.JSON(http.StatusOK, gin.H{
		"list": rs,
	})
}

func UpdateUser(c *gin.Context) {
	ids := c.Request.FormValue("id")
	id, _ := strconv.Atoi(ids)
	telephone := c.Request.FormValue("telephone")
	person := Person{
		Id:        id,
		Telephone: telephone,
	}
	row := person.Update()
	msg := fmt.Sprintf("updated successful %d", row)
	c.JSON(http.StatusOK, gin.H{
		"msg": msg,
	})
}

//删除一条记录
func DelUser(c *gin.Context) {
	ids := c.Request.FormValue("id")
	id, _ := strconv.Atoi(ids)
	row := Delete(id)
	msg := fmt.Sprintf("delete successful %d", row)
	c.JSON(http.StatusOK, gin.H{
		"msg": msg,
	})
}

db\mysql.go

package db

import (
	"database/sql"
	"log"

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

var SqlDB *sql.DB

func init() {
	var err error
	SqlDB, err = sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/go_test?charset=utf8mb4")
	if err != nil {
		log.Fatal(err.Error())
	}
	err = SqlDB.Ping()
	if err != nil {
		log.Fatal(err.Error())
	}
	SqlDB.SetMaxIdleConns(20)
	SqlDB.SetMaxOpenConns(20)
}

models\users.go

package models

import (
	"gin_restful/db"
	"log"
)

type Person struct {
    Id int `json:"id" form:"id"`
    Name string `json:"name" form:"name"`
    Telephone string `json:"telephone" form:"telephone"`
}

//插入
func (person *Person) Create() int64 {
    rs, err := db.SqlDB.Exec("INSERT into users (name, telephone) value (?,?)", person.Name, person.Telephone)
    if err != nil{
        log.Fatal(err)
    }
    id, err := rs.LastInsertId()
    if err != nil{
        log.Fatal(err)
    }
    return id
}

//查询一条记录
func (p *Person) GetRow() (person Person, err error)  {
    person = Person{}
    err = db.SqlDB.QueryRow("select id,name,telephone from users where id = ?", p.Id).Scan(&person.Id, &person.Name, &person.Telephone)
    return
}

//查询所有记录
func (person *Person) GetRows() (persons []Person, err error) {
    rows, err := db.SqlDB.Query("select id,name,telephone from users")
    for rows.Next(){
        person := Person{}
        err := rows.Scan(&person.Id, &person.Name, &person.Telephone)
        if err != nil {
            log.Fatal(err)
        }
        persons = append(persons, person)
    }
    rows.Close()
    return
}

//修改
func (person *Person) Update() int64{
    rs, err := db.SqlDB.Exec("update users set telephone = ? where id = ?", person.Telephone, person.Id)
    if err != nil {
        log.Fatal(err)
    }
    rows, err := rs.RowsAffected()
    if err != nil {
        log.Fatal(err)
    }
    return  rows
}

//删除一条记录
func Delete(id int) int64  {
    rs, err := db.SqlDB.Exec("delete from users where id = ?", id)
    if err != nil {
        log.Fatal()
    }
    rows, err := rs.RowsAffected()
    if err != nil {
        log.Fatal()
    }
    return rows
}

main.go

package main

import "gin_restful/db"

// go mod init  xx_project
// go build
// ./xx_project
func main() {
	defer db.SqlDB.Close()
	router := initRouter()
	router.Run(":8806") // 启动服务了
}

router.go

package main

import (
	. "gin_restful/api"

	"github.com/gin-gonic/gin"
)

func initRouter() *gin.Engine {
	router := gin.Default()
	router.GET("/", IndexUsers) //http://192.168.204.132:8806

	//路由群组
	users := router.Group("api/v1/users")
	{
		users.GET("", GetAll)             //http://192.168.204.132:8806/api/v1/users
		users.POST("/add", AddUsers)      //http://192.168.204.132:8806/api/v1/users/add
		users.GET("/get/:id", GetOne)     //http://192.168.204.132:8806/api/v1/users/get/5
		users.POST("/update", UpdateUser) //http://192.168.204.132:8806/api/v1/users/update
		users.POST("/del", DelUser)       //http://192.168.204.132:8806/api/v1/users/del
	}

	departments := router.Group("api/v1/department")
	{
		departments.GET("", GetAll)             //http://192.168.204.132:8806/api/v1/users
		departments.POST("/add", AddUsers)      //http://192.168.204.132:8806/api/v1/users/add
		departments.GET("/get/:id", GetOne)     //http://192.168.204.132:8806/api/v1/users/get/5
		departments.POST("/update", UpdateUser) //http://192.168.204.132:8806/api/v1/users/update
		departments.POST("/del", DelUser)       //http://192.168.204.132:8806/api/v1/users/del
	}

	return router
}

8.1 gin + mysql rest full api –增

8.2 gin + mysql rest full api –改

http://192.168.204.132:8806/api/v1/users/update

8.3 gin + mysql rest full api –查

http://192.168.204.132:8806/api/v1/users/get/5

8.4 gin + mysql rest full api –获取所有

http://192.168.204.132:8806/api/v1/users

8.5 gin + mysql rest full api –删除 

代码仓库

github.com/yunixiangfeng/gin_restful

2.3 GO微信后台开发实战

微信公众号号后台开发

代码仓库

github.com/yunixiangfeng/devops/tree/main/wechat

1 微信公众号开发逻辑

1.1 注册公众号

注册地址: https://mp.weixin.qq.com/cgi-bin/registermidpage?action=index&lang=zh_CN&token=

1.2 开发者权限

进入公众号管理页面,下拉左边侧

1.3 微信公众号后台接口权限

普通用户只要是接收消息和自动回复消息的权限

1.4 公众号消息回复

1.5 服务器配置

2 HTTP服务

我们先使用原生的http接口来处理,后续改用gin来处理

我们这里主要处理Get和Post方法,见代码

Get:处理token验证 处理token的验证

Post:处理消息回复 处理消息

工程: wechat

main.go

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
	"wechat/wx"
)

const (
	logLevel = "dev"
	port     = 80
	token    = "NmHrEBBrbIX24JFw" // 生成地址:https://suijimimashengcheng.51240.com/
)

// 处理token的认证
func get(w http.ResponseWriter, r *http.Request) {

	client, err := wx.NewClient(r, w, token)

	if err != nil {
		log.Println(err)
		w.WriteHeader(403) // 校验失败
		return
	}

	if len(client.Query.Echostr) > 0 {
		w.Write([]byte(client.Query.Echostr)) // 校验成功返回的是Echostr
		return
	}

	w.WriteHeader(403)
	return
}

// 微信平台过来消息, 处理 ,然后返回微信平台
func post(w http.ResponseWriter, r *http.Request) {

	client, err := wx.NewClient(r, w, token)

	if err != nil {
		log.Println(err)
		w.WriteHeader(403)
		return
	}
	// 到这一步签名已经验证通过了
	client.Run()
	return
}

// 编译方法
// go mod init wechat
// go build
// ./wechat
// 需要自己修改token,以适应自己公众号的token
func main() {
	server := http.Server{
		Addr:           fmt.Sprintf(":%d", port), // 设置监听地址, ip:port
		Handler:        &httpHandler{},           // 用什么handler来处理
		ReadTimeout:    5 * time.Second,          // 读写超时 微信给出来5
		WriteTimeout:   5 * time.Second,
		MaxHeaderBytes: 0,
	}

	log.Println(fmt.Sprintf("Listen: %d", port))
	log.Fatal(server.ListenAndServe())
	defer CloseLog()
}

route.go

package main

import (
	"io"
	"net/http"
	"regexp"
	"time"
)

type WebController struct {
	Function func(http.ResponseWriter, *http.Request)
	Method   string
	Pattern  string
}

var mux []WebController // 自己定义的路由
// ^ 匹配输入字符串的开始位置
func init() {
	mux = append(mux, WebController{post, "POST", "^/"})
	mux = append(mux, WebController{get, "GET", "^/"})
}

type httpHandler struct{} // 实际是实现了Handler interface
// type Handler interface {
// 	ServeHTTP(ResponseWriter, *Request)
// }

func (*httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

	t := time.Now()

	for _, webController := range mux { // 遍历路由
		// 匹配请求的   r.URL.Path  -> webController.Pattern
		if m, _ := regexp.MatchString(webController.Pattern, r.URL.Path); m { // 匹配URL

			if r.Method == webController.Method { // 匹配方法

				webController.Function(w, r) // 调用对应的处理函数

				go writeLog(r, t, "match", webController.Pattern)

				return
			}
		}
	}

	go writeLog(r, t, "unmatch", "")

	io.WriteString(w, "")
	return
}

log.go

package main

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

var LogFile *os.File

func init() {
	// fmt.Println("log init")
	// LogFile, err := os.OpenFile("wechat.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) //打开日志文件,不存在则创建
	// if err != nil {
	// 	fmt.Println(err)
	// }
	// log.SetOutput(LogFile)
	log.SetFlags(log.LstdFlags | log.Lshortfile)
}

func CloseLog() {
	if LogFile != nil {
		LogFile.Close()
	}

}

func writeLog(r *http.Request, t time.Time, match string, pattern string) {

	if logLevel != "prod" {

		d := time.Now().Sub(t)

		l := fmt.Sprintf("[ACCESS] | % -10s | % -40s | % -16s | % -10s | % -40s |", r.Method, r.URL.Path, d.String(), match, pattern)

		log.Println(l)
	}
}

func func_log2fileAndStdout() {
	//创建日志文件
	f, err := os.OpenFile("test.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		log.Fatal(err)
	}
	//完成后,延迟关闭
	defer f.Close()
	// 设置日志输出到文件
	// 定义多个写入器
	writers := []io.Writer{
		f,
		os.Stdout}
	fileAndStdoutWriter := io.MultiWriter(writers...)
	// 创建新的log对象
	logger := log.New(fileAndStdoutWriter, "", log.Ldate|log.Ltime|log.Lshortfile)
	// 使用新的log对象,写入日志内容
	logger.Println("--> logger :  check to make sure it works")
}

 LICENSE

                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU General Public License is a free, copyleft license for
software and other kinds of works.

wx\structs.go

package wx

import (
	"encoding/xml"
	"strconv"
	"time"
)

type Base struct {
	FromUserName CDATAText
	ToUserName   CDATAText
	MsgType      CDATAText
	CreateTime   CDATAText
}

func (b *Base) InitBaseData(w *WeixinClient, msgtype string) {

	b.FromUserName = value2CDATA(w.Message["ToUserName"].(string))
	b.ToUserName = value2CDATA(w.Message["FromUserName"].(string))
	b.CreateTime = value2CDATA(strconv.FormatInt(time.Now().Unix(), 10))
	b.MsgType = value2CDATA(msgtype)
}

type CDATAText struct {
	Text string `xml:",innerxml"`
}

type TextMessage struct {
	XMLName xml.Name `xml:"xml"`
	Base
	Content CDATAText
}

wx\utils.go

package wx

func value2CDATA(v string) CDATAText {
	return CDATAText{"<![CDATA[" + v + "]]>"}
}

wx\wx.go

package wx

import (
	"crypto/sha1"
	"encoding/xml"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"sort"

	"github.com/clbanning/mxj"
)

type weixinQuery struct {
	Signature    string `json:"signature"`
	Timestamp    string `json:"timestamp"`
	Nonce        string `json:"nonce"`
	EncryptType  string `json:"encrypt_type"`
	MsgSignature string `json:"msg_signature"`
	Echostr      string `json:"echostr"`
}

type WeixinClient struct {
	Token          string
	Query          weixinQuery // 请求的一些参数
	Message        map[string]interface{}
	Request        *http.Request
	ResponseWriter http.ResponseWriter
	Methods        map[string]func() bool
}

/// 请求数据Request, 返回数据ResponseWriter, token是自己的
func NewClient(r *http.Request, w http.ResponseWriter, token string) (*WeixinClient, error) {

	weixinClient := new(WeixinClient)

	weixinClient.Token = token // 获取本地的token
	weixinClient.Request = r
	weixinClient.ResponseWriter = w

	weixinClient.initWeixinQuery()
	log.Println("Signature:", weixinClient.Query.Signature)
	if weixinClient.Query.Signature != weixinClient.hashcode() { // 签名认证
		return nil, errors.New("Invalid Signature.")
	}

	return weixinClient, nil
}

func (this *WeixinClient) initWeixinQuery() {

	var q weixinQuery
	log.Println("URL:", this.Request.URL.Path, ", RawQuery:", this.Request.URL.RawPath)
	q.Nonce = this.Request.URL.Query().Get("nonce")
	q.Echostr = this.Request.URL.Query().Get("echostr")
	q.Signature = this.Request.URL.Query().Get("signature")
	q.Timestamp = this.Request.URL.Query().Get("timestamp")
	q.EncryptType = this.Request.URL.Query().Get("encrypt_type")
	q.MsgSignature = this.Request.URL.Query().Get("msg_signature")

	this.Query = q
}

// 根据 Token Timestamp Nonce 生成对应的校验码, Token是不能明文传输的
func (this *WeixinClient) hashcode() string {

	strs := sort.StringSlice{this.Token, this.Query.Timestamp, this.Query.Nonce} // 使用本地的token生成校验
	sort.Strings(strs)
	str := ""
	for _, s := range strs {
		str += s
	}
	h := sha1.New()
	h.Write([]byte(str))
	return fmt.Sprintf("%x", h.Sum(nil))
}

// 读取消息,解析XML
func (this *WeixinClient) initMessage() error {

	body, err := ioutil.ReadAll(this.Request.Body)

	if err != nil {
		return err
	}

	m, err := mxj.NewMapXml(body)

	if err != nil {
		return err
	}

	if _, ok := m["xml"]; !ok {
		return errors.New("Invalid Message.")
	}

	message, ok := m["xml"].(map[string]interface{})

	if !ok {
		return errors.New("Invalid Field `xml` Type.")
	}

	this.Message = message // 保存消息

	log.Println(this.Message)

	return nil
}

func (this *WeixinClient) text() {

	inMsg, ok := this.Message["Content"].(string) // 读取内容

	if !ok {
		return
	}

	var reply TextMessage

	reply.InitBaseData(this, "text")
	reply.Content = value2CDATA(fmt.Sprintf("我收到的是:%s", inMsg)) // 把消息再次封装

	replyXml, err := xml.Marshal(reply) // 序列化

	if err != nil {
		log.Println(err)
		this.ResponseWriter.WriteHeader(403)
		return
	}

	this.ResponseWriter.Header().Set("Content-Type", "text/xml") // 数据类型text/xml
	this.ResponseWriter.Write(replyXml)                          // 回复微信平台
}

func (this *WeixinClient) Run() {

	err := this.initMessage()

	if err != nil {

		log.Println(err)
		this.ResponseWriter.WriteHeader(403)
		return
	}

	MsgType, ok := this.Message["MsgType"].(string)

	if !ok {
		this.ResponseWriter.WriteHeader(403)
		return
	}

	switch MsgType {
	case "text":
		this.text() // 处理文本消息
		break
	default:
		break
	}

	return
}

.github\FUNDING.yml

# These are supported funding model platforms
# leeeboo
github: [wtlyy]

3 token机制

解析请求中的GET参数

微信公众号签名验证的方法

参考:

https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Getting_Started_Guide.html

源码:3-1-token.go

package main

import (
	"bytes"
	"crypto/rand"
	"crypto/sha1"
	"fmt"
	"math/big"
	"sort"
	"strconv"
	"time"
)

func CreateRandomString(len int) string {
	var container string
	var str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
	b := bytes.NewBufferString(str)
	length := b.Len()
	bigInt := big.NewInt(int64(length))
	for i := 0; i < len; i++ {
		randomInt, _ := rand.Int(rand.Reader, bigInt)
		container += string(str[randomInt.Int64()])
	}
	return container
}

// 根据 Token Timestamp Nonce 生成对应的校验码, Token是不能明文传输的
func GenerateSignature(token string) (timestamp string, nonce string, signature string) {

	nonce = CreateRandomString(10)
	timestamp = strconv.FormatInt(time.Now().Unix(), 10) //int64转字符串
	// 排序 微信约定好的
	strs := sort.StringSlice{token, timestamp, nonce} // 使用本地的token生成校验
	sort.Strings(strs)                                // strs: [1607173019 qing qvCyrKEuoS]
	fmt.Println("strs:", strs)                        // 排序
	str := ""
	for _, s := range strs {
		str += s // 拼接字符串
	}
	fmt.Println("str:", str) //str: 1607173019qingqvCyrKEuoS
	h := sha1.New()		// 完全都是自己的服务的时候 你这里你用md5
	h.Write([]byte(str))                      // 转成byte
	signature = fmt.Sprintf("%x", h.Sum(nil)) // h.Sum(nil) 做hash  79efadd80a344c0b73b3bd2c403184f7425a5a67
	return
}

func VerifySignature(token string, timestamp string, nonce string, signature string) bool {
	// str = token + timestamp + nonce
	strs := sort.StringSlice{token, timestamp, nonce} // 使用本地的token生成校验
	sort.Strings(strs)
	str := ""
	for _, s := range strs {
		str += s
	}
	h := sha1.New()	// 完全都是自己的服务的时候 你这里你用md5
	h.Write([]byte(str))
	return fmt.Sprintf("%x", h.Sum(nil)) == signature
}
func main() {
	token := "qing"
	// 产生签名
	timestamp, nonce, signature := GenerateSignature(token) // 发送服务器的时候是发送  timestamp, nonce, signature
	fmt.Printf("1. token %s -> 产生签名:%s, timestamp:%s, nonce:%s\n", token, signature, timestamp, nonce)
	// 验证签名
	ok := VerifySignature(token, timestamp, nonce, signature) // 服务进行校验
	if ok {
		fmt.Println("2. 验证签名正常")
	} else {
		fmt.Println("2. 验证签名失败")
	}
}

3.1 token算法

参考:

https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Getting_Started_Guide.html

按照字母排列顺序

参数 描述

signature

微信加密签名,signature结合了开

发者填写的token参数和请求中的

timestamp参数、nonce参数。

timestamp 时间戳

nonce 随机数

echostr

随机字符串

如果服务器校验成功,返回echostr

如果校验失败,返回””字符串

验证方法
1. 服务器端获取 token , nonce , timestamp
成列表
2. 列表排序
3. 排序后的元素进行摘要
4. 摘要比对 signature
5. 响应 echostr

3.2 token算法-流程图

验证方法

1.服务器端获取token,nonce,timestamp组

成列表

2.列表排序

3.排序后的元素进行摘要

4.摘要比对signature

5.响应echostr

参考:

https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Getting_Started_Guide.html

4 XML解析

微信消息采用 XML 进行封装,所以我们需要先学习 XML 内容解析

4.1 XML解析-解析XML

在代码里,先针对xml的格式,创建对应的struct结构体

4-1-xml.go

package main

import (
	"encoding/xml"
	"fmt"
	"io/ioutil"
	"os"
)

// 如果struct中有一个叫做XMLName,且类型为xml.Name字段,
// 那么在解析的时候就会保存这个element的名字到该字段, 比如这里的config
type SConfig struct {
	XMLName      xml.Name   `xml:"config"`     // 指定最外层的标签为config
	SmtpServer   string     `xml:"smtpServer"` // 读取smtpServer配置项,并将结果保存到SmtpServer变量中
	SmtpPort     int        `xml:"smtpPort"`
	Sender       string     `xml:"sender"`
	SenderPasswd string     `xml:"senderPasswd"`
	Receivers    SReceivers `xml:"receivers"` // 读取receivers标签下的内容,以结构方式获取
}

type SReceivers struct {
	Age    int      `xml:"age"`
	Flag   string   `xml:"flag,attr"` // 读取flag属性
	User   []string `xml:"user"`      // 读取user数组
	Script string   `xml:"script"`    // 读取 <![CDATA[ xxx ]]> 数据
}

func main() {
	file, err := os.Open("4-1-xml.xml") // For read access.
	if err != nil {
		fmt.Printf("error: %v", err)
		return
	}
	defer file.Close()
	data, err := ioutil.ReadAll(file)
	if err != nil {
		fmt.Printf("error: %v", err)
		return
	}
	v := SConfig{}
	err = xml.Unmarshal(data, &v) // 反序列化
	if err != nil {
		fmt.Printf("error: %v", err)
		return
	}

	fmt.Println("文本:", v)
	fmt.Println("解析结果:")
	fmt.Println("XMLName : ", v.XMLName)
	fmt.Println("SmtpServer : ", v.SmtpServer)
	fmt.Println("SmtpPort : ", v.SmtpPort)
	fmt.Println("Sender : ", v.Sender)
	fmt.Println("SenderPasswd : ", v.SenderPasswd)
	fmt.Println("Receivers.Flag : ", v.Receivers.Flag)
	for i, element := range v.Receivers.User {
		fmt.Println(i, element)
	}
}

4-1-xml.xml 

<config>
   <smtpServer>smtp.qq.com</smtpServer>
   <smtpPort>25</smtpPort>
   <sender>you@qq.com</sender>
  <senderPasswd>123456</senderPasswd>
   <receivers flag="true">
     <user>ki@qq.gom</user>
     <user>dar@q.gom</user>
     <script>
     <![CDATA[
        function &%< matchwo(a,b) {
            if (a < b && a < 0) then {
                return 1;
            } else {
                return 0;
            }
        }
        ]]>
     </script>
  </receivers>
 </config>

4.2 XML解析-解析CDATA

XML 文档中的所有文本均会被解析器解析。

只有 CDATA 区段中的文本会被解析器忽略。

术语 CDATA 是不应该由 XML 解析器解析的文本数据。

像 "<" 和 "&" 字符在 XML 元素中都是非法的。

"<" 会产生错误,因为解析器会把该字符解释为新元素的开始。

"&" 会产生错误,因为解析器会把该字符解释为字符实体的开始。

某些文本,比如 JavaScript 代码,包含大量 "<" 或 "&" 字符。为了避免错误,可以将脚本代码定义为 CDATA。

CDATA 部分中的所有内容都会被解析器忽略。

CDATA 部分由 “ <![CDATA[ " 开始,由 "]]>" 结束:

4-2-CDATA.go

package main

import (
	"encoding/xml"
	"fmt"
	"strconv"
	"time"

	"github.com/clbanning/mxj"
)

// tag中含有"-"的字段不会输出
// tag中含有"name,attr",会以name作为属性名,字段值作为值输出为这个XML元素的属性,如上version字段所描述
// tag中含有",attr",会以这个struct的字段名作为属性名输出为XML元素的属性,类似上一条,只是这个name默认是字段名了。
// tag中含有",chardata",输出为xml的 character data而非element。
// tag中含有",innerxml",将会被原样输出,而不会进行常规的编码过程
// tag中含有",comment",将被当作xml注释来输出,而不会进行常规的编码过程,字段值中不能含有"--"字符串
// tag中含有"omitempty",如果该字段的值为空值那么该字段就不会被输出到XML,空值包括:false、0、nil指针或nil接口,任何长度为0的array, slice, map或者string

type CDATAText struct {
	Text string `xml:",innerxml"`
}

type Base struct {
	FromUserName CDATAText
	ToUserName   CDATAText
	MsgType      CDATAText
	CreateTime   CDATAText
}

// 文本消息的封装
type TextMessage struct {
	XMLName xml.Name `xml:"xml"`
	Base
	Content CDATAText
}

// 图片消息的封装
type PictureMessage struct {
	XMLName xml.Name `xml:"xml"`
	Base
	PicUrl  CDATAText
	MediaId CDATAText
}

func value2CDATA(v string) CDATAText {
	return CDATAText{"<![CDATA[" + v + "]]>"}
}

func main() {
	// 1. 解析 XML
	xmlStr := `<xml> 
		<ToUserName><![CDATA[toUser]]></ToUserName> 
		<FromUserName><![CDATA[fromUser]]></FromUserName> 
		<CreateTime>1348831860</CreateTime> 
		<MsgType><![CDATA[text]]></MsgType> 
		<Content><![CDATA[this is a test]]></Content> 
		<MsgId>1234567890123456</MsgId> 
		</xml>`

	var Message map[string]interface{}
	m, err := mxj.NewMapXml([]byte(xmlStr)) //使用了第三方的库

	if err != nil {
		return
	}

	if _, ok := m["xml"]; !ok {
		fmt.Println("Invalid Message.")
		return
	}
	fmt.Println("-->m:", m)
	message, ok := m["xml"].(map[string]interface{}) // 把xml对应的值读取出来

	if !ok {
		fmt.Println("Invalid Field `xml` Type.")
		return
	}

	Message = message

	fmt.Println("1. 解析出来:", Message) // xml对应的字段还是在map

	// 2. 封装XML
	var reply TextMessage
	inMsg, ok := Message["Content"].(string) // 读取内容 .(string)转成什么类型的数据

	if !ok {
		return
	}
	fmt.Println("Message[ToUserName].(string):", Message["ToUserName"].(string)) // 如果服务器要处理

	// 封装回复消息,需要添加 CDATA
	reply.Base.FromUserName = value2CDATA(Message["ToUserName"].(string))
	reply.Base.ToUserName = value2CDATA(Message["FromUserName"].(string))
	reply.Base.CreateTime = value2CDATA(strconv.FormatInt(time.Now().Unix(), 10))
	reply.Base.MsgType = value2CDATA("text")
	reply.Content = value2CDATA(fmt.Sprintf("我收到的是:%s", inMsg))

	replyXml, err := xml.Marshal(reply)                // 得到的是byte
	fmt.Println("2. 生成XML:", string(replyXml))         // []byte -> string
	fmt.Println("2. 生成XML:", []byte(string(replyXml))) // string -> []byte
}

<xml> <ToUserName><![CDATA[toUser]]></ToUserName>

<FromUserName><![CDATA[fromUser]]></FromUserName>

<CreateTime>1348831860</CreateTime>

<MsgType><![CDATA[text]]></MsgType>

<Content><![CDATA[this is a test]]></Content>

<MsgId>1234567890123456</MsgId> </xml>

5 你问我答

1)理解被动消息的含义 2)理解收\发消息机制 预实现功能: 粉丝给公众号一条文本消息,

公众号立马回复一条文本消息给粉丝,不需要通过公众平台网页操作。

 

5.1 你问我答-接收消息协议

参考:

https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standar

d_messages.html

参数 描述

ToUserName 开发者微信号

FromUserName 发送方帐号(一个OpenID)

CreateTime 消息创建时间 (整型)

MsgType 消息类型,文本为text

Content 文本消息内容

MsgId 消息id,64位整型

<xml> <ToUserName><![CDATA[toUser]]></ToUserName>

<FromUserName><![CDATA[fromUser]]></FromUserName>

<CreateTime>1348831860</CreateTime>

<MsgType><![CDATA[text]]></MsgType>

<Content><![CDATA[this is a test]]></Content>

<MsgId>1234567890123456</MsgId> </xml>

5.2 你问我答-被动回复消息协议

参考:

https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Passive_user_reply

_message.html#0

参数 是否必须 描述

ToUserName 是 接收方帐号(收到的OpenID)

FromUserName 是 开发者微信号

CreateTime 是 消息创建时间 (整型)

MsgType 是 消息类型,文本为text

Content 是

回复的消息内容(换行:在

content中能够换行,微信客户端

就支持换行显示)

6 go语言之进阶篇正则表达式

参考官网: https://studygolang.com/pkgdoc

范例:https://www.cnblogs.com/nulige/p/10260149.html

3.1 流媒体知识精讲和架构设计

1.1 直播应用场景

1.2 常用直播功能项 常用

1.3 直播框架示例1

1.4 直播框架示例2-某直播学院框架 

2 直播架构-基本逻辑 

2.0 常见流媒体协议 直播流程

RTP实时传输协议(Real-time Transport Protocol或简写RTP)

RTCP RTP Control Protocol

RTSP (Real Time Streaming Protocol),RFC2326,实时流传输协议

RTMP RTMP是Real Time Messaging Protocol(实时消息传输协议)

HTTP-FLV

HTTP-MP4

HLS

WebRTC

2.1 直播架构-基本流程 软件编码–提高机器的兼容性

2.2 直播常用工具

◼ 推流工具:

• ffmpeg:https://www.ffmpeg.org/download.html

• OBS studio:https://obsproject.com/download

◼ 拉流工具

• ffplay(): https://www.ffmpeg.org/download.html

• cutv www.cutv.com/demo/live_test.swf flash播放器

• vlc

• ijkplayer (基于ffplay): 一个基于FFmpeg的开源Android/iOS视频播放器

(开源)

API易于集成;

编译配置可裁剪,方便控制安装包大小;

支持硬件加速解码,更加省电

简单易用,指定拉流URL,自动解码播放.

◼ 压测工具

• st-load

2.3 流媒体服务器

SRS :一款国人开发的优秀开源流媒体服务器系统

BMS :也是一款流媒体服务器系统,但不开源,是SRS的商业版,

比SRS功能更多

nginx :免费开源web服务器,也常用来配置流媒体服务器。

集成Rtmp_module即可。

Red5:是java写的一款稳定的开源的rtmp服务器。

3 直播框架之CDN

4 拉流框架 

1. 模块初始化顺序

2. 音视频数据队列(packetqueue)控制

3. 音视频解码

4. 音频重采样

5. 视频尺寸变换

6. 音视频帧队列

7. 音视频同步

8. 关键时间点check

9. 其他

4.1 模块初始化顺序

推流模块(网络连接耗时) > 音视频编码模块 >音视频采集模块()

音视频输出模块 >音视频解码模块 > 拉流模块

本质上来讲,就是在数据到来之前准备好一切工作

4.2 音视频数据队列

音视频队列涉及到

1. Audio PacketQueue 还没有解码的

2. Video PacketQueue

两者独立

队列设计要点:

1. 可控制队列大小

1. 根据packet数进行控制

2. 根据总的duration进行控制 音频48khz, 21.3毫秒, 21.3*20 = 426ms

3. 支持packet的size进行入队列累加,出队列则减, 300,200,400, 字节累加

2. 支持packet数量统计

3. 支持packet的duration进行入队列累加,出队列则减

4. 支持阻塞和唤醒

目的:

1. 统计延迟(缓存时间长度)

4.3 音视频数据队列

音视频队列涉及到

1. Audio PacketQueue

2. Video PacketQueue

两者独立

队列设计要点:

1. 可控制队列大小

1. 根据packet数进行控制

2. 根据总的duration进行控制

2. 支持packet数量统计

3. 支持packet的size进行入队列累加,出队列则减

4. 支持packet的duration进行入队列累加,出队列则

5. 支持阻塞和唤醒

4.4 音视频解码

关键点:

1. 编码前: dts

2. 编码后: pts

3. packet释放

4. frame释放

5. 返回值处理

4.5 音频重采样

音频重采样模块AudioResampler:

注意重采样后不能直接使用linesize进行大小判断,需要使用

int size = av_get_bytes_per_sample((AVSampleFormat)(dstframe->format))

* dstframe->channels * dstframe->nb_samples ;

 4.6 视频尺寸变换

图像尺寸变换模块ImageScaler:变换尺寸大小

性能的提升可以考虑 libyuv

4.7 音视频解码后帧队列

FrameQueue

解码后的数据量比较大,需要要控制解码后帧队列的大小

参考ffplay固定大小

#define VIDEO_PICTURE_QUEUE_SIZE 3 // 图像帧缓存数量

#define SUBPICTURE_QUEUE_SIZE 16 // 字幕帧缓存数量

#define SAMPLE_QUEUE_SIZE 9 // 采样帧缓存数量

#define FRAME_QUEUE_SIZE FFMAX(SAMPLE_QUEUE_SIZE,

FFMAX(VIDEO_PICTURE_QUEUE_SIZE, SUBPICTURE_QUEUE_SIZE))

4.8 音视频同步

Avsync模块

目前只支持audio master的方式。

4.9 各个模块关键时间点的监测

4.10 其他

1. 客户端的首帧秒开,本质上就是不做同步先把第一帧显示出来。

2. 推流没有问题时,如果拉流不能正常播放:

1. 没有声音:dump rtmp拉流后的数据是否可以正常播放

2. 声音异常:是否有解码错误报告,重采样前的pcm数据是否正常

3. 没有图像: dump rtmp拉流后的数据是否可以正常播放

4. 画面异常:是否有解码错误报告,scale前的数据是否正常

服务器首帧秒开:这个功能不能降低延迟

5 直播推流框架-模块初始化顺序

推流模块(网络连接耗时) > 音视频编码模块 >音视频采集模块()

 

5.1 采集时间戳-帧间隔模式

5.2 采集时间戳-直接系统时间模式

5.3 采集时间戳-帧间隔+直接系统时间模式

5.4 音视频编解码模块

5.5 音视频队列的控制

5.6 关键时间点

5.7 其他

6 WebRTC

信令服务器由go语言实现

搭建自己的音视频通话web

WebRTC简介

WebRTC通话模型

WebRTC通话模型 Mesh一对一通话网络模型

WebRTC通话模型 Mesh多方通话网络模型

WebRTC Mesh 网络拓扑结构的优劣

WebRTC通话模型 SFU通话网络模型

WebRTC通话模型 MCU通话网络模型

WebRTC 通话网络模型选择

WebRTC建构多人会议系统

WebRTC应用领域

基于webrtc的开源方案

国内音视频通话方案公司

WebRTC开发进阶-SFU级联

学习资源

WebRTC视频通话中最多能容纳多少用户? https://www.jianshu.com/p/9ef708f93499

多媒体开发 https://www.jianshu.com/c/e5b30935c054

WebRTC中文网 https://webrtc.org.cn

WebRTC官网 https://webrtc.org/

WebRTC范例 https://webrtc.github.io/samples/

AppRTC基本原理

AppRTC Demo搭建注意事项

WebRTC通话信令基本设计 – 媒体协商+网络信息candidate + 房间人员

管理

1对1通话信令分析

一对一通话实战复习

多方通话

逻辑分析

3.2 工程代码-apidefs结构体定义

代码仓库

github.com/yunixiangfeng/devops/tree/main/video_server

2 架构分析和API设计

1. 技术要点分析

go流媒体网站技术要点
前后端分离的系统架构设计
RESTful 风格 API 设计与实现
Go 实现 web 服务
系统的服务化解耦
go channel 和并发模型的实践
使用 go 原生 template 完成 web UI 的实现
总体架构

什么是前后端解耦

◼ 前后端解耦是时下流行的Web网站架构

◼ 前端页面和服务通过普通的Web引擎渲染

◼ 后端数据通过渲染后的脚本调用后处理呈现

前后端解耦的优势

◼ 解放生产力

◼ 松耦合的架构更灵活,部署更方便,更符合微服务的设计特性

◼ 性能的提升、可靠性的提升

前后端解耦的缺点

◼ 工作量大

◼ 前后端分离带来团队成本以及学习成本

◼ 系统复杂度加大

2. REST API设计

API

◼ REST是Representational State Transfer(表现层状态转移)的缩写

◼ 常用的行为(查看(view),创建(create),编辑(edit)和删除(delete))

都可以直接映射到HTTP 中已实现的GET,POST,PUT和DELETE方法。

◼ 通常使用Json作为数据封装格式

◼ 统一接口

◼ 无状态

◼ 可缓存

API设计原则

◼ 以URL(统一资源定位符)风格设计API

◼ 通过不同的Method(GET/POST/PUT/DELETE)来区分对资源的CURD

◼ 返回码(Status code)符合HTTP资源描述的规定

3. API设计实战

API设计

API设计:用户

API设计:视频 

API设计:评论  

数据库设计-用户 

CREATE TABLE `video_server`.`users` (
`id` int unsigned primary key auto_increment,
`login_name` varchar(64) unique key,
`pwd` text
);
数据库设计- 视频
CREATE TABLE `video_server`.`video_info` (
`id` varchar(64) NOT NULL,
`author_id` int(10) NULL,
`name` text NULL,
`display_ctime` text NULL,
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
数据库设计- 评论
CREATE TABLE `video_server`.`comments` (
`id` varchar(64) NOT NULL,
`video_id` varchar(64) NULL,
`author_id` int(10) NULL,
`content` text NULL,
`time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);

数据库设计-会话

CREATE TABLE `video_server`.`sessions` (

`session_id` varchar(244) NOT NULL,

`TTL` tinytext NULL,

`login_name` text NULL,

PRIMARY KEY (`session_id`)

);

数据库设计-待删除视频表

CREATE TABLE `video_server`.`video_del_rec` (

`video_id` varchar(64) NOT NULL,

PRIMARY KEY (`video_id`)

);

4. 端口开放

端口开放

◼ api 10000

◼ scheduler 10001

◼ streamserver 1002

◼ web 10003

代码仓库

github.com/yunixiangfeng/video_server 

3.3 stream-scheduler-web详细设计

1. streamserver设计

0 总体架构

1 Streamserver

◼ 静态视频,

◼ 独立的服务,可独立部署

◼ 统一的API格式

1.1 Stream Server-对外接口

◼ /videos/:vid-id -> streamHandler 文件播放

◼ /upload/:vid-id -> uploadHandler 文件上传

1.2 代码整体设计

◼ 流控机制

◼ middleware的作用

1.3 流控机制-token bucket
为什么需要流控
拿到 token 才能继续进一步处理
为什么不用数组
go routine 是并发的,如果用变量则需要加锁
go 处理并发用 channel
limiter.go

1.4 在http middleware加入流控

type middleWareHandler struct {

r *httprouter.Router

l *ConnLimiter

}

func NewMiddleWareHandler(r *httprouter.Router,

cc int) http.Handler {

m := middleWareHandler{}

m.r = r

m.l = NewConnLimiter(cc) // 限制数量

return m

}

1.5 stream handler的实现

◼ streamHandler 读取文件播放

◼ uploadHandler 上传文件

2. scheduler设计

2 Scheduler调度器

◼ 什么是scheduler

◼ 为什么需要scheduler

◼ scheduler通常做什么

异步任务、延时任务、定时任务

2.1 Scheduler包含什么

◼ REST ful 的HTTP server

◼ Timer

◼ 生产者消费者模型下的task runner

2.2 Scheduler架构

2.3 代码架构

◼ dbops 数据库查询和删除

◼ taskrunner 执行任务

◼ runner.go 处理任务流程(生产消费模型)

◼ tasks.go 执行任务(具体的生产、消费)

◼ trmain.go 实现定时任务,比如每3秒执行一次

◼ handlers.go 处理api

◼ main.go程序入口

◼ response.go http响应封装

2.4 task实现

type Runner struct {

Controller controlChan

Error controlChan

Data dataChan

dataSize int

longLived bool

Dispatcher fn

Executor fn

}

◼ Controller 流程控制channel

◼ Error 错误控制channel

◼ Data 真正的任务数据channel

runner.go
tasks.go

2.5 timer实现

trmain.go

type Worker struct {

ticker *time.Ticker

runner *Runner

}

通过定时器实现定时任务

3. web设计

3 web前端服务

◼ Go模板引擎

◼ API处理

◼ API透传

◼ proxy代理

3.0 代码架构

◼ templates html模板

◼ client.go 处理api透传

◼ defs.go 结构体定义

◼ handlers.go api入口处理函数

◼ main.go 主入口

3.1 Go的模板引擎

◼ 模板引擎是将HTML解析和元素预置替换生成最终页面的工具

◼ Go的模板有两种text/template和html/template

◼ Go的模板采用动态生成的模式

3.2 Go的模板引擎-渲染流程

3.3 页面渲染

◼ 主页渲染:homeHandler

◼ 用户页渲染:userHomeHandler

3.4 api透传模块实现

◼ apiHandler 处理逻辑分析

3.5 proxy转发的实现

◼ proxyHandler处理逻辑非分析

代码仓库

github.com/yunixiangyu/devops

4.1 Gin和jwt验证实战

代码仓库

github.com/yunixiangyu/devops/tree/main/gin_practice

gin实战

N ⼊⻔

O RESTful API

结构体

基本的REST ful范例

路由参数

:路由

*路由

P URL查询参数

Gin获取查询参数

原理解析

Q 接收数组和 Map

QueryArray

QueryMap

QueryMap 的原理

T 表单参数

Form 表单

Gin 接收表单数据

PostFormArray()⽅法获取表单参数

Gin PostForm系列⽅法

实现原理

⼩结

T 上传⽂件

上传单个⽂件FormFile

上传多个⽂件MultipartForm

V 分组路由

分组路由

路由中间件

分组路由嵌套

原理解析

GIn中间件

Gin默认中间件

中间件实现HTTP Basic Authorization

针对特定URL的Basic Authorization

⾃定义中间件

V 再谈中间件

定义中间件

⼊⻔案例

注册中间件

为全局路由注册

为某个路由单独注册

为路由组注册中间件

跨中间件存取值

中间件注意事项

gin中间件中使⽤goroutine

gin框架中间件c.Next()理解

W json、struct、xml、yaml、protobuf渲染

各种数据格式的响应

范例

X HTML模板渲染

最简单的例⼦

复杂点的例⼦

静态⽂件⽬录

重定向

NL 异步协程

NN Gin源码简要分析

概述

从DEMO开始

ENGINE

ROUTERGROUP & METHODTREE

.路由注册

路由分组

.中间件挂载

.路由匹配

HANDLERFUNC

CONTEXT

.调⽤链流转和控制

.参数解析

.响应处理

总结

参考⽂献

官⽅⽹站

https://gin-gonic.com/

⼯程代码

https://github.com/gin-gonic/gin.git

测试范例

https://github.com/gin-gonic/examples.git

中间件

https://github.com/gin-gonic/contrib.git

gin框架-JWT验证实践

N token、cookie、session的区别

Cookie

Session

Token

O Json-Web-Token(JWT)介绍

JWT Token组成部分

签名的⽬的

什么时候⽤JWT

JWT(Json Web Tokens)是如何⼯作的

P 基于Token的身份认证和基于服务器的身份认证

N.基于服务器的认证

O.Session和JWT Token的异同

P.基于Token的身份认证如何⼯作

Q.⽤Token的好处

S.JWT和OAuth的区别

Q Go范例

S JWT资源

T 使⽤Gin框架集成JWT

⾃定义中间件

定义jwt编码和解码逻辑

定义登陆验证逻辑

定义普通待验证接⼝

验证使⽤JWT后的接⼝

V 使⽤go进⾏ JWT 验证

使⽤ JWT 的场景

JWT 的结构

总结

4.2 Go ORM实战

 代码仓库 github.com/yunixiangfeng/devops/tree/main/jwt-gorm

GORM实践

L 什么是ORM?为什么要⽤ORM?

N GORM⼊⻔指南

gorm介绍

安装

连接MySQL

GORM基本示例

GORM操作MySQL

O GORM Model定义

gorm.Model

模型定义示例

结构体标记(tags)

⽀持的结构体标记(Struct tags)

关联相关标记(tags)

范例

P 主键、表名、列名的约定

主键(Primary Key)

表名(Table Name)

列名(Column Name)

时间戳跟踪

CreatedAt

UpdatedAt

DeletedAt

Q CRUD

创建

创建记录

默认值

使⽤指针⽅式实现零值存⼊数据库

使⽤Scanner/Valuer接⼝⽅式实现零值存⼊数据库

扩展创建选项

查询

⼀般查询

Where 条件

普通SQL查询

Struct & Map查询

Not 条件

Or条件

内联条件

额外查询选项

FirstOrInit

Attrs

Assign

FirstOrCreate

Attrs

Assign

⾼级查询

⼦查询

选择字段

排序

数量

偏移

总数

Group & Having

连接

Pluck

扫描

链式操作相关

链式操作

⽴即执⾏⽅法

范围

多个⽴即执⾏⽅法

2

更新

更新所有字段

更新修改字段

更新选定字段

⽆Hooks更新

批量更新

使⽤SQL表达式更新

修改Hooks中的值

其它更新选项

删除

删除记录

批量删除

软删除

物理删除

S gorm-错误处理、事务、SQL构建、通⽤数据库接⼝、连接池、复合主键、⽇志

S.N. 错误处理

S.O. 事务

S.O.N. ⼀个具体的例⼦

S.P. SQL构建

S.P.N. 执⾏原⽣SQL

S.P.O. sql.Row & sql.Rows

S.P.P. 迭代中使⽤sql.Rows的Scan

S.Q. 通⽤数据库接⼝sql.DB

S.Q.N. 连接池

S.S. 复合主键

S.T. ⽇志

S.T.N. ⾃定义⽇志

4.3 go-admin架构分析和环境配置

GitHub - go-admin-team/go-admin: 基于Gin + Vue + Element UI & Arco Design & Ant Design 的前后端分离权限管理系统脚手架(包含了:多租户的支持,基础用户管理功能,jwt鉴权,代码生成器,RBAC资源控制,表单构建,定时任务等)3分钟构建自己的中后台项目;项目文档》:https://www.go-admin.pro V2 Demo: https://vue2.go-admin.dev V3 Demo: https://vue3.go-admin.dev Antd 订阅版:https://antd.go-admin.pro

go-admin架构分析和环境配置

N 简介

N.N 在线体验

N.O 特性

N.P 内置

O 安装

O.N 开发⽬录创建

O.O 获取代码

O.P 编译后端项⽬和修改配置⽂件

O.Q 初始化数据库,以及后端服务启动

⽐如
数据库设置为 go_test ,需要在后台提前创建 create database go_test;
1 # 1 ⾸次配置需要初始化数据库资源信息
2 ./go-admin migrate -c config/settings.yml
3 #2 启动项⽬,也可以⽤ IDE 进⾏调试
4 ./go-admin server -c config/settings.yml

  O.S 前端UI交互端启动说明

PS D:\Workspace\Go\src\devops\goadmin\go-admin-ui> npm i --legacy-peer-deps --registry=https://registry.npm.taobao.org

npm run dev

opensslErrorStack: [ ‘error:03000086:digital envelope routines::initialization error‘ ]

  "scripts": {
    "dev": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve",
    "serve": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve",

O.T 发布⽹⻚

P 架构分析

P.N 接⼝

P.O ⽂件⽬录

Q 问题总结

nodejs let notifier = require('update-notifier')({pkg}) 报错

安装NodeJS和NPM

安装命令

更新npm的包镜像源,⽅便快速下载

安装n管理器(⽤于管理nodejs版本)

npm ERR! cb()never called!的错误

Husky requires Git >=O.NP.L. Got vO.V.Q.

Error NPTT: Incorrect string value: '\xEV\xWW\xBN\xET\xWB\xXP...' for column 'dept_name' at row

mysql数据库表结构导出

重点

搭建go-admin项⽬

整体框架分析

各个⽬录和源码的作⽤

jwt鉴权设计

cobra cmd机制 (k8s) 命令⾏功能⾮常强⼤。

使⽤ go cobra创建命令⾏项⽬

代码仓库 github.com/yunixiangfeng/devops/tree/main/cobra

Cobra介绍

实现没有⼦命令的CLIs程序

实现有⼦命令的CLIs程序

附加命令

4.4 go-admin API和数据库设计分析

go-admin后台设计之casbin权限管理

N 概要

O PERM 模型

O casbin 权限库

casbin的主要特性

casbin不做的事情

核⼼概念

model file

model file 定义语法

policy file

RBAC 示例

定义 model file

定义 policy file

测试代码

多租户示例

定义 model file

定义 policy file

测试代码

Has_Role

例⼦:RBAC

Has Tenant Role

gin+gorm+casbin示例

P 总结

Q 参考⽂档

go-admin后台设计之授权机制

N登录过程分析

O ⽤户权限验证

权限⽣成

权限校验

P ⻆⾊权限验证

⻆⾊规则⽣成

接⼝规则

菜单规则

⻆⾊校验

Q 数据库设计

sys_casbin_rule 权限规则

sys_config 配置信息

sys_dept部⻔信息

sys_menu菜单

sys_post岗位名

sys_role⻆⾊类别

sys_role_dept⻆⾊部⻔

sys_role_menu⻆⾊菜单

sys_user⽤户

sys_category

sys_columns

sys_content

sys_dict_data字典数据

sys_dict_type字典类型

sys_file_dir⽂件⽬录

sys_file_info⽂件信息

sys_job

sys_login_log登录⽇志

1

sys_migration

sys_opera_log操作⽇志

sys_setting系统设置

sys_tables

4.5 go-admin添加应用实战

代码仓库

github.com/yunixiangfeng/devops/tree/main/goadmin

go-admin后台设计-添加应⽤实战

L 主要内容

N 新增模块

O 编写 go-admin 应⽤,第 N 步 ⼿动写代码

开始项⽬

⽤于开发的服务器

创建⽂章功能

app\admin\apis\article.go

package apis

import (
	"go-admin/common/models"
	"net/http"

	"github.com/gin-gonic/gin"
)

// GetArticleList 获取⽂章列表
func GetArticleList(c *gin.Context) {
	var res models.Response
	res.Data = "hello world !"
	c.JSON(http.StatusOK, res.ReturnOK())
}

编写第⼀个接⼝

app\other\router\gen_router.go

package router

import (
	"go-admin/app/admin/apis"
	"go-admin/app/other/apis/tools"

	"github.com/gin-gonic/gin"
	jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
)

func init() {
	routerCheckRole = append(routerCheckRole, sysNoCheckRoleRouter, registerDBRouter, registerSysTableRouter)
}

func sysNoCheckRoleRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
	r1 := v1.Group("")
	{
		sys := apis.System{}
		r1.GET("/captcha", sys.GenerateCaptchaHandler)
	}

	r := v1.Group("").Use(authMiddleware.MiddlewareFunc())
	{
		gen := tools.Gen{}
		r.GET("/gen/preview/:tableId", gen.Preview)
		r.GET("/gen/toproject/:tableId", gen.GenCode)
		r.GET("/gen/apitofile/:tableId", gen.GenApiToFile)
		r.GET("/gen/todb/:tableId", gen.GenMenuAndApi)
		sysTable := tools.SysTable{}
		r.GET("/gen/tabletree", sysTable.GetSysTablesTree)
		r.GET("/articleList", apis.GetArticleList) // 新加接⼝
	}
}

func registerDBRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
	db := v1.Group("/db").Use(authMiddleware.MiddlewareFunc())
	{
		gen := tools.Gen{}
		db.GET("/tables/page", gen.GetDBTableList)
		db.GET("/columns/page", gen.GetDBColumnList)
	}
}

func registerSysTableRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
	tables := v1.Group("/sys/tables")
	{
		sysTable := tools.SysTable{}
		tables.Group("").Use(authMiddleware.MiddlewareFunc()).GET("/page", sysTable.GetPage)
		tablesInfo := tables.Group("/info").Use(authMiddleware.MiddlewareFunc())
		{
			tablesInfo.POST("", sysTable.Insert)
			tablesInfo.PUT("", sysTable.Update)
			tablesInfo.DELETE("/:tableId", sysTable.Delete)
			tablesInfo.GET("/:tableId", sysTable.Get)
			tablesInfo.GET("", sysTable.GetSysTablesInfo)
		}
	}
}

 app\other\router\gen_router.go

package router

import (
	"go-admin/app/admin/apis"
	"go-admin/app/other/apis/tools"

	"github.com/gin-gonic/gin"
	jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
)

func init() {
	routerCheckRole = append(routerCheckRole, sysNoCheckRoleRouter, registerDBRouter, registerSysTableRouter)
}

func sysNoCheckRoleRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
	r1 := v1.Group("")
	{
		sys := apis.System{}
		r1.GET("/captcha", sys.GenerateCaptchaHandler)
		r1.GET("/articleList", apis.GetArticleList) // 新加接⼝
	}

	r := v1.Group("").Use(authMiddleware.MiddlewareFunc())
	{
		gen := tools.Gen{}
		r.GET("/gen/preview/:tableId", gen.Preview)
		r.GET("/gen/toproject/:tableId", gen.GenCode)
		r.GET("/gen/apitofile/:tableId", gen.GenApiToFile)
		r.GET("/gen/todb/:tableId", gen.GenMenuAndApi)
		sysTable := tools.SysTable{}
		r.GET("/gen/tabletree", sysTable.GetSysTablesTree)

	}
}

func registerDBRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
	db := v1.Group("/db").Use(authMiddleware.MiddlewareFunc())
	{
		gen := tools.Gen{}
		db.GET("/tables/page", gen.GetDBTableList)
		db.GET("/columns/page", gen.GetDBColumnList)
	}
}

func registerSysTableRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
	tables := v1.Group("/sys/tables")
	{
		sysTable := tools.SysTable{}
		tables.Group("").Use(authMiddleware.MiddlewareFunc()).GET("/page", sysTable.GetPage)
		tablesInfo := tables.Group("/info").Use(authMiddleware.MiddlewareFunc())
		{
			tablesInfo.POST("", sysTable.Insert)
			tablesInfo.PUT("", sysTable.Update)
			tablesInfo.DELETE("/:tableId", sysTable.Delete)
			tablesInfo.GET("/:tableId", sysTable.Get)
			tablesInfo.GET("", sysTable.GetSysTablesInfo)
		}
	}
}

path

P 编写 go-admin 应⽤,第 O 步 ⾃动⽣成代码

数据库配置

代码⽣成

表结构导⼊

编辑模板字段

预览代码

⽣成代码

app\admin\apis\article.go

package apis

import (
    "fmt"

	"github.com/gin-gonic/gin"
	"github.com/go-admin-team/go-admin-core/sdk/api"
	"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
	_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"

	"go-admin/app/admin/models"
	"go-admin/app/admin/service"
	"go-admin/app/admin/service/dto"
	"go-admin/common/actions"
)

type Article struct {
	api.Api
}

// GetPage 获取Go文章列表
// @Summary 获取Go文章列表
// @Description 获取Go文章列表
// @Tags Go文章
// @Param author query string false "作者"
// @Param content query string false "内容"
// @Param status query string false "状态"
// @Param pageSize query int false "页条数"
// @Param pageIndex query int false "页码"
// @Success 200 {object} response.Response{data=response.Page{list=[]models.Article}} "{"code": 200, "data": [...]}"
// @Router /api/v1/article [get]
// @Security Bearer
func (e Article) GetPage(c *gin.Context) {
    req := dto.ArticleGetPageReq{}
    s := service.Article{}
    err := e.MakeContext(c).
        MakeOrm().
        Bind(&req).
        MakeService(&s.Service).
        Errors
   	if err != nil {
   		e.Logger.Error(err)
   		e.Error(500, err, err.Error())
   		return
   	}

	p := actions.GetPermissionFromContext(c)
	list := make([]models.Article, 0)
	var count int64

	err = s.GetPage(&req, p, &list, &count)
	if err != nil {
		e.Error(500, err, fmt.Sprintf("获取Go文章失败,\r\n失败信息 %s", err.Error()))
        return
	}

	e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}

// Get 获取Go文章
// @Summary 获取Go文章
// @Description 获取Go文章
// @Tags Go文章
// @Param id path int false "id"
// @Success 200 {object} response.Response{data=models.Article} "{"code": 200, "data": [...]}"
// @Router /api/v1/article/{id} [get]
// @Security Bearer
func (e Article) Get(c *gin.Context) {
	req := dto.ArticleGetReq{}
	s := service.Article{}
    err := e.MakeContext(c).
		MakeOrm().
		Bind(&req).
		MakeService(&s.Service).
		Errors
	if err != nil {
		e.Logger.Error(err)
		e.Error(500, err, err.Error())
		return
	}
	var object models.Article

	p := actions.GetPermissionFromContext(c)
	err = s.Get(&req, p, &object)
	if err != nil {
		e.Error(500, err, fmt.Sprintf("获取Go文章失败,\r\n失败信息 %s", err.Error()))
        return
	}

	e.OK( object, "查询成功")
}

// Insert 创建Go文章
// @Summary 创建Go文章
// @Description 创建Go文章
// @Tags Go文章
// @Accept application/json
// @Product application/json
// @Param data body dto.ArticleInsertReq true "data"
// @Success 200 {object} response.Response	"{"code": 200, "message": "添加成功"}"
// @Router /api/v1/article [post]
// @Security Bearer
func (e Article) Insert(c *gin.Context) {
    req := dto.ArticleInsertReq{}
    s := service.Article{}
    err := e.MakeContext(c).
        MakeOrm().
        Bind(&req).
        MakeService(&s.Service).
        Errors
    if err != nil {
        e.Logger.Error(err)
        e.Error(500, err, err.Error())
        return
    }
	// 设置创建人
	req.SetCreateBy(user.GetUserId(c))

	err = s.Insert(&req)
	if err != nil {
		e.Error(500, err, fmt.Sprintf("创建Go文章失败,\r\n失败信息 %s", err.Error()))
        return
	}

	e.OK(req.GetId(), "创建成功")
}

// Update 修改Go文章
// @Summary 修改Go文章
// @Description 修改Go文章
// @Tags Go文章
// @Accept application/json
// @Product application/json
// @Param id path int true "id"
// @Param data body dto.ArticleUpdateReq true "body"
// @Success 200 {object} response.Response	"{"code": 200, "message": "修改成功"}"
// @Router /api/v1/article/{id} [put]
// @Security Bearer
func (e Article) Update(c *gin.Context) {
    req := dto.ArticleUpdateReq{}
    s := service.Article{}
    err := e.MakeContext(c).
        MakeOrm().
        Bind(&req).
        MakeService(&s.Service).
        Errors
    if err != nil {
        e.Logger.Error(err)
        e.Error(500, err, err.Error())
        return
    }
	req.SetUpdateBy(user.GetUserId(c))
	p := actions.GetPermissionFromContext(c)

	err = s.Update(&req, p)
	if err != nil {
		e.Error(500, err, fmt.Sprintf("修改Go文章失败,\r\n失败信息 %s", err.Error()))
        return
	}
	e.OK( req.GetId(), "修改成功")
}

// Delete 删除Go文章
// @Summary 删除Go文章
// @Description 删除Go文章
// @Tags Go文章
// @Param data body dto.ArticleDeleteReq true "body"
// @Success 200 {object} response.Response	"{"code": 200, "message": "删除成功"}"
// @Router /api/v1/article [delete]
// @Security Bearer
func (e Article) Delete(c *gin.Context) {
    s := service.Article{}
    req := dto.ArticleDeleteReq{}
    err := e.MakeContext(c).
        MakeOrm().
        Bind(&req).
        MakeService(&s.Service).
        Errors
    if err != nil {
        e.Logger.Error(err)
        e.Error(500, err, err.Error())
        return
    }

	// req.SetUpdateBy(user.GetUserId(c))
	p := actions.GetPermissionFromContext(c)

	err = s.Remove(&req, p)
	if err != nil {
		e.Error(500, err, fmt.Sprintf("删除Go文章失败,\r\n失败信息 %s", err.Error()))
        return
	}
	e.OK( req.GetId(), "删除成功")
}

app\admin\router\article.go

package router

import (
	"github.com/gin-gonic/gin"
	jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"

	"go-admin/app/admin/apis"
	"go-admin/common/middleware"
	"go-admin/common/actions"
)

func init() {
	routerCheckRole = append(routerCheckRole, registerArticleRouter)
}

// registerArticleRouter
func registerArticleRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
	api := apis.Article{}
	r := v1.Group("/article").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
	{
		r.GET("", actions.PermissionAction(), api.GetPage)
		r.GET("/:id", actions.PermissionAction(), api.Get)
		r.POST("", api.Insert)
		r.PUT("/:id", actions.PermissionAction(), api.Update)
		r.DELETE("", api.Delete)
	}
}

app\admin\service\article.go

package service

import (
	"errors"

    "github.com/go-admin-team/go-admin-core/sdk/service"
	"gorm.io/gorm"

	"go-admin/app/admin/models"
	"go-admin/app/admin/service/dto"
	"go-admin/common/actions"
	cDto "go-admin/common/dto"
)

type Article struct {
	service.Service
}

// GetPage 获取Article列表
func (e *Article) GetPage(c *dto.ArticleGetPageReq, p *actions.DataPermission, list *[]models.Article, count *int64) error {
	var err error
	var data models.Article

	err = e.Orm.Model(&data).
		Scopes(
			cDto.MakeCondition(c.GetNeedSearch()),
			cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
			actions.Permission(data.TableName(), p),
		).
		Find(list).Limit(-1).Offset(-1).
		Count(count).Error
	if err != nil {
		e.Log.Errorf("ArticleService GetPage error:%s \r\n", err)
		return err
	}
	return nil
}

// Get 获取Article对象
func (e *Article) Get(d *dto.ArticleGetReq, p *actions.DataPermission, model *models.Article) error {
	var data models.Article

	err := e.Orm.Model(&data).
		Scopes(
			actions.Permission(data.TableName(), p),
		).
		First(model, d.GetId()).Error
	if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
		err = errors.New("查看对象不存在或无权查看")
		e.Log.Errorf("Service GetArticle error:%s \r\n", err)
		return err
	}
	if err != nil {
		e.Log.Errorf("db error:%s", err)
		return err
	}
	return nil
}

// Insert 创建Article对象
func (e *Article) Insert(c *dto.ArticleInsertReq) error {
    var err error
    var data models.Article
    c.Generate(&data)
	err = e.Orm.Create(&data).Error
	if err != nil {
		e.Log.Errorf("ArticleService Insert error:%s \r\n", err)
		return err
	}
	return nil
}

// Update 修改Article对象
func (e *Article) Update(c *dto.ArticleUpdateReq, p *actions.DataPermission) error {
    var err error
    var data = models.Article{}
    e.Orm.Scopes(
            actions.Permission(data.TableName(), p),
        ).First(&data, c.GetId())
    c.Generate(&data)

    db := e.Orm.Save(&data)
    if err = db.Error; err != nil {
        e.Log.Errorf("ArticleService Save error:%s \r\n", err)
        return err
    }
    if db.RowsAffected == 0 {
        return errors.New("无权更新该数据")
    }
    return nil
}

// Remove 删除Article
func (e *Article) Remove(d *dto.ArticleDeleteReq, p *actions.DataPermission) error {
	var data models.Article

	db := e.Orm.Model(&data).
		Scopes(
			actions.Permission(data.TableName(), p),
		).Delete(&data, d.GetId())
	if err := db.Error; err != nil {
        e.Log.Errorf("Service RemoveArticle error:%s \r\n", err)
        return err
    }
    if db.RowsAffected == 0 {
        return errors.New("无权删除该数据")
    }
	return nil
}

配置系统菜单

配置⻆⾊权限

 操作内容管理

ok 

参照doc.zhangwj.com/cms/article-manage

admin/123456

​​​​​​​ 

Go语⾔资源汇总

开篇

Go语⾔该学什么

⽹站

开源项⽬

gin

gim

beego

cobra

pholcus

nsq

codis

delve

micro/micro

4.6 gin-vue-admin实战

github.com/yunixiangfeng/devops/tree/main/gin-vue-admin

【GIN-VUE-ADMIN】手把手教你使用gin-vue-admin(分P合集)

自动化pkg+自动化代码使用_哔哩哔哩_bilibili

https://github.com/flipped-aurora/gin-vue-admin

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Golang运维开发项目实战中,你可以考虑以下几个方向: 1. Linux系统管理及Shell脚本编程:掌握Linux系统管理技能,包括系统配置、服务管理、性能优化等,并学习Shell脚本编程,以便自动化运维任务。 2. Golang编程基本功:熟悉Golang的语法和基本特性,包括变量、函数、数据结构等,以便编写高效可靠的运维工具和脚本。 3. Golang并发编程及并发编程进阶:学习Golang的并发编程模型和相关的并发控制机制,以便处理高并发场景下的运维任务。 4. 程序测试与性能压测:掌握Golang的测试框架和工具,包括单元测试、接口测试、集成测试等,以及性能测试工具,以便保证运维工具的质量和性能。 5. Golang网络编程基础:了解Golang的网络编程基础,包括TCP/UDP通信、HTTP协议等,以便开发网络相关的运维工具和服务。 6. GolangWeb应用框架日志管理方法:学习如何使用Golang的日志库,记录和管理运维工具和服务的日志,以便排查问题和监控运行状态。 7. MySQL应用深度实战:掌握Golang与MySQL的集成开发,包括数据库连接、数据操作、事务处理等,以便处理与数据库相关的运维任务。 8. GORM框架(jinzhu、gorm、xorm):学习使用GORM框架进行ORM(对象关系映射)开发,简化数据库操作,提高开发效率。 以上是一些在Golang运维开发项目实战中的方向,你可以根据自己的兴趣和需求选择适合的方向进行深入学习和实践。\[1\] #### 引用[.reference_title] - *1* *3* [小白怎么入门Golang开发实战?看这篇就够啦!(白嫖60G资料)](https://blog.csdn.net/weixin_70257503/article/details/127059787)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [GPE(Grafana+Prometheus+Exporter)项目实战之Golang篇(上)](https://blog.csdn.net/inthirties/article/details/124923659)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值