1. go是编译形语言,python、php等是 解释形语言
2. go语言 天生支持并发,能自动跑满16核
3. 函数外只能放置标识符 (变量/常量/函数/类型) 的声明, 不能像python 那样直接写 print(a+b)
   例如在 func main() 外不是函数内直接输入 fmt.Println("hello") 是非法的
4. go语言中的变量必须先声明再使用;go语言中变量声明必须使用,不会用就编译不过去,这样会减少编译的体积
    在函数外声明的全局变量,不使用不会报错;但在函数里面声明的变量,不适用会报错
5. 变量
   批量声明
    var (
        name string
        age  int
        isOk bool
    )
    //声明变量同时赋值
    var s1 string = "abc"
    //类型推导 (根据值判断该变量是什么类型)
    var s2 = "20"
    //简短变量声明,只能在函数里面使用 ???
    s3 := "哈哈哈"
    匿名变量 _
    匿名变量不占用声明空间,不会分配内存,_多用于占位(不然会报错),表示忽略值
    注释:
        -1)函数外的每个语句必须以关键字开始(var、const、func等)
        -2):= 不能使用在函数外
        -3) _多用于占位,表示忽略值
6. 常量
    const (
        n1 = 100
        n2
        n3
    )
    批量声明常量时, 如果某一行声明后没有赋值,默认就和上一行一致
    iota
    iota 是go 的常量计数器, 只能在常量的表达式中使用
    iota 在const关键字出现时 将被重置为0,const中 每新增一行常量声明将使 iota 计数一次(iota可
    理解为 const语句块中的行索引)。使用iota能简化定义,在定义枚举时很有用。
7. 进制
    0-9是十进制 ,因为是 到10 要进一位
    权限 是 八进制 777,到8要进一位
    十六进制 是 0-f
    func main() {
       //十进制
       var a int =10
       fmt.Printf("%d \n", a) //10
       //八进制 以0开头
       var b int = 077
       fmt.Printf("%o \n", b)//77
       //十六进制 以0x开头
       var c int = 0xff
       fmt.Printf("%x \n", c) //ff
    }

8. float
    默认 go语言中的小数都是float64类型

9. 组成每个字符串的元素叫作"字符",可以通过遍历或者单个获取字符串元素获得字符。字符用单引号(')包裹起来,如:
    var a := '中'
    go语言的字符有以下两种:
    -1) unit8类型,或者叫buty型,代表了ASCII码的一个字符
    -2) rune类型,代表了一个UTF-8的一个字符
    当需要处理中文、日文或者其他符合字符时,则需要用到rune类型,rune类型实际是一个int32(一个字符是3个字节,有个前缀rune,所以是4*8 32个字节)
    字节 :1字节=8bit(8个二进制)
    1个字符 'A' = 1个字节
    1个utf8编码的汉字 '沙' 一般占3个字节 (生僻字可能占4个字节)
    示例:
        c1 := "红"
        c2 := '红'
        fmt.Printf("c1:%T c2:%T\n",c1,c2)
        打印结果 : c1:string c2:int32
    字符1位的表示byte,字符3位的表示rune

10.if条件判断还有一种特殊的写法,可以在if表达式之前添加一个执行语句,再根据变量值进行判断,
    if score := 65; score >= 90{
          fmt.Println("A")
       }else{
          fmt.Println("继续努力")
       }
       以上写法与
       score := 65;
       if  score >= 90{
              fmt.Println("A")
           }else{
              fmt.Println("继续努力")
           }
    写法不同的是, score的作用域不同,第一种score的作用域是在if条件判断中

11. Go语言中的所有循环类型均可以使用 for关键字来完成
    for循环的基本格式如下 :
        for 初始语句;条件表达式;结束语句{
            循环语句
        }
    //基本格式
       for i := 0;i<10; i++ {
          fmt.Println(i)
       }
       //变种1
       //i := 5
       //for ;i<10 ;i++ {
       // fmt.Println(i)
       //}
       //变种2
       i := 5
       for i<10 {
          fmt.Println(i)
          i++
       }

12. for range(键值循环)
    Go语言种可以使用for range遍历数组、切片、字符串、map及通道(channel)。通过for range 遍历的返回值
    有以下规律:
        -1)数组、切片、字符串返回索引和值。
        -2)map返回键和值。
        -3)通道(channel)只返回道内的值。
        示例 :
        s := "Hello沙河"
       for i, v := range s{
          fmt.Printf("%d %c\n",i, v)  //打印出字符
          fmt.Println(i, v) //打印出 ascii码
       }
       //哑元变量,不想用的都直接给它
       for _,v = range s {
           fmt.Printf("%c\n", v)
       }

13. main函数 是入口函数,它没有参数也没有返回值,如果要编译可执行文件,必须要有main包和main函数(入口函数)
    Go语言函数外的语句必须以关键字开头,函数内部定义的变量必须使用

14.整型
    无符号整型 :uint8、uint16、uint32、uint64
    带符号整型 :uint8、uint16、uint32、uint64
        其中 8 是表示8个二进制 8的2次方 (255,256位是从0开始,一共有256位)
    int : 具体是32位还是64位看操作系统
    unitptr : 表示指针

15. 字符串、字符、字节 都是什么
    字符串 :双引号包裹的是字符串
    字符:单引号包裹的是字符,单个字母、单个符号、单个文字
    字节:1byte=8bit (bit是二进制)
    go语言中字符串都是UTF8编码,UTF8编码中一个常用汉字一般占用3个字符

16. go语言中没办法直接定义二进制
    //八进制
       var n1 = 0777
       //十六进制
       var n2 = 0xff
       fmt.Println(n1, n2)  //打印的默认是10进制  511 255
       fmt.Printf("%o\n", n1) //777
       fmt.Printf("%x\n", n2) //ff

17. 位运算符
    & 参与运算的两数各对应的二进位相与 (两位均为1才为1)
    | 参与运算的两位数各对应的二进位相或 (两位有一个为1就为1)
    ^ 参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1.(两位不一样则为1)
    << 左移n位就是乘以2的n次方。"a<<b" 是把a的各二进位全部左移b位,高位丢弃,低位补0
    >> 右移n位就是除以2的n次方。"a>>b" 是把a的各二进位全部右移b位

18.数组 (array)
    数组是同一种数据类型元素的集合。在go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组
    大小不可变化。基本语法:
    //定义一个长度为3元素类型为int的数组a
    var a [3]int
    数组定义
    var 数组变量名  [元素数量] T

   var a1 [3] bool
   //数组的初始化
   //如果不初始化:默认元素都是零值 (布尔值:false,整型和浮点型都是0,字符串:"")
   //1. 初始化方式1
   a1 = [3]bool{true, true, true}
   fmt.Println(a1)  //[true true true]
   //2.初始化方式2:根据初始值自动推断数组的长度是多少
   a10 := [...]int{0,1,2,34,4,5,6,4,3}
   fmt.Println(a10) //[0 1 2 34 4 5 6 4 3]
   //3.初始化方式3:根据索引来初始化
   a3 := [5]int{0:1,4:2}
   fmt.Println(a3)    //[1 0 0 0 2]
    **************************************************
    //多维数组
       //[[1 2] [3 4] [5 6]] [1 2]中,不像php中使用逗号分隔的,它使用空格分隔的
       var a11 [3][2]int
       a11 = [3][2]int{
          [2]int{1,2},
          [2]int{3,4},
          [2]int{5,6},
       }
       fmt.Println(a11)
       //多维数组的遍历
       for _,v1 := range a11{
          fmt.Println(v1)
          for _, v2 := range v1{
             fmt.Println(v2)
          }
       }
    数组的长度是固定的并且数组长度属于类型的一部分,所以数据有很多的局限性。

19. 切片(slice)
    切片是一个拥有相同类型元素的可变长度的序列。它是基于数组类型的一层封装。它非常灵活,支持自动扩容。
    切片是一个引用类型,它的内部结构包含 地址、长度和容量。切片一般用于快速地操作一块数据集合。
    切片的定义
    var name [] T
    其中,name表示变量名,T表示切片中的元素类型。
    切片指向了一个底层的数组。
    切片的长度就是它元素的个数。
    切片的容量是底层数组从切片的第一个元素到最后一个元素的数量。
    切片是引用类型,都指向了底层的一个数组(数组是数值类型)
    切片的本质就是一个框,框住了一块连续的内存。
    切片并不存储任何元素而只是对现有数组的引用。
    切片之间是不能比较的,我们不能用==操作符来判断两个切片是否含有全部相等元素。切片唯一合法的比较操作是和nil
    比较。一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的
    切片一定是nil
    判断一个切片是否是空的,要用len(s)来判断,不应该 使用 s==nil来判断
    总结:
        -1)切片不保存具体的值
        -2)切片对应一个底层数组
        -3)底层数组都是占用一块连续的内存

    切片 左闭右开 (左边值包含,右边值不包含)
20. append给切片追加元素
    s1 := []string{"北京","上海","深圳"}
       fmt.Printf("s1=%v len(s1)=%d cap(s1)=%d\n",s1, len(s1),cap(s1))
       //s1[3] = "广州" //错误的写法 会导致编译错误:索引越界

       //调用append函数必须用原来的切片变量接收返回值
       //append追加元素,原来的底层数组放不下的时候,Go底层就会把底层数组换一个
       //必须用变量接收append的返回值
       s1 = append(s1,"广州")
       fmt.Printf("s1=%v len(s1)=%d cap(s1)=%d\n",s1, len(s1),cap(s1))
       s1 = append(s1,"杭州","城都")
       fmt.Printf("s1=%v len(s1)=%d cap(s1)=%d\n",s1, len(s1),cap(s1))
       ss := []string{"武汉","西安","苏州"}
       s1 = append(s1, ss...) //...表示拆开
       fmt.Printf("s1=%v len(s1)=%d cap(s1)=%d\n",s1, len(s1),cap(s1))
    copy 切片
    使用copy()函数将切片a中的元素复制到切片c
        a := []int{1,2,3,4,5}
        c := make([]int,5,5)
        copy(c, a)

    make创建切片
    var a = make([]int, 5, 10) //创建切片,长度为5,容量为10
       for i := 0;i<10;i++ {
          a = append(a, i)
       }
       fmt.Println(a)  // [0 0 0 0 0 0 1 2 3 4 5 6 7 8 9]
       fmt.Println(cap(a))  //20
21.指针

   //1. &:取地址
   n := 18
   p := &n
   fmt.Println(p) // 0xc000018078
   fmt.Printf("%T\n", p) // *int:int类型的指针
   //2. *: 根据地址取值
   m := *p
   fmt.Println(m) //18
   fmt.Printf("%T\n", m) //int

    总结:取地址操作符& 和取值操作符 * 是一对互补操作符,&取出地址,*根据地址取出地址指向的值
    变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
        -1)对变量进行取地址(&)操作,可以获取这个变量的指针变量
        -2)指针变量的值是指针地址
        -3)对指针变量进行取值(*)操作,可以获取指针变量指向的原变量的值

22. make是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是
    他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他它们的指针了。
    make 和new 的区别
        -1)make和new都是用来申请内存的
        -2)new很少用,一般用来给基本数据类型申请内存,string、int返回的是对应类型的指针(*string、*int)
        -3)make是用来给slice、map、chan申请内存的,make函数返回的是对应的这三个类型本身

23. map
    map是一种无序的基于key-value 的数据结构,go语言中的map是引用类型,必须初始化才能使用
    go语言 map 的定义语法如下:
        map[KeyType]ValueType
            其中,KeyType表示键的类型;ValueType表示键对应的值的类型
    map类型的变量默认初始值为nil,需要使用make(函数)来分配内存。语法为:
    make(map[KeyType]ValueType,[cap])
        其中cap表示map的容量,该参数虽然不是必须的,但我们应该在初始化map的时候就为其指定一个适合的容量。
    var m1 map[string]int
       fmt.Println(m1 == nil)
       m1 = make(map[string]int,10)
       m1["理想"] = 18
       fmt.Println(m1)

   判断某个键是否存在
   go语言中有个判断map中键是否存在的特殊写法,格式如下:
   value,ok := map[key]
   value,ok := m1["test"]  //约定俗成用ok接收返回的布尔值
   if !ok {}else{}

       //元素类型为map的切片
       var s1 = make([]map[int]string,10,10)
       //没有对内部的map做初始化
       s1[0] = make(map[int]string,1)
       s1[0][10] = "沙河"
       fmt.Println(s1)
       //值为切片类型的map
       var m1 = make(map[string][]int,10)
       m1["北京"] = []int{10,20,30}
       fmt.Println(m1)

24. 切片补充
    切片不存值,它就像一个框,去底层数组框值
    切片:指针、长度、容量
    切片的扩容策略:
        -1)如果申请的容量大于原来的2别,那就直接扩容至新申请的容量
        -2)如果小于1024,那么久直接两倍
        -3)如果大于1024,就按照1.25倍去扩容
        -4)具体存储的值类型不同,扩容策略也有一定的不同

25. 函数类型作为参数和返回值
    func f1(){
       fmt.Println("hello 沙河")
    }

    func f2() int {
       return 10
    }
    func f4(x,y int) int{
       return x + y
    }
    //函数
    func f3(x func() int){
       ret := x()
       fmt.Println(ret)
    }

    func ff(a,b int) int{
       return a + b
    }

    //函数还可以作为返回值
    func f5(x func() int) func(int, int) int{ //返回是一个函数 ,有参数 也有返回值
       return ff
    }

    func main() {
       a := f1
       fmt.Printf("%T\n", a)  // func() a是func()类型
       b := f2
       fmt.Printf("%T\n", b)  // func() int  a是带int的func()类型

       f3(f2)
       f3(b)
       fmt.Printf("%T\n",f4)
       //f3(f4)
       f7 := f5(f2)
       fmt.Printf("%T\n", f7)
    }

    高阶函数
        函数也是一种类型,它可以作为参数,也可以作为返回值

26. 变量作用域
    -1).全局作用域
    -2).函数作用域
        --1)先在函数内部找变量,找不到往外层找
        --2)函数内部的变量,外部是访问不到的
    -3)代码作用域


27. 匿名函数 和 立即执行函数
    匿名函数就是没有名字的函数
    func main() {
       //函数内部没有办法声明带名字的函数
       //匿名函数
       f1 := func(x, y int) {
          fmt.Println(x + y)
       }
       f1(10, 20)
       //如果只是调用一次的函数, 还可以简写成立即执行函数
       func(x,y int) {
          fmt.Println(x + y)
          fmt.Println("hello world !")
       }(100, 200)
    }
    立即执行函数,就是{}后带(),如果没有参数则就是(),有参数()里面填入参数即可

28. 闭包函数
    闭包是什么?
    闭包是一个函数,这个函数包含了它外部作用的一个变量
    底层的原理:
        -1)函数可以作为返回值
        -2)函数内部查找变量的顺序,先在自己内部找,找不到往外层找
    //返回值是函数的实例 - 也是闭包函数实例
        func adder(x int) func(int) int {
           return func(y int) int{
              x += y
              return x
           }
        }
        func main() {
           ret := adder(100)  //此处ret接收的是adder()函数的返回值 - 一个func(int) int 函数
           ret2 := ret(200)  //ret是一个参数是int 返回值是 int 的函数
           fmt.Println(ret2)
        }

    闭包函数 - 实例2
    //关键名 函数名(参数) (返回值) {}
    func calc(base int) (func(int)int, func(int) int){
        add := func(i int) int {
            base += i
            return base
        }
        sub := func(i int) int {  //f2(2)调用的时候,base往外层找,此是base是f1(1)执行过的11,所以是11-2=9
            base -= i
            return base
        }
        return add, sub
    }

    func main() {
        f1, f2 := calc(10)
        fmt.Println(f1(1), f2(2))  //11 9
    }

29.defer
    go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句
    按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行
    go语言中函数的return不是原子操作,在底层是分为两步来执行
    第一步:返回值赋值
    defer
    第二步:真正的RET返回
    函数中如果存在defer,那么defer执行的时机是在第一步和第二步之间

30. func f6() (x int) {
       defer func(x *int) {  // 参数 是 int 类型的 指针
          (*x)++  // *x - 根据内存地址取值 (*根据地址取值)
       }(&x)  //&x - 把x的内存地址传参 (&是取地址符)
       return 5
   }
31.内置函数
    close - 主要用来关闭channel
    len   - 用来求长度,比如string、array、slice、map、channel
    new   - 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
    make  - 用来分配内存,主要用来分配引用类型,比如chan、map、slice
    append - 用来追加元素到数组、slice中
    panic和recover 用来做错误处理
        --go语言中没有异常机制,但是使用panic/recover模式来处理错误。panic可以在任何地方引发,但recover只有
            在defer调用的函数中有效
            --实例
                func funcB() {
                   //刚刚打开数据库连接
                   defer func() {
                      err := recover()
                      fmt.Println(err)
                      fmt.Println("释放数据库连接...")
                   }()
                   panic("出现严重的错误!!!")
                   fmt.Println("b")
                }
                func funcC(){
                   fmt.Println("c")
                }

                func main() {
                   funcA()
                   funcB()
                   funcC()
                }
                --注意:
                    -1)recover()必须搭配defer使用
                    -2)defer一定要在可能引发panic的语句之前定义

32. fmt标准库
    fmt包实现了类似C语言printf 和 scan的格式化I/O。主要分为向外输出内容和获取输入内容两大部分
    向外输出 - 标准库fmt提供了以下几种输出相关函数
    Print
        Print系列函数会将内容输出到系统的标准输出,区别在于Print函数直接输出内容,Printf函数支持格式化输出
        字符串,Println函数会在输出内容的结尾添加一个换行符
    Printf("格式化字符串",值)
        %T:查看类型
        %d:十进制数
        %b:二进制数
        %o:八进制数
        %x:十六进制
        %c:字符
        %p:指针
        %v:值
        %f:浮点数
        %t:布尔值

33. 类型别名和自定义类型
    自定义类型
        在go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型,go语言中可以使用type关键字来定义自定义类型
        自定义类型是定义了一个全新的类型,我们可以基于内置的基本类型定义,也可以通过struct定义
    类型别名
        类型别名规定:TypeAlias只是Type的别名,本质上TypeAlise与Type是同一个类型。就像一个小孩小时候有小名、乳名、上学后用
        学名,英文老师又会给他起英文名,但这些名字都指的是他本人。
            如:type byte = unit8  type rune = int32   //byte 、rune都是类型别名
    自定义类型和类型别名的区别
    //自定义类型
    type NewInt  int
    //类型别名
    type MyInt = int
    func main(){
       var a NewInt
       var b MyInt
       fmt.Printf("type of a:%T\n", a)  //main.NewInt
       fmt.Printf("type of b:%T\n", b)  //int
    }
    结果显示a的类型是main.NewInt, 表示main定义的NewInt类型。b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

34. 结构体  //原来一个string、一个int只能保存一个值,现在需要一个丰富的、有多维度的数据,所以要用结构体
    go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求
    了,go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。也就是我们可以通过struct来定义自己的类型
    了。
    go语言中通过struct来实现面向对象。
    结构体的定义
    使用type 和 struct 关键字来定义结构体,具体代码格式如下:
        type 类型名 struct {
            字段名 字段类型
            字段名 字段类型
        }
            其中:
                类型名:表示自定义结构体的名称,在同一个包内不能重复
                字段名:表示结构体字段名。结构体中的字段名必须唯一。
                字段类型:表示结构体字段的具体类型。

    结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。
        var 结构体实例 结构体类型
        实例:
            type person struct {
               name string
               age int
               gender string
               hobby []string
            }
            func main(){
               //声明一个person类型的变量p
               var p person
               //通过字段赋值
               p.name = "周林"  //通过.来访问结构体的字段(成员变量)
               p.age = 9000
               p.gender = "男"
               p.hobby = []string{"篮球", "足球", "双色球"}
               fmt.Println(p) //{周林 9000 男 [篮球 足球 双色球]}
               //访问变量p的字段
               fmt.Printf("%T\n", p) //main.person
               fmt.Println(p.name) //周林
               var p2 person
               p2.name = "理想"
               p2.age = 18
               fmt.Printf("type:%T value:%v\n", p2, p2) //type:main.person value:{理想 18  []}  -- 结构体没有赋值 就是初始化的 零值
            }

