2025-12-09

package test

import (
    "bytes"
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "net/http"
    _ "net/http/pprof"
    "os"
    "runtime"
    "strings"
    "sync"
    "syscall"
    "testing"
    "time"
    "unsafe"
)

func TestSliceOp(t *testing.T) {
    //a := [3]int{} 数组1
    a := [...]int{1, 2, 3, 4, 5, 6, 7, 8} //数组2
    //不管是数组还是切片,使用[index:index]这种方式来截取时都是引用原地址的,即b c改变元素,相应的a的元素也会改变
    b := a[0:4]
    c := a[3:6]
    for i := 0; i < len(b); i++ {
        t.Log(b[i])
    }

    for i := 0; i < len(c); i++ {
        t.Log(c[i])
    }
    c[0] = 999
    t.Log(a[3], b[3])
    //申明一个切片,3为len,即index为0,1,2都int的零值;8为cap,为2的指数幂
    ab := make([]int, 3, 8) //切片1
    //ab := []int{1, 2, 3} 切片2
    //ab := []int{} 切片3
    //此时下表为3的值为1,而不是0;因为切片是可扩容的,相当于重新找一个之前二倍大小的连续空间,所以append必须赋值给原数组
    ab = append(ab, 1)

    //对于切片中,只申请len,没有申请cap时,则cap = len;下面这个切片,初识长度为3且容量也没3;当继续append元素时则cap x 2变为6;
    //但是cap是建议为2的幂次方的。
    aaa := make([]int, 3)
    t.Log(len(aaa), cap(aaa))
    //slice通过append赋值
    aaa = append(aaa, 1)
    t.Log(len(aaa), cap(aaa))
}

func TestSlice(t *testing.T) {
    a := []int{}
    a = append(a, 1)
    for i, v := range a {
        //a切片下标为0,值为1
        fmt.Println(fmt.Sprintf("a切片下标为%v,值为%v", i, v))
    }
    //创建一个len和cap都为3的切片,len为3之内的元素都是int零值
    b := make([]int, 3)
    b = append(b, 1)
    //注意:切片也可以使用下标赋值,,但有严格的前提条件:
    //1.切片必须已经通过make函数创建
    //2.下标不能超出当前长度范围

    //append:动态添加元素,可能触发扩容,返回新切片
    //下标赋值:修改已有位置的元素值,不改变切片长度和容量

    b[1] = 3 //当前长度为3,所以b[1]可以赋值
    for i, v := range b {
        //b切片下标为0,值为0
        //b切片下标为1,值为3
        //b切片下标为2,值为0
        //b切片下标为3,值为1   //特别注意:60行的append并不是赋值下标为1,而下标为4;也就是说append赋值的下标是len(b)+1
        fmt.Println(fmt.Sprintf("b切片下标为%v,值为%v", i, v))
    }

    c := make([]int, 1, 3)
    c = append(c, 1)
    //panic: runtime error: index out of range [3] with length 2
    //c[3] = 1
    for i, v := range c {
        //c切片下标为0,值为0,len为2,cap为3
        //c切片下标为1,值为1,len为2,cap为3
        fmt.Println(fmt.Sprintf("c切片下标为%v,值为%v,len为%v,cap为%v", i, v, len(c), cap(c)))
    }
    c[1] = 5 // 可以看到这里的值覆盖了74行的1
    for i, v := range c {
        //c切片下标为0,值为0,len为2,cap为3
        //c切片下标为1,值为5,len为2,cap为3
        fmt.Println(fmt.Sprintf("c切片下标为%v,值为%v,len为%v,cap为%v", i, v, len(c), cap(c)))
    }
}
func TestMapOp(t *testing.T) {
    a := map[string]int{}
    a["one"] = 1
    a["two"] = 2
    b := make(map[string]int, 2)
    b["one"] = 1
    b["two"] = 2
    //判断map的key是否存在
    if _, ok := b["three"]; ok {
        t.Log("存在")
    } else {
        t.Log("不存在")
    }
    // 数组,切片,map,通道都可以使用range来遍历
    for key, value := range a {
        t.Log(key, value)
    }
    //4为map的cap而不是len,当容量超过4之后就会扩容;为什么map不可以设置len,因为初始化len时都是零值,而map的key是唯一的,所以不能有len
    //map的cap决定了性能,如果能提前知道容量,则最好设置cap
    // 注意:map是引用类型
    c := make(map[string]int, 4)
    a["one"] = 1
    c["two"] = 2
    t.Log(len(c))

    //map的value可以是一个匿名方法,特别注意当方法的返回值为1个时是可以没有括号的,如果返回值超过1个则必须有括号(go中的方法是可以返回多值的)
    //go中的函数可以当成一个type使用就更好理解了
    aa := map[string]func(op int) (res int, double int){}                    //只是定义了一个匿名函数,只定义方法的入参数和出参数,并不需要定义匿名函数的实现
    aa["one"] = func(op int) (res int, double int) { return res, res * res } //这里实现了匿名的函数
    f := map[string]func(op int) int{}                                       //只是定义了一个匿名函数,只定义方法的入参数和出参数,并不需要定义匿名函数的实现
    f["one"] = func(op int) int { return op }                                //这里实现了匿名的函数
    f["two"] = func(op int) int { return op * op * op }                      //这里实现了匿名的函数
    t.Log(f["one"](2), f["two"](3))
}

// 全局变量只能通过var来申明,var的方式初始值为nil,需要实例化之后才可以使用;或者直接全局变量初始化setA = map[string]int{}这种方式申明
// map的零值为nil,所以必须在方法内实例化
var setA map[string]bool

// 可以看到方法定义返回值时,key为result,类型为bool,这种方式叫命名返回值,相当于var result bool,那么在方法中则可以直接使用result变量,且不需要重新申明了,
// 同时return时不需要显示指定result了;若返回参数值大于1个时则需要使用()
func setHashSet(key string) (result bool) {
    if _, ok := setA[key]; !ok {
        setA[key] = true
        result = true
    } else {
        result = false
    }
    return
}

func getSetList() (result string) {
    parts := []string{}
    for key, val := range setA {
        //%s普通字符串;%d十进制整数int;%f 默认小数形式;%.2f保留俩位小数;%t布尔值;%p输出指针
        //如果不知道类型,则%v是一个通用格式说明符,用于以默认格式输出任何类型的值
        parts = append(parts, fmt.Sprintf("%v:%v", key, val))
    }
    //将数组按照,拼接为字符串
    result = strings.Join(parts, ",")
    return
}

func TestSetA(t *testing.T) {
    //注意:全局变量一定要初始化
    setA = map[string]bool{}
    a := setHashSet("aa")
    t.Log(fmt.Sprintf("%v", a))
    b := setHashSet("bb")
    t.Log(fmt.Sprintf("%v", b))
    c := setHashSet("bb")
    t.Log(fmt.Sprintf("%v", c))
    dd := getSetList()
    t.Log(dd)
}

// ------------------函数----------------------
// go中的函数完全也是一种数据类型,完全可以把它当成一中type,类型int string
// 全局变量只能通过var的方式申请,且需要在函数中实例化,不然报nil指针
var kk func(n int) int //申明变量时,只定义函数体,即入参和出参
var hh []func(n int)   //申明变量时,只定义函数体,即入参和出参

// sum的零值为0,所以可以不用在方法内实例化
var sum int

func TestFunc1(t *testing.T) {
    //为什么不能写成这样呢 append(hh, getSum(2))?因为这里存储的是函数引用而非函数调用结果,实际上append(hh, getSum(2))函数会立即执行并返回
    //所以函数本身就是一个引用
    hh = append(hh, getSum) //实现变量时需要实现结构体
    hh = append(hh, getSum)
    hh = append(hh, getSum)
    for i := 0; i < 3; i++ {
        hh[i](i + 3)
    }
    t.Log(fmt.Sprintf("sum值为%v", sum))
}
func getSum(num int) {
    sum += num
}

var suma int
var a []int
var op []string

func TestFunc2(t *testing.T) {
    a = []int{100, 2, -10, -50}
    op = []string{"+", "-", "*", "/"}
    for i, _ := range a {
        funDefineNum(funcTODO, op[i], i)
    }
    t.Log(fmt.Sprintf("last num is %v", suma))
}
func funDefineNum(a func(op string, index int), operation string, num int) {

    a(operation, num)
}
func funcTODO(op string, index int) {
    switch op {
    case "+":
        suma += a[index]
    case "-":
        suma -= a[index]
    case "*":
        suma *= a[index]
    case "/":
        suma /= a[index]
    }
}

// ---------可变参数和defer--------------
// 1. 通过...可不限制参数数量 2. 所有参数类型都是一致 3. 所有参数是以切片的形式展示
// defer和php中的finally的作用是一样的,是在方法内的代码最后执行,尽管抛出异常同样也是会执行的
// defer可以用调用函数,也可以调用一个闭包
func TestArgs(t *testing.T) {
    //defer会调用一个闭包
    defer func(sum int) {
        t.Log(fmt.Sprintf("defer执行总和为%v", sum))
        //recover可以捕获panic的异常;err则为panic抛出的异常
        if err := recover(); err != nil {
            stack := make([]byte, 1024)
            //获取异常栈
            length := runtime.Stack(stack, false)
            t.Log(fmt.Printf("异常: %v\n堆栈:\n%s", err, stack[:length]))
        }
    }(sum)
    sum := intSum(1, 2, 3, 4, 5, 6, 7, 8)
    t.Log(sum)
    panic("抛出一个错误异常")
}
func intSum(num ...int) (sum int) {
    //num可以像普通切片一样使用,len(num) ,num[i]
    for _, num := range num {
        sum += num
    }
    return
}

