[TOC]
0x01 反射章节
示例1.利用reflect反射实现一个ini配置文件的读取 (ini 文件解析器)。
首先需要准备一个 config.ini
配置文件:
; mysqlDatabaeConfig
[mysql]
address=192.168.1.20
port=3306
username=root
password=mysql
# redusDatabaeConfig
[redis]
host=192.168.1.21
port=6379
password=redis
database=0
test=false
其次是main.go利用反射方法读取赋予结构体对象之中
代码语言:javascript复制package main
import (
"errors"
"fmt"
"io/ioutil"
"os"
"reflect"
"strconv"
"strings"
)
// mysql 数据库配置结构体
type mysqlConfig struct {
Address string `ini:"address"`
Port int `ini:"port"`
Username string `ini:"username"`
Password string `ini:"password"`
}
// redis 数据库配置结构体
type redisConfig struct {
Host string `ini:"host"`
Port int `ini:"port"`
Password string `ini:"password"`
Database string `ini:"database"`
Test bool `ini:"test"`
}
// 结构体嵌套
type Config struct {
mysqlConfig `ini:"mysql"`
redisConfig `ini:"redis"`
}
func loadFileConfig(data interface{}, configfile string) (err error) {
// (1) 参数校验
t := reflect.TypeOf(data)
fmt.Println("参数校验:", t, t.Kind(), t.Elem().Kind())
// 传递的data参数必须是指针类型(因为需要在函数中对其赋值)
if t.Kind() != reflect.Ptr {
// err = fmt.Errorf("[Error] 传递的data参数必须是指针类型") // 不能以标点符号结尾以及输入大写字符开头
err = errors.New("data Param should be a [pointer] type") // 不能以标点符号结尾以及输入大写字符开头
return
}
// 传递来data参数还必须是结构体类型的指针(因为配置文件中各种键值对需要赋予给结构体字段)
if t.Elem().Kind() != reflect.Struct {
err = errors.New("data Param should be a [struct pointer] type") // 不能以标点符号结尾以及输入大写字符开头
return
}
// (2) 读文件得到字节类型的数据
ini, err := ioutil.ReadFile(configfile)
if err != nil {
return
}
// 将文件内容转换为字符串,并按照每行指定字符串进行切割(Linux: 'n', WIN: 'rn')
lineSlice := strings.Split(string(ini), "n")
// 读取每行数据并且提取对于数据到传递的对象之中
var structName string
for index, line := range lineSlice {
// 取消配置中的空格
line = strings.TrimSpace(line)
// 注释判断
if strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
continue
}
// 空行判断
if len(line) == 0 {
continue
}
// 节[section]判断
if strings.HasPrefix(line, "[") {
// 判断尾
if !strings.HasSuffix(line, "]") {
err = fmt.Errorf("configini Line:%d Syntax Error", index 1) // 不能以标点符号结尾以及输入大写字符开头
return
}
// 过滤[]中包含的空格,如果长度为0则表示格式错误,否则拿到节内容
sectionName := strings.TrimSpace(line[1 : len(line)-1])
if len(sectionName) == 0 {
err = fmt.Errorf("configini Line:%d Syntax Error", index 1) // 不能以标点符号结尾以及输入大写字符开头
return
}
// 根据[]中包含的字符串其data参数根据反射找对应结构体
for i := 0; i < t.Elem().NumField(); i { // t.Elem() 拿取指针中的元素
field := t.Elem().Field(i)
if sectionName == field.Tag.Get("ini") {
// 记录对应的嵌套结构体的字段名称
structName = field.Name
fmt.Printf("[ ] 找到%s对应的嵌套结构体: %sn", sectionName, structName)
continue
}
}
} else {
// 如果不是以 [ 开头的行意味是将值对,配置文件 Key=value 判断与赋值给我们定义的结构体
// 格式判断
if !strings.Contains(line, "=") || strings.HasPrefix(line, "=") {
err = fmt.Errorf("configini Line:%d Syntax Error", index 1) // 不能以标点符号结尾以及输入大写字符开头
return
}
// 取出ini中每一行的数据key=value
equalIndex := strings.Index(line, "=")
key := strings.TrimSpace(line[:equalIndex])
value := strings.TrimSpace(line[equalIndex 1:])
// 根据 structName 名称去data匿名把对应嵌套的结构体信息取出
v := reflect.ValueOf(data) // 反射取值
sValue := v.Elem().FieldByName(structName) // 嵌套结构体值信息
sType := sValue.Type() // 嵌套结构体类型信息
if sValue.Kind() != reflect.Struct {
err = fmt.Errorf("[-] data 中 %s 字段应该是一个结构体", structName) // 不能以标点符号结尾以及输入大写字符开头
return
}
// 遍历嵌套结构体中每一个字段,判断Tag是否等于Key。
var fieldName string
var fieldType reflect.StructField
for i := 0; i < sValue.NumField(); i {
field := sType.Field(i) // 反射类型信息中存储了嵌套结构体中的Tag信息
fieldType = field // 反射类型信息中存储了嵌套结构体中的filed信息以供后续值类型判断使用
if field.Tag.Get("ini") == key {
// 找到结构体中对应的字段
fieldName = field.Name
break
}
}
// 如果字段名称不存在嵌套结构体中则跳过
if len(fieldName) == 0 {
continue
}
// 如果key等于tag就给该字段赋值
fieldObj := sValue.FieldByName(fieldName)
fmt.Println(fieldName, fieldType.Type.Kind())
// 将读取对应的字符串转为结构体字段中对应的值的类型,并将其赋予。
switch fieldType.Type.Kind() {
case reflect.String:
fieldObj.SetString(value) // 注意点
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
var valueInt int64 // 注意点
valueInt, err = strconv.ParseInt(value, 10, 64) // 类型转换
if err != nil {
fmt.Printf("configini Line:%d value type error,msg: %s", index 1, err)
return
}
fieldObj.SetInt(valueInt)
case reflect.Float32, reflect.Float64:
var valueFloat float64 // 注意点
valueFloat, err = strconv.ParseFloat(value, 64) // 类型转换
if err != nil {
fmt.Printf("configini Line:%d value type error,msg: %s", index 1, err)
return
}
fieldObj.SetFloat(valueFloat)
case reflect.Bool:
var valueBool bool // 注意点
valueBool, err = strconv.ParseBool(value) // 类型转换
if err != nil {
fmt.Printf("configini Line:%d value type error,msg: %s", index 1, err)
return
}
fieldObj.SetBool(valueBool)
}
}
}
return
}
func main() {
var cfg Config
err := loadFileConfig(&cfg, "./config.ini")
if err != nil {
fmt.Println("[Error] Load File Config Failed")
os.Exit(1)
}
fmt.Printf("嵌套结构体对象值: %v n", cfg)
fmt.Printf("嵌套结构体对象类型与值: %#v n", cfg)
}
执行结果:
代码语言:javascript复制参数校验 *main.Config ptr struct
[ ] 找到mysql对应的嵌套结构体: mysqlConfig
Address string
Port int
Username string
Password string
[ ] 找到redis对应的嵌套结构体: redisConfig
Host string
Port int
Password string
Database string
Test bool
嵌套结构体对象值: {{192.168.1.20 3306 root mysql} {192.168.1.21 6379 redis 0 false}}
嵌套结构体对象类型与值: main.Config{mysqlConfig:main.mysqlConfig{Address:"192.168.1.20", Port:3306, Username:"root", Password:"mysql"}, redisConfig:main.redisConfig{Host:"192.168.1.21", Port:6379, Password:"redis", Database:"0", Test:false}}
0x02 Goroutine 章节
作业1.使用goroutine和channel实现一个计算int64随机数各位数和的程序。 要求:
- 开启一个goroutine循环生成int64类型的随机数,发送到jobChan
- 开启24个goroutine从jobChan中取出随机数计算各位数的和,将结果发送到resultChan
- 主goroutine从resultChan取出结果并打印到终端输出
示例演示:
代码语言:javascript复制package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// 结构体声明
type job struct {
value int64
}
type result struct {
job *job // 结构体嵌套
sum int64
}
// 并发等待组声明
var wg sync.WaitGroup
// 初始化goroutine
var jobChan = make(chan *job, 100)
var resultChan = make(chan *result, 100)
// 参数g通道只写
func generate(g chan<- *job) {
defer wg.Done()
// 循环生成int64随机数
for {
i := rand.Int63()
// 实例化
newJob := &job{
value: i,
}
// 将实例化的对象存入通道中
g <- newJob
time.Sleep(time.Millisecond * 500)
}
}
// 参数r通道i只写,参数g通道只读
func numberSum(r chan<- *result, g <-chan *job) {
defer wg.Done()
// 循环取得number的每个数字并累积其和,将结果发送到通道r中
for {
// 注意点,此处将g通道值赋予给job
job := <-g
sum := int64(0)
number := job.value
// 注意点
for number > 0 {
sum = number % 10 // 取余
number /= 10 // 取商(整)
}
// 注意点,参数 g 传入的job类型的通道
newResult := &result{
job: job,
sum: sum,
}
r <- newResult
}
}
// 此goroutine将会持续不断的生产随机数,以及取出随机数计算各位数的和。(将会一直执行,因为一直在取而另外一边一直在取)
func main() {
// 1.开启一个goroutine循环生成int64类型的随机数,发送到jobChan
wg.Add(1)
go generate(jobChan)
// 2.开启24个goroutine从jobChan中取出随机数计算各位数的和,将结果发送到resultChan
wg.Add(24)
for i := 0; i < 24; i {
go numberSum(resultChan, jobChan)
}
// 3.主goroutine从resultChan取出结果并打印到终端输出
for res := range resultChan {
fmt.Printf("number = %d, sum = %dn", res.job.value, res.sum)
}
wg.Wait()
}
执行结果:
代码语言:javascript复制number = 5577006791947779410, sum = 95
number = 8674665223082153551, sum = 79
number = 6129484611666145821, sum = 81
number = 4037200794235010051, sum = 53
number = 3916589616287113937, sum = 95
number = 6334824724549167320, sum = 80
number = 605394647632969758, sum = 99
number = 1443635317331776148, sum = 77
number = 894385949183117216, sum = 89
number = 2775422040480279449, sum = 80
number = 4751997750760398084, sum = 99
number = 7504504064263669287, sum = 84
number = 1976235410884491574, sum = 88
number = 3510942875414458836, sum = 87
0x03 Socket 网络编程章节
示例1.利用Go实现一个简单的聊天示例程序 描述: 本实践案例结合咱们前面所学的知识,实现一个简单的聊天示例程序,它可以在几个用户之间相互广播文本消息。
服务端程序: 包含4 个 goroutine,分别是一个主 goroutine
和广播(broadcaster)goroutine
,每一个连接里面又包含一个连接处理(handleConn)goroutine
和 一个客户写入(clientwriter)goroutine
。
主 (Main 函数) goroutine 的工作是监听端口,接受连接客户端的网络连接,对每一个连接,它将创建一个新的 handleConn goroutine。
广播(broadcaster 函数)goroutine 的工作主要是负责广播所有用户发送的消息,使用select对三种不同的消息进行响应。
代码语言:javascript复制# select 开启一个多路复用的作用
* 每当有广播消息从 messages 发送进来,都会循环 cliens 对里面的每个 channel 发消息。
* 每当有消息从 entering 里面发送过来,就生成一个新的 key - value,相当于给 clients 里面增加一个新的 client。
* 每当有消息从 leaving 里面发送过来,就删掉这个 key - value 对,并关闭对应的 channel。
连接处理(handleConn 函数)goroutine : 主要工作会为每个过来处理的 conn 都创建一个新的 channel,开启一个新的 goroutine 去把发送给这个 channel 的消息写进 conn。 它创建一个对外发送消息的新通道,然后通过 entering 通道通知广播者新客户到来,接着它读取客户发来的每一行文本,通过全局接收消息通道将每一行发送给广播者,发送时在每条消息前面加上发送者 ID 作为前缀。一旦从客户端读取完毕消息,handleConn 通过 leaving 通道通知客户离开,然后关闭连接。
代码语言:javascript复制# 该函数的执行过程可以简单总结为如下几个步骤:
* 获取连接过来的 ip 地址和端口号;
* 把欢迎信息写进 channel 返回给客户端;
* 生成一条广播消息写进 messages 里;
* 把这个 channel 加入到客户端集合,也就是 entering <- ch;
* 监听客户端往 conn 里写的数据,每扫描到一条就将这条消息发送到广播 channel 中;
* 如果关闭了客户端,那么把队列离开写入 leaving 交给广播函数去删除这个客户端并关闭这个客户端;
* 广播通知其他客户端该客户端已关闭;
* 最后关闭这个客户端的连接 Conn.Close()。
客户写入(clientwriter 函数)goroutine : 主要工作是将传入的ch通道进行遍历并将其中存储的信息传输到conn资源对象中。
客户端程序: 主要包含一个并发匿名函数,用于接收服务端发送的信息打印到本地终端中。
服务端完整代码:
代码语言:javascript复制// server 端是一个接收客户端发送的信息并将信息进行广播.
package main
import (
"bufio"
"fmt"
"log"
"net"
"time"
)
// (1) 声明对外发送消息的全局通道字符串类型.
type client chan<- string
// (2) 声明客户端状态以及发送的信息
var (
entering = make(chan client)
leaving = make(chan client)
messages = make(chan string) // 所有连接的客户端
)
// (4) 并发broadcaster广播器,循环监听通道中是否有内容
func broadcaster() {
// 它使用局部变量 clients 来记录当前连接的客户集合,每个客户唯一被记录的信息是其对外发送消息通道的 ID.
// 初始化声明,使用一个字典来保存用户 clients,字典的 key 是各连接申明的单向并发队列。
clients := make(map[client]bool)
// 循环执行&开启一个多路复用
for {
select {
// 每当有广播消息从 messages 发送进来,都会循环 cliens 对里面的每个 channel 发消息。
case msg := <-messages:
// 把所有接收到的消息广播给所有客户端
for cli := range clients {
cli <- msg // 发送消息通道
}
// 每当有消息从 entering 里面发送过来,就生成一个新的 key - value,相当于给 clients 里面增加一个新的 client。
case cli := <-entering:
clients[cli] = true // 表面客户端上线
// 每当有消息从 leaving 里面发送过来,就删掉这个 key - value 对,并关闭对应的 channel。
case cli := <-leaving:
delete(clients, cli) // 表面客户端离线,并删除对应的连接的 clients,cli
close(cli)
}
}
}
// (5) 并发 handleConn 函数创建一个对外发送消息的新通道,
func handleConn(conn net.Conn) {
// 对外发送客户消息的通道
ch := make(chan string)
go clientWriter(conn, ch)
// 获取客户端;连接过来的网络地址和端口信息
who := conn.RemoteAddr().String()
// 向通道传入客户端信息
ch <- "欢迎 " who
messages <- who " 上线"
entering <- ch
// 读取客户端传入的信息并传入messages通道(利用其进行广播给其他客户端)
input := bufio.NewScanner(conn)
for input.Scan() {
messages <- who ": " input.Text()
}
// 一旦从客户端读取完毕消息,handleConn 通过 leaving 通道通知客户离开,然后关闭连接。
// 注意:忽略 input.Err() 中可能的错误
leaving <- ch
messages <- who " 下线"
conn.Close()
}
// (6) clientWriter 实现向客户端发生通道中的信息.
func clientWriter(conn net.Conn, ch <-chan string) {
for msg := range ch {
fmt.Fprintln(conn, msg) // 注意:忽略网络层面的错误
fmt.Printf("[info] %v - Send -> Client %vn", time.Now().Format("2006-01-02 15:04:06"), msg)
}
}
// (3) 入口函数,主要获得 listener 对象,然后不停的获取链接上来的 conn 对象,最后把这些对象丢给处理链接函数去进行处理。
func main() {
// 3.1 TCP server 端监控
listener, err := net.Listen("tcp", "127.0.0.1:30000")
if err != nil {
log.Fatal(err) // 输出严重的错误并日志退出程序
} else {
fmt.Printf("Server %v Listen Start....n", listener.Addr().String())
}
// 3.2 broadcaster函数.负责广播所有用户发送的消息。
go broadcaster()
// 3.3 循环接收客户端接入请求
for {
conn, err := listener.Accept()
if err != nil {
log.Print(err)
continue
}
// 3.4 每个客户自己的 goroutine
go handleConn(conn)
}
}
客户端执行程序:
代码语言:javascript复制// client 是一个简单的TCP服务器读/写客户端
package main
import (
"fmt"
"io"
"log"
"net"
"os"
)
func main() {
// 1.连接到服务端
conn, err := net.Dial("tcp", "127.0.0.1:30000")
if err != nil {
log.Fatal(err)
}
// 2.声明初始化一个匿名结构体通道(值得学习)
done := make(chan struct{})
// 3.利用goroutine执行任务
go func() {
var wcount int64
// 注意:忽略错误,io.Copy 非常值的学习.
if wcount, err = io.Copy(os.Stdout, conn); err != nil {
log.Fatal(err)
}
if err != nil {
fmt.Println("CONN OUT os.Stdout Error!")
}
fmt.Println("写入的长度: ", wcount)
log.Println("Done")
done <- struct{}{} // 向主Goroutine发出信号 (值的学习,如果不发main函数便不会停止,此时终端亦不会被阻塞)
}()
// 4.将终端输入传递给conn资源对象给服务器
fmt.Println("请输入信息:")
mustCopy(conn, os.Stdin)
// 5.关闭conn资源
defer conn.Close()
<-done // 等待后台goroutine完成
}
func mustCopy(dst io.Writer, src io.Reader) {
if _, err := io.Copy(dst, src); err != nil {
log.Fatal(err)
}
}
下图中展示了在同一台计算机上运行的一个服务端和三个客户端的执行结果:
代码语言:javascript复制# 分别编译执行Server.go与Client
Server $ go build && ./Server
Client $ go build && ./Client
WeiyiGeek.goroutine&Socket网络编程实例
0x04 Unit 单元测试章节
作业1.针对走梯子算法优化后基准测试。 当有如下阶梯对应的走法如下:
代码语言:javascript复制1 => 1 # 1
2 => 2 # [(1,1) 2]
3 => 3 # [(1,1,1) (1,2) (2,1)]
4 => 5 # f(4) = 2f(4-2) f(4-3)
4 => 5 # f(5) = 2f(5-2) f(5-3) # 优化点,减少递归的次数。
示例代码如下:
代码语言:javascript复制// Main 执行时的文件名称,weiyigeek.top/studygo/Day08/07perfomance/testdemo/stepdemotest.go
// 单元测试执行的问题,weiyigeek.top/studygo/Day08/07perfomance/testdemo/stepdemo_test.go
package main
import (
"fmt"
"os"
"testing"
)
// 分别记录 calcStep(n-1),calcStep(n-2),以及程序执行次数
var (
x, y, c uint
)
// # stepladder 常规方法
func calcStep(step uint) uint {
c = 1
if step <= 0 {
fmt.Printf("[ERR] - Input Step (%d) Must Greater than 0 ", step)
os.Exit(-1)
}
switch step {
case 1:
return 1
case 2:
return 2
case 3:
return 3
default:
return calcStep(step-1) calcStep(step-2)
}
}
// stepladder 优化后方法,减少递归次数
// 关系: f(4) = 2(f2) f(1)
func optimizationcalcStep(step uint) uint {
if step <= 3 {
return calcStep(step)
} else {
x = calcStep(step - 2)
y = calcStep(step - 3)
return 2*x y
}
}
// # 单元测试
// 注意Test后接的函数名称首字母大写(Go开发规范)
func TestStepladder(t *testing.T) {
// 常规方法
got := calcStep(16)
want := uint(1597)
if !(got == want) {
// 测试用例失败提醒
t.Errorf("Want: %v But Got:%v n", want, got)
}
}
func TestStepladderOpti(t *testing.T) {
// 优化后
got := optimizationcalcStep(16)
want := uint(1597)
if !(got == want) {
// 测试用例失败提醒
t.Errorf("Want: %v But Got:%v n", want, got)
}
}
// # 基准测试
func benchmarkstepladder(b *testing.B, step uint) {
for i := 0; i < b.N; i {
calcStep(step)
}
}
func benchmarkstepladderOpti(b *testing.B, step uint) {
for i := 0; i < b.N; i {
optimizationcalcStep(step)
}
}
func BenchmarkFib1(b *testing.B) { benchmarkstepladder(b, 16) } // 常规 16 步阶梯有多数种走法
func BenchmarkFibOpt1(b *testing.B) { benchmarkstepladderOpti(b, 16) } // 优化后
func BenchmarkFib2(b *testing.B) { benchmarkstepladder(b, 25) } // 常规 25 步阶梯有多数种走法
func BenchmarkFibOpt2(b *testing.B) { benchmarkstepladderOpti(b, 25) } // 优化后
// # 示例函数
// Example_stepladder 常规方法的示例函数
func Example_stepladder() {
got := calcStep(16)
fmt.Println(got)
// output:
// 1597
}
// Example_stepladderOpti 优化后方法的示例函数
func Example_stepladderOpti() {
got := optimizationcalcStep(16)
fmt.Println(got)
// output:
// 1597
}
func main() {
// 走梯子计算优化函数
ret := func(num uint) uint {
if num <= 3 {
return calcStep(num)
} else {
x = calcStep(num - 2) // 减少递归次数
y = calcStep(num - 3)
return 2*x y
}
}(16)
fmt.Printf("优化走梯子方法. 16步阶梯的走法 = %d,执行次数: %dn", ret, c)
c = 0
fmt.Printf("常规走梯子方法. 16步阶梯的走法 = %d, 执行次数: %dn", calcStep(16), c)
}
执行结果:
代码语言:javascript复制# (1) 执行 Main 看执行相同的值的斐波那契数列对应值,执行的次数、(注意此时文件名: stepdemotest.go)
➜ testdemo go run .
优化走梯子方法. 16步阶梯的走法 = 1597,执行次数: 752 # 可以看到执行的次数,大大减少,此数值会随着
常规走梯子方法. 16步阶梯的走法 = 1597, 执行次数: 1219
# (2) 单元测试 & 示例测试(注意此时文件名: stepdemo_test.go)
➜ testdemo go test -v
=== RUN TestStepladder
--- PASS: TestStepladder (0.00s)
=== RUN TestStepladderOpti
--- PASS: TestStepladderOpti (0.00s) # 单元测试 通过
=== RUN Example_stepladder
--- PASS: Example_stepladder (0.00s) # 示例测试 通过
=== RUN Example_stepladderOpti
--- PASS: Example_stepladderOpti (0.00s)
PASS
ok weiyigeek.top/studygo/Day08/07perfomance/testdemo 0.002s
# (3) 常规与优化后走阶梯比较。(注意此时文件名: stepdemotest.go)
➜ testdemo go test -v -bench=. -benchmem --run=none
goos: linux
goarch: amd64
pkg: weiyigeek.top/studygo/Day08/07perfomance/testdemo
cpu: Intel(R) Core(TM) i5-3570 CPU @ 3.40GHz
BenchmarkFib1
BenchmarkFib1-4 333187 3145 ns/op 0 B/op 0 allocs/op
BenchmarkFibOpt1
BenchmarkFibOpt1-4 632816 1860 ns/op 0 B/op 0 allocs/op
BenchmarkFib2
BenchmarkFib2-4 4388 231205 ns/op 0 B/op 0 allocs/op
BenchmarkFibOpt2
BenchmarkFibOpt2-4 8421 141581 ns/op 0 B/op 0 allocs/op
PASS
ok weiyigeek.top/studygo/Day08/07perfomance/testdemo 6.360s
Tips: 用过上面的数据,可以看到同样的阶梯数,优化后执行的次数以及平均执行耗时都优于常规方式。
0x05 Benchmark 测试章节
作业1.使用gin框架编写一个接口,使用go-wrk进行压测,使用性能调优工具采集数据绘制出调用图和火焰图。