35.匿名结构体
    //匿名结构体:多用于临时场景
       var s struct{  //声明变量s 是个结构体(没有名称)
          x string
          y int
       }
       s.x = "嘿嘿嘿"
       s.y = 100
       fmt.Printf("type:%T value:%v\n", s, s) // type:struct { x string; y int } value:{嘿嘿嘿 100}

36.值类型 就是 ctrl+c ctrl+v
    go语言中函数参数永远是拷贝(副本)
    func f(x person) {
        x.gender = "女" 修改的是副本的gender,原struct的值不会发生改变,要改变原来的值,则需要传地址指定进行修改
    }

37.结构体指针和结构体初始化
    创建指针型结构体
        我么可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。
            type person struct {
               name string
               age int
               gender string
               hobby []string
            }
            func main(){
               var p2  = new(person)
               fmt.Printf("%T\n", p2) //*main.person
               fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", age:0, gender:"", hobby:[]string(nil)}
            }
            从打印结果可以看到p2是一个结构体指针
    取结构体的地址实例化
        使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作
            p3 := &person{}
    结构体初始化
        没有初始化的结构体,其成员变量都是对应其类型的零值。
    使用键值对初始化
        --1)使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值
            p5 := person{
                name: "小王子".
                city: "北京",
                age: 18
            }
        --2)也可以对结构体指针进行键值对初始化,如:(go中无法对指针进行操作,所以操作的就默认是针对对应的值)
            p5 := &person{
                 name: "小王子".
                 city: "北京",
                 age: 18
            }
        --3)当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
            p7 := &person{
                city: "北京"
            }
    使用值得列表初始化
        初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
            p8 := &person{
                "沙河",
                "北京",
                28
            }
         使用这种格式初始化时,需要注意:
            --1)必须初始化结构体的所有字段。
            --2)初始值的填充顺序必须与字段在结构体中的声明顺序一致。
            --3)该方式不能和键值初始化方式混用。

    实例 :
        //go语言中函数传参数永远传的是拷贝
        type person struct {
           name, gender string
        }

        func f(x person) {
           x.gender = "女" //修改的是副本的gender
        }
        func f2(x *person){
           //(*x).gender = "女" //根据内存地址找到那个原变量,修改的就是原来的变量
           x.gender = "女" //语法糖,自动根据指针找到相应的变量
        }

        func main(){
           var p person
           p.name = "周林"
           p.gender = "男"
           f(p)
           fmt.Println(p.gender) //男
           f2(&p)
           fmt.Println(p.gender)
           //结构体指针1
           var p2 = new(person)
           (*p2).name = "理想"
           p2.gender = "保密"
           fmt.Printf("%T\n", p2)  //*main.person
           fmt.Printf("%p\n", p2) //p2保存的值就是一个内存地址
           fmt.Printf("%p\n", &p2)    //求p2的内存地址
           //2.结构体指针2
           //2.1 key-value初始化
           var p3 = &person{
              name: "元帅",
           }
           fmt.Printf("%#v\n", p3)
           //2.2使用值列表的形式初始化,值的顺序要和结构体定义时字段的顺序一致
           p4 := &person{
              "小王子",
              "男",
           }
           fmt.Printf("%#v\n", p4)
        }

38. 结构体 - 构造函数
    构造函数:返回一个结构体变量的函数
    结构体是值类型,赋值的时候都是拷贝
    //构造函数
    type person struct {
       name string
       age int
    }
    //构造函数:约定俗成用new开头
    //返回的是结构体还是结构体指针
    //当结构体比较大的时候尽量使用结构体指针,减少程序的内存开销
    func newPerson(name string, age int) *person {
       return &person{  //此处返回的是结构体指针,与上面返回的*person类型对应(没有&号,就是值类型)
          name: name,
          age: age,
       }
    }
    func main(){
       p1 := newPerson("元帅", 18)
       fmt.Println(p1)
    }

39. 方法和接收者
    go语言中的方法(Method) 是一种作用于特定类型变量的函数。这种特定类型变量叫做接受者(Receiver)。接收者的概念
    就类似于其他语言中的this或者self.
    方法的定义格式如下:
        func(接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
            函数体
        }
    其中:
        接收者变量:接收者中的变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。
        例如,Person类型的接收者变量应该命名为p,Connector类型的接收者变量应该命名为c等。
        接收者类型:接收者类型和参数类型,可以是指针类型和非指针类型。
        方法名、参数列表、返回参数:具体格式与函数定义相同。
    实例:
        //标识符:变量名 函数名 类型名 方法名
        //Go语言中如果标识符首字母是大写的,就表示对外部包可见(暴露的,共有的,例如fmt.Printf;下面的dog 就是私有的)
        type dog struct {
           name string
        }

        //构造函数
        func newDog(name string) dog {
           return dog{
              name:name,
           }
        }
        //方法是作用域特定类型的函数
        //接收者表示的是调用该方法的具体类型变量,多用类型名首字母小写表示
        func (d dog) wang() {
           fmt.Printf("%s:汪汪汪", d.name)
        }
        func main(){
           d1 := newDog("zhoulin")
           d1.wang()
        }

40.值接收者 和 指针接收者
    什么时候应该使用指针类型接收者
        -1)需要修改接收者中的值
        -2)接收者是拷贝代价比较大的对象
        -3)保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者
    实例:
        type person struct {
           name string
           age int
        }
        //构造函数
        func newPerson(name string,age int) *person{
           return &person{
              name: name,
              age: age,
           }
        }
        //使用值接收者:传拷贝进去
        func (p person) guonian() {
           p.age++
        }
        //指针接收者:传内存地址进去
        func (p *person) zhenguonian(){
           p.age++
        }
        func main(){
           p1 := newPerson("元帅", 18)
           fmt.Println(p1.age)
           p1.guonian()
           fmt.Println(p1.age)
           p1.zhenguonian()
           fmt.Println(p1.age)
        }
    任意类型添加方法
        在go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。举个例子,我们基于内置的int类型
        使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

        //给自定义类型加方法
        //不能给别的包里面的类型添加方法,只能给自己的包里的类型添加方法
        type myInt int
        func (m myInt) hello() {
           fmt.Println("我是一个int")
        }
        func main() {
           m := myInt(100)  // 强制转换类型  -- 跟 a := int(11) 写法一样
           fmt.Println(m)
           m.hello()
        }

41. 初始化问题

    type myInt int
    func (m myInt) hello(){
       fmt.Println("我是一个int")
    }

    func main() {
       //声明一个int32类型的变量x(y,z,a),它的值是10
       //方法1:
       var x int32
       x = 10
       //方法2
       var y int32 = 10 //方法1未换行就是该种写法
       //方法3
       var z = int32(10)
       //方法4
       a := int32(10)
       fmt.Println(x, y, z ,a)
       //声明一个myInt类型的变量m,它的值是100
       //方法1:
       var m myInt
       m = 100
       //方法2:
       var n myInt = 100
       //方法3:
       var l = myInt(100)
       //方法4
       k := myInt(100) //强制类型转换
       fmt.Println(m,n,l,k)
       m.hello()
       //问题2:结构体初始化
       type person struct {
          name string
          age int
       }
       //方法1:
       var p person //声明一个person类型的变量p
       p.name = "元帅"
       p.age = 18
       fmt.Println(p)
       //方法2
       s1 := []int{1,2,3,4}  //{}有初始化的作用
       m1 := map[string]int{
          "stu1": 100,
          "stu2": 99,
       }
       fmt.Println(s1, m1)
       //键值对初始化
       var p2 = person{
          name:"冠华",
          age: 15,
       }
       fmt.Println(p2)
       //值列表初始化
       var p3 = person{
          "理想",
          100,
       }
       fmt.Println(p3)
    }

42. 结构体的匿名字段、结构体嵌套
    匿名字段: 实例
        //匿名字段
        //字段比较少也比较简单的场景
        //不常用!!
        type person struct {
           string
           int
        }
        func main() {
           p1 := person{
              "周林",
              9000,
           }
           fmt.Println(p1)
           fmt.Println(p1.string) //有多个string的话这种写法就会报错
        }
    结构体嵌套:实例
        //结构体嵌套
        type address struct {
            province string
            city string
        }
        type workPlace struct {
            province string
            city string
        }
        type person struct {
            name string
            age int
            address //匿名嵌套结构体
            //workPlace
        }
        type company struct {
            name string
            addr address
        }
        func main() {
            p1 := person{
                name: "周林",
                age: 9000,
                address:address{
                    province: "山东",
                    city: "威海",
                },
                /*workPlace:workPlace{
                    province: "北京",
                    city: "北京",
                },*/
            }
            fmt.Println(p1)
            fmt.Println(p1.name, p1.address.city)
            fmt.Println(p1.city)//先在自己结构体找这个字段,找不到就去匿名嵌套的结构体中查找该字段
            //fmt.Println(p1.address.city)
            //fmt.Println(p1.workPlace.city)
        }
43. 结构体模拟实现继承
    //结构体模拟实现其他语言中的"继承"
    type animal struct {
       name string
    }
    //给animal实现一个移动的方法
    func (a animal) move(){ //接收animal的类型
       fmt.Printf("%s会动\n",a.name)
    }
    //狗类
    type dog struct {
       feet uint8
       animal //animal拥有的方法,dog此时也有了 -- 这是重点
    }
    //给dog实现一个汪汪汪的方法
    func (d dog) wang(){
       fmt.Printf("%s在叫:汪汪汪~\n",d.name)
    }
    func main() {
       d1 := dog{
          animal: animal{name: "周林"},
          feet: 4,
       }
       fmt.Println(d1)
       d1.wang()
       d1.move()
    }

44. 结构体 与 JSON
    import (
       "encoding/json"
       "fmt"
    )

    //1.序列化:把go语言中的结构体变量 --> json格式的字符串
    //2.反序列化:json格式的字符串 --> go语言中能够识别的结构体变量
    type person struct {
       Name string `json:"name" db:"name" ini:"name"`  //反引号 tag -- 在json数据中用name(json包调用函数体,首字母必须大写才暴露)
       Age int `json:"age"`
    }
    func main() {
       p1 := person{
          Name: "周林",
          Age: 9000,
       }
       //序列化
       b, err := json.Marshal(p1)
       if err != nil {
          fmt.Printf("marshal failed, err:%v", err)
          return
       }
       fmt.Printf("%v\n", string(b))
       //反序列化
       str := `{"name": "理想", "age": 18}`
       var p2 person
       json.Unmarshal([]byte(str), &p2)  //传指针是为了能在json.Unmarshal内部修改p2的值
       fmt.Printf("%#v\n", p2)
    }

45. go语言中把错误当成值返回,错误通常作为第二个返回值

46. 接口
    接口是一种类型,是一种特殊的类型,它规定了变量有哪些方法
    在编程中会遇到以下场景:
    我不关心一个变量是什么类型,我只关心能调用它的什么方法
    接口的定义
    type 接口名 interface {
        方法名1(参数1,参数2...)(返回值1,返回值2)
        方法名2(参数1,参数2...)(返回值1,返回值2)
    }
    用来给变量\参数\返回值等设置类型.
    接口的实现:
        一个变量如果实现了接口中规定的所有的方法,那么这个变量就实现了这个接口,可以成为这个接口类型的变量

    实例:
        //不管是神牌子的车,都能跑
        //定义一个car接口类型
        //不管是什么结构体,只要有run方法都能是car类型
        type car interface {
           run()
        }
        type falali struct {
           brand string
        }
        func (f falali) run(){
           fmt.Printf("%s速度70迈~\n", f.brand)
        }
        type baoshijie struct {
           brand string
        }
        func (b baoshijie) run(){
           fmt.Printf("%s速度700迈~\n", b.brand)
        }
        //drive函数接收一个car类型的变量
        func drive(c car){
           c.run()
        }

        func main(){
           var f1 = falali{
              brand: "法拉利",
           }
           var b1 = baoshijie{
              brand: "保时捷",
           }
           drive(f1) //法拉利速度70迈~
           drive(b1)  //保时捷速度700迈~
        }

    接口保存的分为两部分:值得类型和值本身(动态类型和动态值,初始值都是nil),这样就实现了接口变量能够存储不同的值
    实例:
        //接口的实现
        type animal interface {
           move()
           eat(string)
        }
        type cat struct{
           name string
           feet int8
        }
        func (c cat) move() {
           fmt.Println("走猫步...")
        }
        func (c cat) eat(food string){
           fmt.Printf("猫吃%s...\n", food)
        }
        type chicken struct{
           feet int8
        }
        func (c chicken) move() {
           fmt.Println("鸡动!")
        }
        func (c chicken) eat(food string) {
           fmt.Println("鸡翅饲料!", food)
        }
        func main() {
           var a1 animal //定义一个接口类型的变量
           fmt.Printf("%T\n", a1)  //<nil>
           bc := cat{ //定义一个cat类型的变量bc
              name: "淘气",
              feet: 4,
           }
           a1 = bc
           a1.eat("小黄鱼")
           fmt.Printf("%T\n", a1)  //main.cat
           fmt.Println(a1)
           kfc := chicken{
              feet: 2,
           }
           a1 =  kfc
           fmt.Printf("%T\n", a1) //main.chicken
        }
47. 指针接收者实现接口和值接收者实现接口的区别
    使用值接收者实现接口,结构体类型和结构体指针类型的变量都能存。
    指针接收者实现接口只能存结构体指针类型的变量。
    实例:
        //使用值接收者和指针接收者的区别
        type animal interface {
           move()
           eat(string)
        }
        type cat struct{
           name string
           feet int8
        }
        //方法使用值接收者
        /*func (c cat) move() {
           fmt.Println("走猫步...")
        }
        func (c cat) eat(food string){
           fmt.Printf("猫吃%s...\n", food)
        }*/
        //使用指针接收者实现了接口的所有方法
        func (c *cat) move() {
           fmt.Println("走猫步...")
        }
        func (c *cat) eat(food string){
           fmt.Printf("猫吃%s...\n", food)
        }

        func main() {
           var a1 animal //定义一个接口类型的变量
           c1 := cat{"tom", 4}
           c2 := &cat{"假老练", 4}
           a1 = &c1
           fmt.Println(a1)
           a1 = c2
           fmt.Println(a1)
        }
48. 实现多个接口和接口嵌套
    接口和类型的关系
        多个类型可以实现同一个接口
        一个类型可以实现多个接口
    实例:
        //同一个结构体可以实现多个接口
        //接口还可以嵌套
        type animal interface {
           mover
           eater
        }
        type mover interface {
           move()
        }
        type eater interface {
           eat()
        }
        type cat struct{
           name string
           feet int8
        }
        //cat 实现了mover接口
        func (c *cat) move() {
           fmt.Println("走猫步...")
        }
        //cat实现了eater接口
        func (c *cat) eat(food string){
           fmt.Printf("猫吃%s...\n", food)
        }

        func main() {

        }

49.空接口
    没有必要起名字,通常定义成下面的格式:
     interface{} //空接口
     所有的类型都实现了空接口,也就是任意类型的变量都能保存到空接口中
    实例:
        //空接口
        //interface:关键字
        //interface{} :空接口类型
        //空接口作为函数参数
        func show(a interface{}) {
           fmt.Printf("type:%T value:%v\n",a ,a)
        }
        func main() {
           var m1 map[string]interface{}
           m1 = make(map[string]interface{}, 16)
           m1["name"]  = "周林"
           m1["age"] = 9000
           m1["hobby"] = [...]string{"唱", "跳", "rap"}
           fmt.Println(m1)
        }

    类型断言  (做类型断言的前提是一定要是接口类型)
        (x.(type) 用法  类型断言和类型判断 if 和 type-switch两种形式)
        实例:
        //类型断言
        func assign(a interface{}){
            fmt.Printf("%T\n", a)
            str, ok := a.(string)
            if !ok {
                fmt.Println("猜错了")
            }else{
                fmt.Println("传进来的是一个字符串:",str)
            }
        }
        func assign2(a interface{}){
            fmt.Printf("%T\n", a)
            switch t := a.(type) {
            case string:
                fmt.Println("是一个字符串:", t)
            case int:
                fmt.Println("是一个int:", t)
            case int64:
                fmt.Println("是一个int64:", t)
            case bool:
                fmt.Println("是一个boll:", t)
            }
        }
        func main() {
            assign(100)
            assign2(true)
            assign2("哈啊哈")
            assign2(int64(200))
        }

50.package
    -定义包
        我们可以根据自己的需要创建自己的包。一个包可以简单理解为一个存放.go 文件的文件夹。该文件夹下面的所有go文件都要在代码
        的第一行添加如下代码,声明该文件归属的包。
            package 包名
        注意事项
            -1)一个文件夹下面只能有一个包,同样一个包的文件不能在多个文件夹下
            -2)包名可以不和文件夹的名字一样,包名不能包含 - 符号。
            -3)包名为main的包为应用程序的入口包,编译时不包含main包的源代码时不会得到可执行文件。
    -可见性
        如果想在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识符必须时对外可见的
        (public)。在go语言中只需要将标识符的首字母大写就可以让标识符对外可见了。
    -注意事项:
        -1)import导入语句通常放在文件开头包声明语句的下面
        -2)导入的包名需要使用双引号包裹起来
        -3)包名是从 $GOPATH/src/ 后开始计算的,使用/进行路径分割
        -4)go语言中禁止循环导入包
    -匿名导入包
        如果只希望导入包,而不使用包内部的数据时,可以使用匿名导入包。具体的格式如下:
            import _ "包的路径"
                匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中
    -init()初始化函数
        在go语言程序执行时导入包语句会自动触发内部init()函数的调用。需要注意的是:init()函数没有参数也没有返回值。
        init()函数在程序运行时自动被调用执行,不能在代码中主动调用它。

    import (
        "fmt"

        zhoulin "code.oldboyedu.com/day05/10calc"
    )
        --以上空一行,是 标准库的包 和 第三方的包进行区分

51.日志可以输出到终端,也可以输出到文件,输出到kafka

52.文件
    文件是什么?计算机中的文件是存储在外部介质(通常是硬盘)上的数据集合,文件分为文本文件和二进制文件
    打开和关闭文件
    os.open()函数能够代开一个文件,返回一个*File和一个err。对得到的文件实例调用close()方法能够关闭
    文件。
    文件读取:
        基本使用
            func (f *File) Read(b []byte) (n int, err error)
            它接收一个字节切片,返回读取的字节数和可能的具体错误,读到文件末尾时会返回0和io.EOF
            读取循环,使用for循环读取文件中的所有数据。
        bufio读取文件
            bufio(缓冲) 是在file的基础上封装了一层API,支持更多的功能
        ioutil读取整个文件
            io/ioutil包 的ReadFile方法能够读取完整的文件,只需要文件名作为参数传入

    文件写入
        os.OpenFlie()函数能够指定模式打开文件, 从而实现文件写入相关功能。
        实例:

        import (
            "bufio"
            "fmt"
            "io/ioutil"
            "os"
        )
        func writeDemo1(){
            fileObj, err := os.OpenFile("./xx.txt",os.O_WRONLY|os.O_CREATE|os.O_TRUNC,0644)
            if err != nil{
                fmt.Printf("open file failed, err:%v", err)
            }
            //write
            fileObj.Write([]byte("周林懵逼了"))  //Write写入字节切片数据
            fileObj.WriteString("周林解释不了")   //WriteString直接写入字符串数据
            fileObj.Close()
        }

        func writeDemo2(){
            fileObj, err := os.OpenFile("./xx.txt",os.O_WRONLY|os.O_CREATE|os.O_TRUNC,0644)
            if err != nil{
                fmt.Printf("open file failed, err:%v", err)
                return
            }
            defer fileObj.Close()
            //创建一个写的对象
            wr := bufio.NewWriter(fileObj)  //buf是缓冲的意思
            wr.WriteString("hello沙河")//写到缓存中
            wr.Flush() //将缓存种的内容写入文件
        }
        func writeDemo3(){
            str := "hello 沙河"
            err := ioutil.WriteFile("./xxx.txt",[]byte(str), 0666)
            if err != nil {
                fmt.Println("write file failed, err:", err)
                return
            }
        }
        func main(){
            //writeDemo1()
            //writeDemo2()
            writeDemo3()
        }