// 当入参或者出参的类型完全一致时,可以通过下面这种方式来定义,只定义最后一个参数的类型即可
// 1. 入参name,motherName,fatherName的类型都是any,即interface{}类型
// 2. 出参old,high,weight的类型都是int
func sameParams(name, motherName, fatherName any) (old, high, weight int) {
    return 1, 1, 1
}

// args是一个任何类型的”切片“
func Info(code int, msgFormat string, args ...interface{}) {
    //args... 是 Go 语言中的可变参数展开语法,... 是展开操作符,将切片中的元素展开为独立的参数
    fmt.Sprintf(msgFormat, args...)
}

// fmt.Sprintf()格式化字符串,返回值为格式化后的字符串,但不可换行
// fmt.Printf()格式化字符串且将格式化字符串标准输出到终端,但不可换行,返回值是写入字节数
// fmt.Println() 不能格式化字符串,只能一串字符串标准输出到终端,且支持换行,返回值为nil
// 1. 如果需要将格式化的字符串输出到终端且支持换行:1. fmt.Println(fmt.Sprintf("年龄为%v",12)) 2. fmt.Printf(fmt.Sprintf("年龄为%v\n",12))
// 2. 如果只需要将一串字符串输出到终端:fmt.Println("我的年龄为12")

// ---------------------struct定义------------------
// todo 看下form 和 json
// strut作为参数传递时是值传递,如果为引用传递则需要加*
type employee struct {
    name string
    age  int
}

// 这里需要注意:struct的属性首字母必须是大写的才可以使用form json这些,小写的只有在当前包才可以使用
type Employee1 struct {
    Name string `form:"name" json:"name" binding:"required"`
    Age  int    `form:"age" json:"age" binding:"required"`
}

// employee类型是值传递
// 可以看到employee的地址和e(new(employee))的地址不一样,也就是该方法中的employee又copy了实例化的对象,存在内存复制,会浪费性能
// employee.name的逻辑是familyName + 原employee.name,也就是这里的employee.name只会作用于该方法内的copy的employee对象,而不会作用于e(new(employee))或者其他employee对象
func (employee employee) getName(familyName string) {
    employee.name = familyName + employee.name
    //getName方法的employee地址为0x1400043c528
    fmt.Println(fmt.Sprintf("getName方法的employee地址为%v\n", unsafe.Pointer(&employee)))
}

// *employee是引用传递
// 可以看到employee的地址和e(new(employee))的地址是一模一一样,也就是该方法中的employee对象作用于e或者所有*employee
// 没有存在内存复制,也就意味着没有浪费性能,类中的方法建议都使用*employee这种方式
func (employee *employee) getAge(addAge int) {
    employee.age = addAge + employee.age
    //getAge方法的employee地址为0x1400043c510
    fmt.Println(fmt.Sprintf("getAge方法的employee地址为%v\n", unsafe.Pointer(employee)))
}

// 实例化struct的几种方式
func TestStudyStruct(t *testing.T) {
    //指定key: value的方式,注意这种方式返回的不是引用地址
    //a := employee{name: "哈哈", age: 12}
    //a.getName("刘")
    //a.getAge(5)
    //t.Log(fmt.Sprintf("%s的年龄为%v;employee的地址为%v", a.name, a.age, unsafe.Pointer(&a)))
    ////哈哈的年龄为17
    //t.Log(fmt.Sprintf("%s的年龄为%v", a.name, a.age))
    //顺序对struct内的属性进行赋值,这种方式必须对所有属性都进行赋值,注意这种方式返回的也不是引用地址
    //b := employee{"哈哈", 12}
    //下面这俩中方式相对于上面的俩种方式,唯一的不同点是,返回的是引用地址
    //c := &employee{name: "哈哈", age: 12}
    //d := &employee{"哈哈", 12}

    //这种方式也是返回引用地址,和c d对象是完全一致的,只是写法不同
    e := new(employee)
    e.name = "哈哈"
    //e.age = 12
    e.getAge(3)
    e.getName("续")
    ////哈哈的年龄为15;employee的地址为0x1400043c510
    t.Log(fmt.Sprintf("%s的年龄为%v;employee的地址为%v", e.name, e.age, unsafe.Pointer(e)))

    //通过json直接赋值给employee struct
    caseJson := `{"name":"小雨aaa","age":18}`
    //字符串底层就是byte数组, []byte(caseJson) 是将字符串转化为byte数组;string(aaa)是将byte数组转化为字符串
    aaa := []byte(caseJson)
    rt := new(Employee1)
    //Unmarshal将caseJson赋值给rt struct
    err := json.Unmarshal(aaa, rt)
    fmt.Println(fmt.Sprintf("rt对象:%v", rt))
    if err == nil {
        //将aaa byte数组转化为字符串
        fmt.Println(rt.Name, string(aaa))
    }
}
func TestJson(t *testing.T) {
    jsonStr := `{"name":"我是你爸爸","age":56}`
    // 1. json字符串转换为对象(定义对象结构体)
    person := &Employee1{}
    // 2. 一个参数是一个byt数组需要使用[]byte(),第二个参数是一个引用地址;将jsonStr转化为person对象
    err := json.Unmarshal([]byte(jsonStr), person)
    if err == nil {
        fmt.Println(fmt.Sprintf("姓名:%v,年龄:%v", person.Name, person.Age))
    }
    // 3. json对象转化为一个字符串
    lastStr, err := json.Marshal(person)
    if err == nil {
        fmt.Println(fmt.Sprintf("字节数组为:%v,json字符串为%v", lastStr, string(lastStr)))
    }
    //------------上面这种json的转化是常用的,但是需要提前定一个一个结构体-------------------------
    //如果并不知道json字符串的结构体怎么办呢?通常定义一个interface的map,来实现
    strMap := make(map[string]interface{})

    err = json.Unmarshal([]byte(jsonStr), &strMap)
    if err != nil {
        return
    }

    if value, ok := strMap["name"]; ok {
        fmt.Println(fmt.Sprintf("您的名字为:%v", value))
    }
    if value, ok := strMap["age"]; ok {
        fmt.Println(fmt.Sprintf("您的年龄为:%v", value))
    }
}

// 泛型:是不用明确指定具体的类型,可以在实际执行时在确定具体的类型
func TestFanxing(t *testing.T) {

    fmt.Println(fmt.Sprintf("%v,%v,%v", fanxing("o"), fanxing(1), fx(43434)))
}
func fanxing(n interface{}) string {
    //获取泛型的类型
    switch n.(type) {
    case string:
        return "字符串"
    case int:
        return "整形"
    case bool:
        return "bool"
    }
    return ""
}

// any和interface{}是一个作用。但是需要注意和any和interface{}是引用类型
func fx(n any) string {
    // 断言
    if _, ok := n.(string); ok {
        return "整形"
    }
    if _, ok := n.(int); ok {
        return "int"
    }
    return "无类型"
}

// -----------------------------------interface接口---------------------------------------------------
// 需要注意:interface是一个引用地址
type school interface {
    // 获取全校人数以及各个班级学生数
    getStudentNum() (nums int, numByClass map[string]int)
    getClassNum(className string) (num int)
}

type middleSchool struct {
    //特别注意:在结构体内申请变量不能使用var,只能使用下面这种方式申请
    classNameNum map[string]int
}

func (middleSchool *middleSchool) getStudentNum() (nums int, numByClass map[string]int) {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    if middleSchool.classNameNum == nil {
        panic("setA 未初始化")
    }
    for _, num := range middleSchool.classNameNum {
        nums += num
    }
    numByClass = middleSchool.classNameNum
    return
}
func (middleSchool *middleSchool) getClassNum(className string) (num int) {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    if middleSchool.classNameNum == nil {
        panic("setA 未初始化")
    }
    if nums, ok := middleSchool.classNameNum[className]; ok {
        num = nums
    }
    return
}

// 入参是一个school的interface,interface是一个指针类型
func hah(bb school) {
}

func TestInterface(t *testing.T) {
    //实例化类
    middleSchool := &middleSchool{}
    //类中的属性也是需要实例化,零值为nil
    aa := make(map[string]int)
    aa["1年纪"] = 20
    aa["2年纪"] = 10
    aa["3年纪"] = 30
    middleSchool.classNameNum = aa
    num := middleSchool.getClassNum("2年纪")
    fmt.Println(fmt.Sprintf("2年纪的人数为%v", num))
    sum, _ := middleSchool.getStudentNum()
    fmt.Println(fmt.Sprintf("所有年纪的总人数为%v", sum))

    //由于middleSchool是引用类型,所以没问题,如果middleSchool := middleSchool{},则hah的入参则会报错
    hah(middleSchool)
}

// ------------------匿名结构体嵌套-------
type company struct {
}

// 部门数
func (company *company) departments() int {
    return 20
}

// 员工数
func (company *company) employees() int {
    return 200
}

// duxiaoman 结构体中匿名嵌套了一个company结构体,这种方式叫做匿名嵌套
// 需要注意的,struct name的首字母为小些时代表包外不可访问,如果duxiaoman 和 company不在同一个包则不能嵌套
type duxiaoman struct {
    company
    //匿名嵌套了middleSchool struct,由于middleSchool实现了school interface,所以duxiaoman 也实现school interface
    //如果被嵌入的结构体实现了某个接口的方法,那么嵌入它的结构体也会隐式地实现该接口,这被称为“方法提升”
    middleSchool
}

// duxiaoman struct重写departments方式
func (duxiaoman *duxiaoman) departments() int {
    return 50
}

func TestExtend(t *testing.T) {
    a := &duxiaoman{}
    //部门数为:50,员工数为:200
    // 1. duxiaoman struct会继承company的所有方式和成员
    // 2. duxiaoman stuct支持重写嵌套的方法和成员
    fmt.Println(fmt.Sprintf("部门数为:%v,员工数为:%v", a.departments(), a.employees()))
    //部门数为:20,员工数为:200
    // 如果想访问“父类”的方法,可以直接像下面通过访问成员变量的方式访问
    fmt.Println(fmt.Sprintf("部门数为:%v,员工数为:%v", a.company.departments(), a.company.employees()))
}

