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
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
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 数组与切片-切片实战5
8. string与slice
string底层就是一个byte的数组,因此,也
可以进行切片操作
str := “hello world”
s1 := str[0:5]
fmt.Println(s1)
s2 := str[5:]
fmt.Println(s2)
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 的切片。
数组是需要指定个数的,而切片则不需要。数组赋值也可是使用如下方式,忽略元素个数,使用
“...”代替
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使用
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 个字节,非英文字符占多个字节。
// 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.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执行。
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 注册公众号
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
如果校验失败,返回””字符串
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解析
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. 技术要点分析
什么是前后端解耦
◼ 前后端解耦是时下流行的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`.`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设计
1 Streamserver
◼ 静态视频,
◼ 独立的服务,可独立部署
◼ 统一的API格式
1.1 Stream Server-对外接口
◼ /videos/:vid-id -> streamHandler 文件播放
◼ /upload/:vid-id -> uploadHandler 文件上传
1.2 代码整体设计
◼ 流控机制
◼ middleware的作用
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
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 初始化数据库,以及后端服务启动
O.S 前端UI交互端启动说明
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/go-admin
go-admin后台设计-添加应⽤实战
L 主要内容
N 新增模块
O 编写 go-admin 应⽤,第 N 步 ⼿动写代码
开始项⽬
⽤于开发的服务器
创建⽂章功能
编写第⼀个接⼝
path
P 编写 go-admin 应⽤,第 O 步 ⾃动⽣成代码
数据库配置
代码⽣成
表结构导⼊
编辑模板字段
预览代码
⽣成代码
配置系统菜单
配置⻆⾊权限
操作内容管理
Go语⾔资源汇总
开篇
Go语⾔该学什么
⽹站
开源项⽬
gin
gim
beego
cobra
pholcus
nsq
codis
delve
micro/micro