53. time包
        time包提供了时间的显示和测量用的函数。日历的计算采用的是公历
        time.Time类型表示时间。我们可以通过time.Now()函数获取当前的时间对象,然后获取时间对象的年月日时分秒等信息。
    时间戳
        时间戳是自1970年1月1日(08:00:00GMT)至当前时间的总毫秒数。它也被称为Unix时间戳(UnixTimestamp)。
        使用time.Unix()函数可以将时间戳转为时间格式。
    时间间隔
        Duration类型代表两个时间之间经过的时间,以纳秒为单位。可表示的最长时间段大约290年。time包中定义的
        时间间隔常量如下:
            const {
                Nanosecond Duration = 1 //纳秒
                Microsecond = 1000 * Nanosecond  //微秒
                Millisecond = 1000* Microsecond //毫秒
                second = 1000 * Millisecond //秒
                Minute = 60 * Second
                Hour = 60 * Minute
            }
    时间操作
        Add
            我们在日常的编码过程中可能会遇到要求时间+时间间隔的要求,Go语言的时间对象有提供Add方法如下:
                func (t Time) Add(d Duration) Time
        Sub
            求两个时间之间的差值
                func (t Time) Sub(u Time) Duration
    定时器
        使用 time.Tick(时间间隔)来设置定时器, 定时器的本质上是一个通道(channel)
    时间格式化
        时间类型有一个自带的方法Format进行格式化,需要注意的是Go语言种格式化时间模板不是常用的Y-m-d H:m:s而是
        使用Go的诞生时间2006年1月2号15点04分(记忆口诀为2006 1 2 3 4)
    实例:
        func main(){
            now := time.Now()
            fmt.Println(now) //    2021-08-07 16:22:48.440435 +0800 CST m=+0.000099269
            //2021 August 7 16 22 48
            fmt.Println(now.Year(), now.Month(), now.Day(),now.Hour(),now.Minute(),now.Second())
            //时间戳
            fmt.Println(now.Unix()) //1628324749
            fmt.Println(now.UnixNano())  //1628324749250368000
            //time.Unix
            ret := time.Unix(1564803999, 0)
            fmt.Println(ret) // 2019-08-03 11:46:39 +0800 CST
            fmt.Println(ret.Year(), ret.Day()) // 2019 3
            //时间间隔
            fmt.Println(time.Second) //1s
            //now + 24小时
            fmt.Println(now.Add(24*time.Hour)) // 2021-08-08 16:27:58.101772 +0800 CST m=+86400.000086358
            //定时器
            //timer := time.Tick(time.Second)
            //for t := range timer{
            // fmt.Println(t) //1秒钟执行一次
            //}
            //格式化时间 把语言中时间对象 转换成字符串类型的时间
            //2019-08-03
            fmt.Println(now.Format("2006-01-02")) // 2021-08-07
            //2019/02/03 11:55:02
            fmt.Println(now.Format("2006/01/02 03:04:05")) // 2021/08/07 04:35:15
            //2019/02/03 11:55:02 AM
            fmt.Println(now.Format("2006-01-02 03:04:05 PM")) // 2021-08-07 04:35:15 PM
            //2019/02/03 11:55:02.342
            fmt.Println(now.Format("2006-01-02 03:04:05.000")) // 2021-08-07 04:35:15.713
            //按照对应的格式解析字符串类型的时间
            timeObj, err := time.Parse("2006-01-02", "2019-08-03")
            if err != nil {
                fmt.Printf("parse time failed, err:%v\n", err)
            }
            fmt.Println(timeObj) // 2019-08-03 00:00:00 +0000 UTC
            fmt.Println(timeObj.Unix()) // 1564790400
            //sleep
            n := 5
            fmt.Println("开始sleep了")
            time.Sleep(time.Duration(n) * time.Second)
            fmt.Println("5秒钟过去了")
        }

54. strconv标准库
    实例:
        import (
            "fmt"
            "strconv"
        )

        func main(){
            //从字符串种解析出对应的数据
            str := "10000"
            //ret1 := int64(str)
            ret1, err := strconv.ParseInt(str, 10, 64)
            if err != nil {
                fmt.Println("parseint failed, err:", err)
                return
            }
            fmt.Printf("%#v %T\n",ret1, int(ret1)) //10000 int
            //Atoi:字符串转换成int (是A不是S ,C语言中没有字符串的概念大致)
            retInt, _ := strconv.Atoi(str)
            fmt.Printf("%#v %T\n", retInt, retInt) 10000 int
            //从字符串中解析出布尔值
            boolStr := "true"
            boolValue, _ := strconv.ParseBool(boolStr)
            fmt.Printf("%#v %T\n", boolValue, boolValue)
            //从字符串种解析出浮点数
            floatStr := "1.234"
            floatValue, _ := strconv.ParseFloat(floatStr, 64) //true bool
            fmt.Printf("%#v %T\n", floatValue, floatValue) // 1.234 float64
            //把数字转换成字符串类型
            i := 97
            ret2 := fmt.Sprintf("%d", i) //"97"
            fmt.Printf("%#v", ret2)
            ret3 := strconv.Itoa(i)
            fmt.Printf("%#v", ret3)//"97"
        }
55. int在32位系统上是4个字节
    int在64位系统上是8个字节

    int32在哪都是4字节
    int64在哪都是8字节
56. 内核态的线程 操作系统自带的 (一般的线程就是操作系统的线程, os)
    用户态的线程 是程序写的
        OS线程(操作系统线程) 一般都要固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小
        的栈(典型情况下2KB),gotoutine 的栈不是固定的,他可以按需增大或缩小,goroutine的栈大小限制可以达到
        1GB,虽然极少会用到这么大。所以Go语言中一次创建十万左右的goroutine也是可以的。
57. 并发与并行
    并发:同一时间段内执行多个任务
    并行:同一时刻执行多个任务
    go语言的并发通过goroutine实现。goroutine类似于线程,属于用户态的线程,我们可以根据需求创建成千上万
    个goroutine并发工作。goroutine是由go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。
    go语言还提供channel在多个goroutine间进行通信。goroutine和channel是go语言秉承的CSP(Communicating
    Sequential Process)并发模式的重要实现基础。
58. goroutine
        在java/c++我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,
        同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。那么能不能有一种机制,
        程序员只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行呢?
        Go语言中 goroutine就是这样一种机制,goroutine的概念类似于线程,但goroutine是由Go的运行时(runtime)调度
        和管理的。Go程序会只能地将goroutine中的人物合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因
        为它在语言层面已经内置了调度和上下文切换的机制。
        在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能-goroutine,当你需要让某个任务并发执
        行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,就是
    使用goroutine
        Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。
        一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。
    启动单个goroutine
        启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面 加上一个go关键字。

    实例:
        func hello(i int) {
           fmt.Println("hello", i)
        }
        //程序启动之后会创建一个主goroutine去执行
        func main() {
           for i:= 0;i <10000; i++ {
              go hello(i) //开启一个单独的goroutine去执行hello函数(任务)
           }
           fmt.Println("main")
           time.Sleep(time.Second)
        }
        goroutine对应的函数结束了, goroutine 结束了
        main函数执行完了,由main函数创建的那些goroutine都结束了

59. sync.WaitGroup
    实例:

    func f() {
        rand.Seed(time.Now().UnixNano()) //保证每次执行的时候都有点儿不一样
        for i := 0; i<5; i++ {
            r1 := rand.Int()
            r2 := rand.Intn(10)
            fmt.Println(0-r1, 0-r2)
        }
    }
    var wg sync.WaitGroup
    func f1(i int) {
        defer wg.Done()
        time.Sleep(time.Second * time.Duration(rand.Intn(3)))
        fmt.Println(i)
    }
    func main() {
        //f()
        for i := 0; i<10; i++ {
            wg.Add(1)
            go f1(i)
        }
        wg.Wait() //等待wg的计数器减为0
    }

60. goroutine 调度
    GMP是Go语言运行时(runtime) 层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程
        -G很好理解,就是goroutine,里面除了存放本goroutine信息外,还有与所在P的绑定等信息
        -M(machine)是Go运行时(runtime) 对操作系统内核线程的虚拟,M与内核线程一般是一一映射的关系,
            一个goroutine最终要放在M上执行的;
        -P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地
        址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行
        后续的goroutine等等) 当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P
        的队列里抢任务
        P与M一般也是一一对应的。他们关系是:P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime
        会新建一个M,阻塞G所在的P会把其他的G挂载在新建的M上。当旧的阻塞完成或者认为其已经死掉时回收旧的M。
        P的个数是通过runtime.GOMAXPROCS设定(最大256),
        单从线程调度讲,Go语言相比其他语言的优势在于OS线程是OS内核来调度的,goroutine则是由Go运行时(runtime)
        自己的调度器调度的,这个调度器使用一个成为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。某一大特点
        是goroutine的调度是在用户态下完成的,不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用
        户态维护着一大块的内存池,不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。另一方
        面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上,再加上本身goroutine的超轻量,以上种
        种保证了go调度方面的性能。
61. GOMAXPROCS
    go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认是机器上的CPU核心数。例如在
    一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度种的n)
    go语言种可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。
    示例:
        var wg sync.WaitGroup
        func a() {
            defer wg.Done()
            for i := 0; i<10; i++ {
                fmt.Printf("A:%d\n" ,i)
            }
        }
        func b() {
            defer wg.Done()
            for i := 0; i<10; i++ {
                fmt.Printf("B:%d\n", i)
            }
        }
        func main() {
            runtime.GOMAXPROCS(4) //默认CPU的逻辑核心数,默认跑满整个CPU
            fmt.Println(runtime.NumCPU())
            wg.Add(2)
            go a()
            go b()
            wg.Wait()
        }
62. channel
    单纯地将函数并发执行是没有意义的,函数与函数间需要交换数据才能体现并发执行函数的意义。
    虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用
    互斥量对内存进行加锁,这种做法势必造成性能问题。
    go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过贡献内存而实现通信。
    如果说goroutine是go程序并发的执行体,channel就是他们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine
    的通信机制。
    go语言种的通道channel是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,
    保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候选要为其指定元素类型。
    channel类型
        声明通道类型的格式如下:
            var 变量 chan 元素类型
    创建channel
        通道是引用类型,通道类型的空值是 nil
        var ch chan int
        声明的通道后需要使用make函数初始化才能使用。
        make(chan 元素类型, [缓冲大小])  缓冲大小是可选的
    channel操作
        通道有发送(send)、接收(reveive)和关闭(close)三种操作。发送和接收都使用 <- 符号。
            发送
                将一个值发送到通道中
                    ch <- 10 //把10发送到ch中
            接收
                从一个通道接收值
                    x := <- ch //从ch中接收值并赋值给变量x
                    <- ch //从ch中接收值,忽略结果
            关闭
                close(ch)

    实例:
        import (
            "fmt"
            "sync"
        )
        var b chan int //需要指定通道中元素的类型
        var wg sync.WaitGroup
        func noBufChannel() {
            fmt.Println(b) //nil
            b = make(chan int) //不带缓冲区通道的初始化
            wg.Add(1)
            go func(){
                defer wg.Done()
                x := <- b
                fmt.Println("后台goroutine从通道b中取到了", x)
            }()
            b<- 10
            fmt.Println("10发送到通道b中了")
        }
        func bufChannel() {
            fmt.Println(b)
            b = make(chan int, 10)
            b <- 10
            fmt.Println("10发送到通道中b中了....")
            b <- 20
            fmt.Println("20发送到通道中b中了....")
            x := <-b
            fmt.Println("从通道b中取到了", x)
            close(b)
        }

        func main() {
            //noBufChannel()
            //b = make(chan int, 16)
            //fmt.Println(b)
            //wg.Wait()
            bufChannel()
        }

63. channel联系
    //channel练习
    //1. 启动一个goroutine,生成100个数字发送到ch1
    //2. 启动一个goroutine,从ch1中取值,计算其平方放到ch2中
    //3.在main中 从ch2取值打印出来
    var wg sync.WaitGroup
    var once sync.Once
    func f1 (ch1 chan int){
        defer wg.Done()
        for i :=0 ;i <100; i++ {
            ch1 <- i
        }
        close(ch1)
    }
    func f2(ch1,ch2 chan int){
        defer wg.Done()
        for {
            x, ok := <-ch1
            if !ok {
                break
            }
            ch2 <- x * x
        }
        once.Do(func() {close(ch2)}) //确保某个操作只执行一次
    }

    func main() {
        a := make(chan int, 100)
        b := make(chan int, 100)
        wg.Add(3)
        go f1(a)
        go f2(a, b)
        go f2(a, b)
        wg.Wait()
        for ret := range b {
            fmt.Println(ret)
        }
    }


    func main(){
        ch1 := make(chan bool, 2)
        ch1 <- true
        ch1 <- true
        close(ch1)
        //for x := range ch1{
        // fmt.Println(x)
        //}
        <-ch1
        <-ch1
        x, ok := <-ch1
        fmt.Println(x, ok)
        x, ok = <-ch1
        fmt.Println(x, ok)
        x, ok = <-ch1
        fmt.Println(x, ok)
    }

64. 单项通道
    chan<- int是一个只能发送的通道,可以发送但是不能接收;<-chan是一个只能接收的通道,可以接收但是
    不能发送。在函数传参及任何赋值操作种将双向通道转换为单向通道是可以的,但反过来是不可以的
65. channel 异常情况总结
    channel       nil      非空      空的      满了      没满
    接收          阻塞      接收值     阻塞      接收值    接收值
    发送          阻塞      发送值     发送值    阻塞      发送值
    关闭          panic  关闭成功,读完 关闭成功, 关闭成功,读完 关闭成功,读完
                         数据后返回零值  返回零值   数据后返回零值 数据后返回零值
66. worker pool (goroutine池)
    实例1 :
        func worker(id int, jobs <-chan int, results chan<- int){
            for j:= range jobs {
                fmt.Printf("worker: %d start job:%d\n", id, j)
                time.Sleep(time.Second)
                fmt.Printf("worker:%d end job:%d\n",id ,j)
                results <- j*2
            }
        }
        func main(){
            jobs := make(chan int, 100)
            results := make(chan int, 100)
            //开启3个goroutine
            for w := 1;w <= 3; w++ {
                go worker(w, jobs, results)
            }
            //5个任务
            for j := 1;j <=5;j++ {
                jobs <- j
            }
            close(jobs)
            //输出结果
            for a := 1;a<=5;a++{
                <-results
            }
        }
    实例2:
        /**
        使用goroutine和channel实现一个计算int64随机数各位数和的程序
        1.开启一个goroutine循环生成int64类型的随机数,发送到jobChan
        2.开启24个goroutine从jobChan中取出随机计算各位数的和,将结果发送到resultChan
        3.主goroutine从resultChan取出结果并打印到终端输出
         */
        type job struct {
            value int64
        }
        type result struct {
             job *job
             sum int64
        }
        var jobChan = make(chan *job, 100)
        var resultChan = make(chan *result,100)
        var wg sync.WaitGroup
        func zhoulin(zl chan<- *job) {
            defer wg.Done()
            //循环生成int64类型的随机数,发送到jobChan
            for {
                x := rand.Int63()
                newJob := &job{
                    value: x,
                }
                zl <- newJob //将一个值发送到通道中,zl是接收通道
                time.Sleep(time.Millisecond *500)
            }
        }
        func baodelu(zl <-chan *job, resultChan chan<- *result){
            defer wg.Done()
            //从jobChan中取出随机数计算各位数的和,将结果发送到resultChan
            for {
                job := <- zl
                sum := int64(0)
                n := job.value
                for n >0 {
                    sum += n % 10
                    n = n/ 10
                }
                newResult := &result{
                    job: job,
                    sum: sum,
                }
                resultChan <- newResult
            }
        }
        func main() {
            wg.Add(1)
            go zhoulin(jobChan)
            //开启24个goroutine执行baodelu
            wg.Add(24)
            for i := 0;i < 24;i++ {
                go baodelu(jobChan, resultChan)
            }
            //主goroutine从resultChan取出结果并打印到终端输出
            for result := range resultChan{
                fmt.Printf("value: %d sum:%d\n",result.job.value,result.sum)
            }
        }

67. select多路复用
    在某些场景下我们需要同时从多个通道接收数据。通道在接收数据室,如果没有数据可以接收将会发生阻塞。
    go内置了select关键字,可以同时响应多个通道的操作。select 的使用类似于switch语句,它有一些列case
    分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。select会一直等待,直到某个case
    的通信操作完成时,就会执行case分支对应的语句。格式如下:
        select{
            case <-ch1:
                ...
            case data := <- ch2
                ...
            case ch3<-data:
                ...
            default:
                默认操作
        }

68.  channel是一种类型,一种引用类型。make函数初始化之后才能使用。(slice、map、channel)

69. channel练习题:
    var wg sync.WaitGroup
    var notifyCh = make(chan struct{}, 5)
    func worker(id int, jobs <-chan int, results chan<-int){
       for j := range jobs {
          time.Sleep(time.Second)
          fmt.Printf("worker:%d end job:%d\n", id, j)
          results <- j * 2
          notifyCh <- struct{}{} //struct{}后面再加一个{}就是实例化了
       }
    }
    func main() {
       jobs := make(chan int, 100)
       results := make(chan int, 100)
       //5个任务
       go func() {
          for j := 1; j <= 5; j++ {
             jobs <- j
          }
          close(jobs)
       }()
       //开启3个goroutine
       for w := 1; w <= 3; w++ {
          go worker(w, jobs, results)
       }
       go func() {
          for i := 0; i < 5; i++ {
             <- notifyCh
          }
          close(results)
       }()
       //输出结果
       for x := range results {
          fmt.Println(x)
       }
    }

70. 日志库的简单实现
    日志分级别
        -1)Debug
        -2)Trace
        -3)Info
        -4)Warning
        -5)Error
        -6)Fatal




71."code.oldboyedu.com/day06/mylogger"
    log := mylogger.NewLog("Info")   //NewLog函数在 文件console.go中
        --以上中, code.oldboyedu.com/day06/mylogger(文件夹)  是个包
                  mylogger.NewLog 中 NewLog 是属于包mylogger的函数
    函数在一个包里面只能有一个,而方法可以有很多个(参数不同)

72. 反射
    反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行
    部分。在运行程序时,程序无法获取自身的信息。
    支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口
    访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
    go程序在运行期使用reflect包访问程序的反射信息。
    reflect包
        在go语言的反射机制中,任何接口值都是由 一个具体类型 和 具体类型的值 两部分组成的(我们在上一篇接口的博客中有介绍
        相关概念)。在go语言中反射的相关功能由内置的reflect包提供,任何接口值在反射中都可以理解为由reflect.Type和reflect.
        Value两部分组成,并且reflect包提供了reflect.TypeOf 和 reflect.ValueOf两个函数来获取任意对象的Value和Type。

    实例:
        type student struct {
           Name string `json:"name"`
           Score int `json:"score"`
        }
        func main() {
           stu1 := student{
              Name: "小王子",
              Score: 90,
           }
           t := reflect.TypeOf(stu1)
           fmt.Println(t.Name(), t.Kind())
           //通过for循环遍历结构体的所有字段信息
           for i := 0; i<t.NumField(); i++ {
              field := t.Field(i)
              fmt.Printf("name:%s index:%d type:%v json tag:%v\n",field.Name,field.Index,field.Type,field.Tag.Get("json"))
           }
           //通过字段名获取制定结构体字段信息
           if scoreField, ok := t.FieldByName("Scroe"); ok {
              fmt.Printf("name:%s index:%d type:%v json tag:%v\n",scoreField.Name,scoreField.Index,scoreField.Type,scoreField.Tag.Get("json"))
           }
        }
73. 日志库简单实现中,有如下总结点
   -1)报错 package ‘xxxx‘ is not in GOROOT ,执行go mod init 目录
   -2)包就是目录,同时在文件开头有声明 package mylogger
   -3)Printf(),是把格式字符串输出到标准输出(一般是屏幕,可以重定向) 是和标准输出文件(stdout)关联的,Fprintf则没有这个限制
      Sprintf() 是把格式字符串输出到指定字符串中,所以参数比printf多一个char*,那就是目标字符串地址
        示例:
      Fprintf() 是把格式字符串输出到指定文件设备中,所以参数比printf多一个文件指针FILE*
      主要用于文件操作,Fprintf()是格式化输出到一个stream,通常是到文件