// ------------go中的错误机制-------------
// 1. 没有try catch 机制
// 2. 只能通过方法的多返回值特性,一层一层的返回error
// 3. 可以发现error是一个接口,有一个方法Error()返回string类型,错误信息通过该方法Error()返回
// 4. affa的返回值需要返回一个实现error接口的struct
func affa() (int, error) {
    //1. error是一个接口,所以errors.New方法内可以看到return &errorString{text},返回的是一个引用类型
    //2. errorString实现了error接口,定义了一个属性s string(s即为错误信息)和实现了 error接口的Error(),该接口内返回错误信息
    return 0, errors.New("报错了")
}

//-------------包管理mod---------------------
//go.mod文件结构
//  module:声明模块路径
//  go:指定Go语言版本
//  require:列出依赖项及其版本
//  replace:替换依赖包路径或版本
//  exclude:排除特定依赖版本

//go常用命令
// go mod init:创建go.mod文件
// go mod download:下载go.mod文件中指定的所有依赖
// go mod tidy:添加缺失的依赖并删除未使用的依赖,确保go.mod与代码中的导入匹配
// go mod vendor:创建vendor目录副本
// go mod verify:验证依赖项的哈希值是否与go.sum记录一致

//与传统GOPATH的区别
// 与GOPATH相比,go mod允许代码开发不局限于特定目录,依赖管理更加自动化,无需手动管理每个包。使用go mod后,项目可以放在任意目录,不再需要严格的GOPATH/src结构
// 通过go mod管理依赖,可以确保不同机器上构建环境的一致性,大大简化了Go项目的依赖管理流程

// -------------------------------协程 start----------------------------------
func TestXiecheng(t *testing.T) {
    for i := 0; i < 10; i++ {
        //协程:通过go关键字加一个闭包实现
        go func(i int) {
            //打印出来的是1-10的乱序,因为每个协程是异步的,且执行完成时间不同
            fmt.Println(fmt.Sprintf("first:%v", i))
            //这里将i变量传递入协程中,i类型为int,则为值传递,所以打印出来的是1-10
        }(i)
    }
    for i := 0; i < 10; i++ {
        go func() {
            //打印出来的全是10
            //由于并没有将i变量传入闭包,所以所有的协程都共享了i变量。又由于协程的启动是异步的,不保证立即执行,而循环执行非常快,可能在所有协程启动前就已经结束
            //当协程实际执行时,i已经递增到结束值10
            fmt.Println(fmt.Sprintf("sencod:%v", i))
        }()
        time.Sleep(10)
    }
    //为什么这里要使用sleep。因为可能TestXiecheng方法执行完成后,10个协程还没有执行完,这种情况就造成了内存泄漏。下面又更有的当时group.done()来实现
    time.Sleep(10)
}

var n int

// 锁机制 Mutex是互斥锁,核心作用是确保同一时间只有一个 goroutine 能访问临界区代码,防止数据竞争和竞态条件。也就是主要是作用于goroutine内资源竞争的
//
//      在同一 goroutine 中不可重复加锁,否则会引发运行时异常(panic)。
//      未加锁Lock则解锁Unlock,也会导致panic。
//      该锁是线程锁(进程锁,它主要用于保护同一进程内的共享资源,它只能协调单个 Go 进程内多个 goroutine 对共享资源的访问。而微服务通常部署在多台机器上,
//      每个实例都有自己的进程,sync.Mutex 无法在这些独立的进程或机器之间提供同步控制。如果是需要多个进程间对资源进行加锁,则需要使用分布式锁,例如redis锁
//   下面这个例子比较简单,可能不符合业务场景,可以继续看下面的updateMap1方法,可以深刻体会下sync.Mutex的使用方法
func TestSyncLock(t *testing.T) {
    // 对于Mutex和WaitGroup通常引用变量,因为本身就是协调协程之间资源的,肯定要是引用地址
    lock := &sync.Mutex{}
    // waitGroup可以让主线程优雅等待所有子协程执行完成后在推出。
    //      1. group.Add(1) 表示添加一个子协程认为
    //      2. group.Done() 表示该子协程任务执行完成
    //      3. group.Wait() 主线程等待所有子协程done,才可以继续执行,否则主线程这里hold等待阻塞
    //      4. sync.WaitGroup一定要使用指针类型&sync.WaitGroup,因为是作用多个协程之间的
    // 总结:waitGroup是为了主线程在子协程都执行完成后继而再退出,避免主线程提前退出而子协程仍然在执行中,这种情况就造成了协程都内存泄漏
    group := &sync.WaitGroup{}
    for i := 0; i < 100; i++ {
        group.Add(1)
        go lockInfo(group, lock)
    }
    group.Wait()
    // 正常输出100
    fmt.Println(fmt.Sprintf("n最终执行为:%v", n))
}

// 特别注意:WaitGroup和Mutex必须是引用类型的,因为是作用于多个协程之间资源的
func lockInfo(group *sync.WaitGroup, mutex *sync.Mutex) {
    defer func() {
        group.Done()
        //注意:如果在加锁之前报错,导致没有加锁成功,这里解锁则会直接报错
        mutex.Unlock()
    }()
    mutex.Lock()
    //特别注意:为什么这里要对n加锁?
    //        因为:n是全局变量,多协程之间是共享的,所以要必须对公共资源加锁
    n++
}

// --------------------在介绍另外一种锁读写锁-sync.RWMutex---------------------------
// sync.RWMutex是读写锁,而sync.mutex是互斥锁,RWMutex相对于mutex锁的粒度更小,性能更好点,但不是所有场景都适合使用读写锁,还是分场景的。
// sync.Mutex:当一个goroutine获得锁后,其他goroutine无论进行读操作还是写操作都必须等待锁释放,这种锁适用于需要严格保证数据一致性的场景,但并发性能相对较低
// sync.mutex的主要方法是
//  1. mutex.lock 加锁,多goroutine之间只有一个goroutine可以获取成功,其他goroutine获取锁时则阻塞
//  2. mutex.unlock 解锁,对加锁的goroutine进行解锁,解锁成功之后,其他goroutine则可以重新获取到锁
//
// sync.RWMutex:实现了读写分离机制,它允许多个goroutine同时获取读锁进行读操作,但写锁仍然是互斥的,具体表现为
//  1. 读锁之间不互斥,多个goroutine可同时获得读锁
//  2. 写锁之间完全互斥,同一时间只能有一个goroutine持有写锁
//  3. 写锁与读锁互斥,存在读锁时写锁阻塞,存在写锁时读锁阻塞
//
// sync.RWMutex的主要方法
//  1. RLock:加读锁,多goroutine可同时获取到读锁
//  2. RUnlock:解读锁,对加读锁的goroutine进行解锁
//  3. Lock:加写锁,多goroutine之间只有一个goroutine可以获取写锁成功,其他goroutine获取锁时则阻塞
//  4. Unlock:解写锁,对加写锁的goroutine进行解锁,解锁成功之后,其他goroutine则可以重新获取到写锁
//
// sync.Mutex和sync.RWMutex适用的场景也不同
//  1. sync.Mutex适用于严格保护共享数据,确保同一时刻“只有一个goroutine访问的临界区”,类似于库存超卖,以及下面TestSyncMap方法,可以体会下,适用sync.Mutex的场景通常有几个特点
//     1>. “同一个方法内”同时存在对一个对象的“读写操作”,而“写操作依赖读操作的数据属性”,同时“写操作的数据属性也会影响读操作的数据”
//     2>. “读操作开始前需要加锁”,“写操作之后需要解锁”
//     3>. 当多goroutine访问该方法时,如果A goroutine处于刚读完读操作,还未到写操作时,此时另一个B goroutine也开始读取读操作了,那么B goroutine此时读操作读取的数据就是脏数据了,
//     因为A goroutine还没有执行完写操作,那么此时使用sync.Mutex进行加锁则非常合适了
//  2. sync.RWMutex适用于“读写分离”的场景,且读多写少的场景,最重要的是“读写操作没有依赖的场景”,通常也有几个特点
//     1>. “读写分离”的场景怎么理解:可以理解对一个对象有单独的读场景 和 单独的写场景,或是有单独的俩个方法分别是读或者写
//     2>. 读和写场景之间没有完全依赖,是解耦的
//     3>. 最适合缓存系统、配置系统
//
// 如果说了这么多还是不懂,则看下面的例子吧,这是一个配置管理系统,有一个全局的配置对象,且有读配置和写配置操作,对于配置文件系统通常需要有下面加个特点
//  1. readConfig读配置:程序中频繁读取配置,加读锁RLock
//  2. writeConfig写配置:管理员偶尔更新配置,加写锁Lock
//  3. 当读取配置时需要判断此时是否正在更新配置;
//     1>. 如果是则读操作阻塞,只有等更新操作更新完成才能读取,否则将会读取到没有更新完成的配置数据,就造成了数据不一致;
//     2>.  如果否则可以放肆读取配置了,多goroutine之间读配置不加锁
//  4. 当更新配置时判断此时是否正在更新配置
//     1>. 是:只有等更新操作更新完成才能再次更新,否则将会更新到还没有更新完成的配置数据,就造成了数据错乱
//     2>. 否:那么此时仍需要判断是否正在读取配置
//     1>. 是:只有等读取操作完成之后才能更新,否则立刻更新时,读取数据则有可能读到更新的数据,也造成了数据不一致的
//     2>. 否:当更新配置时,即没有正在更新和正在读取配置的操作是,才可以立即更新配置,否则只能阻塞
//
// 可能有人问了,如果读配置不加任何锁,而写配置时加互斥锁或者写锁,会发生什么情况?
//
//  在配置管理系统中,如果读配置操作不加任何锁,而写配置操作使用互斥锁或写锁,会导致严重的数据一致性问题,具体表现为:当读操作不加锁时,多个goroutine可以
//  同时读取配置数据,而写操作在修改数据时虽然会加锁保证独占性,但无法阻止其他goroutine在写操作执行过程中读取到中间状态的数据,例如,在写操作更新DatabaseURL
//  和CacheSize两个字段的过程中,读操作可能读取到部分更新的配置——获取到新的DatabaseURL但仍然是旧的CacheSize值,最终造成了数据错乱,数据不一致的场景
type config struct {
    databaseUrl string
    caseSize    int
    debugMode   bool
}

var configRWMutexLock = &sync.RWMutex{}

// 比如是一个加载到内存的变量
var currentConfig = &config{"127.0.0.1:8801", 10, true}

func readConfig(group *sync.WaitGroup, i int) (conf *config) {
    defer func() {
        configRWMutexLock.RUnlock()
        group.Done()
        fmt.Println(fmt.Sprintf("读取配置结束%v", i))
    }()
    configRWMutexLock.RLock()
    fmt.Println(fmt.Sprintf("读取配置开始%v", i))
    time.Sleep(time.Millisecond * 100)
    conf = currentConfig
    return
}
func writeConfig(group *sync.WaitGroup, i int) {
    defer func() {
        configRWMutexLock.Unlock()
        group.Done()
        fmt.Println(fmt.Sprintf("写入配置结束%v", i))
    }()
    configRWMutexLock.Lock()
    fmt.Println(fmt.Sprintf("写入配置开始%v", i))
    time.Sleep(time.Millisecond * 100)
    //更新配置
    currentConfig.caseSize = 12
}

func TestReadConfig(t *testing.T) {
    group := &sync.WaitGroup{}
    for i := 0; i < 3; i++ {
        group.Add(1)
        go readConfig(group, i)
    }
    group.Wait()
    fmt.Println("全部读取流程DONE")
    //读取配置开始2
    //读取配置开始0
    //读取配置开始1
    //读取配置结束1
    //读取配置结束2
    //读取配置结束0
    //全部读取流程DONE
    //说明可以同时获取多个读锁的,读读不互斥
}

func TestWriteConfig(t *testing.T) {
    group := &sync.WaitGroup{}
    for i := 0; i < 3; i++ {
        group.Add(1)
        go writeConfig(group, i)
    }
    group.Wait()
    fmt.Println("全部读写流程DONE")
    //写入配置开始2
    //写入配置结束2
    //写入配置开始0
    //写入配置结束0
    //写入配置开始1
    //写入配置结束1
    //全部读写流程DONE
    //可以看出只有当前协程释放写锁之后新的协程才可以获取写锁,说明写锁是互斥锁
}

func TestRWConfig(t *testing.T) {
    group := &sync.WaitGroup{}
    for i := 0; i < 3; i++ {
        group.Add(2)
        go readConfig(group, i)
        go writeConfig(group, i)
    }
    group.Wait()

    //写入配置开始2
    //写入配置结束2
    //读取配置开始2
    //读取配置开始0
    //读取配置开始1
    //读取配置结束1
    //读取配置结束2
    //读取配置结束0
    //写入配置开始1
    //写入配置结束1
    //写入配置开始0
    //写入配置结束0
    // 可以看出先获取写锁时,读锁是不可以获取的(写入配置开始2,写入配置结束2);只有写锁释放后读锁才可以获取且读锁之间不互斥(读取配置开始2,读取配置开始0,读取配置开始1,读取配置结束1,读取配置结束2,读取配置结束0)
    // 只有等待读锁和写锁全部释放后才可以再次获取写锁(写入配置开始1,写入配置结束1,写入配置开始0,写入配置结束0)
}

// --------------------------------------通道channel-----------------------------
//  1. 不同协程中的数据交互或者数据共享可以使用channel的方式来实现
//  2. channel的方式通常是一个协程生产数据,另一个协程来消费数据,这种方式特别适用于解耦:当比如原流程是A->B俩个同步步骤,每个步骤各需要1s,那么整体流程就是2s,
//     此时可以创建俩个协程,1个协程用于执行A步骤,并把B步骤需要的数据写入到channel中;在创建一个协程用于获取channel中的数据来实行B步骤;由于协程是异步的,也就是
//     这俩协程是可以并行处理任务的,所以通过channel的这种方式最终1s就可以执行完原2s的流程,正是由于协程的这个机制,对于一些高响应高耗时的任务来说非常合适。
//  3. channel也是引用类型,创建channel的方式为make(chan type)
//  4. 如果通道关闭,继续往通道内写入消息则抛panic
//  5. channel分为俩中,一种是阻塞channel,另一种非阻塞channel
//     1> make(chan int)是阻塞channel,只有当生产者和消费者同时就位时
//  6. 必须确保通道只被关闭一次,关闭的通道如果再次关闭则会出发panic
//  7. 关闭通道后,该通道也会收到消息,data,ok := <-channel,data为channel的零值,而ok为false;
//     特别注意:关闭通道后,并不是只往通道发送一条消息,而是只要for循环data,ok := <-channel都可以拉到data为channel的零值,而ok为false的消息

// 全局变量只是申明name2old的类型,零值为nil,需要在后续进行实例话
// 特别注意:这是全局变量,对于协程来说是共享的,由于该例子在协程中是只读操作,所以没有加锁,如果协程中对该变量有写操作的话,则需要在执行写操作的协程中进行加锁和解锁
var name2old map[string]int
var olds int

func init1() {
    //特别注意:对于这种全局变量一定要重新申明
    name2old = map[string]int{}
    name2old["xiaoyu"] = 20
    name2old["xiaoxia"] = 21
    name2old["liuyu"] = 22
    name2old["aini"] = 10
    name2old["haha"] = 22
    name2old["yuqi"] = 10
}

func getOld(name string, oldChan chan int, group *sync.WaitGroup) {
    defer group.Done()
    //生产者的逻辑
    old := name2old[name]
    oldChan <- old
}

func getOld1(names []string, oldChan chan int, group *sync.WaitGroup) {
    defer group.Done()
    //生产者的逻辑
    for _, name := range names {
        old := name2old[name]
        oldChan <- old
    }
    //特别注意:关闭生产者的通道,必须放在生产者的协程里,因为只有在全部发送完数据之后才可以关闭通道;如果放在父协程内执行,则有可能消息还没发生
    //完成就已经关闭了通道
    //还有一个点:为什么要关闭通道呢?背景是什么呢?首先生产者总有发送完消息的时候吧,那么消费者也不能一直等生产发送消息吧,对于这种问题有俩种解决方式
    //  1. 生产者和消费者预定固定标识,生产者在生产完消息之后写入特定标识,如果消费者读到了该标识就以为消息没了,消费者可以退出了,这种方式有俩个明显的弊端
    //      1> 约定的标识可能和生产的消息一样,这样就容易造成消费者提前退出,有的消息消费不到
    //      2> 生产者数量小于消费者数量时,会导致部分消费者收不到约定标识,也就会支持等待,造成资源浪费
    //  2. 生产者在生产完数据时,直接close通道,go机制会通过广播的形式给所有的消费者发送关闭通知。
    //      1> 当通道被 close() 关闭时,会向所有正在或即将从该通道接收数据的 goroutine 发送广播信号;所有阻塞在接收操作上的 goroutine 会立即被唤醒并返回
    //      2> 通道关闭后,消费者可以继续从通道中读取已缓冲但尚未消费的数据;当所有缓冲数据都被读取完毕后,后续的接收操作会立即返回该通道类型的零值,而不会阻塞
    close(oldChan)
}

func getSumOld(oldChan chan int, group *sync.WaitGroup) {
    defer group.Done()
    for {
        select {
        case old, ok := <-oldChan:
            //当ok为false时则代表通道已关闭且数据已读完
            //当 ok 为 true 时,表示通道处于激活状态,接收到的数据有效
            if !ok {
                //
                fmt.Println(fmt.Sprintf("结束原始为chan类型int的零值:%v", old))
                return
            } else {
                olds = olds + old
            }
        }
    }

}

// 同步的方式获取所选择用户的年龄总和,通常的步骤如下
func TestOrigin(t *testing.T) {
    init1()
    names := []string{"xiaoyu", "liuyu"}
    for _, name := range names {
        // 1. 通过姓名获取年龄 1s
        old := name2old[name]
        // 2. 获取所选姓名的年龄综合 1s
        olds = olds + old
    }
    fmt.Println(fmt.Sprintf("所选年龄总和为:%v", olds))
}

// -------------错误实例------------
// 获取所选择用户的年龄总和,通常的步骤如下
//  1. 通过姓名获取年龄 1s
//  2. 获取所选姓名的年龄综合 1s
func TestChannel(t *testing.T) {
    init1()
    names := []string{"xiaoyu", "liuyu"}
    group := &sync.WaitGroup{}
    chan1 := make(chan int)
    group.Add(1)
    go getSumOld(chan1, group)

    for _, name := range names {
        group.Add(1)
        go getOld(name, chan1, group)
    }
    // 输出panic: send on closed channel 顾名思义:往关闭的channel中发生数据
    // 可以发现写通道消息的方法getOld是协程调用,也就是异步,而close(chan1)是同步的,那么就会可能造成消息还没有全部写入到通道就发生关闭通道的
    // 操作。对于channel来说往关闭的通道内写入数据则会直接跑出panic
    close(chan1)

    group.Wait()
    // 那既然会提前关闭通道,那么把close(chan1) 放到group.Wait()之后,是否可以呢?
    //有人说,可以把close(chan1) 放到group.Wait()之后,这样等协程都完成在关闭channel就可以了,但是这里边包含消费者的协程,该协程只有等到close(chan1)
    //才会退出,也就是说把close(chan1) 放到group.Wait()之后,则group.Wait()会永远阻塞,程序造成死循环。
    fmt.Println(fmt.Sprintf("所选年龄总和为:%v", olds))
}