74. 反射 reflect
    golang语言实现了反射,反射机制就是在运行时动态的调用对象的方法和属性,官方自带的reflect包就是反射相关的,只要包含这个包就可以使用
    golang的官方包 reflect 实现了 运行时反射(run-time reflection)。运用得当,可谓威力无穷。
    go语言程序中对指针获取反射对象时,可以通过 reflect.Elem() 方法获取这个指针指向的元素类型。这个获取过程被称为取元素,等效于对指针类型变量做了一个*操作,
    基本知识
        首先,反射主要是与golang的interface类型相关。一个interface类型的变量包含了两个指针:一个指向变量的类型,另一个指向变量的值。
        最常用的莫过于这两个参数:
            func main(){
             s := "hello world"
             fmt.Println(reflect.ValueOf(s))  // hello world
             fmt.Println(reflect.TypeOf(s))  // string
            }
        其中,reflect.ValueOf() 返回值类型: reflect.Vlue
            reflect.TypeOf() 返回值类型:reflect.Type
        我们可以使用 reflect.Value.Elem() 来取得其实际的值:
            t := reflect.TypeOf(data)  //data是 一个结构体的指针
                 t.Kind()  // t.Kind()返回该类型的特定类型  -- *main.Config ptr
                 t.Elem().Kind()   // t.Elem() 方法获取这个指针指向的元素类型 加上Kind()方法  -- struct

75. ini配置文件解析
    实例:
        import (
           "errors"
           "fmt"
           "io/ioutil"
           "reflect"
           "strconv"
           "strings"
        )

        //ini配置文件解析器
        //MysqlConfig MySQL配置结构体
        type MysqlConfig struct {
           Address string `ini:"address"`
           Port int `ini:"port"`
           UserName string `ini:"username"`
           Password string `ini:"password"`
        }
        //RedisConfig ...
        type RedisConfig struct {
           Host string `ini:"host"`
           Port int `ini:"port"`
           Password string `ini:"password"`
           Database int `ini:"database"`
           Test bool `ini:"test"`
        }
        //Config ...
        type Config struct {
           MysqlConfig `ini:"mysql"`
           RedisConfig `ini:"redis"`
        }
        func loadIni(fileName string, data interface{}) (err error) {
           //0. 参数的校验
           //0.1传进来的data参数必须是指针类型(因为需要在函数中才能赋值)
           t := reflect.TypeOf(data)
           if t.Kind() != reflect.Ptr {  // t.Kind()返回该类型的特定类型
              err = errors.New("data param should be a pointer") //新创建一个错误
              return
           }
           //0.2 传进来的data参数必须是结构体类型指针(因为配置文件中各种键值对需要赋值给结构体的字段)
           if t.Elem().Kind() != reflect.Struct { //t.Elem() 方法获取这个指针指向的元素类型
              err = errors.New("data param should be a pointer") //新创建一个错误
              return
           }
           //1.读文件得到字节类型数据
           b, err := ioutil.ReadFile(fileName)
           if err != nil {
              return
           }
           //string(b) //将字节类型的文件内容转换成字符串

           lineSlice := strings.Split(string(b), "\n")
           fmt.Printf("%#v\n", lineSlice)
           //2.一行一行的读数据
           var structName string
           for idx, line := range lineSlice {
              //去掉字符串收尾的空格
              line = strings.TrimSpace(line)
              //如果是空行就跳过
              if len(line) == 0 {
                 continue
              }
              //2.1 如果是注释就跳过
              if strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#"){ //strings.HasPrefix()函数用来检测字符串是否以指定的前缀开头。
                 continue
              }
              //2.2 如果是[开头的就表示是节(section)
              if strings.HasPrefix(line, "["){
                 if line[0] != '[' || line[len(line) -1] != ']' {
                    err = fmt.Errorf("line: %d syntax error", idx + 1)
                    return
                 }
                 //把这一行首尾的[]去掉,取到中间的内容把首尾的空格去掉拿到内容
                 sectionName := strings.TrimSpace(line[1 : len(line) -1])
                 if len(sectionName) == 0 {
                    err = fmt.Errorf("line:%d syntax error", idx + 1)
                    return
                 }
                 // 根据字符串sectionName 去data里面根据反射找到对应的结构体
                 for i := 0;i < t.Elem().NumField();i++ {
                    field := t.Elem().Field(i)
                    if sectionName == field.Tag.Get("ini") {
                       //说明找到了对应的嵌套结构体,把字段名记下来
                       structName = field.Name
                       fmt.Printf("找到%s对应的嵌套结构体%s\n", sectionName, structName)
                    }
                 }
              }else{
                 //2.3 如果不是[开头就是=分割的键值对
                 //1. 以等号分割这一行,等号左边是key,等号右边是value
                 if strings.Index(line, "=") == -1 || strings.HasPrefix(line, "=") {
                    err = fmt.Errorf("line:%d syntax error", idx +1)
                    return
                 }
                 index := strings.Index(line, "=")
                 key := strings.TrimSpace(line[:index])
                 value := strings.TrimSpace(line[index+1:])

                 //2. 根据structName去 data里面把对应的嵌套结构体给取出来
                 v := reflect.ValueOf(data)
                 if len(structName) == 0 { //如果没有[]开头的 则没有相应的structName数据
                    continue
                 }
                 sValue := v.Elem().FieldByName(structName) //拿到嵌套结构体的值信息
                 sType := sValue.Type() // 拿到嵌套结构体的类型信息

                 if sType.Kind() != reflect.Struct { // 返回该类型的特定类型
                    err = fmt.Errorf("data 中的 %s 字段应该是一个结构体", structName)
                    return
                 }
                 var fieldName string
                 var fileType reflect.StructField
                 //3.遍历嵌套结构体的每一个字段,判断tag是不是等于key
                 for i:= 0; i< sValue.NumField(); i++ {
                    filed := sType.Field(i) //tag信息是存储在类型信息中的
                    fileType = filed
                    if filed.Tag.Get("ini") == key {
                       //找到对应的字段
                       fieldName = filed.Name
                       break
                    }
                 }
                 //4. 如果key = tag,给这个字段赋值
                 //4.1 根据fieldName,去取出这个字段
                 if len(fieldName) == 0 {
                    //在结构体中找不到对应的字符
                    continue
                 }
                 fileObj := sValue.FieldByName(fieldName) //拿到嵌套结构体的值信息

                 //4.2对其赋值
                 fmt.Println(fieldName, fileType.Type.Kind(),value)
                 switch fileType.Type.Kind() {
                 case reflect.String:
                    fileObj.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 {
                       return
                    }
                    fileObj.SetInt(valueInt)
                 case reflect.Bool:
                    var valueBool bool
                    valueBool, err = strconv.ParseBool(value)
                    if err != nil {
                       err = fmt.Errorf("line: %d value type error", idx +1)
                       return
                    }
                    fileObj.SetBool(valueBool)
                 case reflect.Float32, reflect.Float64:
                    var valueFload float64
                    valueFload, err = strconv.ParseFloat(value, 64)
                    if err != nil {
                       err = fmt.Errorf("line: %d value type error", idx +1)
                       return
                    }
                    fileObj.SetFloat(valueFload)
                 }
                 fmt.Println(fileObj,data,"test")
              }
           }

           return
        }

        func main() {
           var cfg Config
           err := loadIni("./conf.ini", &cfg)
           if err != nil {
              fmt.Printf("load ini failed, err:%v\n", err)
           }
           fmt.Printf("%#v\n", cfg)
        }

76. 函数可变参数
    编码中有时会遇到以下写法
    func (f *FileLogger) Error(format string, a ...interface{}){
       f.log(ERROR, format, a...)
    }
    示例:
        func f1(a ...interface{}) {
           fmt.Printf("type:%T value:%#v\n", a, a)
        }
        func main() {
           var s = []interface{}{1, 3, 5, 7, 9}
           f1(s) //type:[]interface {} value:[]interface {}{[]interface {}{1, 3, 5, 7, 9}}
           f1(s...) //type:[]interface {} value:[]interface {}{1, 3, 5, 7, 9}
        }
    以上示例中,f1(s...)表示把参数分开进行传递
        所以f.log(ERROR, format, a...) 就是a...传递的是拆开的参数
            log.Error("这是一条Error日志,id:%d,name:%s", id, name)  可这么写
77. 互斥锁
    互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。
    实例:
        import (
           "fmt"
           "sync"
        )
        var x = 0 //全局变量 - 共享资源
        var wg sync.WaitGroup
        var lock sync.Mutex

        func add() {
           for i:=0;i <500000; i++ {
              lock.Lock()
              x = x + 1
              lock.Unlock()
           }
           wg.Done()
        }
        func main() {
           wg.Add(2)
           go add()
           go add()
           wg.Wait()
           fmt.Println(x)
        }
    读写互斥锁
        互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没必要加锁的,这种场景下使用读写锁是
        更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。
        读写锁分为两种:读锁和写锁。当一个goroutine 获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine
        获取写锁之后,其他的goroutine 无论是获取读锁还是写锁都会等待。
            实例:
                var (
                   x  = 0
                   wg sync.WaitGroup
                   //lock   sync.Mutex
                   rwLock sync.RWMutex
                )

                //读操作
                func read() {
                   defer wg.Done()
                   //lock.Lock()
                   rwLock.RLock()
                   fmt.Println(x)
                   time.Sleep(time.Millisecond)
                   //lock.Unlock()
                   rwLock.RUnlock()
                }
                func write() {
                   defer wg.Done()
                   //lock.Lock()
                   rwLock.Lock()
                   x = x + 1
                   fmt.Println(x,"写入")
                   time.Sleep(time.Millisecond * 5)
                   //lock.Unlock()
                   rwLock.Unlock()
                }
                func main() {
                   start := time.Now()
                   for i := 0; i < 10; i++ {
                      go write()
                      wg.Add(1)
                   }
                   time.Sleep(time.Second)
                   for i := 0; i < 20; i++ {
                      go read()
                      wg.Add(1)
                   }
                   wg.Wait()
                   fmt.Println(time.Now().Sub(start))
                }

78. ssmp-cpm中加入  channel

   //根据Tyte判断来源CSMP/通用第三方
   project := &entity.Project{}
   err = utils.TransModel(commonParam, project)
   //param, err := getProjectData(commonParam)
   if err != nil {
      res.SetStatusCode(1).SetStatusText(err.Error())
      ctx.JSON(200, res)
      return
   }
   ch := make(chan *entity.Project ,10000)
   ch <- project

   //创建project数据
   projectService := project_service.NewProjectService(internal.GlobalResource.DB)
   x := <-ch
   err, project = projectService.CreateProject(x)
   if err != nil {
      res.SetStatusCode(1).SetStatusText(err.Error())
      ctx.JSON(200, res)
      return
   }

79. sync.WaitGroup
   在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用 sync.WaitGroup来实现并发任务的同步。
   sync.WaitGroup有以下几个方法:
        (wg *WaitGroup)Add(delta int)  计数器+delta
        (wg *WaitGroup) Done()          计数器-1
        (wg *WaitGroup) Wait()          阻塞直到计数器变为0
        sync.WaitGroup  内部维护者一个计数器, 计数器的值可以增加和减少。例如当我们启动了N个并发任务时,就将计数器值增加N。每个任务完成时通过
        调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

    sync.Once
    在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。
    Go语言中的sync包中提供了一个针对只执行一次场景的解决方案 - sync.Once。
    sync.Once 只有一个Do方法,其签名如下:
        func (o *Once) Do(f func()) {}
        备注:如果要执行的函数f 需要传递参数就需要搭配闭包来使用
            用法:
                once sync.Once
                onec.Do(func() { close(ch2) }) //确保某个操作只执行一次

    sync.Map
    Go语言中内置的map不是并发安全的,当并发多了(开启多个goroutine)执行就会报fatal error:concurrent map writes错误
    像这种场景下就需要为map加锁来保证并发的安全性了,go语言的sync包中提供了一个开箱即用的并发安全版map-sync.Map。开箱即用表示不用像内置的map
    一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。
        实例:
            //Go内置的map不是并发安全的
            /*var m = make(map[string] int)
            var lock sync.Mutex
            func get(key string) int {
               return m[key]
            }
            func set(key string, value int) {
               m[key] = value
            }
            func main() {
               wg := sync.WaitGroup{}
               for i:= 0;i <21; i++ {
                  wg.Add(1)
                  go func(n int){
                     key := strconv.Itoa(n)
                     lock.Lock()
                     set(key, n)
                     lock.Unlock()
                     fmt.Printf("k=:%v,v:=%v\n", key, get(key))
                     wg.Done()
                  }(i)
               }
               wg.Wait()
            }*/
            var m2 = sync.Map{}

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

               }
               wg.Wait()
            }

80. 保证并发安全,因为原子操作是 Go语言提供的方法,它在用户态就可以完成,因此性能比加锁操作更好。Go语言中原子操作由内置的标准库sync/atomic提供
    atomic包
   atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者sync包的函数
   /类型实现同步更好。
    实例:
        //原子操作
        var x int64
        var wg sync.WaitGroup
        var lock sync.Mutex

        func add() {
           /*lock.Lock()
           x = x + 1
           lock.Unlock()*/
           atomic.AddInt64(&x, 1)
           wg.Done()
        }
        func main() {
           wg.Add(100000)
           for i := 0; i < 100000; i++ {
              go add()
           }
           wg.Wait()
           fmt.Println(x)
        }

81. channel关闭还能取值吗

82. 内容回顾
    锁
        sync.Mutex
        是一个结构体,是值类型。给函数传参数的时候要传指针。
        两个方法
            var lock sync.Mutex
            lock.Lock() //加锁
            lock.UnLock() //写锁
        为什么要用锁?
        防止同一时刻多个goroutine操作同一个资源
    读写互斥锁
        应用场景
            适用于读多写少的场景下,才能提高程序的执行效率。
        特点
            -1)读的goroutine来了获取的是读锁,后续的goroutine 能读不能写
            -2)写的goroutine来了获取的是写锁,后续的goroutine 不管读还是写都要等待获取锁
        使用
            var rwLock sync.RWMutex
            rwLock.RLock() //获取读锁
            rwLock.RunLock() //释放读锁
            rwLock.Lock() //获取写锁
            rwLock.UnLock() //释放写锁
    等待组
        sync.WaitGroup
            用来等goroutine 执行完再继续
            是一个结构体,是值类型,给函数传参数的时候要传指针。
        使用
            var wg sync.WaitGroup
            wg.Add(1) //起几个goroutine就加几个计数
            wg.Done() //在goroutine 对应的函数中,函数要结束的时候表示goroutine完成,计数器-1
            wg.Wait() //阻塞,等待所有的goroutine都结束
    Sync.once
        使用场景
        某些函数只需要执行一次的时候,就可以使用sync.Once
        比如blog加截图片那个例子
            var once sync.Once
            once.Do() //接受一个没有参数也没有返回值的函数,如果需要可以使用闭包
    sync.Map
        使用场景
            并发操作一个map的时候,内置的map不是并发安全的。
        使用
            是一个开箱即用(不需要make初始化) 的并发安全的map
        var syncMap sync.Map
        //Map[key] = value //原生map
        syncMap.Store(key, value)
        syncMap.Load(key)
        syncMap.LoadOrStore()
        syncMap.Delete()
        syncMap.Range()
    原子操作
        go语言内置了一些针对内置的基本数据类型的一些并发安全的操作
            var i int64 = 10
            atomic.AddInt(&i, 1)

83. http_server端
    实例:
        import "net/http"
        func f1(w http.ResponseWriter, r *http.Request) {
           str := "Hello 沙河!"
           w.Write([]byte(str))
        }
        func main(){
           http.HandleFunc("posts/Go/15_socket", f1)
           http.ListenAndServe("127.0.0.1:9000", nil)
        }

84. go学习文档  https://studygolang.com/pkgdoc
85. go语言做单例就是用全局变量

86.单元测试
    Go语言种的测试依赖 go test 命令。
    go test 命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀的源代码文件都是
    go test测试的一部分,不会被go build编译到最终的可执行文件中。
    在 *_test.go文件中 有三种类型的函数,单元测试函数、基准测试函数和示例函数。
        类型          格式                  作用
        测试函数    函数名前缀为Test      测试程序的一些逻辑行为是否正确
        基准函数    函数名前缀为Benchmark 测试函数的性能
        示例函数    函数名前缀为Example   为文档提供示例文档
    go test 命令会遍历所有的 *_test.go文件 种符合上诉命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,
    然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

        实例:
            split_string/ split.go 、split_test.go文件
            split.go :

                package split_string
                import "strings"
                func Split(str string, sep string) []string {
                   var ret []string
                   index := strings.Index(str, sep)
                   for index >= 0 {
                      ret = append(ret, str[:index])
                      str = str[index+len(sep):]
                      index = strings.Index(str, sep)
                   }
                   ret = append(ret, str)
                   return ret
                }

            split_test.go:
                package split_string

                import (
                   "reflect"
                   "testing"
                )
                func TestSplit(t *testing.T){
                   ret := Split("babcbef", "b")
                   want := []string{"", "a", "c", "e"}
                   if !reflect.DeepEqual(ret, want){
                      //测试用例失败了
                      t.Errorf("want:%v but got :%v\n",want, ret)
                   }
                }
        切换到 /Users/mac/GolandProjects/src/study/day09/split_string 目录下,
        先执行 go mod init split_string,
        然后执行
                go test (go test -v :查看详情)

    测试组 和 子测试
        测试组:
            func TestSplit(t *testing.T){
               type testCase struct {
                  str string
                  sep string
                  want []string
               }
               testGroup := []testCase {
                  {"babcbef", "b", []string{"", "a", "c", "ef"}},
                  {"a:b:c", ":",[]string{"ac", "b", "c"}},
               }
               for _, tc := range testGroup{
                  got := Split(tc.str, tc.sep)
                  if !reflect.DeepEqual(got, tc.want) {
                     t.Fatalf("want: %#v got:%#v\n", tc.want, got)
                  }
               }
            }
        子测试:

            func TestSplit(t *testing.T){
                type testCase struct {
                    str string
                    sep string
                    want []string
                }
                testGroup := map[string]testCase{
                    "case_1" : testCase{"babcbef", "b", []string{"", "a", "c", "ef"}},
                    "case_2" : testCase{"a:b:c", ":",[]string{"ac", "b", "c"}},
                }
                for name, tc := range testGroup{
                    t.Run(name, func(t *testing.T){
                        got := Split(tc.str, tc.sep)
                        if !reflect.DeepEqual(got, tc.want) {
                            t.Fatalf("want: %#v got:%#v\n", tc.want, got)
                        }
                    })
                }
            }
                -- go test -run=TestSplit/case_3 (做单独测试)
            测试函数覆盖率 : 100%
            测试覆盖率 : 60%

87.  基准测试
    基准测试函数格式
    基准测试就是在一定的工作负载之下检测程序性能的一种方法。基准测试的基本格式如下:
        func BenchmarkName(b *testing.B) {
            //...
        }
    基准测试以Benchmark为前缀,需要一个 *testing.B类型的参数b,基准测试必须要执行b.N次,这样的测试才有对照性,
    b.N的值是系统根据实际情况去调整的,从而保证测试的稳定性。
        实例:
            func BenchmarkSplit(b *testing.B) {
               for i := 0; i<b.N;i++ {
                  Split("a:b:c:d:e", ":")
               }
            }
        运行命令:
            go test -bench=Split -benchmem
88. go语言标准库flag基本使用
    go语言内置的 flag 包实现了命令行参数的解析,flag包使得开发命令行工具更为简单
    os.Args

89. golang pprof 中 pprof 的英文全称是什么? - 知乎
        performance profiles