// 通过协程加channel的方式获取所选择用户的年龄总和
// 一个生产者一个消费者模式
func TestChannel1(t *testing.T) {
    init1()
    names := []string{"xiaoyu", "liuyu"}
    group := &sync.WaitGroup{}
    // 主线程创建一个通道,这是一个阻塞通道,channel其实有俩中类型
    // 1. make(chan int)阻塞队列:其容量为1,只有生产者和消费者同时在线时才会返回。也就是当生产者写入消息时会阻塞,消费者拉取消息时,生产者则立即可以返回,并可以立即写入消息,
    //    理解容量为1的真正含义即可
    // 2. make(chan int,10)非阻塞队列,容量为10:即生产者可以在消费者不存在的情况下,连续写入10条消息,当channel的消息大于10时,生产者再次写入消息时则会发送阻塞
    chan1 := make(chan int)
    group.Add(1)
    // 创建一个协程消费者,消费者从通道内消费数据
    go getSumOld(chan1, group)
    group.Add(1)
    // 创建一个协程生产者,生产者往通道内生产数据
    go getOld1(names, chan1, group)
    // 这里有一个小技巧是先创建消费者,后创建生产者,因为对于阻塞channel来说,只有当生产者和消费者同时就绪时才可以消息互通,那么就可以提前将
    // 消费者执行,当生产者一生产消息时,可以立即被消费到

    group.Wait()
    fmt.Println(fmt.Sprintf("所选年龄总和为:%v", olds))
    // 这里可能会有人疑问了?和下面那种同步的方式相比,使用异步协程+阻塞channel的方式代码复杂度高不少,还需要waitGroup,Mutex等,具体收益在哪里?
    // 答案就是相比同步的方式,异步协程+阻塞channel响应时间更快,效率更高,因为生产者和消费都是异步的,所以理论上是可以并行执行的,这样就可以减少一部分时间了
    // 比如步骤1和步骤2都需要1s中时对比下面俩种方式
    // 异步协程+阻塞channel(1个生产者+1个消费者)              同步执行
    //--------------------------------------         -----------------------
    //|时间    |   生产者任务   |   消费者任务  |         |时间    |   任务        |
    // --------------------------------------         -----------------------
    //| 1S  |   任务A步骤1   |              |         |1s    |   任务A步骤1   |
    //---------------------------------------         -----------------------
    //|  2S  |   任务B步骤1  |  任务A步骤2   |        |2s    |   任务A步骤2   |
    //_______________________________________         -----------------------
    //|  3S  |   任务C步骤1   |  任务B步骤2   |       |3s    |   任务B步骤1   |
    //_______________________________________         -----------------------
    //|  4S  |               |   任务C步骤2  |         |4s   |   任务B步骤2   |
    //---------------------------------------         -----------------------
    //通过上面的对比可以很明显的看出异步协程+阻塞channel的方式更优,4s可以执行3个任务,而同步执行的4s只可以执行2个任务。
    //上面例子是1个生产者+1个消费者的方式对比结果,这种同一秒最多一个生产者和消费者同时执行,所以和同步执行的看起来效果不是太明显,
    //而如果多个生产者多个消费者,也就意味着同一秒可以多个生产者和消费者同时执行, 那相比同步任务的效果就更加明显了
    //
    //对于阻塞channel,消费者数量1个和多个效果是一样的呢?因为只有生产者和消费者同时在时才可以呢?继续看下面的实验
    //  比如步骤1执行需要1s,而步骤2都需要2s时,对于1个消费者和2个消费者的区别(只有一个生产者)
    //      一个生产者1s 一个消费者2s                                     一个生产者1s 俩个消费者2s
    //--------------------------------------               --------------------------------------------------------
    //|时间    |   生产者任务   |   消费者任务  |              |时间   |   生产者任务   |   消费者1任务  |  消费者2任务 |
    // --------------------------------------              --------------------------------------------------------
    //| 1S  |   任务A步骤1   |              |              | 1s  |  任务A步骤1   |               |             |
    //---------------------------------------              --------------------------------------------------------
    //|  2S  |   任务B步骤1  |  任务A步骤2   |             | 2s  |  任务B步骤1   | 任务A步骤2     |             |
    //_______________________________________              --------------------------------------------------------
    //|  3S  |               |              |              | 3s  |  任务C步骤1   |              |   任务B步骤2  |
    //_______________________________________              --------------------------------------------------------
    //|  4S  |  任务C步骤1    |   任务B步骤2  |            | 4s  |  任务D步骤1   |  任务C步骤2    |             |
    //---------------------------------------              --------------------------------------------------------
    //|  5S  |               |             |               | 5s  |  任务E步骤1   |               | 任务D步骤2   |
    //---------------------------------------               ---------------------------------------------------------
    // 一个生产者和一个消费者,可以发现5s内就执行了俩个任务
    //      1. 在第3s的生产者写消息时其实是阻塞的,因为消费者任务A还没有执行完成,消费者没有准备就绪
    //      2. 在第4s时消费者消费完任务A,此时消费就绪,生产者则继续写入任务c,而消费者则继续拉去任务B
    //      3. 在第5s的生产者写消息时其实是阻塞的,因为消费者任务B还没有执行完成,消费者没有准备就绪
    // 一个生产者和俩个消费者,可以发现5s内就执行了4个任务,相比一个消费者效率大大提高
    //      1. 在第三秒时,由于消费者1仍在执行任务A,所以不能继续拉去任务,而消费者B则可以继续拉去任务B,由于此时channel的消息为0,则生产者可以继续写入任务C
    // 总结:对于阻塞channel来说,多个消费者相比于一个消费者,效率可以大大提高

    //生产者是一个还是多个效率更高呢?肯定是多个生产者效率高,那么什么时候使用一个消费者,什么时候使用多个呢?
    //    1. 当对任务量不确定时则只能为1个生产者
    //    2. 当任务量是明确的,则可以分批处理,比如任务量是100,则可以每10个任务使用一个生产者来生产,但是多生产者模式一定要特别注意正确关闭通道,不能多个生产者多个关闭通道
    //          1)可以通过waitGroup来控制
    //
    //  比如步骤1执行需要2s,而步骤1都需要1s时,对于1个生产者和2个生产者的区别(只有一个消费者)
    //      一个生产者1s 一个消费者2s                                     一个生产者1s 俩个消费者2s
    //--------------------------------------               --------------------------------------------------------
    //|时间    |   生产者任务   |   消费者任务  |              |时间   |   生产者1任务   |   生产者2任务  |  消费者任务 |
    // --------------------------------------              --------------------------------------------------------
    //| 1S  |   任务A步骤1   |              |              | 1s  |  任务A步骤1   |               |             |
    //---------------------------------------              --------------------------------------------------------
    //|  2S  |               |  任务A步骤2   |             | 2s  |              |   任务B步骤1   |  任务A步骤2   |
    //_______________________________________              --------------------------------------------------------
    //|  3S  |  任务B步骤1    | 任务B步骤2    |            | 3s  |  任务C步骤1   |               |   任务B步骤2  |
    //_______________________________________              --------------------------------------------------------
    //|  4S  |              |   任务B步骤2  |              | 4s  |              |  任务D步骤1    | 任务C步骤2    |
    //---------------------------------------              --------------------------------------------------------
    //|  5S  | 任务C步骤1     |  任务C步骤2    |               | 5s  |  任务E步骤1   |               | 任务D步骤2   |
    //---------------------------------------               ---------------------------------------------------------

    // 总结:对于阻塞队列和非阻塞队列来说,多生产者和多消费者都比单生产者或者单消费者效率要高
}

// 虽然通道关闭了,但是每次获取通道消息都可以拉到值,data为channel的零值,ok为false
func TestChan(t *testing.T) {
    done := make(chan int)
    close(done)
    //for {
    //  select {
    //  case data, ok := <-done:
    //      fmt.Println(fmt.Sprintf("%v,%v", data, ok))
    //  }
    //}
    for i := 0; i < 5; i++ {
        //这里会输出
        //0,false
        //0,false
        //0,false
        //0,false
        //0,false
        // 也就是虽然通道关闭了,但是每次获取通道消息都可以拉到值,data为channel的零值,ok为false
        data, ok := <-done
        fmt.Println(fmt.Sprintf("%v,%v", data, ok))
    }
}