90. 内容回顾
    net/http包的用法
        如何发送请求
        当需要频繁发送请求的时候(每5秒从阿里云同步接口数据):定义一个全局的client,后续发请求的操作都使用这个全局的client.
    单元测试
        单元测试
            xxx/ccc.go
            单元测试文件名必须是 xxx/ccc_test.gp
            go内置的测试工具:
                go test
            单元测试函数的格式:
                import "testing"
                //Test开头后接函数名
                    func TestSplit(t *testing.T) {
                        t.Fatal()
                    }
        性能测试/基准测试
            函数格式:
                func BenchmarkSplit(b *testing.B){
                    //b.N: 被测试函数执行的次数
                }
            并行测试
            setup 和 teardown
    pprof
        记录CPU快照信息
        记录内存的快照信息
    flag
        os.Args
            ./xxx a b c
            osArgs["./xxx" "a" "b" "c"]
        flag标准库
            声明变量
                ./xxx -name="周林" -age=9000 a b c
                两种方法
                    nameVal1 := flag.String("name","卢明辉","请输入dsb的名字") //返回的是指针变量
                    var nameVal2 string
                    flag.StringVar(&nameVal2, "name","卢明辉","请输入dsb的名字") //把一个已经存在的变量绑定到命令行flag参数
                    必须要调用:
                        flag.Parse() //解析命令传入的参数,赋值给对应的变量
                其他方法
                    flag.Args() //返回命令行参数后的其他参数,以[]string 类型
                    flag.NArg() //返回命令行参数后的其他参数个数
                    flag.NFlag() //返回使用的命令行参数个数
91. mysql密码 123456
    mysql.server start  启动mysql

92. go get -u github.com/go-sql-driver/mysql报错dial tcp 172.217.27.145:443: i/o
    什么叫造化弄人啊—国内把mod墙了, 根本访问不了. 这个要么科学上网设置代理端口再make, 要么换个下载源. 参照这篇:一键解决go get golang.org/x 包失败
    我看了之后, 主要是设置GOPROXY环境变量(主要是改一下代理), 之后启用go module, 用下面的命令:

    #export GOPROXY=https://goproxy.io
    #export GO111MODULE=on
    链接:https://blog.csdn.net/weixin_43395063/article/details/106132109

93.go操作MySQL
    连接
        go语言中的database/sql包提供了保证sql或类sql数据库的泛用接口,并不提供具体的数据库驱动。使用database/sql包时
        必须注入(至少)一个数据库驱动。
        我们常用的数据库基本上都有完整的第三方实现。例如:MySQL驱动
            下载依赖
                go get -u github.com/go-sql-driver/mysql
            使用Mysql驱动
                func Open(driverName, dataSourceName string) (*DB, error)
            Open打开一个driverName指定的数据库,dataSourceName指定数据源,一般包括至少数据库文件名和(可能的)连接信息
                实例:
                    import (
                        "database/sql"
                        "fmt"

                        _"github.com/go-sql-driver/mysql" //init()
                    )
                    func main() {
                        //数据库信息
                        dsn := "root:root@tcp(127.0.0.1:3306)/goday10"
                        //连接数据库
                        db, err := sql.Open("mysql", dsn) //不会校验用户名和密码是否正确
                        if err != nil {
                            fmt.Printf("dsn:%s invalid, err:%v\n", dsn, err)
                            return
                        }
                        err = db.Ping() //尝试连接数据库
                        if err != nil {
                            fmt.Printf("dsn:%s invalid, err:%v\n", dsn, err)
                            return
                        }
                        fmt.Println("连接数据库成功!")
                    }
    SetMaxOpenCones
        func (db *DB) SetMaxOpenConns(n int)
        SetMaxOpenCones设置与数据库建立连接的最大数目。如果n大于0且小于最大闲置连接数,会将最大闲置连接数减小到匹配最大
        开启连接数的限制。如果 n<= 0,不会限制最大开启连接数,默认为0 (无限制)
    SetMaxIdleConns
        func (db *DB) SetMaxIdleConns(n int)
        SetMaxIdleConns设置连接池种的最大闲置连接数。如果n大于最大开启连接数,则新的最大闲置连接数会减小到匹配最大开启连接数的
        限制,如果 n<=0 ,不会保留闲置连接。

94. mysql查询
    单行查询
        单行查询 db.QueryRow()执行一次查询,并期望返回最多一行结果(即Row)。QueryRow总是返回非nil的值,直到返回值的Scan方法被调用
        时,才会返回被延迟的错误
            func (db *DB) QueryRow(query string, args ...interface{}) *Row
    多行查询
        多行查询 db.Query()执行一次查询,返回多行结果(即Rows),一般用于执行select命令。参数args表示query中占位参数
        func (db *DB) Query(query string, args ...interface{}) (*Rows, error)
    插入/更新/删除数据
        插入、更新和删除操作都使用方法:
            func (db *DB) Exec(query string, args ...interface{}) (Result, error)
            Exec执行一次命令(包括查询、删除、更新、插入等),返回的Result是对已执行的SQL命令的总结。参数args表示query中的占位参数。
    实例:
        import (
            "database/sql"
            "fmt"
            _"github.com/go-sql-driver/mysql" //init()
        )

        var db *sql.DB
        func initDB() (err error) {
            //数据库信息
            //用户名:密码@tcp(ip:端口号)/数据库的名字
            dsn := "root:123456@tcp(127.0.0.1:3306)/sql_test"
            //连接数据库
            db, err = sql.Open("mysql" ,dsn) //不会校验用户名和密码是否正确
            if err != nil {
                return
            }
            err = db.Ping() //尝试连接数据库
            if err != nil {
                return
            }
            db.SetMaxOpenConns(10) //设置数据库连接池的最大连接数
            db.SetMaxIdleConns(5) //设置最大空闲数据库
            return
        }
        type user struct {
            id int
            name string
            age int
        }
        //查询单个记录
        func queryOne(id int) {
            var u1 user
            //1.写查询单条记录的sql语句
            sqlStr := `select id, name, age from user where id=?;`
            //2.执行并拿到结果
            //必须对rowObj对象调用scan方法,因为该方法会释放数据库连接 //从连接池里拿一个连接出来去数据库查询单条记录
            db.QueryRow(sqlStr, id).Scan(&u1.id, &u1.name, &u1.age)
            fmt.Printf("u1:%#v\n", u1)
        }
        func queryMore(n int) {
            //1.sql语句
            sqlStr := `select id, name, age from user where id > ?;`
            //2.执行
            rows, err := db.Query(sqlStr, n)
            if err != nil {
                fmt.Printf("exec %s query failed, err:%v\n",sqlStr, err)
                return
            }
            //3.一定要关闭rows:关闭rows释放持有的数据库连接
            defer rows.Close()
            //4.循环取值
            for rows.Next() {
                var u1 user
                err := rows.Scan(&u1.id, &u1.name, &u1.age)
                if err != nil {
                    fmt.Printf("scan fialed, err:%v\n", err)
                }
                fmt.Printf("u1:%#v\n", u1)
            }
        }
        func insert() {
            //1.写入sql语句
            sqlStr := `insert into user(name, age) values ("图朝阳", 28)`
            //2.exec
            ret, err := db.Exec(sqlStr)
            if err != nil {
                fmt.Printf("insert failed, err: %v\n", err)
                return
            }
            //如果是插入数据的操作,能够拿到插入数据的id
            id, err := ret.LastInsertId()
            if err != nil {
                fmt.Printf("get id failed, err:%v\n", err)
                return
            }
            fmt.Println("id:", id)
        }
        func updateRow(newAge int, id int) {
            sqlStr := `update user set age=? where id = ?`
            //2.exec
            ret, err := db.Exec(sqlStr, newAge, id)
            if err != nil {
                fmt.Printf("update failed, err: %v\n", err)
                return
            }
            n, err := ret.RowsAffected()
            if err != nil {
                fmt.Printf("get id failed, err:%v\n", err)
                return
            }
            fmt.Printf("更新了%d行数据\n", n)
        }

        func delete(id int) {
            sqlStr := `delete from user where id = ?`
            //2.exec
            ret, err := db.Exec(sqlStr, id)
            if err != nil {
                fmt.Printf("delete failed, err: %v\n", err)
                return
            }
            n, err := ret.RowsAffected()
            if err != nil {
                fmt.Printf("get id failed, err:%v\n", err)
                return
            }
            fmt.Printf("删除了%d行数据\n", n)
        }
        func main() {
            err := initDB()
            if err != nil {
                fmt.Printf("init DB failed, err:%v\n", err)
            }
            fmt.Println("连接数据库成功!")
            //queryOne(2)
            //queryMore(0)
            //insert()
            //updateRow(10, 1)
            delete(3)
        }

95. 预处理
    普通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注入问题
    go中的
        func (db *DB) Prepare(query string) (*Stmt, error)
        Prepare 方法会先将sql语句发送给mysql服务端,返回一个准备好的状态用于之后的查询和命令。返回值可以同时执行多个查询和命令
    实例:
        //预处理方式插入多条数据
        func prepareInsert() {
           sqlStr := `insert into user(name, age) values (?, ?)`
           stmt, err := db.Prepare(sqlStr) //把SQL语句先发给MySQL预处理一下
           if err != nil {
              fmt.Printf("prepare failed, err: %v\n", err)
           }
           defer stmt.Close()
           //后续只需要拿到stmt去执行一些操作
           var m = map[string]int {
              "刘启强":30,
              "网相继":32,
              "田硕" : 87,
              "百汇街":91,
           }
           for k, v := range m {
              stmt.Exec(k, v) //后续只需要传值
           }
        }
            --在main()函数中可以直接调用prepareInsert()
96. mysql事务
    开始事务
        func(db *DB) Begin (*Tx, error)
    提交事务
        func(tx *Tx) Commit() error
    回滚事务
        func (tx *Tx) Rollback() error
    实例:
        func transactionDemo() {
            //1.开启事务
            tx, err := db.Begin()
            if err != nil {
                fmt.Printf("begin failed, err :&v\n", err)
            }
            //执行多个sql操作
            sqlStr1 := `update user set age=age-2 where id=1`
            sqlStr2 := `update user set age=age+2 where id=2`
            //执行sql1
            _, err = tx.Exec(sqlStr1)
            if err != nil {
                //要回滚
                tx.Rollback()
                fmt.Println("执行sql1出错啦,要回滚!")
            }
            //执行sql2
            _, err = tx.Exec(sqlStr2)
            if err != nil {
                //要回滚
                tx.Rollback()
                fmt.Println("执行sql2出错啦,要回滚!")
            }
            //上面两部sql都执行成功,就提交本次实物
            err = tx.Commit()
            if err != nil {
                //要回滚
                tx.Rollback()
                fmt.Println("提交出错啦,要回滚!")
                return
            }
            fmt.Println("事务执行成功!")
        }
97. sqlx 使用
    第三方库sqlx能够简化操作,提高开发效率
    安装
        go get github.com/jmoiron/sqlx
    实例:
        import (
            "fmt"
            "github.com/jmoiron/sqlx"
            _ "github.com/go-sql-driver/mysql" //init()
        )

        var db *sqlx.DB
        func initDB() (err error) {
            //数据库信息
            //用户名:密码@tcp(ip:端口号)/数据库的名字
            dsn := "root:123456@tcp(127.0.0.1:3306)/sql_test"
            //连接数据库
            db, err = sqlx.Connect("mysql" ,dsn) //不会校验用户名和密码是否正确
            if err != nil {
                return
            }
            err = db.Ping() //尝试连接数据库
            if err != nil {
                return
            }
            db.SetMaxOpenConns(10) //设置数据库连接池的最大连接数
            db.SetMaxIdleConns(5) //设置最大空闲数据库
            return
        }
        type user struct {
            Id int
            Name string
            Age int
        }
        func main(){
            err := initDB()
            if err != nil {
                fmt.Printf("init DB failed, err :%v\n", err)
            }
            sqlStr1 := `select id, name, age from user where id=1`
            var u user
            db.Get(&u, sqlStr1)
            fmt.Printf("u:%#v\n", u)
            var userList = make([]user, 0, 10)
            sqlStr2 := `select id,name,age from user`
            err = db.Select(&userList, sqlStr2)
            if err != nil {
                fmt.Printf("selet failed, err:%v\n", err)
                return
            }
            fmt.Printf("userList:%#v\n", userList)
        }
98. redis
    github.com/go-redis/redis
    var redisdb *redis.Client
    func initRedis(err error) {
        redisdb = redis.NewClient(
            Addr: "127.0.0.1:6379",
            Password : "",
            DB: 0,
    })
    _, er = reidsdb.Ping().Result()
    return

99. NSQ
    go语言开发的轻量级的消息队列
        组件
            nsqdlookupd.exe


100. gin
    介绍
        Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,具有快速灵活、容错方便等特点。
        对于golang而言,web框架的依赖远比python、java之类的要小。自身的net/http足够简单,性能也非常不做。
        借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范。

    安装
        go get -u github.com/gin-gonic/gin
    路由
        gin框架中采用的路由库是基于 httprouter做的
        httprouter会将所有路由规则构造一颗前缀树,例如有 root and as cn com
        地址为https://github.com/julienschmidt/httprouters
        r := gin.Default()
        r.POST("/", xxx)
    接收参数
        通过Context的Param 接收 API参数
        通过DefaultQuery 或 Query(方法获取 URL参数)
        通过 PostForm()方法来获取 表单参数
101. gin数据解析和绑定
    -1)json格式
        实例:
            //定义接收数据的结构体
            type Login struct {
                //binding :"required" 修饰的字段,若接收为空值,则报错,是必须字段
                User string `form:"username" json:"user" uri:"user" binding:"required"`
                Password string `form:"password" json:"password" uri:"password" binding:"required"`
            }

            func main() {
                //1.创建路由
                //默认使用了2个中间件 Logger(),Recover()
                r := gin.Default()
                // JSON绑定
                r.POST("loginJSON",func(c *gin.Context) {
                    //声明接收的变量
                    var json Login
                    //将request的body中的数据,自动按照json格式解析到结构体
                    if err := c.ShouldBindJSON(&json) ;err != nil{
                        //返回错误信息
                        //gin.H封装了生成json数据工具
                        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
                        return
                    }
                    //判断用户名密码是否正确
                    if json.User != "root" || json.Password != "admin" {
                        c.JSON(http.StatusBadRequest, gin.H{"status": "304"})
                        return
                    }
                    c.JSON(http.StatusOK, gin.H{"status": "200"})
                })
                //3. 监听端口,默认在8000
                r.Run(":8000")
            }
        执行:curl http://127.0.0.1:8000/loginJSON -H 'content-type:appliction/json' -d "{\"user\":\"root\",\"password\":\"admin3\"}" -X POST
    -2) form格式
            //定义接收数据的结构体
            type Login struct {
               //binding :"required" 修饰的字段,若接收为空值,则报错,是必须字段
               User string `form:"username" json:"user" uri:"user" binding:"required"`
               Password string `form:"password" json:"password" uri:"password" binding:"required"`
            }

            func main() {
               //1.创建路由
               //默认使用了2个中间件 Logger(),Recover()
               r := gin.Default()
               // JSON绑定
               r.POST("/loginForm",func(c *gin.Context) {
                  //声明接收的变量
                  var form Login
                  //Bind()默认解析并绑定form格式
                  //根据请求头中context-type自动推断
                  if err := c.Bind(&form) ;err != nil{
                     //gin.H封装了生成json数据工具
                     c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
                     return
                  }
                  //判断用户名密码是否正确
                  if form.User != "root" || form.Password != "admin" {
                     c.JSON(http.StatusBadRequest, gin.H{"status": "304"})
                     return
                  }
                  c.JSON(http.StatusOK, gin.H{"status": "200"})
               })
               //3. 监听端口,默认在8000
               r.Run(":8000")
            }
    -3)uri 格式
            //定义接收数据的结构体
            type Login struct {
               //binding :"required" 修饰的字段,若接收为空值,则报错,是必须字段
               User string `form:"username" json:"user" uri:"user" binding:"required"`
               Password string `form:"password" json:"password" uri:"password" binding:"required"`
            }

            func main() {
               //1.创建路由
               //默认使用了2个中间件 Logger(),Recover()
               r := gin.Default()
               // JSON绑定
               r.GET("/:user:password",func(c *gin.Context) {
                  //声明接收的变量
                  var login Login
                  //Bind()默认解析并绑定form格式
                  //根据请求头中context-type自动推断
                  if err := c.ShouldBindUri(&login) ;err != nil{
                     //gin.H封装了生成json数据工具
                     c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
                     return
                  }
                  //判断用户名密码是否正确
                  if login.User != "root" || json.Password != "admin" {
                     c.JSON(http.StatusBadRequest, gin.H{"status": "304"})
                     return
                  }
                  c.JSON(http.StatusOK, gin.H{"status": "200"})
               })
               //3. 监听端口,默认在8000
               r.Run(":8000")
            }

102. gin渲染
    各种数据格式的响应
        json、结构体、XML、YAML类似于java的properties、ProtoBuf

103. gin同步异步
    - goroutine机制可以方便地是线异步处理
    - 另外,在启动新的goroutine时, 不应该使用原始上下文,必须使用他的只读副本
        实例:
            func main() {
               //1.创建路由
               //默认使用了2个中间件 Logger(),Recover()
               r := gin.Default()
               //1.异步
               r.GET("/long_async",func(c *gin.Context) {
                  //需要搞一个副本
                  copyContext := c.Copy()
                  //异步处理
                  go func() {
                     time.Sleep(3 *time.Second)
                     log.Println("异步执行" + copyContext.Request.URL.Path)
                  }()
               })
               //2.同步
               r.GET("/long_sync",func(c *gin.Context) {
                  time.Sleep(3 * time.Second)
                  log.Println("同步执行:" + c.Request.URL.Path)
               })
               //3. 监听端口,默认在8000
               r.Run(":8009")
            }
            // 执行结果 (看下请求时间和 打印的时间差)
            [GIN] 2021/08/21 - 11:36:39 | 200 |       3.065µs |       127.0.0.1 | GET      "/long_async"
            2021/08/21 11:36:42 异步执行/long_async
            2021/08/21 11:36:57 同步执行:/long_sync
            [GIN] 2021/08/21 - 11:36:57 | 200 |  3.005054911s |       127.0.0.1 | GET      "/long_sync"
104 gin中间件
    -gin可以构建中间件,但它只对注册过的路由函数起作用
    -对于分组路由,嵌套使用中间件,可以限定中间件的作用范围
    -中间件分为全局中间件,单个路由中间件和群组中间件
    -gin中间件必须是一个gin.HandlerFunc类型
        实例:
            //定义中间件
            func MiddleWare() gin.HandlerFunc{
               return func(c *gin.Context) {
                  t := time.Now()
                  fmt.Println("中间件开始执行了")
                  //设置变量到Context的key中,可以通过Get()取
                  c.Set("request", "中间件")
                  //执行中间件
                  c.Next()
                  status := c.Writer.Status()
                  fmt.Println("中间件执行完毕", status)
                  t2 := time.Since(t)  // Since返回从t开始经过的时间。.Now(). sub (t)的简写。
                  fmt.Println("time", t2)
               }
            }
            func main() {
               //1.创建路由
               r := gin.Default()
               //注册中间件
               r.Use(MiddleWare())
               //{}为了代码规范
               {
                  r.GET("middleware", func(c *gin.Context){
                     //取值
                     req, _ := c.Get("request")
                     fmt.Println("request", req)
                     //页面接收
                     c.JSON(200,  gin.H{"request":req})
                  })
               }
               r.Run(":8000")
            }
    练习实例:
        //定义中间件
        func myTime(c *gin.Context){
            start := time.Now()
            c.Next()
            //统计时间
            since := time.Since(start)
            fmt.Println("程序用时:", since)
        }
        func main() {
            //1.创建路由
            r := gin.Default()
            //注册中间件
            r.Use(myTime)
            //{}为了代码规范
            shoppingGroup := r.Group("/shopping")
            {
                shoppingGroup.GET("/index", shopIndexHandler)
                shoppingGroup.GET("/home", shopHomeHandler)
            }
            r.Run(":8009")
        }
        func shopIndexHandler(c *gin.Context) {
            time.Sleep(5 * time.Second)
        }
        func shopHomeHandler(c *gin.Context) {
            time.Sleep(3 * time.Second)
        }

105. cookie 和 session
    cookie实例 :ß
        import (
            "github.com/gin-gonic/gin"
            "net/http"
        )
        // 定义中间件
        func AuthMiddleWare() gin.HandlerFunc{
            return func(c *gin.Context){
                if cookie, err := c.Cookie("abc"); err == nil {
                    if cookie == "123" {
                        c.Next()
                        return
                    }
                }
                //返回错误
                c.JSON(http.StatusUnauthorized, gin.H{"error" : "err"})
                //若验证不通过,不再调用后续的函数处理
                c.Abort()
                return
            }
        }
        func main() {
            //1.创建路由
            r := gin.Default()
            r.GET("login", func(c *gin.Context){
                //设置cookie
                c.SetCookie("abc","123",60,"/","localhost",false,true)
                //返回信息
                c.String(200, "Login success")
            })
            r.GET("/home", AuthMiddleWare(), func(c *gin.Context){
                c.JSON(200, gin.H{"data": "home"})
            })

            r.Run(":8000")
        }
106. .var的 用法,自动返回值,例如
    NewMemorySession(sessionId).var,自动变成
    memorySession := NewMemorySession(sessionId)

107. 数据库简单
    book目录 文件: db.go、main.go、model.go
        main.go
            package main

            import (
               "fmt"
               "github.com/gin-gonic/gin"
               "net/http"
            )

            func main() {
               //初始化数据库
               err := initDB()
               if err != nil {
                  panic(err)
               }
               r := gin.Default()
               r.GET("/book/list", bookListHandler)
               _ = r.Run(":8000")
            }
            func bookListHandler(c *gin.Context) {
               bookList, err := queryAllBook()
               if err != nil {
                  c.JSON(http.StatusOK, gin.H{
                     "code": 1,
                     "msg": err,
                  })
                  return
               }
               fmt.Println("到这里了")
               //返回数据
               c.JSON(http.StatusOK,gin.H{
                  "code": 0,
                  "data": bookList,
               })
            }
        db.go
            package main

            import (
               "fmt"
               _ "github.com/go-sql-driver/mysql"
               "github.com/jmoiron/sqlx"
            )
            var db *sqlx.DB

            func initDB() (err error) {
               fmt.Println("开始初始化")
               addr := "root:123456@tcp(127.0.0.1:3306)/test"
               db, err = sqlx.Connect("mysql", addr)
               if err != nil{
                  return err
               }
               //最大连接
               db.SetMaxOpenConns(100)
               db.SetMaxIdleConns(16)
               fmt.Println("初始化成功")
               return
            }
            func queryAllBook() (bookList []*Book, err error) {
               sqlStr := "select id, title, price from book"
               fmt.Println(bookList)
               err = db.Select(&bookList, sqlStr)
               if err != nil {
                  fmt.Println("查询失败",err)
                  return
               }
               fmt.Println("查询结束")
               return
            }
            func insertBook(title string, price int64) (err error) {
               sqlStr := "insert into(title, price) values(?,?)"
               _, err = db.Exec(sqlStr, title, price)
               if err != nil {
                  fmt.Println("插入失败")
                  return
               }
               return
            }
            func deleteBook(id int64) (err error) {
               sqlStr := "delete from book where id=?"
               _, err = db.Exec(sqlStr, id)
               if err != nil {
                  fmt.Println("删除失败")
                  return
               }
               return
            }

        model.go
            package main

            type Book struct {
               ID int64 `db:"id"`
               Title string `db:"title"`
               Price int64 `db:"price"`
            }
108. mysql中bigint对应 go中的int64

109.gin项目目录结构
    model: 实体
    dao:数据层
    service:业务逻辑
    controller:页面控制相关
    static: css js
    utils:工具
    views: HTML模板
    main.go:入口,定义路由
110. raft
    raft 是consoul和etcd的核心算法
    raft介绍
        -raft提供了一种在计算系统集群中分布状态的通用方法,确保集群中的每个节点都同意一系列相同的状态转换。
        -它有许多开源参考是线,具有go,c++,jave和scala中的完整规范实现。
        -一个Raft集群包含若干个服务器节点,通常是5个,这允许整个系统容忍2个节点的失败,每个节点处于一下三种状态之一:
            -1)follower(跟随者):所有节点都以follower的状态开始。如果没有收到leader消息则会变成candidate状态。
            -2)candidate(候选人):会向其他节点"拉选票",如果得到大部分的票则成为leader,这个过程叫作Leader选举(Leader Election)
            -3)leader(领导者):所有对系统的修改都会西先经过leader.
    raft一致性算法
        -raft通过选出一个leader来简化日志副本的管理,例如,日志项(log entry)只允许从leader流向follower
        -基于leader的方法,Raft算法可以分解成三个子问题
            -1)Leader election(领导选举):原来的leader挂掉后,必须选出一个新的leader
            -2)Log replication(日志复制):leader从客户端接收日志,并复制到整个集群中
            -3)Safety(安全性):如果有任意的server将日志项回放到状态机中,那么其他的server只会回方相同的日志项
    raft动画演示
        -地址:http://thesecretlivesofdata.com/raft/
        -动画主要包含三部分:
            -1)第一部分介绍简单版的领导者选举和日志复制的过程
            -2)第一部分介绍详细版的领导者选举和日志复制的过程
            -3)第三部分介绍如果遇到网络分区(脑裂),raft算法是如何恢复网络一致的
    Leader election(领导选举)
        -Raft 使用一种心跳机制来出发领导人选举
        -当服务器程序启动时,节点都是 follower(跟随者)身份
        -如果一个跟随者在一段时间里没有接收到任何消息,也就是选举超时,然后他就会认为系统中没有可用的领导者然后开始进行选举以选出新的领导者
        -要开始一次选举过程,follower会给当前term 加1并且转换成candidate状态,然后它会并行的向集群中的其他服务器节点发送请求投票的RPCs
            来给自己投票。
        -候选人的状态维持直到发生以下任何一个条件发生的时候
            -1)他自己赢得了这次的选举
            -2)其他的服务器成为领导者
            -3)一段时间之后没有任何一个获胜的人
    Log replication(日志复制)
        -当选出leader后,它会开始接收客户端请求,每个请求会带有一个指令,可以被回放到状态机中
        -leader把指令追加成一个log entry,然后通过AppendEntries RPC并行地发送给其他的server,当该entry被多数server复制后,leader会
            把该entry回放到状态机中,然后把结果返回给客户端
        -当 follower 宕机或者运行较慢时,leader 会无限地重发 AppendEntries 给这洗follower,直到所有的follower都复制了改log entry
        -raft 的log replication 要保证如果两个log entry有相同的index 和term,那么它们存储相同的指令
        -leader在一个特定的term和index下,只会创建一个 log entry

111. 服务注册组件开发
    -1)服务注册和发现的原理
                Registry
        (pull,push)      (register)
    Client     (call)         Server

    -2)注册中心选型
        服务名 :[node1,node2,node3] weight
        zk太重量级了,AWS不是分布式存储,是主从的 这里选用etcd
    -3)选项设计模式
        -有一个结构体,初始化结构体
        代码实例:
            package main

            import "fmt"

            //结构体
            type Options struct {
               strOption1 string
               strOption2 string
               strOption3 string
               intOption1 int
               intOption2 int
               intOption3 int
            }
            //声明一个函数类型的变量,用于传参
            type Option func(opts *Options)

            //初始化结构体
            func InitOptions1(opts ...Option) {
               options := &Options{}
               //遍历opts,得到每一个函数
               for _, opt := range opts{
                  //调用函数,在函数里,给穿进去的对象赋值
                  opt(options)
               }
               fmt.Printf("init options %#v\n", options)
            }
            //定义具体给某个字段赋值的方法
            func WithStrOption1(str string) Option {
               return func(opts *Options) {
                  opts.strOption1 = str
               }
            }
            func WithStrOption2(str string) Option {
               return func(opts *Options) {
                  opts.strOption2 = str
               }
            }
            func WithStrOption3(str string) Option {
               return func(opts *Options) {
                  opts.strOption3 = str
               }
            }
            func WithIntOption1(i int) Option {
               return func(opts *Options) {
                  opts.intOption1 = i
               }
            }
            func WithIntOption2(i int) Option {
               return func(opts *Options) {
                  opts.intOption2 = i
               }
            }
            func WithIntOption3(i int) Option {
               return func(opts *Options) {
                  opts.intOption3 = i
               }
            }
            func main() {
               InitOptions1(WithStrOption1("str1"), WithIntOption1(3))
            }
    -4)注册组件接口开发
        目标
            --支持多注册中心,既支持consul又支持etcd
            --支持可扩展
            --提供基于名字的插件管理函数,用来注册插件
                    服务主从接口

        etcdRegister                    ConsulRegister
                (etcd)                      (consul)
                    调用方
            分析,类似于session中间件
                -定义服务注册总接口Registry,定义方法:
                    --Name():插件名,例如传etcd
                    --Init(opts ...Option):初始化,里面用选项设计模式做初始化
                    --Register():服务注册
                    --Unregister(): 服务反注册,例如服务端停了,注册列表销毁
                    --GetService:服务发现(ip port[] string)
                -抽象出一些结构体:
                    --Node: 单个节点的结构体,包含id ip port weight(权重)
                    --service: 里面有服务点,还有节点列表,一个服务多台服务器支撑
                -选项设计模式,实现参数初始化
                -插件管理类
                    --可以用一个大map管理,key字符串,value
                    --用户自定义去调用,自定义插件
                    --实现注册中心的初始化,供系统使用

112. 如果要实例的话,就需要传指针
    Register(ctx context.Context, service *Service)(err error)

113.etcd续期
    package main

    import (
       "context"
       "fmt"
       "log"
       "time"
    )

    func main() {
       cli, err := clientv3.New(clientv3.Config{
          Endpoint: []string{"127.0.0.1:2379"},
          DialTimeout: time.Second,
       })
       if err != nil {
          log.Fatal(err)
       }
       defer cli.Close()
       //设置续期5秒
       resp, err := cli.Grant(context.TODO(), 5)
       if err != nil {
          log.Fatal(err)
       }
       //将k-v设置到etcd
       _, err = cli.Put(context.TODO(), "root", "admin", clientv3.WithLease(resp.ID))
       if err != nil {
          log.Fatal(err)
       }
       //若想一直有效,设置自动续期
       ch, err := cli.KeepAlive(context.TODO(), resp.ID)
       if err != nil {
          log.Fatal(err)
       }
       for {
          c := <-ch
          fmt.Println("c", c)
       }
    }

114. 初始化顺序
    main包、import、全局const、全局var、init、main()函数

115. map里结构体无法寻址
    type S struct {
        name string
    }
    func main() {
        //map里结构体无法直接寻址,必须取址
        m := map[string]*S{"x": &S{"one"}}  // 缺少*则报错
        m["x"].name = "two"
        fmt.Println(m["x"].name)
    }
        --map中的结构体元素是无法取地址的,即:map[string]struct类型,取&map["tmp"]是错误的。会提示报错:
         cannot assign to struct field elem["count"].count in map。
        原因为:map中的元素并不是一个变量,而是一个值。因此,我们不能对map的元素进行取址操作。

116. golang种的stack 和 heap
    区别:栈(stack) 由编译器自动分配和释放,存变量名、各种名
         堆:在C里由程序员分配和释放内存,go自动了,存栈变量的数据值
         make(xxx)  a := 3   -- a就在栈,3在堆

117. 服务发现
    -1)服务发现的方案
        -使用DNS进行服务发现
        -基于SDK进行服务发现
    -2)传统DNS服务发现
        -不支持动态变更
        -没健康检查,不是高可用
        -效率不是太高
        -K8s有一个kube-DNS可以解决上面问题
    -3)基于SDK的服务发现
        -etcd版本
        -思想:
            --注册中心挂了,不影响大体,解决方式:缓冲,将服务注册列表缓冲到客户端一份,存在问题就是客户端有可能存的不是最新的
            --准时去更新,10秒更新一次注册列表

118. gRPC入门
    -1)gRPC简介
        -gRPC由google开发,是一款语言中立、平台中立、开源的远程过程调用系统
        -gRPC客户端和服务端可以在多种环境中运行和交互,例如用java写服务端,可以用go语言写客户端调用。
    -2)gRPC与Protobuf介绍
        -微服务结构中,由于每个服务对应的代码库是独立运行的,无法直接调用,彼此间的通信就是个大问题
        -gRPC可以实现微服务,将大的项目拆分为多个小且独立的业务模块,也就是服务,各服务间使用高效的protobuf协议进行RPC调用,gRPC默认使用protocol
            buffers,这是google开源的一套成熟的结构数据序列化机制(当然也可以使用其他数据格式和JSON)
        -可以用proto files创建gRPC服务,用message类型 来定义方法参数和返回类型

119. Go Micro
    -1)go-micro简介
        -Go Micro 是一个插件化的基础框架,基于此可以构建微服务,Micro的设计哲学是可插拔的插件化架构
        -在架构之外,它默认实现了consul作为服务发现(2019年源码修改了默认使用mdns),通过http进行通信,通过protobuf和json进行编解码
    -2)go-micro的主要功能
        --服务发现,自动服务注册和名称解析。服务发现是微服务开发的核心。当服务A需要与服务B通话时,它需要该服务的位置。默认发现机制是多播DNS(mdns),
          一种零配置系统。您可以选择选用SWIM协议为p2p网络设置八卦,或者为弹性云原生设置consul
        --负载均衡:基于服务发现构建的客户端负载均衡。一旦我们获得了服务的任意数量实例的地址,我们现在需要一种方法来决定要路由到哪个节点。我们使用随机
         散列负载均衡来提供跨服务的均匀分布,并在出现问题时重试不同的节点。
        --消息编码:基于内容类型的动态消息编码。客户端和服务器将使用编码器和内容类型为您无缝编码和解码Go类型。可以编码任何种类的消息并从不同的客户端
         发送。客户端和服务器默认处理此问题。这包括默认的protobuf和json
        --请求/响应:基于RPC的请求/响应,支持双线流。我们提供了同步通信的抽象。对服务的请求将自动解决,负载平衡,拨号和流式传输。启用tls时,默认传输
           为http/1.1或http2
        --Async Messaging:PubSub是异步通信和事件驱动架构的一流共鸣。事件通知是微服务开发的核心模块。启动tls时,默认消息传递是点对点http/1.1或
          http2
        --可插拔接口:Go Micro为每个分布式系统抽象使用Go接口,因此,这些接口是可插拔的,并允许Go Micro 与运行时无关,可以插入任何基础技术。
                插件地址: https://github.com/micro/go-plugins

120. 定义结构体里 结构体 有 *的区别
    type User struct {
        a string
        b *abc  //其中,abc是结构体,如果有*,则是实例化后的,有值;没有*则是一个新的结构体
    }

121.go module
    go module是Go1.11版本之后官方推出的版本管理工具,并且从Go1.13版本开始,go module将是Go语言默认的依赖管理工具
    GO111MODULE
        要启用go module 支持首先要设置环境变量 GO111MODULE,通过它可以开启或关闭模块支持,它有三个可选值:off、on、auto,默认是auto。
        -1)GO111MODULE=off 禁用模块支持,编译时会从GOPATH和vendor文件夹中查找包
        -2)GO111MODULE=on 启用模块支持,编译时会忽略GOPATH和vendor文件夹,只根据go.mod下载依赖。
        -3)GO111MODULE=auto ,当项目在$GOPATH/src外且项目根目录有go.mod文件时,开启模块支持。
        简单来说,设置 GO111MODULE = on 之后就可以使用go module了,以后就没有必要在GOPATH中创建项目了,并且还能够很好的管理项目依赖的
        第三方包信息。
        使用go module 管理依赖后 会在项目根目录下生成两个文件 go.mod 和 go.sum
    GOPROXY
        Go1.11之后设置GOPROXY命令为:
            export GOPROXY=https://goproxy.cn
        Go1.13之后 GOPROXY默认值为https://proxy.golang.org,在国内无法访问的,所以十分建议大家设置GOPROXY,这里推荐使用goproxy.cn
            go env -w GOPROXY=https://goproxy.cn,direct
    go mod 命令
        go mod init 初始化当前文件夹,创建 go.mod文件
    go.mod
        go.mod文件记录了项目所有的依赖信息,其结构大致如下:
            module github.com/Q1mi/studygo/blogger

            go 1.12

            require (
                github.com/gin-gonic/gin v1.4.0
            )
        其中:
            -1) module用来定义包名
            -2) require 用来定义依赖包及版本
            -3) indirect  表示简介引用
    go mod tidy //检查代码里的依赖去更新go.mod文件中的依赖
    go get
        在项目中执行go get命令可以下载依赖包,并且还可以制定下载的版本。1.运行 go get -u 将会升级到最新的次要版本或者修订版本(x.y.z,z
        是修订版本号,y是次要版本号) 2.运行go get -u=patch 将会升级到最新的修订版本 3.运行go get package@version将会升级到指定的版本
        号version
        如果下载所有依赖可以使用go mod download 命令
    在项目中使用go module
        既有项目
            如果需要对一个已经存在的项目启用go module,可以按照以下步骤操作:
                -1) 在项目目录下执行 go mod init,生成一个 go.mod 文件
                -2) 执行go get ,查找并记录当前项目的依赖,同时生成一个go.sum 记录每个依赖库的版本 和哈希值
        新项目
            对于一个新创建的项目,我们可以在项目文件夹下按照以下步骤操作:
                -1)执行go mod init 项目命令,在当前项目文件夹下创建一个go.mod文件
                -2)手动编辑go.mod中的require依赖项或执行go get自动发现、维护依赖。

122. go标准库 Context
    在go http包的Server中,每一个请求都有一个对应的goroutine去处理。请求处理函数通常会启动额外的goroutine用来访问后端服务,比如数据库和
    RPC服务。用来处理一个请求的goroutine通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。当
    一个请求被取消或超时时,所有用来处理该请求的goroutine都应该迅速退出,然后系统才能释放这些goroutine占用的资源
        通知子goroutine迅速退出实例:
            import (
               "context"
               "fmt"
               "sync"
               "time"
            )

            //为什么需要context?
            var wg sync.WaitGroup
            func f2(ctx context.Context){
               defer wg.Done()
            FORLOOP:
               for{
                  fmt.Println("保德路")
                  time.Sleep(time.Millisecond * 500)
                  select {
                  case <- ctx.Done(): //等待上级通知
                     break FORLOOP
                  default:
                  }
               }
            }
            func f(ctx context.Context) {
               defer wg.Done()
               go f2(ctx)
            FORLOOP:
               for{
                  fmt.Println("周林")
                  time.Sleep(time.Millisecond * 500)
                  select {
                  case <- ctx.Done():  //等待上级通知
                     break FORLOOP
                  default:
                  }
               }
            }
            func main(){
               ctx, cancel := context.WithCancel(context.TODO())
               wg.Add(1)
               go f(ctx)
               time.Sleep(time.Second *5)
               //如何通知子goroutine退出
               cancel() //通知子goroutine结束
               wg.Wait()
            }

    Context 初识
        Go1.7加入了一个新的标准库 context, 它定义了Context类型,专门用来简化对于处理单个请求的多个goroutine之间与请求域的数据、取消信
        号、截止时间等相关操作,这些操作可能涉及多个API调用。
        对服务器传入的请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传递上下文,或者可以用WithCancel、
        WithDeadline、WithTimeout或WithValue创建的派生上下文。当一个上下文被取消时,它派生的所有上下文也被取消。
    Context接口
        context.Context是一个接口,该接口定义了四个需要实现的方法。具体签名如下:
            type Context interface {
                Deadline() (deadline time.Time, ok bool)
                Done() <-chan struct{}
                Err() error
                Value(key interface{}) interface{}
            }
        其中,
            -1)Deadline方法需要返回当前Context被取消的时间,也就是完成工作的截止时间(deadline) ;
            -2)Done方法需要返回一个Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done方法会返回同一个Channel
            -3)Err方法会返回当前Context 结束的原因,它只会在 Done 返回的Channel 被关闭时才会返回非空的值
                --1)如果当前Context 被取消就会返回 Canceled 错误;
                --2)如果当前Context 超时就会返回 DeadlineExceeded 错误
            --4)Value 方法会从 Context 中返回键对应的值,对于同一个上下文来说,多次调用Value并传入相同的key会返回相同的结果,该方法仅用于
            传递跨API和进程间跟请求域的数据
    Background() 和 TODO()
        GO内置两个函数:Background() 和 TODO(),这两个函数分别返回一个实现了Context接口的background 和 todo。我们代码中最开始都是
        以这两个内置的上下文对象作为最顶层的parent context,衍生出更多的子上下文对象。
        Background()主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context.
        TODO(),它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。
        background和todo本质上都是 emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context
    With系列函数
        此外,context 包中还定义了四个With系列函数。
            WithCancel、WithTimeout、WithDeadline、WithValue