// 是不是所有的子协程都需要传入父协程的cxt呢?是不是所有的子协程都需要监听父协程的cxt.Done()呢?
//
//         1.对于执行可能被取消的长时间运行任务(如网络请求、I/O操作、复杂计算等)的协程,必须通过select语句监听ctx.Done()通道,以便在父协程取消时能及时退出,避免资源浪费和内存泄漏 。
//               例如,处理上传文件或调用外部API的协程,如果不对取消信号做出响应,即使主请求已结束,它们仍会继续运行并占用系统资源
//         2.执行短期、快速且必须完成的任务的协程通常无需监听ctx.Done()。这些任务通常在毫秒或秒级内完成,其短暂的生命周期使得强制中断的收益不大,反而可能增加逻辑复杂性。但是需要知道
//           的是,如果子协程不监听父协程的cxt.Done(),当父协程取消退出时,此时子协程仍然在运行,直到运行结束
func getOldTreeProducer(cxt context.Context, list []employee, oldChan chan int, group *sync.WaitGroup) {
    defer func() {
        //这里做资源回收
        group.Done()
        close(oldChan)
    }()

    fmt.Println(fmt.Sprintf("入参数input为:%v", cxt.Value("input").(string)))
    timeOutCxt, _ := context.WithTimeout(cxt, time.Second*2)

    //这里启用了3个生产者协程,为了使生产者都生产完消息后在正确关闭通道,所以这里要单独设置一个&sync.WaitGroup,专门用于生产者
    wait := &sync.WaitGroup{}
    for _, data := range list {
        wait.Add(1)
        go func(data employee) {
            defer wait.Done()
            //打开这行代码可以测试timeOutCxt超时场景
            //time.Sleep(time.Second * 3)
            oldChan <- data.age

        }(data)
        // 这里可能有个疑问?为什么只传入了data变量,而wait变量和oldChan都没传入,这样会有问题吗
        // 是没问题的,因为而wait变量和oldChan变量都是引用地址,传入方法和不传入都是一样的,而data是employee类型,属于struct类型,struct
        // 是属于值类型,对于值类型的变量则必须传入。
        // 总结:对于引用类型的变量如果外层能够获取到,则无需传入到协程中;对于值类型的变量则必须通过参数传入到协程中,如果不通过参数传入,则必须使用
        // &sync.mutex来加锁,保证协程安全
    }

    // 这段代码是一个小技巧,一定要学会
    done := make(chan struct{})
    go func() {
        //time.Sleep(time.Second * 3)
        wait.Wait()
        close(done)
    }()

    for {
        select {
        //这里监听了timeOutCxt就可以了,因为timeOutCxt是继承了cxt,那么cxt取消时,timeOutCxt.Done()也会收到消息,同理timeOutCxt取消时,timeOutCxt.Done()也会收到消息
        //注意:当创建了子context时,记住只需要监听子context.Done即可
        case <-timeOutCxt.Done():
            fmt.Println("生产者耗时超过2s或者cxt收到退出通知")
            return
        case <-done:
            fmt.Println("生产者任务完成")
            return
        }
    }
}

/**
分3个生产者 和3个消费者来获取所有任务的年龄总和
*/
// ------------------这里详细介绍下context----------------------------
//context出现的背景
//    1. 设计之初,协程是没有协程id的,不像进程pid或线程tid,所以想kill调一些协程时是比较困难的,所以需要一种能使协程正常退出的机制
//    2. 正常情况下主协程因为某些原因退出后(如进程被kill或者超时),避免子线程仍然执行。主线程退出后子协程也应该退出,如果子协程不退出则变成僵尸协程,这就造成了资源浪费和内存泄漏
//正是基于这两个背景,context应运而生,就是为了主协程退出后,也能保证子协程也正常退出。有没有发现和&sync.waitGroup很相似,waitGroup是为了不让主协程提前退出,而是等待子协程都
//执行完成后,主协程在退出;也是为了避免主协程退出后而子协程仍在执行的,这种子协程也变成僵尸协程,这就造成了资源浪费和内存泄漏。
//所以context和.waitGroup的出发点是不同的,但作用都是相同的
//    1. context出发点是程序异常(如提前kill掉进程)导致主协程异常退出了,这种情况下子协程也要提前退出,否则子协程会成为僵尸协程,这就造成了资源浪费和内存泄漏。
//    2. waitGroup出发点是程序是正常运行,由子协程是异步的,所以主协程必须等待子协程执行完后主协程才可以退出,否则主协程提前退出的话,子协程会成为僵尸协程,这就造成了资源浪费和内存泄漏。
//context的创建方式有4种
//    1. WithCancel():创建可取消的Context,返回的是context和cancel()
//    2. WithTimeout(): 创建带超时的Context, 返回的是context和cancel()
//    3 .WithDeadline():创建带截止时间的Context,返回的是context和cancel()
//    4. WithValue():创建带键值对的Context,返回Context
//context的主要作用有三个
//    1. 取消;WithCancel、WithDeadline都会返回cancel()和context, 当调用cancel()时,context.Done()该channel将会关闭通道,<-context.Done()将会会收到消息,
//          也就是说context.Done()平时是阻塞的,只有调用context的cancel()时context.Done() channel才会收到消息,当子协程
//          收到该channel的消息时,可根据策略觉得是立即return,还是继续执行,还是说会滚之前任务状态
//    2. 超时: WithTimeout返回的是context,也就是说所属协程在所设定的时间还没有返回时context.Done()会关闭通道,<-context.Done()将会会收到消息
//    2. 传递数据:WithValue(),可以设置key value的方式设置值,如果是一个http服务,可以将请求参数传入context,因为context是伴随整个生命周期的。
//context是一个树状结构,子context继承父context的所有属性,父context不继承子context的属性,当父Context取消时,所有派生的子Context也会收到取消的消息。这种机制特别适用于需要级联取消的分布式系统任务
//        父   cancelCxt,cancel         := context.WithCancel(context.Background())
//        子   valueCxt,valueCancel     := context.WithValue(cancelCxt,"input","{'name':'hello','age':20}")
//        孙   timeOutCxt,timeOutCancel := context.WithTimeout(valueCxt,time.Second * 3)
//    1. 当调用cancel()时,cancelCxt.Done()、timeOutCxt。Done()、valueCxt.Done()通道都会收到关闭通知
//    2. 当调用valueCancel()时,timeOutCxt。Done()、valueCxt.Done()通道都会收到关闭通知,而cancelCxt.Done()不会收到通知
//    3. 孙context timeOutCxt.Value("input").(string)也可以获取到子context传入的key
//特别注意:context应该作为协程方法的必要入参,每个协程都需要传入context参数,且cancelCxt、valueCxt或者timeOutCxt是一个interface,也就是说context是一个引用地址
//
//这里肯定有个疑问点?context是怎么实现主协程退出后,也能保证子协程也正常退出的呢?
//    1. 通常是将父协程的根context作为子协程的入参数传入,而子协程一直监听父协程的context.Done()通道的消息,如果有消息则代表父协程退出,则该子协程也应该退出了
//    2.利用context的树状特性和子协程传入父协程的context的方式,只要父协程执行cancel,则子协程,孙协程...都可以收到父协程的context.Done()通道的取消消息,既而孙子协程可以根据自身实际情况做出任务取消的逻辑。
//
//对于协程编码来说,比较复杂,很容易出现主线程已经结束,子协程还在运行的情况,run或者debug之后出现PASS之后还有输出情况,,则代表主线程已经结束,子协程还在运行;pass是表主线程已经结束,后面仍有输出则子协程在主协程退出后仍然执行
//    === RUN   TestManyProducer
//    --- PASS: TestManyProducer (0.00s)
//    PASS
//    消费者收到退出通知
func TestManyProducer(t *testing.T) {
    employee1 := []employee{}
    employee1 = append(employee1, employee{"xiaoyu", 12})
    employee1 = append(employee1, employee{"liuyu", 16})
    employee1 = append(employee1, employee{"baba", 21})
    employee1 = append(employee1, employee{"laba", 20})
    group := &sync.WaitGroup{}
    chan1 := make(chan int)
    // 创建一个可取消的cxt
    cancelCxt, cancel := context.WithCancel(context.Background())
    //继承cancelCxt,并添加key
    valueCxt := context.WithValue(cancelCxt, "input", "{'old':20}")
    // 三个消费者
    for i := 0; i < 3; i++ {
        group.Add(1)
        go func(i int) {
            defer group.Done()
            // 1. 多路选择select:我们知道select语句本身是阻塞操作,会等待任意一个case对应的channel操作就绪,并执行对应的case。
            // 2. select的第二个case是获取<-chan1的消息,需要循环获取,直接chan1通道关闭,所以对于消费者来说,必须使用for select的结构
            // 3. 对于for select的结构特别注意的一点是,命中某个case之后一点要记得return,退出for select结构,不然会无限循环下去
            for {
                select {
                // 收到父协程的取消任务通知,则子协程也做取消任务,直接return
                case <-valueCxt.Done():
                    fmt.Println("消费者收到退出通知")
                    return
                case old, ok := <-chan1:
                    if !ok {
                        // 收到chan1通道关闭的消息,则可以退出for select循环,这里返回return
                        fmt.Println(fmt.Sprintf("消费者%v任务完成", i))
                        return
                    } else {
                        // 收到chan1的消息,且chan1通道未关闭,则不return,继续接受chan1通道的消息
                        olds = olds + old
                    }
                }
            }
        }(i)
        // 这里可能有个疑问?为什么group变量和chan1和cancelCxt都没传入,这样会有问题吗
        // 是没问题的,因为而group变量和chan1变量都是引用地址,而cancelCxt是interface,也是属于引用地址,传入方法和不传入都是一样,而如果协程内的操作的变量是值类型的,则该变量则必须传入。
        // 总结:对于引用类型的变量如果外层能够获取到,则无需传入到协程中;对于值类型的变量则必须通过参数传入到协程中,如果不通过参数传入,则必须使用
        // &sync.mutex来加锁,保证协程安全
    }
    group.Add(1)
    // 创建多个生产者是难点,因为考虑的点是,只有多个生产者都在生产完消息后才可以关闭channel
    //   1. close channel必须是在getOldTreeProducer协程内关闭
    //   2. 使用&sync.waitGroup来等待所有生产者生产完任务之后在关闭channel
    go getOldTreeProducer(valueCxt, employee1, chan1, group)

    //一个用于接收操作系统信号的缓冲通道
    sign := make(chan os.Signal, 1)

    //这里运用一个小技巧
    //done := make(chan struct{})
    //  go func() {
    //      group.Wait()
    //      close(done)
    //  }() 其实group.Wait() close(done)就可以代替这段代码,效果是完全一样的,也不会出现内存泄漏的情况,使用新起一个协程这种方式有哩个原因
    //        1. 原方式会阻塞主线程,新方式使用新起一个协程+通道的方式+for select <-channel的方式,可以避免主线程阻塞
    //        2. 如果线程需要监控cxt.Done()的消息,则必须使用这种for select的新方式,同时监控cxt.Done()和done,谁先收到消息则退出循环;
    //           如果done先收到消息则表示任务执行完成,主线程可以退出;如果cxt.Done()收到消息则代表主线程需要取消任务,主线程也可以退出

    //如果不使用协程+通道的方式+for select <-channel的这种方式,那代码只能是这样写。close(done)就代表任务已经执行完成了,后面在for select
    //则会造成死循环,该程序只有在收到cxt的cancel()时才会退出,我们理想的程序是收到cxt的cancel()或者程序正常结束时都应该退出。
    //group.Wait()
    //close(done)
    //for {
    //      select {
    //      case signType := <-sign:
    //          switch signType {
    //          case syscall.SIGKILL:
    //              fmt.Println("程序被kill掉,提前退出")
    //              cancel()
    //              return
    //          }
    //      }
    //  }
    done := make(chan struct{})
    go func() {
        group.Wait()
        close(done)
    }()

    //多路选择和超时控制
    //   1. select语句中本身就是阻塞操作,会等待任意一个case对应的阻塞操作就绪,同时执行对应case后面的逻辑,最终退出select.
    //   2. 如果select中有default,那么当所有的case都在阻塞时,则会运行default后的逻辑,并最终退出select
    //   3. 由于select中的所有case都是阻塞的,所以通常需要<-time.After(time)来控制阻塞时长,当select的阻塞时间大于time时,则select退出阻塞
    //   4. 对用的case执行后不需要手动return或者break,会自动退出select循环
    //   5. 本次select中的所有case,只要又一个case就绪就可以退出了,不需要循环for select结构
    select {
    // done通道关闭后,<-done会收到消息,收到消息后则直接退出,代表程序运行完成
    case <-done:
        fmt.Println("程序正常运行完成")
        fmt.Println(fmt.Sprintf("所选年龄总和为:%v", olds))
    case signType := <-sign:
        switch signType {
        case syscall.SIGKILL:
            fmt.Println("程序被kill掉,提前退出")
            cancel()
        case syscall.SIGINT:
            fmt.Println("程序被control+c,提前退出")
            cancel()
        }
    //time.After也是一个channel,通过设置时间可以达到超时控制功能,因为这个for循环不能一直循环吧,如果<-done一直没有收到信息则无限时长循环到这里了,
    //所以可以加一个超时控制,当5s之后,没有收到<-done或者 <-sign消息,则主动退出程序
    case <-time.After(time.Second * 5):
        cancel()
    }
}