123. 日志收集项目架构
    组件介绍
        LogAgent: 日志收集客户端,用来收集服务器上的日志。Kafka:高吞吐量的分布式队列(Linkin开发,apache顶级开源项目) ElasticSearch:
        开源的搜索引擎,提供基于HTTP RESTful的web接口。Kibana:开源的ES数据分析和可视化工具。Hadoop: 分布式计算框架,能够对大量数据进行
        分布式处理的平台。Storm:一个免费并开源非分布式实时计算系统。
        将学到的技能:
            -1)服务端agent开发
            -2)后端服务组件开发
            -3)Kafka和zookeeper的使用 (美[ˈzuːkiːpər])
            -4)ES 和Kibana的 使用 ([kɪbana])
            -5)etcd的使用
124. 消息队列的通信模型
    点对点模式(queue)
        消息生产者生产消息发送到queue中,然后消息消费者从queue中取出并且消费消息。一条消息被消费以后,queue中就没有了,不存在重复消费。
    发布/订阅(topic)
        消息生产者(发布) 将消息发布到topic中,同时有多个消息消费者(订阅) 消费该消息。和点对点方式不同,发布到topic的消息会被所有订阅者
        消费(类似于关注了微信公众号的人都能收到推送的文章)
125.Kafka
    Apache Kafka由著名职业社交公司Linkedin开发,最初是被设计用来解决Linkedin公司内部海量日志传输等问题。Kafka使用Scala语言编写,于.
    2011年开源并进入Apache孵化器,2012年10月正式毕业,现在为Apache顶级项目。
    介绍
        Kafka是一个分布式数据流平台,可以运行在单台服务器上,也可以在多台服务器上部署形成集群。它提供了发布和订阅功能,使用者可以发送数据到
        Kafka中,也可以从Kafka中读取数据 (以便进行后续的处理)。Kafka具有高吞吐、低延迟、高容错等特点。
        -Producer:Producer 即生产者,消息的产生者,是消息的入口。
        -kafka cluster:kafka集群,一台或多台服务器组成
            -Broker:Broker是指部署了kafka实例的服务器节点。每个服务器上游一个或多个kafka的实例,我们姑且认为每个broker对应一台服务器。
            每个kafka集群内的broker都有一个不重复的编号,如图中的broker-0、broker-1等...
            -Topic:消息的主题,可以理解为消息的分类,kafka的数据就保存在topic。在每个broker上都可以创建多个topic。实际应用中通常是一个
            业务线建一个topic。
            -Partition:Topic的分区,每个topic可以有多个分区,分区的作用是做负载,提高kafka的吞吐量。同一个topic在不同的分区的数量是
            不重复的,partition的表现形式就是一个一个的文件夹!
            -Replication:每一个分区都有多个副本,副本的作用是做备胎。当主分区(Leader)故障的时候会选择一个备胎(Follower)上位,成为
            Leader。在kafka中默认副本的最大数量是10个,且副本的数量不能大于Broker的数量,follower和leader绝对是在不同的机器,同一
            机器对同一个分区也只可能存放在一个副本(包括自己)。
        -Consumer:消费者,即消息的消费方,是消息的出口。
            -Consumer Group:我们可以将多个消费组组成一个消费者组,在kafka的设计中同一个分区的数据只能被消费者组中的某一个消费者消费。
            同一个消费者组的消费者可以消费同一个topic的不同分区的数据,这也是为了提高kafka的吞吐量!
    producer在写入数据的时候会把数据写入到leader中,不会直接将数据写入follower! 那leader怎么找呢?写入的流程又是怎样的呢?
        -1) 生产者从kafka集群获取分区leader信息
        -2) 生产者将消息发送给leader
        -3) leader将消息写入本地磁盘
        -4) follower 从leader拉取消息数据
        -5) follower 将消息写入本地磁盘后 向leader发送ACK
        -6) leader收到所有的follower 的ACK之后向生产者发送ACK
    选择partition的原则
        在kafka中,如果某个topic有多个partition,producer又怎么知道该将数据发往那个partition呢?kafka中有几个原则:
            -1)partition在写入的时候可以指定需要写入的partition,如果有指定,则写入对应的partition
            -2)如果没有指定partition,但是设置了数据的key,则会根据key的值hash出一个partition
            -3)如果既没有指定partition,又没有设置key,则会采用轮询方式,即每次取出一小段时间的数据写入某个partition,下一小段时间写入
            下一个partition
    ACK应答机制
        producer 在向kafka写入消息的时候,可以设置参数来确定是否确认kafka接收到数据,这个参数可设置的值为0 、1 、all
            -0代表producer往集群发送数据不需要等到集群的返回,不确保消息发送成功。安全性最低但是效率最高。
            -1代表producer往集群发送数据只要leader应答就可以发送下一条,只确保leader发送成功。
            -all代表producer往集群发送数据需要所有的follower都完成从leader的同步才会发送下一条,确保leader发送成功和所有的副本都完成
            备份。安全性最高,但是效率最低。
            最后要注意的是,如果往不存在的topic写数据,kafka会自动创建topic,partition和replication的数量默认配置都是1。

126.zookeeper
    zookeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,它是集群的管理者,监视者集群中各个节点的
    状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能文档的系统提供给用户。

127.tail模块的介绍和使用
    tail包使用
    实例:
        ???????????????? (154节)

128. 包指定版本号
    在go.mod文件中
        require github.com/Shopify/sarama v1.19.0
    然后执行 go mod download 即可下载
129. 日志收集项目
    为什么要自己写不用ELK?
    ELK:部署 的时候 麻烦,每一个filebeat 都需要配置一个配置文件
    使用etcd 来管理被收集的日志项
130. etcd
    etcd 是使用Go语言开发的一个开源的、高可用的分布式key-value存储系统,可以用于配置共享和服务的注册和发现。
    类似项目有zookeeper和consul
    etcd 具有以下特点:
        完全复制、高可用性、一致性、简单、安全、快速、可靠
    etcd 应用场景

131. kafka 消息列表
    tailf 从文件里读日志
    go-ini 解析配置文件

132. Elasticsearch
    ES是一个基于Lucene构建的开源、分布式、RESTful接口的全文搜索引擎。ES还是一个分布式文档数据库,其中每个字段均可被索引,而且每个字段的数据均可
    被搜索,ES能够横向扩展至数以百计的服务器存储以及处理PB级的数据。可以在极短的时间内存储、搜索和分析大量的数据。通常作为具有复杂搜索场景情况下
    的核心发动机。
    场景:
        当你经营一家网上商店,你可以让你的客户搜索你卖的商品。在这种情况下,你可以使用ES来存储你的整个产品目录和 库存信息,为客户提供精准搜索,可以
        为客户推荐相关商品。
        github的搜索是基于ES构建的。
    ES基本概念
        Near Realtime(NRT) 几乎实时
            ES是一个几乎实时的搜索平台。意思是,从索引一个文档到这个文档可被搜索只需要一点点的延迟,这个时间一般为毫秒级。
        Cluster 集群
            集群是一个或多个节点(服务器)的集合,这些节点共同保存整个数据,并在所有节点上提供联合索引和搜索功能。一个集群由一个唯一集群ID确定,并
            指定一个集群名(默认为"elasticsearch")。该集群名非常重要,伊因为节点可以通过这个集群名加入集群,一个节点只能是集群的一部分。
            确保在不同的环境中不要使用相同的集群名称,否则可能会导致接连错误的群集节点。例如,你可以使用logging-dev、logging-stage、logging-prod
            分别为开发、阶段产品、生产集群做记录。
        Node节点
            节点是单个服务器实例,它是集群的一部分,可以存储数据,并参与集群的索引和搜索功能。
        Index索引
            索引是具有相似特征的文档集合。例如,可以为客户数据提供索引,为产品目录建立另一个索引,以及为订单数据建立另一个索引。索引由名称(必须全部为小写)
            标识,该名称用于在对其中的文档执行索引、搜索、更新和删除操作时引用索引。在单个集群中,你可以定义尽可能多的索引。
        Type类型
            在索引中,可以定义一个或多个类型。类型是索引的逻辑类型/分区,其语义完全取决于你。一般来说,类型定义为具有公共字段集的文档。例如,假设你运行
            一个博客平台,并将所有数据存储在一个索引中。在这个索引中,你可以为用户数据定义一种类型,为博客定义另一种类型,以及为注释数据定义另一类型。
        Document 文档
            文档是可以被索引的信息的基本单位。例如,你可以为单个客户提供一个文档,单个产品提供另一个文档,以及单个订单提供另一个文档。本文件的表示形式为JSON
            (JavaScript Object Notation)格式,这是一种非常普遍的互联网数据交换格式。
            在索引/类型中,你可以存储尽可能多的文档。请注意,尽管文档物理驻留在索引中,文档实际上必须索引或分配到索引中的类型。
        Shards & Replicas 分片与 副本
            索引可以存储大量的数据,这些数据可能超过单个节点的硬件限制。例如,十亿个文件占用磁盘空间1TB的单指标可能不适合对单个节点的磁盘或可能太慢服务仅从
            单个节点的搜索要求。
            为了解决这一问题,ES提供细分你的指标分成多个块成为分片的能力。当你创建一个索引,你可以简单地定义你想要的分片数量。每个分片本身是一个全功能的、独立
            的"指数",可以托管在集群中的任何节点。
        ES基本概念与关系型数据库的比较
            ES概念                                        关系型数据库
            Index(索引)支持全文检索                         Database(数据库)
            Type(类型)                                    Table(表)
            Document(文档),不同文档可以有不同的字段集合       Row(数据行)
            Field(字段)                                   Colume(数据列)
            Mapping(映射)                                 Schema(模式)
    启动ES (windows)
        D:\softTool>cd elasticsearch-7.14.0
        D:\softTool\elasticsearch-7.14.0>bin\elasticsearch.bat
    ES操作
        创建 index - 127.0.0.1:9200/student  -- 请求方式:PUT
        插入数据  - 127.0.0.1:9200/student/go (其中,student是 index,go是type)  --请求方式 :POST
                                                    参数(raw):  {
                                                                   "name": "lixiang",
                                                                   "age": 74,
                                                                   "married": true
                                                                }
        查询语句  - 127.0.0.1:9200/student/go/_search
                                                  参数(raw):  {
                                                               "query": {
                                                                  "match": {"married":"false"}
                                                               },
                                                               "size": 1
                                                              }
133. go操作es实例 :

        import (
           "context"
           "fmt"
           "github.com/olivere/elastic/v7"
        )

        type Person struct {
           Name string `json:"name"`
           Age int `json:"age"`
           Married bool `json:"married"`
        }
        func main() {
           client, err := elastic.NewClient(elastic.SetURL("http://127.0.0.1:9200"))
           if err != nil {
              //Handle error
              panic(err)
           }
           fmt.Println("connect to es success")
           p1 := Person{Name: "rion",Age: 22, Married: false}
           put1, err := client.Index().Index("student").Id("go").BodyJson(p1).Do(context.Background())
           if err != nil {
              panic(err)
           }
           fmt.Printf("Indexed student %s to index %s, type %s\n", put1.Id, put1.Index, put1.Type)
        }

134. 链式操作
    type Student struct {
       Name string `json:"name"`
       Age int `json:"age"`
       Married bool `json:"married"`
    }
    func (s *Student) run() *Student{
       fmt.Printf("%s在跑...",s.Name)
       return s
    }
    func (s *Student) wang() {
       fmt.Printf("%s在汪汪汪的叫...", s.Name)
    }

    func main() {
       luminghui := Student{
          Name: "卢明辉",
          Age: 9000,
          Married:false,
       }
       luminghui.run().wang()
    }

135. etcd 搭建
    etcd 启动
        D:\softTool\etcd-v3.5.0-windows-amd64>etcd.exe
        操作代码:
            package main

            import (
               "context"
               "fmt"
               "go.etcd.io/etcd/clientv3"
               "time"
            )
            func main() {
               cli, err := clientv3.New(clientv3.Config{
                  Endpoints: []string{"127.0.0.1:2379"},
                  DialTimeout: 5 * time.Second,
               })
               if err != nil {
                  fmt.Printf("connect to etcd failed, err:%v\n", err)
                  return
               }
               fmt.Println("connect to etcd success")
               defer cli.Close()
               //watch
               //派一个哨兵 一直监视着 luminghui这个key的变化 (新增、修改、删除)
               ch := cli.Watch(context.Background(), "luminghui")
               //从通道尝试取值(监视的信息)
               for wresp := range ch {
                  for _, evt := range wresp.Events {
                     fmt.Printf("Type:%v key:%v value:%v\n", evt.Type,string(evt.Kv.Key),string(evt.Kv.Value))
                  }
               }
            }

136. SET GO111MODULE=on
    SET GOPROXY=https://goproxy.cn

137. 研发+Kubernetes+Docker
    Kubernetes 又叫 k8s,首字母+中间8个字母+尾字母
138. docker
    -1)部署演变
        --在一台物理机部署Application
        --虚拟化技术
    -2)容器的必要性
        --开发人员开发一个Application需要各种环境,各种依赖
        --运维人员部署Application时也需要搭建各种环境
    -3)容器解决的问题
        --解决了开发和运维之间的矛盾
    -4)容器是什么
        --对软件和其依赖的标准化打包
        --应用之间相互隔离
        --共享同一个OS Kernel
        --可以运行在很多驻留操作系统上
    -5)虚拟机和容器的区别
        虚拟机是物理层民的格式,容器是Application层面的隔离
    -6)docker是什么
        --docker是目前最流行的容器技术的实现
        --2004-2008 年 Linux已经出现了LXC,2013年docker包装了LXC,2013年三月开源,2016年docker分为企业版和社区版
    -7)docker能做什么
        --简化配置
        --提高效率
    -8)docker 和 kubernetes
        --docker可以被k8s管理
        --kubernetes,简称k8s
    -9)DevOps
        --DevOps -- 解决开发和运维间合作和沟通
        --不仅仅依赖docker,还需要版本管理,保持集成等
    -10) docker 的应用

139.docker的镜像和容器
    -1)docker的底层技术实现架构
        --docker提供了打包运行app的平台
        --将app与底层基础设施隔离
    -2)docker engine
        docker engine是核心, 里面有后台进程dockerd,提供了REST API接口,还提供了CLI接口,另外,docker就是一种C/S的架构
    -3)整体架构
        docker 中,images跑起来就是 containers
        代码中写一个student类,跑程序时给student类初始化赋值后,就是一个对象实例了
    -4)底层技术支持
        --NameSpaces: 做网络隔离
        --Control groups: 做资源限制,例如设置占用多少内存,CPU
        --Union file systems: image 和 container 分层
    -5)docker image 概述
        --是文件和meta data的集合
    -6)制作baseImage
        --baseImage基于系统的基础镜像
    -7)container概念和使用
        --container可以理解为运行时的实例,与image不同
    -8)创建Image的两种方式
        --基于image创建container后,如果在container做了一些变化,例如安装了某个软件,可以将这些改变,commit成一个新的image,也可以简写为
            docker commit
        --使用Dokcerfile,通过build制作成image,可以简写为docker build

140. docker命令
    docker image ls -- 查看镜像列表
    docker build -t gochaochao/hello-world  -- 构建 docker-image
    docker run gochaochao/hello-world  -- docker跑个go程序 (gochaochao/hello-world 是镜像)
    docker container ls -- 查看容器
    docker container ls -a - 包含已经运行完的实例
    docker container rm ID --删除之前运行完的实例(或实例)
    docker container ls -aq -- 列出所有的container 的ID (docker rm $(docker container ls -aq) --删除所有的容器 )
    docker container ls -f "status=exited" -- 查询停止的容器
    docker rm $(docker container ls -f "status=exited" -q) -- 删除所有停止的容器(-q 返回容器的ID)
    docker run centos -- 拉取镜像 (运行一个不存在的image,本地没有会先去拉镜像)
    docker ps -- 列出容器
    systemctl restart docker -- 重启docker
    exit  -- 退出容器
    docker run -itd centos -- 镜像后台运行
    docker stop ID -- 停止container








141. Dockerfile文件
    -1)编写Dokcerfile必须是这个文件名,里面关键字尽量大写
    -2)详解
        --1)FROM:文件的开始
            FROM scratch  #从头开始制作一个简单的
            FROM centos   #使用centos作为系统,若没有则拉取
            FROM contos:7.0  #指定系统 + 版本号
        --2)LABEL:相当于注释或者说明信息
            LABEL version="1.0"
            LABEL author="sjc"
        --3)RUN:执行命令,每执行一条RUN,就会多一层
            RUN yum -y update && yum -y install lrzsz
        --4)WORKDIR:进入或创建目录
            WORKDIR/root  #进入/root目录
            WORKDIR/test  #自动创建目录
            WORKDIR demo
            RUN pwd  #/test/demo
        --5)ADD and COPY:将本地文件,添加到镜像里
            ADD可以解压缩文件
            ADD hello
            ADD xxx.tar.gz/ #添加并解压到根目录
            WORKDIR /root/test
            COPY hello . #/root/test/hello
        --6)ENV
            ENV MYSQL_VERSION5.6 #设置常量
            RUN apt-get -y install mysql-server="${MYSQL_VERSION}"

        --7)CMD and ENTRYPOINT
            shell和Exec格式
                #shell
                    RUN apt-get-y install lrzsz
                    CMD echo "hello docker"
                    ENTRYPOINT echo "hello docker"
                #exec格式
                    RUN["apt-get","-y","lrzsz"]
            若docker指定了其他命令,CMD会被忽略
            若定义多个CMD,只会执行最后一个
        --8)分享 docker image
            image 名字一定要以自己 docker hub的用户名开头
            前提 :在docker官网先注册个账号,dockerHub也是用那个账户
            docker image push gochaochao/mycentos 前缀必须是账户名
            docker pull gochaochao/mycentos 从dockerhub 拉取镜像到本地
        --9) 分享Dockerfile
            docker image 不如分享 Dockerfile,更加安全
        --10) Dockerfile案例
            快速搭建stress,是做 linux系统压力测试
        --11) 对容器资源限制
            对内存的限制
                -docker run --memory=200M gochaochao/ubuntu-stress-vm 1 --verbose
            对CPU的限制
                -docker run -cpu-shares=4 -name=test1 gochaochao/ubuntu-stress






142. docker build后面的.
    我们在构建镜像文件时无非是使用:
    docker build -t test/ubuntu:v1 .
    或者
    docker build -f /docker/test/Dockerfile .
    来进行构建镜像 ,用第一个命令时任务. 指代的是当前目录下的dockerfile ,但是第二个命令-f指定了dockerfile的目录所以肯定不是当前目录
    . 是上下文路径
    上下文路径,是指 docker 在构建镜像,有时候想要使用到本机的文件(比如复制),docker build 命令得知这个路径后,会将路径下的所有内容打包。
    原理:由于 docker 的运行模式是 C/S。我们本机是 C,docker 引擎是 S。实际的构建过程是在 docker 引擎下完成的,所以这个时候无法用到我们本机的文件。这就
    需要把我们本机的指定目录下的文件一起打包提供给 docker 引擎使用。
    如果未说明最后一个参数,那么默认上下文路径就是 Dockerfile 所在的位置。
    注意:上下文路径下不要放无用的文件,因为会一起打包发送给 docker 引擎,如果文件过多会造成过程缓慢。

143. yum -y install telnet  (自动应答yes)