// 多路选择与超时控制
func TestSelec(t *testing.T) {
    aa := make(chan int)
    select {
    case <-aa:
        fmt.Println("hah")
    case <-time.After(time.Second * 3):
        fmt.Println("超时3s退出")
    }
    fmt.Println("bb")
}

// 上面介绍了sync.waitGroup,sync.mutex,接下来继续介绍下sync的相关组建
// -------------------sync.once---------------------------
// 在多线程中希望只被访问一次,例如单例模式
// 1. &sync.once 必须是全局变量,是需要多协程共享的,且直接赋值
// 2. 多线程执行&sync.once.Do(func())时,可以保证func()只被执行一次
type car struct{}

var instance car

// 全局变量初始化(之前以为不能初始化呢)
// &sync.Once 必须是全局变量,因为是多协程之间保证只执行一次,那么肯定是需要同一个&sync.Once来保证实现
// 全局变量初始化是var关键字加上=,注意不是:=
var once = &sync.Once{}

func init2() car {

    once.Do(func() {
        fmt.Println("只进来一次")
        instance = car{}
    })
    return instance
}
func TestOnce(t *testing.T) {
    group := &sync.WaitGroup{}
    for i := 0; i < 5; i++ {
        group.Add(1)
        go func() {
            instance := init2()
            //返回的instance的地址都是同一个
            fmt.Println(fmt.Sprintf("地址为%v", unsafe.Pointer(&instance)))
            group.Done()
        }()
    }
    group.Wait()
    //只进来一次
    //地址为0x1013438a0
    //地址为0x1013438a0
    //地址为0x1013438a0
    //地址为0x1013438a0
    //地址为0x1013438a0
}

// ---------------------------sync.map------------------------------
// 这是个原生map,在goroutine并发读写或者并发写写时都会直接panic。
//
//      1.如果map在读取过程中被其他goroutine修改,读取方可能会获取到不一致的数据状态,即使只有一个写操作配合任意数量的读操作(并发读写),同样会触发fatal error: concurrent map read and map write
//      2.当多个goroutine同时对map进行写操作时,会破坏map的内部数据结构,直接引发fatal error: concurrent map writes
//      3.当多个goroutine同时对map进行读写操作时,即使没有上面俩种报错,也会发送一些发未定义的行为,包括数据错乱,可以看下面这个例子
//      4.如果多goroutine之间只有读操作,肯定没有写操作,这种情况下原生map是并发安全的;多goroutine之间只要有map的写操作,那么原生map就是并发不安全的。
//

var map1 = make(map[string]int)

func updateMap(group *sync.WaitGroup) {
    defer group.Done()
    //读取map
    if sum, ok := map1["sum"]; ok {
        //写入map
        map1["sum"] = sum + 1
    } else {
        //写入map
        map1["sum"] = 0
    }
    //sum值为010 <nil>
    //sum值为110 <nil>
    //sum值为210 <nil>
    //sum值为310 <nil>
    // 可以看出上面的输出其实不是我们的预期输出,预期应该是0 1 2 3这种,可见发生了数据的错乱
    fmt.Println(fmt.Printf("sum值为%v", map1["sum"]))
}

// 这是一个多goroutine之间使用原生map并发读写的案例,就发送了一些未定义的行为,包括数据错乱
func TestSyncMap(t *testing.T) {
    group := &sync.WaitGroup{}
    for i := 0; i < 10; i++ {
        group.Add(1)
        go updateMap(group)
    }
    group.Wait()
}

// 上面知道了原生的map在多goroutine并发读写的情况下出现的问题,接下来使用sync.map来解决
// 注意这里并没有像原生map定义key和value的类型
var map2 = &sync.Map{}
var lock = &sync.Mutex{}

func updateMap1(group *sync.WaitGroup) {
    defer group.Done()
    //读取map
    //注意返回值value any, ok bool,value是一个interface,所以需要断言
    sum, ok := map2.Load("sum")
    if ok {
        //断言为int
        sum = sum.(int) + 1
        //写入map 没有返回值
        map2.Store("sum", sum)
    } else {
        //写入map
        map2.Store("sum", 0)
    }
    fmt.Println(sum)
    //1
    //2
    //3
    //4
    //5
    //4
    //6
    //8
    //7
    // 发现返回了俩个4?问题原因是在1076行sum, ok := map2.Load("sum")
    //    1. sync.map是并发安全的,怎么理解这个并发安全呢?只是相对于原生map时并发读写或者并发写写不会panic。不要理解为并发时都不需要加锁了,数据时完成正确的
    //    2. sync.map是使用俩个map来解决并发读写的,读map不加锁,多goroutine之间可以并发读,保证高效能,写map是写互斥锁,写并发时会阻塞。
    //    3. 出现上面的问题,原因是 map2.Load("sum")时并没有加锁,导致并发时可能获取到的值是一样的,也就导致map2.Store("sum", sum)写入sum值可能是相同的,也就出现了俩个4
    //    4. 本质原因是 map2.Load("sum")读锁和map2.Store("sum", sum)写锁不是原子性的,所以如果一个goroutine中涉及到多种类型的sync.map的操作时,往往会有原子性问题,一定要注意,所以一个goroutine要不写要不读,实在不行就加锁
    //    5. 其实sync.map提供了很多原子性操作,具体可以看下下面的操作,目的就是在多goroutine可以并发安全正确的操作
    // 解决这个问题也很简单,有以下几种方式
    //    1. 使用sync.map加锁
    //          lock.Lock()
    //          sum, ok := map2.Load("sum")
    //          if ok {
    //              //断言为int
    //              sum = sum.(int) + 1
    //              //写入map 没有返回值
    //              map2.Store("sum", sum)
    //          } else {
    //              //写入map
    //              map2.Store("sum", 0)
    //          }
    //          lock.Unlock()
    //    2.
}

// 那么对于原生map并发不安全的情况,通常有俩种解决方式
//
//  1.互斥锁:通过sync.Mutex或者sync.RWMutex来控制map的访问
//  2.sync.map : 采用空间换时间的策略,通过两个map(read map和dirty map)实现读写分离,read map使用原子操作保证高性能读取,dirty map则在需要写入时通过互斥锁保护 ,这种设计还通过entry封装value,实现了延迟删除机制
//      1.sync.map.Load(key) 获取key的值,获取的值是any类型,需要进行断言
//      2.sync.map.Store(key,value) 存储key的值为value
//      3.sync.map.Delete(key) 删除key
//      4.sync.map.Range() 遍历整个map,是无序的,且需要通过回调函数来实现遍历,func(key, value interface{}) bool
func TestSyncMap1(t *testing.T) {
    group := &sync.WaitGroup{}
    for i := 0; i < 10; i++ {
        group.Add(1)
        go updateMap1(group)
    }
    group.Wait()
    sum, _ := map2.Load("sum")
    fmt.Println(fmt.Sprintf("总和sum值为:%v", sum))
    //删除key
    map2.Delete("sum")
    //存储key
    map2.Store("哈哈", 12)
    map2.Store("呀呀", 14)
    map2.Store("啦啦", 20)
    //遍历key,当返回 true 时继续遍历下一个键值对,返回 false 时立即停止遍历
    //由于 sync.Map 的键和值都是 interface{} 类型,通常需要进行类型断言来获取具体的类型
    map2.Range(func(key, value any) bool {
        fmt.Println(fmt.Sprintf("key为%v,value为%v", key, value))
        return true
    })

    //原子操作:判断参数key是否存在,返回值 (actual any, loaded bool)
    // 1.参数key存在时:则actual参数返回该key值且loaded 返回 true
    // 2.参数key不存在时:则赋值参数key值为参数value值,且actual返回新存储的value,loaded 返回 false
    data, status := map2.LoadOrStore("哈哈", 34)
    if status {
        fmt.Println(fmt.Sprintf("key为哈哈,已经存在,值为%v", data))
    } else {
        fmt.Println(fmt.Sprintf("key为哈哈,不存在,值为%v", data))
    }

    //原子操作:判断参数key是否存在,(value any, loaded bool)
    //  1.如果key存在:则删除该key,value返回原key值,loaded返回true
    //  2.如果key不存在:则value返回nil(any为引用类型,引用类型的零值为nil),loaded为false
    data, status = map2.LoadAndDelete("来来来")
    if !status {
        fmt.Println(fmt.Sprintf("key为来来来不存在,data为%v", data))
    }

    //原子操作:判断参数key的值是否等于old参数,返回值为(deleted bool)
    //  1.如果相等:删除该key,并返回true
    //  2.如果不相等:不执行删除操作,并返回false
    //  3.如果该key不存在:不删除,并返回false
    status = map2.CompareAndDelete("哈哈", 12)
    if status {
        fmt.Println("key为哈哈存在,且值为12,删除成功")
    } else {
        fmt.Println("key为哈哈存在,但是值不为12,删除失败")
    }

    //原子操作:判断参数key的值是否等于old参数,返回值为bool
    //  1. 如果key存在且值为old:则将该key的值更新为new,并返回true
    //  2. 如果key存在且值不为old:则不进行更新操作,并返回false
    //  3. 如果key不存在,则直接返回false
    status = map2.CompareAndSwap("呀呀", 14, 50)
    if status {
        fmt.Println("key为呀呀存在,且值为14,则成功更新为50")
    } else {
        fmt.Println("key为呀呀更新失败")
    }
}

// --------------------------http服务--------------------
// go test go_test.go 对于_test.go的文件来说,只能用go test命令运行,运行后会执行所有Test开始的function
// go test -run testFunc test.go 如果只想运行test文件中的某个方法,则可以使用这种方式指定执行
// go run a.go 可执行程序的入口必须是 main 包中的 main 函数,也就是a.go文件必须属于main 包,且含有main 函数
// go run 执行当前目录下main包下的main函数(需注意,该目录下只有有一个main包下的main函数,如果多个,build时则报错)
// go build a.go 是会对a.go文件生成一个可执行文件,然后执行该文件,默认会从 main 包中的 main 方法开始执行
// go build 编译当前目录,生成与当前目录一致的可执行文件,执行文件时执行main包下的main函数(需注意,该目录下只有有一个main包下的main函数,如果多个,build时则报错)
// 下面这个是一个http服务,通常希望程序可以一直运行(如kill或者资源不足时也可以退出),可以使用下面这种方式
//
//      /usr/bin/nohup go run a.go >> b.txt 2>&1 & 或者  /usr/bin/nohup ./a >> b.txt 2>&1 &
//      1.nohup: 是一个二进制命令,可以保证关闭当前窗口或者当前终端时服务仍然运行,即使SSH连接断开或终端关闭,进程也不会被终止。如果在A窗口执行go run a.go,然后关闭A窗口后该服务则会被杀死,加上nohup则不会
//      2.>> b.txt表示:将程序的标准输出写入到b.txt文件中
//      3.2>&1: 重定向标准错误到标准输出的文件,也就是标准错误也写入到b.txt文件中。2代表标准错误(stderr),1代表标准输出(stdout);&1表示引用标准输出的目标位置;将错误输出也重定向到同一个日志文件中
//      4.& :表示后台运行;将进程放入后台运行,不阻塞当前终端;用户可以继续在终端中执行其他命令
func TestHttp(f *testing.T) {
    http.HandleFunc("/a", func(w http.ResponseWriter, r *http.Request) {
        //终端会输出
        fmt.Println("hello")
    })
    http.HandleFunc("/b", func(w http.ResponseWriter, r *http.Request) {
        //页面上会输出
        time.Sleep(time.Second * 5)
        w.Write([]byte("很高兴认识你"))
    })
    http.HandleFunc("/c", func(w http.ResponseWriter, r *http.Request) {
        for i := 0; i < 10; i++ {
            go profile1111()
        }
        w.Write([]byte("test start"))
    })
    fmt.Println("开始连接")
    //会hold住
    http.ListenAndServe(":8080", nil)
}

func profile1111() {
    time.Sleep(time.Second * 100000)
}

// -------------------------------------benchmark基准测试----------------------------------
// 为什么要使用基准测试呢? 当对于同一个问题有多种实现方案时,不知道哪种方案更好性能更优时,则可以使用基准测试来进行对比。最常用的一种场景时引入第三方库时哪个库效果更好?则可以使用benchmark来进行测试
//  1. go test -bench=. 执行当前目录下的所有_test.go文件中的测试函数(Test开头)和基准测试函数(Benchmark开头)
//  2. go test x_test.go -bench=. 执行x_test.go文件中的测试函数(Test开头)和基准测试函数(Benchmark开头)
//  3. go test x_test.go -bench=. -run=^$ 只执行x_test.go文件中的基准测试函数(Benchmark开头)
//  4. go test x_test.go -bench=. -run=^$ -benchtime=3s 默认的测试时间是1s,可以通过 -benchtime参数来指定基准测试的时长
//     参数如下:
//     a. -benchtime 3s: 指定测试时间
//     b. -benchmem:打印出申请内存的次数。一般用于简单的性能测试,不会导出数据文件
//     c. -blockprofile block.out: 将协程的阻塞数据写入特定的文件(block.out)。如果-c,则写成二进制文件
//     d. -cpuprofile cpu.out: 将协程的CPU使用数据写入特定的文件(cpu.out)。如果-c,则写成二进制文件
//     e. -memprofile mem.out: 将协程的内存申请数据写入特定的文件(mem.out)。如果-c,则写成二进制文件
//     f. -mutexprofile mutex.out: 将协程的互斥数据写入特定的文件(mutex.out)。如果-c,则写成二进制文件
//     g. -trace trace.out:将执行调用链写入特定文件(trace.out)
//
// bench=. 代表的是执行所有Benchmark开头的函数,这里是可以使用正则匹配执行的函数的
//
// BenchmarkJoinStr-8         21444            111678 ns/op                3555266 B/op         10 allocs/op
// BenchmarkBufferStr-8    28842253                46.50 ns/op             24 B/op          0 allocs/op
// 这是执行go test -bench=. -run=^$ -benchtime=3s -benchmem 命令行返回的结果
//  1. BenchmarkJoinStr-8:测试函数名,-8表示使用8个CPU核心
//  2. 21444表示在3s内执行了21444次BenchmarkJoinStr函数;28842253表示3s内执行了28842253次BenchmarkBufferStr函数
//  3. 111678 ns/op:每次操作平均耗时111,678纳秒(约0.11毫秒);46.50 ns/op:每次操作平均耗时46.5纳秒
//  4. 3555266 B/op: 每次操作分配3555266 byte内存;24 B/op每次操作分配24 byte内存
//  5. 10 allocs/op:每次操作发生10次内存分配;什么情况下会发生内存分配呢?
//     a.使用make()创建切片、映射,或使用new()分配对象
//     b.当局部变量在函数返回后仍被引用时,会从栈逃逸到堆
//     c.字符串拼接:频繁的字符串连接操作会产生大量临时对象
// 多次内存分配,会造成频繁分配会触发垃圾回收,影响程序性能;内存分配本身需要时间,分配次数越多耗时越长;大量小对象分配可能导致内存利用率下降
// 总结:可以看出在相同的时间内,ns/op和allocs/op 越小性能越优秀,BenchmarkBufferStr执行的次数更多且每次执行的时间更少,可以看出BenchmarkBufferStr方法对于拼接字符串更优秀
// 注意:基准测试只能_test.go文件中进行测试

// 基准测试就是让不同的程序在相同的时间内执行,对比程序之间执行的时间和消耗的内存
// 使用+来拼接字符串
// go test -bench=JoinStr -run=^$ 表示执行BenchmarkJoinStr为开头的方法,所以BenchmarkJoinStr和BenchmarkJoinStrBuffer方法都回执行基准测试
// go test -bench=BenchmarkJoinStr$ -run=^$ 表示只执行BenchmarkJoinStr方法来进行基准测试
func BenchmarkJoinStr(b *testing.B) {
    //b.StartTimer()用于开始会在恢复计数;b.ResetTimer()用于重置即使
    b.ResetTimer()
    var joinStr string
    // b.N是不固定的,执行次数是执行时间动态调整得出的,执行时间默认为1s.通过参数-benchtime调整
    // 测试的核心代码放在for i := 0; i < b.N; i++下面
    for i := 0; i < b.N; i++ {
        elem := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}
        for _, v := range elem {
            joinStr += v
        }
    }
    //停止计数
    b.StopTimer()
}

// 使用bytes.Buffer来拼接字符串
func BenchmarkJoinStrBuffer(b *testing.B) {
    var buffer bytes.Buffer
    b.ResetTimer()
    b.StartTimer()
    for i := 0; i < b.N; i++ {
        elem := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}
        for _, v := range elem {
            buffer.WriteString(v)
        }
    }
    b.StopTimer()
}

// ------------------pprof性能调优-------------
// 具体看这篇文章吧https://zhuanlan.zhihu.com/p/396363069

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容