144. docker 的网络
    -1)网络分类
        单机
            -Bridge Network
            -Host Network
            -None Network
        多机
            -Overlay Network
    -2) Linux网络命名空间 namespace
        命名空间是docker底层重要的概念
    -3) Bridge详解
        记性多容器通信
    -4)容器通信
        有时写代码时,并不知道要请求的IP地址
    -5)端口映射
        实现外界访问
    -6)网络的none 和 host
        none应用场景:安全性要求极高,存储绝密数据等
        host网络类似于NAT
    -7)多容器部署和应用
        flask做web 服务,redis做自增
    -8)多机器多容器通信
        例如一台做redis,一台做web处理
    -9)overlay网络 和 etcd通信


145. docker Compose 多容器部署
    -1)docker 部署 wordpress
        docker pull wordpress
        docker pull mysql:5.5
        docker tag mysql:5.5 mysql:latest
    -2)docker compose 介绍
        多容器的APP很难部署和管理,docker compose 就类似于批处理
    -3) docker compose 安装和 使用
        安装后需要赋权限
            vi docker-compose.yml
            docker-compose -f docker-compose.yml up # 启动所有容器,-d 将会在后台启动并运行所有的容器,-f  指定使用的 Compose 模板文件,默认为 docker-compose.yml,可以多次指定。
            docker-compose down #停用移除所有容器以及网络相关

146. docker的网络
    -1)linux网络命名空间 namespace
        ip netns list
        ip netns add test1
        ip netns detele test1
    -2)Bridge
        进行多容器通信
            docker network ls
            docker network inspect 5b9e7fs4g6s
    -3)容器通信
        有时写代码时,并不知道要请求的地址 mysql
            docker run -d -name test3 busybox /bin/sh -c "while true;do sleep 3600; done"
            docker exec -it test4 /bin/sh
    -4)端口映射
        实现外界访问
        docker run --name web2 -d -p 80:80 ngxin
    -5)网络的none 和 host
        none应用场景:安全性要求极高,存储绝密数据等
        host 网络类似于NAT
            docker run -d --name test6 --network host busybox /bin/sh -c "while true;do sleep 3600; done"
    -6)多容器部署和应用
        flask做web服务,redis做自增
    -7)多机器多容器通信
        例如一台做redis,一台做web处理
        思路:多机情况下,要保证容器IP不冲突,所以要记录所有IP,使用分布式存储记录
    -8)overlay 网络 和 etcd 通信

147. docker的持久化存储和数据共享
    -1)数据持久化引入
        容器中数据存在丢失的风险
    -2)数据持久化方案
        -基于本地文件系统的Volume
        -基于plugin的 Volume
    -3)Volume的类型
        -受管理的data Volume:由docker后台自动创建
        -绑定挂载的Volume:具体挂载位置可以由用户指定
    -4)数据持久化 - data Volume
        https://hub.docker.com 搜mysql,可以看到官方的Dockerfile中也定义了VOLUME
        -docker run -d --name mysql -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql
        -docker volume ls
        -volume inspect 08...
                --找 "Mountpoint" :"/var/lib/docker"
        -cd /var/lib/docker/volumes
        -ll
        -docker stop mysql1
        -docker rm mysql1
        -docker volume prune
        -docker run -d --name mysql -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql
        数据已经持久化了,并且后续创建指定到同一位置,会自动读取原数据

    -5)数据持久化 - bind mouting
        可以指定一个与容器同步的文件,容器变化,文件同步变化

148. recover 的用法
    实例:
        package main
        import "fmt"
        func testa() {
            fmt.Println("aaaaaaaaaaaaaaaaa")
        }
        func testb(x int) {
            //设置recover,recover只能放在defer后面使用
            defer func() {
                //recover() //可以打印panic的错误信息
                //fmt.Println(recover())
                if err := recover(); err != nil { //产生了panic异常
                    fmt.Println(err)
                }
                fmt.Println("再次执行")

            }() //别忘了(), 调用此匿名函数

            var a [10]int
            a[x] = 111 //当x为20时候,导致数组越界,产生一个panic,导致程序崩溃
            fmt.Println(a)
        }

        func testc() {
            fmt.Println("cccccccccccccccccc")
        }

        func main() {
            testa()
            testb(1) //当值是1的时候,就不会越界,值是20的时候,就会越界报错。 1 - [0 111 0 0 0 0 0 0 0 0]
            testc()
        }

149. golang中的WaitGroup
   程序在使用多协程的时候,协程还没有执行完,程序就退出了。为了避免这个问题,我们使用WaitGroup
   WaitGroup有3个API:
   Add(delta int):增加/减少若干计数       [创建协程时执行]
   Done:减少 1 个计数,等价于 Add(-1)    [协程执行完执行]
   Wait:等待,直到计数等于 0      [多协程末尾执行]
150.知识点汇总
    -1)flag包
        该包提供了一系列解析命令行参数的功能接口
            实例:
                package main
                import (
                  "flag"
                  "fmt"
                )
                //声明变量用于接收命令行传入的参数值
                var (
                  name string
                  age int
                  address *string
                  id *int
                )

                func init() {
                  //通过传入变量地址的方式,绑定命令行参数到string变量
                  flag.StringVar(&name,   //第一个参数:存放值的参数地址
                                 "name",  //第二个参数:命令行参数的名称
                                "匿名",    //第三个参数:命令行不输入时的默认值
                                "您的姓名") //第四个参数:该参数的描述信息,help命令时会显示

                  //通过传入变量地址的方式,绑定命令行参数到int变量
                  flag.IntVar(&age,        //第一个参数:存放值的参数地址
                              "age",       //第二个参数:命令行参数的名称
                              -1,          //第三个参数:命令行不输入时的默认值
                              "您的年龄")   //第四个参数:该参数的描述信息,help命令时会显示

                  //和前面两个变量的获取方式不同,这个api没有传入变量地址,而是把命令行参数值的地址返回了
                  address = flag.String("address", //第一个参数:命令行参数的名称
                                           "未知",     //第二个参数:命令行不输入时的默认值
                                          "您的住址")   //第三个参数:该参数的描述信息,help命令时会显示

                  id = flag.Int("id", //第一个参数:命令行参数的名称
                                           -1,     //第二个参数:命令行不输入时的默认值
                                          "身份ID")   //第三个参数:该参数的描述信息,help命令时会显示
                }

                func main() {
                  //处理入参
                  flag.Parse()
                  //入参已经被赋值给各个变量,可以使用了
                  fmt.Printf("%s您好, 您的年龄:%d, 您的住址:%s, 您的ID:%d\n\n", name, age, *address, *id)
                  fmt.Println("---遍历有输入的参数(开始)---")
                  //Visit方法会遍历有输入的参数,flag.Flag可以将参数的名称、值、默认值、描述等内容取到
                  flag.Visit(func(f *flag.Flag){
                    fmt.Printf("参数名[%s], 参数值[%s], 默认值[%s], 描述信息[%s]\n", f.Name, f.Value, f.DefValue, f.Usage)
                  })
                  fmt.Println("---遍历有输入的参数(结束)---\n")
                  fmt.Println("---遍历所有的参数(开始)---")
                  //VisitAll方法会遍历所有定义的参数(包括没有在命令行输入的),flag.Flag可以将参数的名称、值、默认值、描述等内容取到
                  flag.VisitAll(func(f *flag.Flag){
                    fmt.Printf("参数名[%s], 参数值[%s], 默认值[%s], 描述信息[%s]\n", f.Name, f.Value, f.DefValue, f.Usage)
                  })
                  fmt.Println("---遍历所有的参数(结束)---\n")
                }
                -- 第一种:StringVar和IntVar等方法,第一个参数是变量的地址;
                   第二种:String和Int等方法,将入参的值存入一个变量中,再将此变量的地址作为返回值返回;
    -2)命令行启动 项目 go run main.go

    -3)文件更新 是重新编译再 进行执行吗

    -4)包的使用
        go.uber.org/zap  -- 日志包
        gorm.io/gorm  -- GORM是一个比较流行且功能齐全的orm库
        github.com/spf13/viper -- Viper 是 Go 应用程序的完整配置解决方案,包括 12-Factor 应用程序。它旨在在应用程序中工作,并且可以处理所有类型的配置需求和格式。
        gorm.io/driver/postgres
        go.uber.org/zap/zapcore --
        github.com/robfig/cron  -- 定时任务

151. time.Time time.Duration 的使用
        定义结构体时的类型
            UpdatedAt      time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP" json:"updated_at" form:"updated_at"`
            ConnMaxLifeTime time.Duration `yml:"connMaxLifeTime"`

152.
    fmt.Println(toModel, &toModel,"9999999999")
    &{0      0  0   0 0 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC} 0xc0003ed8a0
        --以上两种经过 err = json.Unmarshal(oriJson, &toModel) / err = json.Unmarshal(oriJson, toModel) 值是一样的
    调用如下:
    err = utils.TransModel(commonParam, Param)

153. var _ TestService = (*testSer) (nil)  -- 这种特殊写法
    实例:
        (*HelloServiceClient)(nil)
        var _ HelloServiceInterface = (*HelloServiceClient)(nil) // 要求客户端接口实现server接口

        -1) 创建一个HelloServiceInterface地址,但不会分配内存的,并且如果给字段赋值会报错。
        -2)在代码中判断HelloServiceClient这个struct是否实现了HelloServiceInterface这个interface。

        a)当没有实现Foo接口say方法时报错
            type Foo interface {
                say()
            }
            type Dog struct {
                Name string
            }
            //func (p Dog) say() {
            //    fmt.PrintLn("java")
            //}

            var _ Foo = (*Dog)(nil)
        b)当实现Foo接口say方法时没有报错
            type Foo interface {
                 say()
            }
            type Dog struct {
                 Name string
           }
           func (p Dog) say() {
              fmt.PrintLn("java")
           }

           var _ Foo = (*Dog)(nil)

154. 接收 一个 值
       var appIdMap map[string]string
           {
               "app_id":"123,456,789"
            }
       err := ctx.ShouldBindJSON(&appIdMap)

155. 分析以下的 acsChannel *chan *accessResponse
    是 通道中元素类型为 accessResponse 的指针的 channel 类型的指针
    func handleAccessLog(acsChannel *chan *accessResponse, channelIndex int) {

}
    --//var b chan int //需要指定通道中元素的类型

156. //记录日志中间件
    func AccessLogMiddleware() gin.HandlerFunc { //gin中间件必须是一个gin.HandlerFunc类型
       accessChannel = make(chan *accessResponse, ap.channelBufferNum)
       fmt.Println("开始goroutien")
       for i := 0; i < int(ap.goroutineHandleNum); i++ {
          go func(i int) {
             handleAccessLog(&accessChannel, i)
          }(i)
       }

    初次执行后, 以上的goroutine就开始跑了, 中间不会停
    //此处没有数据,会报异常,然后继续走handleAccessLog
       defer func() {
          if err := recover(); err != nil {
             go handleAccessLog(acsChannel, channelIndex)
          }
       }()
    --但是执行程序时,却没有再走这个步骤了,不知道是
        r.Use(
              access_log.AccessLogMiddleware(),
              //recover_catch.RecoverCatchMiddleware(),
           )
    中间件的作用,还是其他 的什么原因,后续可跟踪下
157.Kubernetes
    介绍:Kubernetes(k8s)是Google开源的容器集群管理系统,是一个全新的基于容器技术的分布式架构方案,为容器化的应用提供部署运行、资源调度、服务发现和动态伸缩等
        一系列完整功能,提高了大规模容器集群管理的便捷性。
        Kubernetes是一个完备的分布式系统支撑平台,具有完备的集群管理能力,多层次的安全防护和准入机制、透明的服务注册和发现机制、内建只能负载均衡器、强大的故障
        发现和自我修复能力、服务滚动升级和在线扩容能力、可扩展的资源自动调度机制以及多粒度的资源配额管理能力。同时Kubernetes提供完善的管理工具,覆盖了包括开发、
        部署测试、运维监控在内的各个环节。
    Kubernetes特性
        -1)自动装箱
            构建于容器之上,基于资源依赖及其他约束自动完成容器部署且不影响其可用性,并通过调度机制使工作负载于不同节点以提升资源利用率。
        -2)自我修复
            支持容器故障后自动重启,节点故障后重新调度容器,以及其他可用节点,健康状态检查失败后关闭容器并重新创建等自我修复机制。
        -3)水平扩展
            支持通过简单命令或UI手动水平扩展,以及基于CPU等资源负载率的自动水平扩展机制
        -4)服务发现和负载均衡
            Kubernetes通过其附加组件之一的KubeDNS为系统内置了服务发现能力,它会为每个Service配置DNS服务,并允许集群内的客户端直接使用此名称发出访问请求。
        -5)自动发布和回滚
            Kubernetes支持“灰度”更新应用程序或其配置信息,它会监控更新过程中应用程序的健康状态,以确保它不会在同一时刻杀掉所有实例,而此过程中一旦有故障发生,
            就会立即自动执行回滚操作。
        -6)存储编排
            Kubernetes支持Pod对象按需自动挂载不同类型的存储系统,包括节点本地存储、云存储等。
        -7)批量处理执行
            除了服务型应用,Kubernetes还支持批量处理作业,如果需要,一样可以实现容器故障后恢复。

158.Kubernetes集群组件
    一个典型的Kubernetes集群由多个工作节点(worker node) 和 一个主节点(control plane,即Master),以及一个集群状态存储系统(etcd)组成。其中Master节点
    负责整个集群的管理工作,为集群提供管理接口,并监控和编排集群中的各个工作节点。各节点负责以Pod的形式运行容器,因此,各节点需要事先配置好容器运行依赖的所有服务
    和资源,入容器运行时环境等。
    Master组件
        Kubernetes的集群控制平面由多个组件组成。这些组件可统一运行于单一Master节点,也可以多副本的方式同时运行于多个节点,以为Master提供高可用功能,甚至还
        可以运行于Kubernetes集群自身之上。Master主要包含以下几个组件。
           -1)Api Server
            API Server负责输出RESTFUL风格的Kubernetes API,它是发往集群的所有REST操作命令的接入点,并负责接收、校验并响应所有的REST请求,结果状态被
            持久存储于etcd中。因此,API Server是整个集群的网关。
           -2)集群状态存储(Cluster State Store)
             Kubernetes 集群的所有状态信息都需要持久存储于存储系统 etcd 中,不过, etcd 是由 Cores 基于 Raft 协议开发的分布式键值存储,可用于服务发现
             、共享配置以及一致性保障(如数据库主节点选择、分布式锁等)。因此, etcd 是独立的服务组件,并不隶属于 Kubernetes 集群自身。 生产环境中应该以
             etcd 集群的方式运行以确保其服务可用性。
             etcd 不仅能够提供键值数据存储,而且还为其提供了监听( Watch )机制,用于监听和准送变更。Kubernetes 集群系统中, etcd 中的键值发生变化时会
             通知到 APIServer ,并由其通过 watchAPI 向客户端输出。基于 Watch 机制, Kubernetes 集群的各组件实现了高效协同。
           -3)控制器管理器( Controller Manager )
             Kubernetes 中,集群级别的大多数功能都是由几个被称为控制器的进程执行实现的,这几个进程被集成于 kube - controller - manager 守护进程中。
             由控制器完成的功能主要包括生命周期功能和 API 业务逻辑,具体如下。
                -生命周期功能:包括 Namespace 创建和生命周期、 Event 垃圾回收、 Pod 终止相关的垃圾回收、级联垃圾回收及 Node 垃圾回收等。
                -API 业务逻辑:例如,由 ReplicaSet 执行的 Pod 扩展等。
           -4)调度器( Scheduler )
            Kubernetes 是用于部署和管理大规模容器应用的平台,根据集群规模的不同,其托管运行的容器很可能会数以千计甚至更多。
            API Server 确认 Pod 对象的创建请求之后,便需要由 Scheduler 根据集群内各节点的可用资源状态,以及要运行的容器的资源需求做出调度决策。
            另外, Kubernetes 还支持用户自定义调度器。
    Node 组件
        Node 负责提供运行容器的各种依赖环境,并接受 Master 的管理。
        每个 Node 主要由以下几个组件构成。
           -1)Node 的核心代理程序 kubelet
            kubelet 是运行于工作节点之上的守护进程,它从 APIServer 接收关于 Pod 对象的配置信息并确保它们处于期望的状态( desired state )。
            kubelet 会在 APIServer 上注册当前工作节点,定期向 Master 汇报节点资源使用情况,并通过 advisor 监控容器和节点的资源占用状况。
           -2)容器运行时环境
            每个 Node 都要提供一个容器运行时( Container Runtime )环境,它负责下载镜像并运行容器。
            kubelet 并未固定链接至某容器运行时环境,而是以插件的方式载入配置的容器环境。
            这种方式清晰地定义了各组件的边界。 目前, Kubernetes 支持的容器运行环境至少包括 Docker 、 RKT 、 cri - o 和 Fraki 等。
           -3)kube-proxy
            每个工作节点都需要运行一个 kube-proxy 守护进程,它能够按需为 Service 资源对象生成 iptables 或 ipvs 规则,从而捕获访问当前 Service 的 Cluster 的
            流量并将其转发至正确的后端Pod对象。
    Kubernetes的核心对象
        Kubernetes集群将所有节点上的资源都整合到一个大的虚拟资源池里,以代替一个个单独的服务器。如果把集群类比为一台传统的服务器,那么Kubernetes(Master)就好比是操作
        系统内核,其主要职责在于抽象资源并调度任务,而Pod资源对象就是那些运行于用户空间中的进程。
           -1)Pod资源对象
                Pod资源对象是一种集合了一到多个应用容器,存储资源,专用IP及支撑容器运行的其他选项的逻辑组件。换言之,Pod代表着Kubernetes的部署单元及原子运行单元,即一个应
                用程序的单一运行实例。
                一个Pod对象代表某个应用程序的一个特定实例,如果需要扩展应用程序,则意味着为此应用程序同时创建多个Pod实例,每个实例均代表应用程序的一个运行的副本(replica)。
                这些副本化的的Pod对象的创建和管理通常由另一组称之为"控制器"(Controller)的对象实现,例如,Deployment控制器对象。
                基于期望的目标状态和各节点的资源可用性,Master会将Pod对象调度至某选定的工作节点运行,工作节点于指向的镜像仓库(Image registry)下载镜像,并与本地的容器运行
                时环境中启动容器。Master会将整个集群的状态保存于etcd中,并通过API Server共享给集群的各组件及客户端。
                Kubernetes引入Pod主要
                Kubernetes引入Pod主要基于下面两个目的:
                    -1)可管理性
                        有些容器天生就是需要紧密联系,一起工作。Pod提供了比容器更高层次的抽象,将它们封装到一个部署单元中。Kubernetes以Pod为最小单元进行调度、扩展、
                        共享资源、管理生命周期。
                    -2)通信和资源共享
                        Pod中的所有容器使用同一网络namespace,即相同的IP地址和Port空间。它们可以直接用localhost通信。同样的,这些容器可以共享存储,当kubernetes
                        挂载volume到Pod,本质上是将volume挂载到Pod中的每一个容器。
                Pods有两种使用方式:
                    -1)运行单一容器。one-container-per-Pod 是Kubernetes最常见的模型,这种情况下,只是将单个容器简单封装成Pod。即便是只有一个容器,Kubernetes管理
                    的也是Pod而不是直接管理容器。
                    -2)运行多个容器。但问题在于:哪些容器应该放到一个Pod中?答案是:这些容器联系必须非常紧密,而且需要直接共享资源。
            -2)Controller
                Controller Manager作为集群内部的管理控制中心,负责集群内的Node、Pod副本、服务端点(Endpoint)、命名空间(NameSpace)、服务账号(ServiceAccout)/
                资源定额(ResourceQuota)的管理,当某个Node意外宕机时,Controller Manage会及时发现并执行自动化修复流程,确保集群始终处于预期的工作状态。
            -3)Service
                Service定义了一个服务的访问入口地址,前端的应用通过这个入口地址访问其背后的一组由Pod副本组成的集群实例,来自外部的访问请求被负载均衡到后端的各个容器应用
                上。Service与其后端Pod副本集群之间则是通过Label Selector 来实现对接的。Service定义可以基于Post方式,请求apiserver创建新的实例。一个Service
                在Kubernetes中是一个REST对象。