1 介绍

1.1 主要特征

01.简介
    a.来历
        很久以前,有一个IT公司,这公司有个传统,允许员工拥有20%自由时间来开发实验性项目。
        在2007的某一天,公司的几个大牛,正在用c++开发一些比较繁琐但是核心的工作,主要包括庞大的分布式集群,
        大牛觉得很闹心,后来c++委员会来他们公司演讲,说c++将要添加大概35种新特性。
        这几个大牛的其中一个人,名为:Rob Pike,听后心中一万个xxx飘过,“c++特性还不够多吗?简化c++应该更有成就感吧”。
        于是乎,Rob Pike和其他几个大牛讨论了一下,怎么解决这个问题,
        过了一会,Rob Pike说要不我们自己搞个语言吧,名字叫“go”,非常简短,容易拼写。
        其他几位大牛就说好啊,然后他们找了块白板,在上面写下希望能有哪些功能(详见文尾)。
        接下来的时间里,大牛们开心的讨论设计这门语言的特性,经过漫长的岁月,
        他们决定,以c语言为原型,以及借鉴其他语言的一些特性,来解放程序员,解放自己,然后在2009年,go语言诞生。
    b.思想
        Less can be more 大道至简,小而蕴真 让事情变得复杂很容易,让事情变得简单才难 深刻的工程文化
    c.优点
        自带gc。
        静态编译,编译好后,扔服务器直接运行。
        简单的思想,【没有继承,多态,类】等。
        丰富的库和详细的开发文档。
        语法层支持并发,和拥有同步并发的channel类型,使并发开发变得非常方便。
        简洁的语法,提高开发效率,同时提高代码的阅读性和可维护性。
        超级简单的交叉编译,仅需更改环境变量。
        -----------------------------------------------------------------------------------------------------
        Go 语言是谷歌 2009 年首次推出并在 2012 年正式发布的一种全新的编程语言,
        可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:
        我们之所以开发 Go,是因为过去10多年间软件开发的难度令人沮丧。Google 对 Go 寄予厚望,
        其设计是让软件充分发挥多核心处理器同步多工的优点,并可解决面向对象程序设计的麻烦。
        它具有现代的程序语言特色,如垃圾回收,帮助开发者处理琐碎但重要的内存管理问题。
        Go 的速度也非常快,几乎和 C 或 C++ 程序一样快,且能够快速开发应用程序。
    d.Go语言的主要特征:
        1.自动立即回收。
        2.更丰富的内置类型。
        3.函数多返回值。
        4.错误处理。
        5.匿名函数和闭包。
        6.类型和接口。
        7.并发编程。
        8.反射。
        9.语言交互性。
    e.Golang文件名:
        所有的go源码都是以 ".go" 结尾
    f.Golang命令
        go env用于打印Go语言的环境信息。
        go run命令可以编译并运行命令源码文件。
        go get可以根据要求和实际情况从互联网上下载或更新指定的代码包及其依赖包,并对它们进行编译和安装。
        go build命令用于编译我们指定的源码文件或代码包以及它们的依赖包。
        go install用于编译并安装指定的代码包及它们的依赖包。
        go clean命令会删除掉执行其它命令时产生的一些文件和目录。
        go doc命令可以打印附于Go语言程序实体上的文档。我们可以通过把程序实体的标识符作为该命令的参数来达到查看其文档的目的。
        go test命令用于对Go语言编写的程序进行测试。
        go list命令的作用是列出指定的代码包的信息。
        go fix会把指定代码包的所有Go语言源码文件中的旧版本代码修正为新版本的代码。
        go vet是一个用于检查Go语言源码中静态错误的简单工具。
        go tool pprof命令来交互式的访问概要文件的内容。

02.Go语言命名
    a.Go的函数、变量、常量、自定义类型、包(package)的命名方式遵循以下规则:
        1)首字符可以是任意的Unicode字符或者下划线
        2)剩余字符可以是Unicode字符、下划线、数字
        3)字符长度不限
    b.Go只有25个关键字
        break        default      func         interface    select
        case         defer        go           map          struct
        chan         else         goto         package      switch
        const        fallthrough  if           range        type
        continue     for          import       return       var
    c.Go还有37个保留字
        Constants:    true  false  iota  nil
        Types:    int  int8  int16  int32  int64
                  uint  uint8  uint16  uint32  uint64  uintptr
                  float32  float64  complex128  complex64
                  bool  byte  rune  string  error
        Functions:   make  len  cap  new  append  copy  close  delete
                     complex  real  imag
                     panic  recover
    d.可见性:
        1)声明在函数内部,是函数的本地值,类似private
        2)声明在函数外部,是对当前包可见(包内所有.go文件都可见)的全局值,类似protect
        3)声明在函数外部且首字母大写是所有包可见的全局值,类似public
    e.Go语言声明:
        有四种主要声明方式:
        var(声明变量), const(声明常量), type(声明类型) ,func(声明函数)。
        Go的程序是保存在多个.go文件中,文件的第一行就是package XXX声明,用来说明该文件属于哪个包(package),
        package声明下来就是import声明,再下来是类型,变量,常量,函数的声明。
    f.语法注意事项
        (1)源文件以"go"为扩展名。
        (2)程序的执行入口是main()函数。
        (3)严格区分大小写。
        (4)方法由一条条语句构成,每个语句后不需要分号(Go语言会在每行后自动加分号),这也体现出Golang的简洁性。
        (5)Go编译器是一行行进行编译的,因此我们一行就写一条语句,不能把多条语句写在同一个,否则报错
        (6)定义的变量或者import的包如果没有使用到,代码不能编译通过。
        (7)大括号都是成对出现的,缺一不可

03.init函数和main函数
    a.init函数
        go语言中init函数用于包(package)的初始化,该函数是go语言的一个重要特性。
        有下面的特征:
        1.init函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等
        2.每个包可以拥有多个init函数
        3.包的每个源文件也可以拥有多个init函数
        4.同一个包中多个init函数的执行顺序go语言没有明确的定义(说明)
        5.不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序
        6.init函数不能被其他函数调用,而是在main函数执行之前,自动被调用
    b.main函数
        Go语言程序的默认入口函数(主函数):func main()
        函数体用{}一对括号包裹。
        func main(){
            //函数体
        }
    c.init函数和main函数的异同
        相同点:
            两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。
        不同点:
            init可以应用于任意包中,且可以重复定义多个。
            main函数只能用于main包中,且只能定义一个。
    d.两个函数的执行顺序:
        对同一个go文件的init()调用顺序是从上到下的。
        对同一个package中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init()函数。
        对于不同的package,如果不相互依赖的话,按照main包中"先import的后调用"的顺序调用其包中的init(),如果package存在依赖,则先调用最早被依赖的package中的init(),最后调用main函数。
        如果init函数中使用了println()或者print()你会发现在执行过程中这两个不会按照你想象中的顺序执行。这两个函数官方只推荐在测试环境中使用,对于正式环境不要使用。

04.输入、输出、缓冲
    a.示例
        print("This is a print demo.")                                                                                 print 直接打印字符串,没有自动换行
        println("This is a println demo.")                                                                             println 打印字符串,并自动换行
        fmt.Println("This is a fmt.Println demo.")                                                                     fmt.Println 打印字符串,并自动换行
        fmt.Printf("Hello, %s! You have %d new messages.\n", "Alice", 5)                                               fmt.Printf 格式化并打印字符串
        message := fmt.Sprintf("Hello, %s!", "Alice")                                                                  fmt.Sprintf 格式化字符串并返回,不输出
        fmt.Fprint(os.Stdout, "This is a fmt.Fprint demo.\n")                                                          fmt.Fprint 打印到指定的 io.Writer,例如 os.Stdout
        fmt.Fprintf(os.Stdout, "Hello, %s!\n", "Alice")                                                                fmt.Fprintf 格式化并打印到指定的 io.Writer
        fmt.Fprintln(os.Stdout, "This is a fmt.Fprintln demo.")                                                        fmt.Fprintln 打印到指定的 io.Writer 并自动换行
        log.Print("This is a log.Print demo.")                                                                         log.Print 打印日志信息
        log.Println("This is a log.Println demo.")                                                                     log.Println 打印日志信息并自动换行
        log.Printf("Hello, %s!", "Alice")                                                                              log.Printf 格式化并打印日志信息
        os.Stdout.Write([]byte("This is an os.Stdout.Write demo.\n"))                                                  os.Stdout.Write 直接写入字节数组到标准输出
        io.WriteString(os.Stdout, "This is an io.WriteString demo.\n")                                                 io.WriteString 写入字符串到指定的 io.Writer
        writer := bufio.NewWriter(os.Stdout) writer.WriteString("This is a buffered write demo.\n") writer.Flush()     使用 bufio.NewWriter 对输出进行缓冲
    b.标准
        在os包下有三个外暴露的文件描述符,其类型都是*os.File,分别是:
        Stdin - 标准输入
        Stdout - 标准输出
        Stderr - 标准错误
        Go中的控制台输入输出都离不开它们。
    c.输出
        输出一句Hello 世界!,比较常用的有三种方法,第一种是调用os.Stdout
        os.Stdout.WriteString("Hello 世界!")

        第二种是使用内置函数println
        println("Hello 世界!")

        第三种也是最推荐的一种就是调用fmt包下的Println函数
        fmt.Println("Hello 世界!")
        fmt.Println会用到反射,因此输出的内容通常更容易使人阅读,不过性能很差强人意。
    d.输入
        需要注意的是,Go中输入的默认分隔符号是空格

        // 扫描从os.Stdin读入的文本,根据空格分隔,换行也被当作空格
        func Scan(a ...any) (n int, err error)

        // 与Scan类似,但是遇到换行停止扫描
        func Scanln(a ...any) (n int, err error)

        // 根据格式化的字符串扫描
        func Scanf(format string, a ...any) (n int, err error)
    e.缓冲
        输入
        func main() {
           // 读
           scanner := bufio.NewScanner(os.Stdin)
           scanner.Scan()
           fmt.Println(scanner.Text())
        }
        -----------------------------------------------------------------------------------------------------
        输出
        func main() {
           // 写
           writer := bufio.NewWriter(os.Stdout)
           writer.WriteString("hello world!\n")
           writer.Flush()
           fmt.Println(writer.Buffered())
        }
    f.格式化
        0   格式化 描述                                       接收类型
        1   %%     输出百分号%                                任意类型
        2   %s     输出string/[] byte值                       string,[] byte
        3   %q     格式化字符串,输出的字符串两端有双引号""   string,[] byte
        4   %d     输出十进制整型值                           整型类型
        5   %f     输出浮点数                                 浮点类型
        6   %e     输出科学计数法形式 ,也可以用于复数         浮点类型
        7   %E     与%e相同                                   浮点类型
        8   %g     根据实际情况判断输出%f或者%e,会去掉多余的0 浮点类型
        9   %b     输出整型的二进制表现形式                   数字类型
        10  %#b    输出二进制完整的表现形式                   数字类型
        11  %o     输出整型的八进制表示                       整型
        12  %#o    输出整型的完整八进制表示                   整型
        13  %x     输出整型的小写十六进制表示                 数字类型
        14  %#x    输出整型的完整小写十六进制表示             数字类型
        15  %X     输出整型的大写十六进制表示                 数字类型
        16  %#X    输出整型的完整大写十六进制表示             数字类型
        17  %v     输出值原本的形式,多用于数据结构的输出     任意类型
        18  %+v    输出结构体时将加上字段名                   任意类型
        19  %#v    输出完整Go语法格式的值                     任意类型
        20  %t     输出布尔值                                 布尔类型
        21  %T     输出值对应的Go语言类型值                   任意类型
        22  %c     输出Unicode码对应的字符                    int32
        23  %U     输出字符对应的Unicode码                    rune,byte
        24  %p     输出指针所指向的地址                       指针类型

05.内置类型、内置函数、内置接口error
    a.内置类型
        a.值类型
            bool
            int(32 or 64), int8, int16, int32, int64
            uint(32 or 64), uint8(byte), uint16, uint32, uint64
            float32, float64
            string
            complex64, complex128
            array    -- 固定长度的数组
        b.引用类型:(指针类型)
            slice   -- 序列数组(最常用)
            map     -- 映射
            chan    -- 管道
    b.内置函数
        Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。
        它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,
        例如:panic。因此,它们需要直接获得编译器的支持。
        ---------------------------------------------------------------------------------------------------------
        append          -- 用来追加元素到数组、slice中,返回修改后的数组、slice
        close           -- 主要用来关闭channel
        delete            -- 从map中删除key对应的value
        panic            -- 停止常规的goroutine  (panic和recover:用来做错误处理)
        recover         -- 允许程序定义goroutine的panic动作
        imag            -- 返回complex的实部   (complex、real imag:用于创建和操作复数)
        real            -- 返回complex的虚部
        make            -- 用来分配内存,返回Type本身(只能应用于slice, map, channel)
        new                -- 用来分配内存,主要用来分配值类型,比如int、struct。返回指向Type的指针
        cap                -- capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
        copy            -- 用于复制和连接slice,返回复制的数目
        len                -- 来求长度,比如string、array、slice、map、channel ,返回长度
        print、println     -- 底层打印函数,在部署环境中建议使用 fmt 包
    c.内置接口error
        //只要实现了Error()函数,返回值为String的都实现了err接口
        type error interface {
            Error()    String
        }

06.下划线:“_”是特殊标识符,用来忽略结果
    a.下划线在import中
        在Golang里,import的作用是导入其他package。
        -----------------------------------------------------------------------------------------------------
       import 下划线(如:import hello/imp)的作用:
        当导入一个包时,该包下的文件里所有init()函数都会被执行,然而,有些时候我们并不需要把整个包都导入进来,
        仅仅是是希望它执行init()函数而已。这个时候就可以使用 import 引用该包。
        即使用【import _ 包路径】只是引用该包,仅仅是为了调用init()函数,所以无法通过包名来调用包中的其他函数。
        -----------------------------------------------------------------------------------------------------
        示例:
        代码结构
            src
            |
            +--- main.go
            |
            +--- hello
                   |
                    +--- hello.go
        -----------------------------------------------------------------------------------------------------
        package main

        import _ "./hello"

        func main() {
            // hello.Print()
            //编译报错:./main.go:6:5: undefined: hello
        }
        -----------------------------------------------------------------------------------------------------
        package hello

        import "fmt"

        func init() {
            fmt.Println("imp-init() come here.")
        }

        func Print() {
            fmt.Println("Hello!")
        }
        -----------------------------------------------------------------------------------------------------
        输出结果:
        imp-init() come here.
    b.下划线在代码中
        a.示例
            package main

            import (
                "os"
            )

            func main() {
                buf := make([]byte, 1024)
                f, _ := os.Open("/Users/***/Desktop/text.txt")
                defer f.Close()
                for {
                    n, _ := f.Read(buf)
                    if n == 0 {
                        break

                    }
                    os.Stdout.Write(buf[:n])
                }
            }
        b.解释1
            下划线意思是忽略这个变量.
            比如os.Open,返回值为*os.File,error
            普通写法是f,err := os.Open("xxxxxxx")
            如果此时不需要知道返回的错误值
            就可以用f, _ := os.Open("xxxxxx")
            如此则忽略了error变量
        c.解释2
            占位符,意思是那个位置本应赋给某个值,但是咱们不需要这个值。
            所以就把该值赋给下划线,意思是丢掉不要。
            这样编译器可以更好的优化,任何类型的单个值都可以丢给下划线。
            这种情况是占位用的,方法返回两个结果,而你只想要一个结果。
            那另一个就用 "_" 占位,而如果用变量的话,不使用,编译器是会报错的。
        d.补充:
            import "database/sql"
            import _ "github.com/go-sql-driver/mysql"
            第二个import就是不直接使用mysql包,只是执行一下这个包的init函数,把mysql的驱动注册到sql包里,然后程序里就可以使用sql包来访问mysql数据库了。

07.基础语法
    a.包
        a.介绍
            在Go中完全禁止循环导入,不管是直接的还是间接的。例如包A导入了包B,包B也导入了包A,这是直接循环导入,
            包A导入了包C,包C导入了包B,包B又导入了包A,这就是间接的循环导入,存在循环导入的话将会无法通过编译。
        b.导入
            例如创建一个example包,包下有如下函数
            package example

            import "fmt"

            func SayHello() {
               fmt.Println("Hello")
            }

            在main函数中调用
            package main

            import "example"

            func main() {
               example.SayHello()
            }
            -------------------------------------------------------------------------------------------------
            还可以给包起别名
            package main

            import e "example"

            func main() {
               e.SayHello()
            }
            -------------------------------------------------------------------------------------------------
            批量导入时,可以使用括号()来表示
            package main

            import (
               "fmt"
               "math"
            )

            func main() {
               fmt.Println(math.MaxInt64)
            }
            -------------------------------------------------------------------------------------------------
            或者说只导入不调用,通常这么做是为了调用该包下的init函数
            package main

            import (
               "fmt"
                _ "math" // 下划线表示匿名导入
            )

            func main() {
               fmt.Println(1)
            }
        c.导出
            在Go中,导出和访问控制是通过命名来进行实现的,如果想要对外暴露一个函数或者一个变量,
            只需要将其名称首字母大写即可,例如example包下的SayHello函数。
            package example

            import "fmt"

            // 首字母大写,可以被包外访问
            func SayHello() {
               fmt.Println("Hello")
            }
            -------------------------------------------------------------------------------------------------
            如果想要不对外暴露的话,只需将名称首字母改为小写即可,例如下方代码
            package example

            import "fmt"

            // 首字母小写,外界无法访问
            func sayHello() {
               fmt.Println("Hello")
            }
            -------------------------------------------------------------------------------------------------
            对外暴露的函数和变量可以被包外的调用者导入和访问,如果是不对外暴露的话,那么仅包内的调用者可以访问,
            外部将无法导入和访问,该规则适用于整个Go语言,例如后续会学到的结构体及其字段,方法,自定义类型,接口等等。
        d.内部包
            go中约定,一个包内名为internal 包为内部包,外部包将无法访问内部包中的任何内容,否则的话编译不通过,下面看一个例子。
            /home/user/go/
                src/
                    crash/
                        bang/              (go code in package bang)
                            b.go
                    foo/                   (go code in package foo)
                        f.go
                        bar/               (go code in package bar)
                            x.go
                        internal/
                            baz/           (go code in package baz)
                                z.go
                        quux/              (go code in package main)
                            y.go
            由文件结构中可知,crash包无法访问baz包中的类型。
    b.标识符
        标识符就是一个名称,用于包命名,函数命名,变量命名等等,命名规则如下:
        只能由字母,数字,下划线组成
        只能以字母和下划线开头
        严格区分大小写
        不能与任何已存在的标识符重复,即包内唯一的存在
        不能与Go任何内置的关键字冲突
        -----------------------------------------------------------------------------------------------------
        下方列出所有的内置关键字
        break        default      func         interface    select
        case         defer        go           map          struct
        chan         else         goto         package      switch
        const        fallthrough  if           range        type
        continue     for          import       return       var
    c.运算符
        Precedence    Operator
        5             *  /  %  <<  >>  &  &^
        4             +  -  |  ^
        3             ==  !=  <  <=  >  >=
        2             &&
        1             ||
        -----------------------------------------------------------------------------------------------------
        有一点需要稍微注意下,go语言中没有选择将~作为取反运算符,而是复用了^符号,当两个数字使用^时,
        例如a^b,它就是异或运算符,只对一个数字使用时,例如^a,那么它就是取反运算符。go也支持增强赋值运算符,如下。
        a += 1
        a /= 2
        a &^= 2
        -----------------------------------------------------------------------------------------------------
        Go语言中没有自增与自减运算符,它们被降级为了语句statement,并且规定了只能位于操作数的后方,所以不用再去纠结i++和++i这样的问题。
        a++ // 正确
        ++a // 错误
        a-- // 正确
        还有一点就是,它们不再具有返回值,因此a = b++这类语句的写法是错误的。
    d.风格
        在Go中所有的花括号都不应该换行。
        // 正确示例
        func main() {
            fmt.Println("Hello 世界!")
        }

        // 错误示例
        func main()
        {
            fmt.Println("Hello 世界!")
        }
        -----------------------------------------------------------------------------------------------------
        花括号,花括号在任何时候都不能够省略,就算是只有一行代码
        // 正确示例
        if a > b {
            a++
        }
        // 错误示例
        if a > b a++

1.2 数据类型1

01.值类型
    值类型是直接存储数据的类型。当你将一个值类型变量赋值给另一个变量时,实际上是将数据的副本复制过去。值类型包括:
    a.基本数据类型:
        布尔型:bool:默认值:false
        整数类型:int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64、uintptr:默认值:0
        浮点型:float32、float64:默认值:0.0
        复数类型:complex64、complex128:默认值:0 + 0i
        字符型:rune(等同于 int32)、byte(等同于 uint8):默认值:0(对应 ASCII 字符 '\x00')
    b.其他值类型:
        结构体:struct
        数组:[n]T(其中 n 为数组长度,T 为元素类型)

02.引用类型
    引用类型是存储数据的引用(地址),而不是实际的数据。当你将一个引用类型变量赋值给另一个变量时,实际上是将引用的地址复制过去,两个变量指向同一块内存区域。引用类型包括:
    切片:[]T:默认值:nil(即空切片)
    字典:map[K]V:默认值:nil(即空字典)
    通道:chan T:默认值:nil(即空通道)
    指针:*T:默认值:nil(即空指针)

03.派生类型
    类型       例子
    数组       [5]int,长度为5的整型数组
    切片       []float64,64位浮点数切片
    映射表     map[string]int,键为字符串类型,值为整型的映射表
    结构体     type Gopher struct{},Gopher结构体
    指针       *int,一个整型指针。
    函数       type f func(),一个没有参数,没有返回值的函数类型
    接口       type Gopher interface{},Gopher接口
    通道       chan int,整型通道

04.零值
    官方文档中零值称为zero value,零值并不仅仅只是字面上的数字零,而是一个类型的空值或者说默认值更为准确。
    ---------------------------------------------------------------------------------------------------------
    类型                                  零值
    数字类型                              0
    布尔类型                              false
    字符串类型                            ""
    数组                                  固定长度的对应类型的零值集合
    结构体                                内部字段都是零值的结构体
    切片,映射表,函数,接口,通道,指针    nil

05.nil
    源代码中的nil,可以看出nil仅仅只是一个变量。
    ---------------------------------------------------------------------------------------------------------
    var nil Type
    Go中的nil并不等同于其他语言的null,nil仅仅只是一些类型的零值,并且不属于任何类型,所以nil == nil这样的语句是无法通过编译的。

1.3 数据类型2

00.汇总
    a.说明
        Golang 更明确的数字类型命名,支持 Unicode,支持常用数据结构。
        -----------------------------------------------------------------------------------------------------
        支持八进制、 六进制,以及科学记数法。标准库 math 定义了各数字类型取值范围。
        a, b, c, d := 071, 0x1F, 1e9, math.MinInt16
        -----------------------------------------------------------------------------------------------------
        空指针值 nil,而非C/C++ NULL
    b.图示
        类型            | 长度(字节) | 默认值 | 说明
        |---------------|-----------|------- |------------------------------------
        | bool          | 1         | false  |
        | byte          | 1         | 0      | uint8
        | rune          | 4         | 0      | Unicode Code Point, int32
        | int, uint     | 4或8      | 0      |  32 或 64 位
        | int8, uint8   | 1         | 0      | -128 ~ 127, 0 ~ 255,byte是uint8 的别名
        | int16, uint16 | 2         | 0      | -32768 ~ 32767, 0 ~ 65535
        | int32, uint32 | 4         | 0      | -21亿~ 21亿, 0 ~ 42亿,rune是int32 的别名
        | int64, uint64 | 8         | 0      |
        | float32       | 4         | 0.0    |
        | float64       | 8         | 0.0    |
        | complex64     | 8         |        |
        | complex128    | 16        |        |
        | uintptr       | 4或8      |        | 以存储指针的 uint32 或 uint64 整数
        | array         |           |        | 值类型
        | struct        |           |        | 值类型
        | string        |           | ""     | UTF-8 字符串
        | slice         |           | nil    | 引用类型
        | map           |           | nil    | 引用类型
        | channel       |           | nil    | 引用类型
        | interface     |           | nil    | 接口
        | function      |           | nil    | 函数

01.整型
    整型分为以下两个大类:
    按长度分为:int8、int16、int32、int64对应的无符号整型:uint8、uint16、uint32、uint64
    其中,uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型

02.浮点型
    Go语言支持两种浮点型数:float32和float64。
    这两种浮点型数据格式遵循IEEE 754标准:float32 的浮点数的最大范围约为3.4e38,可以使用常量定义:math.MaxFloat32
    float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64

03.复数
    complex64和complex128
    复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部和虚部为64位。

04.布尔值
    Go语言中以bool类型进行声明布尔型数据,布尔型数据只有true(真)和false(假)两个值。
    注意:
    布尔类型变量的默认值为false。
    Go 语言中不允许将整型强制转换为布尔型.
    布尔型无法参与数值运算,也无法与其他类型进行转换。

05.字符串
    Go语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64 等)一样。
    Go 语言里的字符串的内部实现使用UTF-8编码。
    字符串的值为双引号(")中的内容,可以在Go语言的源码中直接添加非ASCII码字符,例如:
    s1 := "hello"
    s2 := "你好"

06.字符串转义符
    Go语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示。
    转义  含义
    \r    回车符(返回行首)
    \n    换行符(直接跳到下一行的同列位置)
    \t    制表符
    \'    单引号
    \"    双引号
    \     反斜杠

07.多行字符串
    Go语言中要定义一个多行字符串时,就必须使用反引号字符:
    ---------------------------------------------------------------------------------------------------------
    s1 := `第一行
    第二行
    第三行
    `
    fmt.Println(s1)
    ---------------------------------------------------------------------------------------------------------
    反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。

08.字符串的常用操作
    方法                                    介绍
    len(str)                                求长度
    +或fmt.Sprintf                          拼接字符串
    strings.Split                           分割
    strings.Contains                        判断是否包含
    strings.HasPrefix, strings.HasSuffix    前缀/后缀判断
    strings.Index(), strings.LastIndex()    子串出现的位置
    strings.Join(a[]string, sep string)     join操作

09.byte和rune类型
    组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(’)包裹起来,如:
    var a := '中'
    var b := 'x'
    ---------------------------------------------------------------------------------------------------------
    Go 语言的字符有以下两种:
    uint8类型,或者叫 byte 型,代表了ASCII码的一个字符。
    rune类型,代表一个 UTF-8字符。
    ---------------------------------------------------------------------------------------------------------
    当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32。
    Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode的文本处理更为方便,也可以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾
    // 遍历字符串
    func traversalString() {
        s := "pprof.cn博客"
        for i := 0; i < len(s); i++ { //byte
            fmt.Printf("%v(%c) ", s[i], s[i])
        }
        fmt.Println()
        for _, r := range s { //rune
            fmt.Printf("%v(%c) ", r, r)
        }
        fmt.Println()
    }
    输出:
    112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 229(å) 141() 154() 229(å) 174(®) 162(¢)
    112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 21338(博) 23458(客)
    ---------------------------------------------------------------------------------------------------------
    因为UTF8编码下一个中文汉字由3~4个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的字符串,否则就会出现上面输出中第一行的结果。
    字符串底层是一个byte数组,所以可以和[]byte类型相互转换。字符串是不能修改的 字符串是由byte字节组成,所以字符串的长度是byte字节的长度。 rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成。

10.修改字符串
    要修改字符串,需要先将其转换成[]rune或[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。
    func changeString() {
        s1 := "hello"
        // 强制类型转换
        byteS1 := []byte(s1)
        byteS1[0] = 'H'
        fmt.Println(string(byteS1))

        s2 := "博客"
        runeS2 := []rune(s2)
        runeS2[0] = '狗'
        fmt.Println(string(runeS2))
    }

11.类型转换
    Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。
    ---------------------------------------------------------------------------------------------------------
    强制类型转换的基本语法如下:
    T(表达式)
    ---------------------------------------------------------------------------------------------------------
    其中,T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等.
    比如计算直角三角形的斜边长时使用math包的Sqrt()函数,该函数接收的是float64类型的参数,而变量a和b都是int类型的,这个时候就需要将a和b强制类型转换为float64类型。
    func sqrtDemo() {
        var a, b = 3, 4
        var c int
        // math.Sqrt()接收的参数是float64类型,需要强制转换
        c = int(math.Sqrt(float64(a*a + b*b)))
        fmt.Println(c)
    }

1.4 变量和常量

00.变量和常量
    a.常量
        常量的值无法在运行时改变,一旦赋值过后就无法修改,其值只能来源于:
        字面量
        其他常量标识符
        常量表达式
        结果是常量的类型转换
        iota
        -----------------------------------------------------------------------------------------------------
        常量只能是基本数据类型,不能是
        除基本类型以外的其它类型,如结构体,接口,切片,数组等
        函数的返回值
        -----------------------------------------------------------------------------------------------------
        常量的值无法被修改,否则无法通过编译
        -----------------------------------------------------------------------------------------------------
        一般在Go中,都是通过自定义类型 + const + iota来实现【枚举】,通过官方工具Stringer来自动生成枚举
    a.变量
        a.声明
            a.常见
                变量是用于保存一个值的存储位置,允许其存储的值在运行时动态的变化。每声明一个变量,都会为其分配一块内存以存储对应类型的值
                ---------------------------------------------------------------------------------------------
                赋值会用到运算符=,例如
                var name string
                name = "jack"
                ---------------------------------------------------------------------------------------------
                也可以声明的时候直接赋值
                var name string = "jack"
                ---------------------------------------------------------------------------------------------
                或者这样也可以
                var name string
                var age int
                name, age = "jack", 1
                ---------------------------------------------------------------------------------------------
                第二种方式每次都要指定类型,可以使用官方提供的语法糖:短变量初始化,可以省略掉var关键字和后置类型,具体是什么类型交给编译器自行推断。
                name := "jack" // 字符串类型的变量。
                ---------------------------------------------------------------------------------------------
                虽然可以不用指定类型,但是在后续赋值时,类型必须保持一致,下面这种代码无法通过编译。
                a := 1
                a = "1"
                ---------------------------------------------------------------------------------------------
                还需要注意的是,短变量初始化不能使用nil,因为nil不属于任何类型,编译器无法推断其类型。
                name := nil // 无法通过编译
                ---------------------------------------------------------------------------------------------
                短变量声明可以批量初始化
                name, age := "jack", 1
            b.常见2
                短变量声明方式无法对一个已存在的变量使用,比如
                // 错误示例
                var a int
                a := 1

                // 错误示例
                a := 1
                a := 2
                但是有一种情况除外,那就是在赋值旧变量的同时声明一个新的变量,比如
                a := 1
                a, b := 2, 2
                这种代码是可以通过编译的,变量a被重新赋值,而b是新声明的。
                ---------------------------------------------------------------------------------------------
                在go语言中,有一个规则,那就是所有在函数中的变量都必须要被使用,比如下面的代码只是声明了变量,但没有使用它
                func main() {
                    a := 1
                }
                那么在编译时就会报错,提示你这个变量声明了但没有使用
                a declared and not used
                这个规则仅适用于函数内的变量,对于函数外的包级变量则没有这个限制,下面这个代码就可以通过编译。
                var a = 1
                func main() {

                }
        b.交换
            在Go中,如果想要交换两个变量的值,不需要使用指针,可以使用赋值运算符直接进行交换,语法上看起来非常直观,例子如下
            num1, num2 := 25, 36
            nam1, num2 = num2, num1
            -------------------------------------------------------------------------------------------------
            三个变量也是同样如此
            num1, num2, num3 := 25, 36, 49
            nam1, num2, num3  = num3, num2, num1
            -------------------------------------------------------------------------------------------------
            由于在函数内部存在未使用的变量会无法通过编译,但有些变量又确实用不到,这个时候就可以使用匿名变量_,使用_来表示该变量可以忽略,例如
            a, b, _ := 1, 2, 3
        c.比较
            变量之间的比较有一个大前提,那就是它们之间的类型必须相同,go语言中不存在隐式类型转换,像下面这样的代码是无法通过编译的
            func main() {
                var a uint64
                var b int64
                fmt.Println(a == b)
            }
            编译器会告诉你两者之间类型并不相同
            invalid operation: a == b (mismatched types uint64 and int64)
            所以必须使用强制类型转换
            func main() {
                var a uint64
                var b int64
                fmt.Println(int64(a) == b)
            }
            -------------------------------------------------------------------------------------------------
            在没有泛型之前,早期go提供的内置min,max函数只支持浮点数,到了1.21版本,go才终于将这两个内置函数用泛型重写。使用min函数比较最小值
            minVal := min(1, 2, -1, 1.2)
            使用max函数比较最大值
            maxVal := max(100, 22, -1, 1.12)
            -------------------------------------------------------------------------------------------------
            它们的参数支持所有的可比较类型,go中的可比较类型有
            布尔
            数字
            字符串
            指针
            通道 (仅支持判断是否相等)
            元素是可比较类型的数组(切片不可比较)
            字段类型都是可比较类型的结构体(仅支持判断是否相等)
            -------------------------------------------------------------------------------------------------
            除此之外,还可以通过导入标准库cmp来判断,不过仅支持有序类型的参数,在go中内置的有序类型只有数字和字符串。
            import "cmp"

            func main() {
                cmp.Compare(1, 2)
                cmp.Less(1, 2)
            }
        d.代码块
            在函数内部,可以通过花括号建立一个代码块,代码块彼此之间的变量作用域是相互独立的。例如下面的代码
            func main() {
                a := 1

                {
                    a := 2
                    fmt.Println(a)
                }

                {
                    a := 3
                    fmt.Println(a)
                }
                fmt.Println(a)
            }
            它的输出是
            2
            3
            1
            -------------------------------------------------------------------------------------------------
            块与块之间的变量相互独立,不受干扰,无法访问,但是会受到父块中的影响。
            func main() {
                a := 1

                {
                    a := 2
                    fmt.Println(a)
                }

                {
                    fmt.Println(a)
                }
                fmt.Println(a)
            }
            它的输出是
            2
            1
            1

01.变量
    a.变量的来历
        程序运行过程中的数据都是保存在内存中,我们想要在代码中操作某个数据时就需要去内存上找到这个变量,
        但是如果我们直接在代码中通过内存地址去操作变量的话,代码的可读性会非常差而且还容易出错,
        所以我们就利用变量将这个数据的内存地址保存起来,以后直接通过这个变量就能找到内存上对应的数据了。
    b.变量类型
        变量(Variable)的功能是存储数据。不同的变量保存的数据类型可能会不一样。
        经过半个多世纪的发展,编程语言已经基本形成了一套固定的类型,常见变量的数据类型有:整型、浮点型、布尔型等。
        Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用。
    c.变量声明
        Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。并且Go语言的变量声明后必须使用。
    d.标准声明
        Go语言的变量声明格式为:
        var 变量名 变量类型
        -----------------------------------------------------------------------------------------------------
        变量声明以关键字var开头,变量类型放在变量的后面,行尾无需分号。 举个例子:
        var name string
        var age int
        var isOk bool
        -----------------------------------------------------------------------------------------------------
        当要声明多个相同类型的变量时,可以只写一次类型
        var numA, numB, numC int
    e.批量声明
        每声明一个变量就需要写var关键字会比较繁琐,go语言中还支持批量变量声明:
        var (
            a string
            b int
            c bool
            d float32
        )
    f.变量的初始化
        a.介绍
            Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。
            每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0。
            字符串变量的默认值为空字符串。 布尔型变量默认为false。 切片、函数、指针变量的默认为nil。
            -------------------------------------------------------------------------------------------------
            当然我们也可在声明变量的时候为其指定初始值。变量初始化的标准格式如下:
            var 变量名 类型 = 表达式
            举个例子:
                var name string = "pprof.cn"
                var sex int = 1
            或者一次初始化多个变量
                var name, sex = "pprof.cn", 1
        b.类型推导
            有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化。
            var name = "pprof.cn"
            var sex = 1
        c.短变量声明
            在函数内部,可以使用更简略的 := 方式声明并初始化变量。
            package main

            import (
                "fmt"
            )
            // 全局变量m
            var m = 100

            func main() {
                n := 10
                m := 200 // 此处声明局部变量m
                fmt.Println(m, n)
            }
        d.匿名变量
            在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。
            匿名变量用一个下划线_表示,例如:
            func foo() (int, string) {
                return 10, "Q1mi"
            }
            func main() {
                x, _ := foo()
                _, y := foo()
                fmt.Println("x=", x)
                fmt.Println("y=", y)
            }
            匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。 (在Lua等编程语言里,匿名变量也被叫做哑元变量。)
        e.注意事项:
            函数外的每个语句都必须以关键字开始(var、const、func等)
            :=不能使用在函数外。
            _多用于占位,表示忽略值。

02.常量
    a.介绍
        常量是一个简单值的标识符,在程序运行时,不会被修改的量。
        常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
        常量的定义格式:
        const identifier [type] = value
        -----------------------------------------------------------------------------------------------------
        你可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型。
        显式类型定义: const b string = "abc"
        隐式类型定义: const b = "abc"
        多个相同类型的声明可以简写为:
        const c_name1, c_name2 = value1, value2
        -----------------------------------------------------------------------------------------------------
        相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把var换成了const,常量在定义的时候必须赋值。
        const pi = 3.1415
        const e = 2.7182
        声明了pi和e这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了。
        -----------------------------------------------------------------------------------------------------
        多个常量也可以一起声明:
        const (
            pi = 3.1415
            e = 2.7182
        )
        -----------------------------------------------------------------------------------------------------
        const同时声明多个常量时,如果省略了值则表示和上面一行的值相同。 例如:
        const (
            n1 = 100
            n2
            n3
        )
        上面示例中,常量n1、n2、n3的值都是100。
     b.iota
        a.介绍
            iota是go语言的常量计数器,只能在常量的表达式中使用。
            iota在const关键字出现时将被重置为0。
            const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。
            使用iota能简化定义,在定义枚举时很有用。
            举个例子:
            const (
                n1 = iota //0
                n2        //1
                n3        //2
                n4        //3
            )
        b.几个常见的iota示例
            a.使用_跳过某些值
                const (
                    n1 = iota //0
                    n2        //1
                    _
                    n4        //3
                )
            b.iota声明中间插队
                const (
                    n1 = iota //0
                    n2 = 100  //100
                    n3 = iota //2
                    n4        //3
                )
                const n5 = iota //0
            c.定义数量级
                这里的<<表示左移操作,1<<10表示将1的二进制表示向左移10位,也就是由1变成了10000000000,
                也就是十进制的1024。同理2<<2表示将2的二进制表示向左移2位,也就是由10变成了1000,也就是十进制的8。
                const (
                    _  = iota
                    KB = 1 << (10 * iota)
                    MB = 1 << (10 * iota)
                    GB = 1 << (10 * iota)
                    TB = 1 << (10 * iota)
                    PB = 1 << (10 * iota)
                )
            d.多个iota定义在一行
                const (
                    a, b = iota + 1, iota + 2 //1,2
                    c, d                      //2,3
                    e, f                      //3,4
                )
    c.枚举
        c.介绍
            Go语言没有为枚举单独设计一个数据类型,不像其它语言通常会有一个enum来表示。
        b.一般在Go中,都是通过自定义类型 + const + iota来实现枚举,下面是一个简单的例子
            type Season uint8

            const (
                Spring Season = iota
                Summer
                Autumn
                Winter
            )
        c.这些枚举实际上就是数字,Go也不支持直接将其转换为字符串,但我们可以通过给自定义类型添加方法来返回其字符串表现形式,实现Stringer接口即可。
            func (s Season) String() string {
                switch s {
                case Spring:
                    return "spring"
                case Summer:
                    return "summer"
                case Autumn:
                    return "autumn"
                case Winter:
                    return "winter"
                }
                return ""
            }
        d.不过它有以下缺点:
            类型不安全,因为Season是自定义类型,可以通过强制类型转换将其他数字也转换成该类型 Season(6)
            繁琐,字符串表现形式需要自己实现
            表达能力弱,因为const仅支持基本数据类型,所以这些枚举值也只能用字符串和数字来进行表示
        e.通过官方工具Stringer来自动生成枚举

1.5 运算符

01.算术运算符
    算术运算符用于执行基本的数学运算。
    +:加法
    -:减法
    *:乘法
    /:除法
    %:取模(取余数)

02.关系运算符
    关系运算符用于比较两个值,结果为布尔值 (true 或 false)。
    ==:等于
    !=:不等于
    >:大于
    <:小于
    >=:大于等于
    <=:小于等于

03.逻辑运算符
    逻辑运算符用于对布尔值进行逻辑运算。
    &&:逻辑与(AND)
    ||:逻辑或(OR)
    !:逻辑非(NOT)

04.位运算符
    位运算符用于对整数的位进行操作。
    &:按位与(AND)
    |:按位或(OR)
    ^:按位异或(XOR)
    &^:按位清除(AND NOT)
    <<:左移
    >>:右移

05.赋值运算符
    赋值运算符用于将值赋给变量。
    =:赋值
    +=:加法赋值
    -=:减法赋值
    *=:乘法赋值
    /=:除法赋值
    %=:取模赋值
    &=:按位与赋值
    |=:按位或赋值
    ^=:按位异或赋值
    &^=:按位清除赋值
    <<=:左移赋值
    >>=:右移赋值

06.其他运算符
    其他运算符包括:
    ,:逗号,用于分隔多个变量声明或函数参数
    .:点,用于访问结构体字段和方法
    []:方括号,用于数组或切片索引
    ():圆括号,用于优先级控制、函数调用
    ...:变长参数

1.6 流程控制

01.条件语句if
    if 语句:由一个布尔表达式后紧跟一个或多个语句组成
    if...else 语句:if 语句 后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行
    if 嵌套语句:可以在 if 或 else if 语句中嵌入一个或多个 if 或 else if 语句。
    ---------------------------------------------------------------------------------------------------------
    if n := "abc"; x > 0 {     // 初始化语句未必就是定义变量, 如 println("init") 也是可以的。
        println(n[2])
    } else if x < 0 {    // 注意 else if 和 else 左大括号位置。
        println(n[1])
    } else {
        println(n[0])
    }
    ---------------------------------------------------------------------------------------------------------
    不支持三元操作符(三目运算符) "a > b ? a : b"

02.switch语句
    a.普通switch
        switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上直下逐一测试,直到匹配为止。
        Golang switch 分支表达式可以是任意类型,不限于常量。可省略 break,默认自动终止。
        -----------------------------------------------------------------------------------------------------
        package main

        import "fmt"

        func main() {
           /* 定义局部变量 */
           var grade string = "B"
           var marks int = 90

           switch marks {
              case 90: grade = "A"
              case 80: grade = "B"
              case 50,60,70 : grade = "C"
              default: grade = "D"
           }

           // switch语句也可以没有入口处的表达式
           switch {
              case grade == "A" :
                 fmt.Printf("优秀!\n" )
              case grade == "B", grade == "C" :
                 fmt.Printf("良好\n" )
              case grade == "D" :
                 fmt.Printf("及格\n" )
              case grade == "F":
                 fmt.Printf("不及格\n" )
              default:
                 fmt.Printf("差\n" )
           }
           fmt.Printf("你的等级是 %s\n", grade )
        }
    b.type-switch
        switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。
        -----------------------------------------------------------------------------------------------------
        package main

        import "fmt"

        func main() {
            var x interface{}
            //写法一:
            switch i := x.(type) { // 带初始化语句
            case nil:
                fmt.Printf(" x 的类型 :%T\r\n", i)
            case int:
                fmt.Printf("x 是 int 型")
            case float64:
                fmt.Printf("x 是 float64 型")
            case func(int) float64:
                fmt.Printf("x 是 func(int) 型")
            case bool, string:
                fmt.Printf("x 是 bool 或 string 型")
            default:
                fmt.Printf("未知型")
            }
            //写法二
            var j = 0
            switch j {
            case 0:
            case 1:
                fmt.Println("1")
            case 2:
                fmt.Println("2")
            default:
                fmt.Println("def")
            }
            //写法三
            var k = 0
            switch k {
            case 0:
                println("fallthrough")
                fallthrough
                /*
                    Go的switch非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;
                    而如果switch没有表达式,它会匹配true。
                    Go里面switch默认相当于每个case最后带有break,
                    匹配成功后不会自动向下执行其他case,而是跳出整个switch,
                    但是可以使用fallthrough强制执行后面的case代码。
                */
            case 1:
                fmt.Println("1")
            case 2:
                fmt.Println("2")
            default:
                fmt.Println("def")
            }
            //写法三
            var m = 0
            switch m {
            case 0, 1:
                fmt.Println("1")
            case 2:
                fmt.Println("2")
            default:
                fmt.Println("def")
            }
            //写法四
            var n = 0
            switch { //省略条件表达式,可当 if...else if...else
            case n > 0 && n < 10:
                fmt.Println("i > 0 and i < 10")
            case n > 10 && n < 20:
                fmt.Println("i > 10 and i < 20")
            default:
                fmt.Println("def")
            }
        }

03.select语句
    a.select语句
        a.介绍
            select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。
            如果没有case可运行,它将阻塞,直到有case可运行。
            select 是Go中的一个控制结构,类似于用于通信的switch语句。
            每个case必须是一个通信操作,要么是发送要么是接收。
            select 随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。
        b.语法
            每个case都必须是一个通信
            所有channel表达式都会被求值
            所有被发送的表达式都会被求值
            如果任意某个通信可以进行,它就执行;其他被忽略。
            如果有多个case都可以运行,Select会随机公平地选出一个执行。其他不会执行。
            否则:
            如果有default子句,则执行该语句。
            如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。
        c.实例
            package main

            import "fmt"

            func main() {
               var c1, c2, c3 chan int
               var i1, i2 int
               select {
                  case i1 = <-c1:
                     fmt.Printf("received ", i1, " from c1\n")
                  case c2 <- i2:
                     fmt.Printf("sent ", i2, " to c2\n")
                  case i3, ok := (<-c3):  // same as: i3, ok := <-c3
                     if ok {
                        fmt.Printf("received ", i3, " from c3\n")
                     } else {
                        fmt.Printf("c3 is closed\n")
                     }
                  default:
                     fmt.Printf("no communication\n")
               }
            }
        d.select可以监听channel的数据流动
            select的用法与switch语法非常类似,由select开始的一个新的选择块,每个选择条件由case语句来描述
            与switch语句可以选择任何使用相等比较的条件相比,select由比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作
            -------------------------------------------------------------------------------------------------
            在一个select语句中,Go会按顺序从头到尾评估每一个发送和接收的语句。
            如果其中的任意一个语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。
            如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:
            ①如果给出了default语句,那么就会执行default的流程,同时程序的执行会从select语句后的语句中恢复。
            ②如果没有default语句,那么select语句将被阻塞,直到至少有一个case可以进行下去。
        e.实例
            package main

            import "fmt"

            func main() {
               var c1, c2, c3 chan int
               var i1, i2 int
               select {
                  case i1 = <-c1:
                     fmt.Printf("received ", i1, " from c1\n")
                  case c2 <- i2:
                     fmt.Printf("sent ", i2, " to c2\n")
                  case i3, ok := (<-c3):  // same as: i3, ok := <-c3
                     if ok {
                        fmt.Printf("received ", i3, " from c3\n")
                     } else {
                        fmt.Printf("c3 is closed\n")
                     }
                  default:
                     fmt.Printf("no communication\n")
               }
            }

            //输出:no communication
    b.Golang select的使用及典型用法
        a.超时判断
            //比如在下面的场景中,使用全局resChan来接受response,如果时间超过3S,resChan中还没有数据返回,则第二条case将执行
            var resChan = make(chan int)
            // do request
            func test() {
                select {
                case data := <-resChan:
                    doData(data)
                case <-time.After(time.Second * 3):
                    fmt.Println("request time out")
                }
            }

            func doData(data int) {
                //...
            }
        b.退出
            //主线程(协程)中如下:
            var shouldQuit=make(chan struct{})
            fun main(){
                {
                    //loop
                }
                //...out of the loop
                select {
                    case <-c.shouldQuit:
                        cleanUp()
                        return
                    default:
                    }
                //...
            }

            //再另外一个协程中,如果运行遇到非法操作或不可处理的错误,就向shouldQuit发送数据通知程序停止运行
            close(shouldQuit)
        c.判断channel是否阻塞
            //在某些情况下是存在不希望channel缓存满了的需求的,可以用如下方法判断
            ch := make (chan int, 5)
            //...
            data:=0
            select {
            case ch <- data:
            default:
                //做相应操作,比如丢弃data。视需求而定
            }

04.循环语句for
    a.Go语言的For循环有3中形式,只有其中的一种使用分号
        for init; condition; post { }
        for condition { }
        for { }
        -----------------------------------------------------------------------------------------------------
        init: 一般为赋值表达式,给控制变量赋初值;
        condition: 关系表达式或逻辑表达式,循环控制条件;
        post: 一般为赋值表达式,给控制变量增量或减量。
        -----------------------------------------------------------------------------------------------------
        for语句执行过程如下:
        ①先对表达式 init 赋初值;
        ②判别赋值表达式 init 是否满足给定 condition 条件,若其值为真,满足循环条件,则执行循环体内语句,
        然后执行 post,进入第二次循环,再判别 condition;
        否则判断 condition 的值为假,不满足条件,就终止for循环,执行循环体外语句。
        -----------------------------------------------------------------------------------------------------
        s := "abc"

        for i, n := 0, len(s); i < n; i++ { // 常见的 for 循环,支持初始化语句。
            println(s[i])
        }

        n := len(s)
        for n > 0 {                // 替代 while (n > 0) {}
            println(s[n])        // 替代 for (; n > 0;) {}
            n--
        }

        for {                    // 替代 while (true) {}
            println(s)            // 替代 for (;;) {}
        }
    b.循环嵌套
        for [condition |  ( init; condition; increment ) | Range]
        {
           for [condition |  ( init; condition; increment ) | Range]
           {
              statement(s)
           }
           statement(s)
        }
        -----------------------------------------------------------------------------------------------------
        package main

        import "fmt"

        func main() {
           /* 定义局部变量 */
           var i, j int

           for i=2; i < 100; i++ {
              for j=2; j <= (i/j); j++ {
                 if(i%j==0) {
                    break // 如果发现因子,则不是素数
                 }
              }
              if(j > (i/j)) {
                 fmt.Printf("%d  是素数\n", i)
              }
           }
        }
    c.无限循环
        package main

        import "fmt"

        func main() {
            for true  {
                fmt.Printf("这是无限循环。\n");
            }
        }

05.循环语包range
    a.介绍
        Golang range类似迭代器操作,返回 (索引, 值) 或 (键, 值)。
        for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
        for key, value := range oldMap {
            newMap[key] = value
        }
        Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。
        在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。
        -----------------------------------------------------------------------------------------------------
        for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
        for key, value := range oldMap {
            newMap[key] = value
        }
        以上代码中的 key 和 value 是可以省略。
        -----------------------------------------------------------------------------------------------------
        如果只想读取 key,格式如下:
        for key := range oldMap
        或者这样:
        for key, _ := range oldMap
        -----------------------------------------------------------------------------------------------------
        如果只想读取 value,格式如下:
        for _, value := range oldMap
    b.总结
        a.遍历数组(Array)
            遍历数组的索引和元素值
            忽略索引或忽略值
        b.遍历切片(Slice)
            遍历切片的索引和元素值
            忽略索引或忽略值
        c.遍历字符串(String)
            遍历字符串中的字节
            遍历字符串中的Unicode字符(rune)
            忽略索引或忽略值
        d.遍历字典(Map)
            遍历字典的键和值
            仅遍历键或值
        e.遍历通道(Channel)
            从通道中接收数据直到通道关闭
        f.使用 range 遍历时修改数据
            在遍历数组、切片或字典时修改数据
    b.for 和 for range有什么区别?
        a.for
            遍历array和slice
            遍历key为整型递增的map
            遍历string
        b.for range
            for range可以完成所有for可以做的事情,却能做到for不能做的,包括
            遍历key为string类型的map并同时获取key和value
            遍历channel
    c.索引参数
                     参数1        参数2
        string       index        s[index]    unicode, rune
        array/slice  index        s[index]
        map          key          m[key]
        channel      element
    d.代码
        func rangeExamples() {
            s := "abc"

            fmt.Println("字符串的 range 示例:")
            // 使用 range 遍历字符串的索引
            for i := range s {
                fmt.Println(s[i])
            }

            fmt.Println("字符串的 range 示例 (忽略索引):")
            // 使用 range 遍历字符串的字符值
            for _, c := range s {
                fmt.Println(c)
            }

            fmt.Println("字符串的 range 示例 (忽略全部返回值):")
            // 使用 range 遍历字符串,仅迭代
            for range s {
            }

            m := map[string]int{"a": 1, "b": 2}
            fmt.Println("map 的 range 示例:")
            // 使用 range 遍历 map 的键值对
            for k, v := range m {
                fmt.Println(k, v)
            }
        }

06.循环控制Goto、Break.Continue
    1.三个语句都可以配合标签(label)使用
    2.标签名区分大小写,定以后若不使用会造成编译错误
    3.continue、break配合标签(label)可用于多层循环跳出
    4.goto是调整执行位置,与continue、break配合标签(label)的结果并不相同
    ---------------------------------------------------------------------------------------------------------
    // 展示 goto 的用法
    func demoGoto() {
        fmt.Println("Goto 示例:")

        // 定义标签
        fmt.Println("在 start 标签处")
        goto end // 跳转到 end 标签

        fmt.Println("这行不会被执行") // 被跳过

        // 定义标签
        end:
        fmt.Println("在 end 标签处")
    }

    // 展示 break 和 continue 的用法
    func demoBreakContinue() {
        fmt.Println("Break 和 Continue 示例:")

        outerLoop:
        for i := 0; i < 3; i++ {
            fmt.Println("外层循环:", i)
            for j := 0; j < 3; j++ {
                if j == 2 {
                    // 使用 break 跳出外层循环
                    break outerLoop
                }
                if j == 1 {
                    // 使用 continue 跳过本次内层循环迭代
                    continue
                }
                fmt.Println("  内层循环:", j)
            }
        }

        fmt.Println("外层循环已跳出")
    }

    // 展示 break 的使用
    func demoBreak() {
        fmt.Println("Break 示例:")

        for i := 0; i < 5; i++ {
            if i == 3 {
                break // 跳出当前循环
            }
            fmt.Println(i)
        }
        fmt.Println("循环已结束")
    }

    // 展示 continue 的使用
    func demoContinue() {
        fmt.Println("Continue 示例:")

        for i := 0; i < 5; i++ {
            if i == 3 {
                continue // 跳过本次循环迭代
            }
            fmt.Println(i)
        }
        fmt.Println("循环结束")
    }

1.7 错误处理

00.汇总
    在 Go 语言中,异常处理的主要方式是通过错误处理机制。
    Go 不支持传统意义上的异常处理(如 try-catch 语句),而是采用显式的错误返回值来处理错误。

01.error 类型:
    Go 语言通过内置的 error 类型来表示错误。error 是一个接口,定义了一个方法:
    type error interface {
        Error() string
    }
    当函数可能出现错误时,它通常会返回一个 error 类型的值。如果没有错误发生,error 为 nil。

02.return 语句:
    在函数中,通常会返回一个 error 类型的值来指示操作是否成功。例如:
    func divide(a, b int) (int, error) {
        if b == 0 {
            return 0, fmt.Errorf("division by zero")
        }
        return a / b, nil
    }

03.fmt.Errorf 函数:
    fmt.Errorf 函数用于创建带有格式化信息的错误。例如:
    err := fmt.Errorf("an error occurred: %v", someDetail)

04.panic 和 recover:
    Go 语言提供了 panic 和 recover 机制来处理程序中的致命错误(即不期望发生的错误),但这不常用于普通的错误处理,而是用于异常情况,如程序的严重错误或不可恢复的状态。
    ---------------------------------------------------------------------------------------------------------
    panic 用于引发一个运行时错误,它会导致程序的执行中断,直到最上层的函数返回或者程序崩溃
    func riskyOperation() {
        panic("something went wrong")
    }
    ---------------------------------------------------------------------------------------------------------
    recover 用于从 panic 中恢复程序的正常执行状态。它只能在 defer 的函数中调用:
    func safeOperation() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("Recovered from panic:", r)
            }
        }()
        riskyOperation()
    }

05.defer 关键字:
    defer 关键字用于延迟执行某些操作,直到函数执行结束。
    这在处理资源清理(如关闭文件、解锁)时非常有用。结合 recover 可以安全地处理异常情况。

1.8 匿名字段

01.go支持只提供类型而不写字段名的方式,也就是匿名字段,也称为嵌入字段
    package main

    import "fmt"

    //    go支持只提供类型而不写字段名的方式,也就是匿名字段,也称为嵌入字段

    //人
    type Person struct {
        name string
        sex  string
        age  int
    }

    type Student struct {
        Person
        id   int
        addr string
    }

    func main() {
        // 初始化
        s1 := Student{Person{"5lmh", "man", 20}, 1, "bj"}
        fmt.Println(s1)

        s2 := Student{Person: Person{"5lmh", "man", 20}}
        fmt.Println(s2)

        s3 := Student{Person: Person{name: "5lmh"}}
        fmt.Println(s3)
    }
    输出结果:
    {{5lmh man 20} 1 bj}
    {{5lmh man 20} 0 }
    {{5lmh  0} 0 }

02.同名字段的情况
    package main

    import "fmt"

    //人
    type Person struct {
        name string
        sex  string
        age  int
    }

    type Student struct {
        Person
        id   int
        addr string
        //同名字段
        name string
    }

    func main() {
        var s Student
        // 给自己字段赋值了
        s.name = "5lmh"
        fmt.Println(s)

        // 若给父类同名字段赋值,如下
        s.Person.name = "枯藤"
        fmt.Println(s)
    }
    输出结果:
    {{  0} 0  5lmh}
    {{枯藤  0} 0  5lmh}

03.所有的内置类型和自定义类型都是可以作为匿名字段去使用
    package main

    import "fmt"

    //人
    type Person struct {
        name string
        sex  string
        age  int
    }

    // 自定义类型
    type mystr string

    // 学生
    type Student struct {
        Person
        int
        mystr
    }

    func main() {
        s1 := Student{Person{"5lmh", "man", 18}, 1, "bj"}
        fmt.Println(s1)
    }
    输出结果:
    {{5lmh man 18} 1 bj}

04.指针类型匿名字段
    package main

    import "fmt"

    //人
    type Person struct {
        name string
        sex  string
        age  int
    }

    // 学生
    type Student struct {
        *Person
        id   int
        addr string
    }

    func main() {
        s1 := Student{&Person{"5lmh", "man", 18}, 1, "bj"}
        fmt.Println(s1)
        fmt.Println(s1.name)
        fmt.Println(s1.Person.name)
    }
    输出结果:
    {0xc00005c360 1 bj}
    zs
    zs

1.9 通道channel

00.介绍
    a.说明
        在 Go 语言中,通道(channel)是用于在 goroutine 之间进行通信和同步的核心机制。
        通道支持多种操作,这些操作允许你创建、发送、接收、关闭通道等。
    b.channel异常情况总结
        channel:接收
        nil:阻塞
        非空:接收值
        空的:阻塞
        满了:接收值
        没满:接收值
        -----------------------------------------------------------------------------------------------------
        channel:发送
        nil:阻塞
        非空:发送值
        空的:发送值
        满了:阻塞
        没满:发送值
        -----------------------------------------------------------------------------------------------------
        channel:关闭
        nil:panic
        非空:关闭成功,读完数据后返回零值
        空的:关闭成功,返回零值
        满了:关闭成功,读完数据后返回零值
        没满:关闭成功,读完数据后返回零值

01.创建通道
    a.创建无缓冲通道
        ch := make(chan int)
    b.创建有缓冲通道
        ch := make(chan int, 10)  // 创建一个容量为10的缓冲通道

02.发送和接收数据
    a.发送数据到通道
        ch <- 42
    b.从通道接收数据
        value := <-ch
    c.接收数据并处理
        value, ok := <-ch
        if !ok {
            // 通道已关闭
        }

03.关闭通道
    a.关闭通道
        close(ch)
    b.检查通道是否关闭
        value, ok := <-ch
        if !ok {
            // 通道已关闭
        }

04.通道的发送和接收方向
    a.仅发送方向通道
        func sendData(ch chan<- int) {
            ch <- 42
        }
    b.仅接收方向通道
        func receiveData(ch <-chan int) {
            value := <-ch
            fmt.Println(value)
        }

05.通道的选择(select)
    使用 select 语句进行多路选择
    select {
    case msg1 := <-ch1:
        // 处理 ch1 中的消息
    case msg2 := <-ch2:
        // 处理 ch2 中的消息
    case ch3 <- 3:
        // 发送消息到 ch3
    default:
        // 当没有其他通道准备好时执行
    }

06.通道的缓冲区
    a.创建和使用有缓冲通道
        ch := make(chan int, 2)
        ch <- 1
        ch <- 2
        // ch <- 3 // 这会阻塞,直到有接收者接收数据
    b.接收缓冲区中的数据
        value1 := <-ch
        value2 := <-ch

07.通道的遍历
    遍历通道中的数据(通常在通道关闭后)
    for value := range ch {
        fmt.Println(value)
    }

08.通道的默认值
    使用通道的默认分支
    select {
    case msg := <-ch:
        fmt.Println("Received", msg)
    default:
        fmt.Println("No message received")
    }

09.通道的关闭行为
    关闭通道时的行为
    ch := make(chan int)
    go func() {
        for i := 0; i < 3; i++ {
            ch <- i
        }
        close(ch)
    }()
    for value := range ch {
        fmt.Println(value)
    }

10.无缓冲通道和有缓冲通道的区别
    无缓冲通道:发送操作会阻塞,直到接收操作准备好。
    有缓冲通道:发送操作只有在缓冲区满时才会阻塞,接收操作只有在缓冲区空时才会阻塞。

1.11 附:标准库

01.输入输出(I/O)操作
    fmt:格式化输入输出。
    io:基础 I/O 接口和函数,例如 Closer、Writer 等。
    io/fs:文件系统接口,用于文件系统操作的抽象。
    os:操作系统交互接口,提供文件系统及 I/O 操作。
    os/signal:操作系统信号监听,主要用于优雅地关闭程序。
    os/exec:执行操作系统命令。
    bufio:带缓冲的 I/O 操作。

02.网络与通讯
    net:网络操作包,支持 TCP、UDP 等协议。
    net/http:HTTP 协议实现包,用于网络请求。
    net/rpc:远程过程调用(RPC)支持包。

03.字符串与字符操作
    strings:字符串处理函数。
    strconv:字符串与其他类型之间的转换。
    unicode:Unicode 字符集操作。
    regexp:正则表达式处理。

04.时间与日期
    time:时间处理和格式化。

05.文件系统与路径操作
    path:处理斜杠分隔路径(如 URL)操作。
    path/filepath:处理操作系统文件路径。

06.并发与同步
    sync:并发操作,如互斥锁。
    context:上下文包,用于控制超时和取消操作。

07.容器与数据结构
    container/heap:最小堆的实现。
    container/list:双向链表的实现。
    container/ring:环形链表的实现。

08.排序与集合操作
    sort:排序操作。
    maps:对 map 进行操作。
    slices:切片操作的函数。

09.加密与编码
    crypto:加密操作(含很多子包,如 sha1、rsa)。
    encoding:编码操作,含多种子包(如 json、xml 和 base64)。

10.数据库与存储
    database/sql:与数据库交互的标准接口。

11.压缩与归档
    archive/zip:ZIP 压缩。
    archive/tar:TAR 归档。
    compress:压缩算法实现(如 gzip、flate 等)。

12.数学与科学计算
    math:数学运算包。
    math/bits:位运算。
    math/cmplx:复数运算。
    math/rand:伪随机数生成。
    math/big:高精度大数计算。

13.图像处理
    image:图像操作。

14.日志与调试
    log:日志记录包。
    testing:测试包,用于编写和运行测试。
    runtime:Go 运行时操作,控制协程、GC 等。

15.操作系统调用与不安全操作
    syscall:系统调用接口。
    unsafe:允许不受类型限制的指针操作。

16.HTML与模板
    html:HTML 模板处理。

17.Go源码与解析
    go/ast:Go 语言源代码的抽象语法树。
    go/parser:将源代码解析为语法树。
    go/importer:导入器接口。
    go/format:Go 源代码格式化。

18.实验性包
    arena:手动内存分配与释放(实验阶段)。

1.10 附:工具包

01.常见信息1
    a.分类1
        github.com/fatih/color:输出对应编码颜色的包
        github.com/spf13/cobra:创建带有输入选项和相关文档的复杂脚本的包
        github.com/schollz/progressbar:为执行时间过久的任务创建进度条
        github.com/jimlawless/whereami:捕获源代码的文件名、行号、函数等信息的包
    b.分类2
        go-kit/kit:工具包
        golang.org/x/sync/singleflight:防缓存击穿
        golang-standards/project-layout:工具包
    c.分类3
        gopkg:gopool协程池
        Pholcus:数据采集利器

02.常用信息2
    a.Web框架
        名称                    描述                                        仓库
        gin                     最经典的 web 框架                           https://github.com/gin-gonic/gin
        beego                   国人开发的 web 框架                         https://github.com/beego/beego
        iris                    号称最快的 web 框架                         https://github.com/kataras/iris
        echo                    极简高性能的 web 框架                       https://github.com/labstack/echo
        goji                    简洁的 web 框架                             https://github.com/zenazn/goji
        revel                   高可用的全栈 web 框架                       https://github.com/revel/revel
        buffalo                 可以简单的构建全栈项目 web 框架             https://github.com/gobuffalo/buffalo
        hertz                   具有高性能和强扩展性的微服务 HTTP 框架      https://github.com/cloudwego/hertz
        dotweb                  一个简单的微型 web 框架                     https://github.com/devfeel/dotweb
        fiber                   Node.js Express 风格的 Web 框架             https://github.com/gofiber/fiber
    b.ORM
        名称                    描述                                        仓库
        grom                    开发者友好的 ORM 库                         https://github.com/go-gorm/gorm
        xorm                    简单强大的 ORM                              https://gitea.com/xorm/xorm
        ent                     FaceBook 开源的 ORM                         https://github.com/ent/ent
        sqlx                    对 sql 库的强大拓展                         https://github.com/jmoiron/sqlx
        beego/orm               beego 自带的 orm                            https://github.com/astaxie/beego/tree/master/orm
        rel                     可拓展的现代 ORM                            https://github.com/go-rel/rel
        bun                     SQL 优先的 ORM                              https://github.com/uptrace/bun
    c.微服务框架
        名称                    描述                                         仓库
        kratos                  云原生微服务框架(B 站开源)                 https://github.com/go-kratos/kratos
        go-kit                  一个微服务开发的工具库                       https://github.com/go-kit/kit
        kitex                   高性能和高拓展的微服务框架(字节开源)       https://github.com/cloudwego/kitex
        go-zero                 云原生微服务框架(七牛云开源)               https://github.com/zeromicro/go-zero
        go-micro                一个国外的微服务框架                         https://github.com/go-micro/go-micro
        kite                    微服务框架(很久没更新)                     https://github.com/koding/kite
        dubbo-go                java dubbo 在 go 实现(阿里开源)            https://github.com/apache/dubbo-go
        tarsgo                  tars 在 go 中的实现(腾讯开源)              https://github.com/TarsCloud/TarsGo
        juptiers                面向治理的微服务框架(斗鱼开源)             https://github.com/douyu/jupiter
        redsync                 redis 分布式锁                               https://github.com/go-redsync/redsync
    d.日志组件
        名称                    描述                                         仓库
        logrus                  结构化日志库                                 https://github.com/sirupsen/logrus
        zap                     uber 开源的高性能日志库                      https://github.com/uber-go/zap
        glog                    分级执行日志                                 https://github.com/golang/glog
        zerolog                 零内存分配的 json 日志                       https://github.com/rs/zerolog
        apex/log                结构化日志库                                 https://github.com/apex/log
        lumberjack              日志分割库,支持大小分割,日期分割,文件压缩 https://github.com/natefinch/lumberjack
    e.测试组件
        名称                    描述                                         仓库
        testify                 最流行的测试工具包                           https://github.com/stretchr/testify
        ginkgo                  现代化的测试框架                             https://github.com/onsi/ginkgo
        ramsql                  基于内存的 SQL 引擎,主要用于SQL的单元测试   https://github.com/proullon/ramsql
        go-sqlmock              用于测试的 SQL Mock                          https://github.com/DATA-DOG/go-sqlmock
        goconvey                在浏览器可视化中测试                         https://github.com/smartystreets/goconvey
        go-stress-testing       压测工具                                     https://github.com/link1st/go-stress-testing
        xgo                     go 打桩测试框架,通过编译期重写代码来实现    https://github.com/xhd2015/xgo
        gomonkey                go 打桩测试框架,通过修改修改函数地址实现    https://github.com/agiledragon/gomonkey
    f.数据处理
        名称                    描述                                         仓库
        mapstructure map        与结构体互转                                 https://github.com/mitchellh/mapstructure
        cast                    可以很方便的数据类型转换                     https://github.com/spf13/cast
        deepcopy                深度复制                                     https://github.com/mohae/deepcopy
        copier                  可以在结构体之间同名字段复制值               https://github.com/jinzhu/copier
        go-pinyin               汉字转拼音                                   https://github.com/mozillazg/go-pinyin
        go-streams              流式数据处理                                 https://github.com/reugn/go-streams
        stream                  流式处理                                     https://github.com/xyctruth/stream
        go-humanize             将数据转换成人类可以阅读的格式               https://github.com/dustin/go-humanize
        uniseg                  在 Go 中进行 Unicode 文本分段、字包装        https://github.com/rivo/uniseg
    g.数据验证
        名称                    描述                                         仓库
        go-playground/validator 数据验证器                                   https://github.com/go-playground/validator
        go-cmp                  谷歌开源的用于比较值的库                     https://github.com/google/go-cmp
        ozzo-validation         基于规则的数据校验库                         https://github.com/go-ozzo/ozzo-validation
        go-tagexpr              结构体 tag 验证库                            https://github.com/bytedance/go-tagexpr
    h.数据结构
        名称                  描述                                           仓库
        gods                  常见数据结构的实现                              https://github.com/emirpasic/gods
        go-datastructures     常见数据结构的实现                              https://github.com/Workiva/go-datastructures
        biset                 go 中 bitsets 的实现                           https://github.com/bits-and-blooms/bitset
        bloom                 go 中 bloom filters 的实现                     https://github.com/bits-and-blooms/bloom
        deque                 高性能双端队列的实现                            https://github.com/edwingeng/deque
        concurrent-map        并发安全的分片 map 实现                         https://github.com/orcaman/concurrent-map
        samber/lo             Lodash 风格的数据处理库,支持泛型               https://github.com/samber/lo
        google/btree          谷歌实现的 BTree 库,支持泛型                   https://github.com/google/btree
        gostl                 像 C++STL 一样的数据结构库                      https://github.com/liyue201/gostl
    i.数学计算
        名称                  描述                                           仓库
        gonum                 类比 numpy                                     https://github.com/gonum/gonum
        decimal               高精度浮点数操作库                              https://github.com/shopspring/decimal
        crunch                一个简化字节和位操作的库                        https://github.com/superwhiskers/crunch
        math-engine           数学表达式解析计算引擎库                        https://github.com/dengsgo/math-engine
    j.模板引擎
        名称                  描述                                           仓库
        pongo2                Django 风格的模板引擎                          https://github.com/flosch/pongo2
        ace                   html 模板引擎                                  https://github.com/yosssi/ace
        mustache              mustache 在 go 中的实现                        https://github.com/hoisie/mustache
        hero                  功能强大,快速的模板引擎                        https://github.com/shiyanhui/hero
        quictemplate          顾名思义,高性能的模板引擎                      https://github.com/valyala/quicktemplate
        amber                 源于 HAML 和 Jade 的模板引擎                   https://github.com/eknkc/amber
    k.缓存组件
        名称                  描述                                           仓库
        golang-lru            线程安全的 LRU,以及 LRU 2Q 缓存                https://github.com/hashicorp/golang-lru
        ttlcache              基于内存的缓存,支持 TTL,泛型                   https://github.com/jellydator/ttlcache
        gocache               缓存中间件管理器                                 https://github.com/eko/gocache
        go-cache              基于内存的缓存,适用于单机应用,支持TTL          https://github.com/patrickmn/go-cache
        ristretto             高性能的内存缓存                                 https://github.com/dgraph-io/ristretto
        bigcache              基于内存的高效率的大 key 缓存                   https://github.com/allegro/bigcache
    l.数据库&驱动
        名称                  描述                                            仓库
        modernc.org/sqlite    sqlite 驱动,纯 go 编写,不需要 cgo              https://gitlab.com/cznic/sqlite
        mattn/go-sqlite3      sqlite 驱动,需要 cgo                           https://github.com/mattn/go-sqlite3
        denisenkom/go-mssqldb sqlserver 驱动,不怎么更新了,建议使用微软的版本  https://github.com/denisenkom/go-mssqldb
        microsoft/go-mssqldb  sqlserver 驱动,微软 fork 的新分支并维护         https://github.com/microsoft/go-mssqldb
        pgx                   postgreSQL 驱动                                 https://github.com/jackc/pgx/
        mysql                 mysql 驱动                                      https://github.com/go-sql-driver/mysql
        oci-go-sdk            oracle 官方驱动                                 https://github.com/oracle/oci-go-sdk
        go-ora                oracle 驱动,纯 go 编写                         https://github.com/sijms/go-ora
        badger                嵌入式的 kv 数据库,基于 LSM                     https://github.com/dgraph-io/badger
        boltdb                嵌入式的 kv 数据库,基于 B+Tree                  https://github.com/boltdb/bolt
        goleveldb             go 语言实现的 leveldb                           https://github.com/syndtr/goleveldb
        qmgo                  七牛云开源的 mongodb 操作库                      https://github.com/qiniu/qmgo
        mongo-go-driver        mongodb 官方的 go 驱动                         https://github.com/mongodb/mongo-go-driver
        rqlite                 基于 sqlite 的轻量级分布式关系数据库            https://github.com/rqlite/rqlite/
        go-mysql               一个强大的 MySQL 工具集合                       https://github.com/go-mysql-org/go-mysql
        go-mysql-elasticsearch MySQL 数据同步到 Elasticsearch 的工具           https://github.com/go-mysql-org/go-mysql-elasticsearch
        gofound                单机亿级全文检索引擎,                          https://github.com/sea-team/gofound
        bleve                  全文检索库                                     https://github.com/blevesearch/bleve
    m.序列化
        名称                   描述                                           仓库
        go-ini                 ini 文件序列化库                                https://github.com/go-ini/ini
        sonic                  字节开源的高性能 json 序列化库                   https://github.com/bytedance/sonic
        easyjson               json 快速序列化库                               https://github.com/mailru/easyjson
        gjson                  快速获取 json 键值,非传统的序列化库             https://github.com/tidwall/gjson
        go-yaml                yaml 序列化库                                   https://github.com/go-yaml/yaml
        go-toml                toml 序列化库                                   https://github.com/pelletier/go-toml
        properties             properties 序列化库                             https://github.com/magiconair/properties
        viper                  支持多种数据格式序列化,同时也是配置管理器        https://github.com/spf13/viper
        configor               gorm 作者写的多种数据格式序列化器,配置管理器     https://github.com/jinzhu/configor
    l.命令行
        名称                   描述                                            仓库
        pflag                  POSIX/GUN 的风格的 flag 包                      https://github.com/spf13/pflag
        go-flags               命令参数解析器                                  https://github.com/jessevdk/go-flags
        cobra                  现代命令行程序构建脚手架                         https://github.com/spf13/cobra
        dimiro1/banner         美观的 banner 构建库                            https://github.com/dimiro1/banner
        go-pretty              输出美观的命令行表格,文字,进度条               https://github.com/jedib0t/go-pretty
        progressbar            线程安全的命令行进度条                           https://github.com/schollz/progressbar
        go-ansi                用于 Go 语言的 Windows 便携式 ANSI 转义序列程序  https://github.com/k0kubun/go-ansi
        go-isatty              用于判断 tty 的库                               https://github.com/mattn/go-isatty
    m.压缩解压
        名称                   描述                                            仓库
        klauspost/compress     对 compress 标准库的优化改造                     https://github.com/klauspost/compress
        alexmullins/zip        archive/zip 标准库的 fork 分支,支持密码         https://github.com/alexmullins/zip
        mholt/archiver         支持很多格式的压缩解压缩工具库(个人非常推荐)    https://github.com/mholt/archiver
        go-car                  CAR 归档文件在 go 中的实现                     https://github.com/ipld/go-car
        go-unarr                一个压缩解压缩库                               https://github.com/gen2brain/go-unarr
        xz                      用于读写 xz 压缩文件的纯 Golang 库             https://github.com/ulikunitz/xz
    o.时期时间
        名称                   描述                                            仓库
        carbon                 时间日期处理库                                   https://github.com/golang-module/carbon
        robfig/cron            定时任务库                                       https://pkg.go.dev/github.com/robfig/cron/v3
        gron                   定时任务库                                       https://github.com/roylee0704/gron
        jobrunner              异步定时任务框架                                 https://github.com/bamzi/jobrunner
        dataparse              可以在不知道格式的情况下解析时间字符串            https://github.com/araddon/dateparse
        jinzhu/now             日期工具库                                      https://github.com/jinzhu/now

03.常用信息3
    a.依赖注入
        名称                   描述                                           仓库
        dig                    uber 开源的依赖注入库,基于反射                 https://darjun.github.io/2020/02/22/godailylib/dig/
        wire                   谷歌开源的依赖注入库,基于代码生成               https://github.com/google/wire
        inject                 依赖注入工具                                    https://github.com/codegangsta/inject
        di                     依赖注入容器                                    https://github.com/sarulabs/di
    b.地理位置
        名称                   描述                                           仓库
        geoip2-golang          IP 转地理信息                                   https://github.com/oschwald/geoip2-golang
        ip2location-go         IP 转地理信息                                   https://github.com/ip2location/ip2location-go
    c.爬虫框架
        名称                   描述                                           仓库
        colly                  简单强大的爬虫框架                              https://github.com/gocolly/colly
        goquery                类似 j-thing                                    https://github.com/PuerkitoBio/goquery
    d.网络工具
        名称                   描述                                             仓库
        gentleman              插件驱动,可拓展的 http 客户端                   https://github.com/h2non/gentleman
        resty                  restful http 客户端                              https://pkg.go.dev/github.com/go-resty/resty/v2
        gopeed                 支持所有平台的现代下载管理器,基于 go 和 flutter https://github.com/GopeedLab/gopeed
    e.电子邮件
        名称                   描述                                             仓库
        jordan-wright/email    健壮灵活的邮件发送库                             https://github.com/jordan-wright/email
        gomail                 邮件发送库                                       https://github.com/go-gomail/gomail
        go-simple-mail         简单的邮件发送库                                 https://github.com/xhit/go-simple-mail
        go-mail                易于使用,全面的邮件发送库                       https://github.com/wneessen/go-mail
        email-verifier         验证邮箱是否有效,且不需要发送邮件               https://github.com/AfterShip/email-verifier
        maddy                  组合式的邮件服务器                               https://github.com/foxcpp/maddy
        mox                    全面开源,高维护性,自托管的邮件服务端           https://github.com/mjl-/mox
        hermes                 邮件模板生成库                                   https://github.com/matcornic/hermes
        listmonk               高性能,子托管,可视化的邮件列表管理             https://github.com/knadh/listmonk
        go-smtp                go 编写的 SMTP 客户端与服务端                    https://github.com/emersion/go-smtp
        go-imap                go 编写的 IMAP 客户端与服务端                    https://github.com/emersion/go-imap
    f.游戏开发
        名称                   描述                                             仓库
        ebitengine             一个超级简单的 2d 游戏引擎                       https://github.com/hajimehoshi/ebiten
        Azul3D                 一个由 go 编写的 3d 游戏引擎                     https://github.com/azul3d/engine
        engo                   由 go 编写的开源 2d 游戏引擎                     https://github.com/EngoEngine/engo
        g3n/engine             go3d 游戏引擎                                    https://github.com/g3n/engine
        gonet                  一个游戏服务端框架                               https://github.com/xtaci/gonet
        leaf                   游戏服务端框架                                   https://github.com/name5566/leaf
        cloud-game             基于 web 的云游戏服务                            https://github.com/giongto35/cloud-game
    g.GUI
        名称                   描述                                             仓库
        fyne                   跨平台的 GUI 开发工具箱(真有点东西)            https://github.com/fyne-io/fyne
        go-flutter             用 go 写 flutter                                 https://github.com/go-flutter-desktop/go-flutter
    h.系统交互
        名称                   描述                                             仓库
        gopsutil               获取操作系统信息,兼容主流系统                   https://github.com/shirou/gopsutil
        flock                  基于操作系统调用的文件锁                         https://github.com/gofrs/flock
        sys                    官方的操作系统交互库                             https://cs.opensource.google/go/x/sys
    i.跨语言交互
        名称                   描述                                             仓库
        gopher-lua             go 编写的 lua 虚拟机                             https://github.com/yuin/gopher-lua
        go-lua                 go 编写的 lua 虚拟机                             https://github.com/Shopify/go-lua
        goja                   支持 es5.1+                                      https://github.com/dop251/goja
        tengo                  Tengo 是一种小型、动态、快速、安全的 Go 脚本语言 https://github.com/d5/tengo
        goby                   受 ruby 启发,由 go 实现的一种解释型脚本语言     https://github.com/goby-lang/goby
        go+                    七牛云脚本语言,可以与go无缝交互,又称Q语言      https://github.com/goplus/gop
        go-python              go 调用 cpython2                                 https://github.com/sbinet/go-python
        go-pytyon3             go 调用 cpython3                                 https://github.com/DataDog/go-python3
    j.图像处理
        名称                   描述                                             仓库
        plot                   一个绘图库,多用于数据可视化                     https://github.com/gonum/plot
        gg                     2d 绘图库                                        https://github.com/fogleman/gg
        gocv                   支持 opencv4+                                    https://github.com/hybridgroup/gocv
        imaging                一个简单的图像处理库                             https://github.com/disintegration/imaging
    k.文字处理
        名称                   描述                                             仓库
        vale                   语法感知的文本校对工具                           https://github.com/errata-ai/vale
    l.认证授权
        名称                   描述                                             仓库
        casbin                 灵活强大的权限管理库                             https://github.com/casbin/casbin
        openfga                高性能权限/授权库,源于 oogle Zanzibar           https://github.com/openfga/openfga
    m.代码生成
        名称                   描述                                             仓库
        jennifer               代码生成库                                       https://github.com/dave/jennifer
    n.正则处理
        名称                   描述                                             仓库
        commonregx             一个收集了常用的正则表达式的库                   https://github.com/mingrammer/commonregex
    o.文件处理
        名称                   描述                                             仓库
        filebox                文件操作工具库                                   https://github.com/dstgo/filebox
        size                   快速完成文件大小与字符串之间的转换               https://github.com/dstgo/size
        checksum               一个计算文件哈希签名的库                         https://github.com/codingsince1985/checksum
        pdfcpu                 pdf 处理器                                       https://github.com/pdfcpu/pdfcpu
        unioffice              office 处理库                                    https://github.com/unidoc/unioffice
        gooxml                 office 处理库                                    https://github.com/carmel/gooxml
        pdfcpu                 PDF 处理库                                       https://github.com/pdfcpu/pdfcpu
        excelize               Excel 处理库                                     https://github.com/360EntSecGroup-Skylar/excelize
    p.通用工具
        名称                   描述                                             仓库
        lancet                 多功能工具库,类比 java 中的 common 包           https://github.com/duke-git/lancet
        bytebufferpool         字节缓存池                                       https://github.com/valyala/bytebufferpool
    q.开发框架
        名称                   描述                                             仓库
        goframe                现代企业级 go 开发框架                           https://github.com/gogf/gf
    r.共识协议
        名称                   描述                                             仓库
        hashicorp/raft         consul 开源的 raft 库                            https://github.com/hashicorp/raft
        hashicorp/memberlist   consul 开源的 gossip 库                          https://github.com/hashicorp/memberlist
        etcd-io/raft           etcd 开源的 raft 库                              https://github.com/etcd-io/raft
    s.OCR
        名称                   描述                                             仓库
        gosseract              使用 Tesseract C + + 库的 OCR 库                 https://github.com/otiai10/gosseract

99.路线图
    a.先决条件
        Go
        Go 编程
        SQL 基础理解
    b.基本开发技能
        学习 Go 依赖管理工具
        语义版本控制 (Semantic Versioning)
        版本、调度、存储及其它特性
        基本 Authentication, OAuth, JWT 等
        SOLID, YAGNI, KISS
        GIT
        HTTP/HTTPS
        数据结构与算法
        Scrum, 看板 (Kanban) 敏捷管理与项目管理
    c.命令行界面
        cobra
        urfave/cli
    d.Web框架和路由
        Echo
        Beego
        Gin
        Revel
        Chi
    e.对象关系映射,orm
        Gorm
        Xorm
    f.高速缓存,caching
        GCache
        Go-Redis
        GoMemcache
    g.分布式缓存,Distributed Cache
        Go-Redis
        GoMemcache
    h.实时通讯
        Melody
        Centrifugo
        graphql-go
        gqlgen
    i.API客户端,API Clients
        Gentleman
        GRequests
        Heimdall
    j.最好知道的库
        Validator
        Glow
        GJson
        Authboss
        Go-Underscore
    k.测试
        单元测试:
        Testify
        Ginkgo
        GoMega
        GoCheck
        -----------------------------------------------------------------------------------------------------
        模拟:
        GoMock
        -----------------------------------------------------------------------------------------------------
        行为测试:
        GoDog
        GoConvey
        GinkGo
        -----------------------------------------------------------------------------------------------------
        集成测试
        Testify
        -----------------------------------------------------------------------------------------------------
        端对端测试
        GinkGo
        Endly
        Selenium
    l.消息代理
        RabbitMQ
        Apache Kafka
        ActiveMQ
        Azure Service Bus
    m.微服务
        消息总线 (Message-Bus)
        消息代理
        框架:Go-Kit:Micro
    n.RPC
        Protocol Buffers
        gRPC-Go
        gRPC-gateway
        rpcx
    o.任务调度
        gron
        jobrunner
    q.Go模式
        Creational
        Structural
        Behavioral
        Synchronization
        Concurrency
        Messaging
        Stability

99.go语言为什么编译速度快
    a.go的优点
        编译速度、执行速度、内存管理以及并发编程
    b.静态编译和动态编译的区别
        静态编译:编译器在编译可执行文件时,要把使用到的链接库提取出来,链接打包进可执行文件中,编译结果只有一个可执行文件
        动态编译:可执行文件需要附带独立的库文件,不打包库到可执行文件中,减少可执行文件体积,在执行的时候再调用库即可
        -----------------------------------------------------------------------------------------------------
        两种方式有各自的优点和缺点,前者不需要去管理不同版本库的兼容性问题,后者可以减少内存和存储的占用
        (因为可以让不同程序共享同一个库),两种方式孰优孰弱,要对应到具体的工程问题上,Go默认的编译方式是静态编译。
    c.Go编译速度快主要四个原因:
        1.使用了import的引用管理方式
        2.没有模板的编译负担
        3.1.5版本后的自举编译器优化
        4.更少的关键字
        所以为了加快编译速度、放弃C++而转入Go的同时,也要考虑一下是否要放弃泛型编程的优点
    d.C++编译慢的主要两个原因:
        1.头文件的include方式
        2.模板的编译
        -----------------------------------------------------------------------------------------------------
        C++使用include方式引用头文件,会让需要编译的代码有乘数级的增加,例如当同一个头文件被同一个项目下的N个文件include时,
        编译器会将头文件引入到每一份代码中,所以同一个头文件会被编译N次(这在大多数时候都是不必要的)
        -----------------------------------------------------------------------------------------------------
        C++使用的模板是为了支持泛型编程,在编写对不同类型的泛型函数时,可以提供很大的便利,
        但是这对于编译器来说,会增加非常多不必要的编译负担。
        -----------------------------------------------------------------------------------------------------
        头文件的方式,import解决了重复编译的问题,当然Go也是使用的import方式;
        在模板的编译问题上,由于Go在设计理念上遵循从简入手,所以没有将泛函编程纳入到设计框架中,
        所以天生的没有模版编译带来的时间开销

1.12 附:语言特性

00.工具
    gofmt格式工具
    Stringer枚举类

01.主要特征
    输入、输出、缓冲
    init函数和main函数
    在Go中,导出和访问控制是通过命名来进行实现的,如果想要对外暴露一个函数或者一个变量,只需要将其名称首字母大写即可,例如example包下的SayHello函数。
    go中约定,一个包内名为internal 包为内部包,外部包将无法访问内部包中的任何内容,否则的话编译不通过
    在Go中所有的花括号都不应该换行。
    花括号,花括号在任何时候都不能够省略,就算是只有一行代码
    Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用

02.数据类型
    var nil Type,Go中的nil并不等同于其他语言的null,nil仅仅只是一些类型的零值,并且不属于任何类型,所以nil == nil这样的语句是无法通过编译的。
    Go 语言中不允许将整型强制转换为布尔型。布尔型无法参与数值运算,也无法与其他类型进行转换。默认False(0 false,1 true)

03.变量和常量
    常量的值无法被修改,否则无法通过编译
    一般在Go中,都是通过自定义类型 + const + iota来实现【枚举】,通过官方工具Stringer来自动生成枚举
    在go语言中,有一个规则,那就是所有在函数中的变量都必须要被使用,比如下面的代码只是声明了变量,但没有使用它,这个规则仅适用于函数内的变量,对于函数外的包级变量则没有这个限制,下面这个代码就可以通过编译。
    变量之间的比较有一个大前提,那就是它们之间的类型必须相同,go语言中不存在隐式类型转换
    在Go中,如果想要交换两个变量的值,不需要使用指针,可以使用赋值运算符直接进行交换

04.运算符
    有一点需要稍微注意下,go语言中没有选择将~作为取反运算符,而是复用了^符号,当两个数字使用^时,例如a^b,它就是异或运算符,只对一个数字使用时,例如^a,那么它就是取反运算符。go也支持增强赋值运算符
    Go语言中没有自增与自减运算符,它们被降级为了语句statement,并且规定了只能位于操作数的后方,所以不用再去纠结i++和++i这样的问题。a++ // 正确  ++a // 错误 a-- // 正确  还有一点就是,它们不再具有返回值,因此a = b++这类语句的写法是错误的。

05.流程控制
    条件语句if:不支持三元操作符(三目运算符) "a > b ? a : b"
    普通switch:Golang switch 分支表达式可以是任意类型,不限于常量。可省略 break,默认自动终止。
    type-switch:switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。
    select语句:select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。每个case必须是一个通信操作,要么是发送要么是接收。select 随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。
    循环语句for:for init; condition; post { }
                for condition { }
                for { }
                for [condition |  ( init; condition; increment ) | Range] {
                   for [condition |  ( init; condition; increment ) | Range] {
                      statement(s)
                   }
                   statement(s)
                }
    循环语包range:遍历字符串(String)、数组(Array)、切片(Slice)、字典(Map)、通道(Channel)、range遍历时修改数据
    循环控制Goto、Break.Continue:三个语句都可以配合标签(label)使用

1.13 附:版本特性

01.Go 1.24.0
    a.总体
        Swiss table的引入,Weak等新包
    b.语言层面的变更
        Go 1.24 现在完全支持泛型类型别名:类型别名可以像定义的类型一样被参数化。详情请参见语言规范。
        目前,可以通过设置 GOEXPERIMENT=noaliastypeparams 禁用该功能;
        但 aliastypeparams 设置将在 Go 1.25 中移除。
    c.Runtime
        运行时的多项性能改进使 CPU 开销在一系列具有代表性的基准测试中平均降低了 2-3%。
        结果可能因应用而异。这些改进包括基于 Swiss Tables 的新内置 map 实现、
        更高效的小对象内存分配以及新的运行时内部互斥实现。
    d.编译器
        编译器已经禁止使用 cgo 生成的接收器类型定义新方法,但可以通过别名类型规避这一限制。
        现在,如果接收器直接或间接(通过别名类型)表示 cgo 生成的类型,Go 1.24 总是会报错。
    e.标准库
        a.有目录限制的文件系统访问
            新的 os.Root 类型提供了在特定目录内执行文件系统操作的能力。
            os.OpenRoot 函数打开一个目录,并返回一个 os.Root 。os.Root 上的方法在目录内操作,
            不允许路径指向目录外的位置,包括目录外的符号链接。os.Root 上的方法反映了 os 包中的大多数文件系统操作,
            例如包括 os.Root.Open 、 os.Root.Create 、 os.Root.Mkdir 和 os.Root.Stat 。
        b.新基准方法
            基准现在可以使用速度更快、更不易出错的 testing.B.Loop 方法来执行基准迭代,
            如 for b.Loop() { ... } ,以取代涉及 b.N 的典型循环结构,如 for range b.N 。这提供了两个重要优势:
            基准函数每个 -count 将精确执行一次,因此昂贵的设置和清理步骤只执行一次。
            函数调用的参数和结果会保持不变,从而防止编译器对循环体进行完全优化。
        c.Improved finalizers
            新的 runtime.AddCleanup 函数是一种终结机制,它比 runtime.SetFinalizer 更灵活、更高效、更不易出错。
            AddCleanup 为对象附加了一个清理函数,一旦对象不再可访问,该函数就会运行。
            但是,与 SetFinalizer 不同的是,一个对象可以附加多个清理函数,清理函数可以附加到内部指针,
            当对象形成循环时,清理函数一般不会导致泄漏,而且清理函数不会延迟释放对象或其指向的对象。
            新代码应首选 AddCleanup 而不是 SetFinalizer 。
        d.New weak 包
            新的 weak 包提供了弱指针。
            弱指针是一种低级原语,用于创建节省内存的结构,例如用于关联值的弱映射、用于包 unique
            未涵盖内容的规范化映射以及各种缓存。为了支持这些用例,本版本还提供了 runtime.AddCleanup
            和 maphash.Comparable 。
        e.New crypto/mlkem 包
            新的 crypto/mlkem 软件包实现了 ML-KEM-768 和 ML-KEM-1024。
            ML-KEM 是一种后量子密钥交换机制,以前称为 Kyber,在 FIPS 203 中做了规定。
        f.新的 crypto/hkdf、crypto/pbkdf2 和 crypto/sha3 包
            新的 crypto/hkdf 软件包实现了 RFC 5869 中定义的基于 HMAC 的提取和展开密钥推导函数 HKDF。
            新的 crypto/pbkdf2 软件包实现了 RFC 8018 中定义的基于密码的密钥推导函数 PBKDF2。
            新的 crypto/sha3 软件包实现了 FIPS 202 中定义的 SHA-3 哈希函数以及 SHAKE 和 cSHAKE 可扩展输出函数。
            所有这三个软件包都基于已有的 golang.org/x/crypto/... 软件包。

2 容器

2.1 汇总

01.常用信息1
    a.数字Math
        math
        gonum
        decimal
    b.字符串String
        在Go语言中,string 是一种内建的数据类型,表示一段不可变的字节序列。
        在Go中,字符串本质上是一个不可变的只读的字节数组,也是一片连续的内存空间
        字面量:普通字符串由""双引号表示,支持转义,不支持多行书写
        访问:因为字符串本质是字节数组,所以字符串的访问形式跟数组切片完全一致,例如访问字符串第一个元素,输出是字节而不是字符
        转换:字符串可以转换为字节切片,而字节切片或字节数组也可以转换为字符串,字符串的内容是只读的不可变的,无法修改,但是字节切片是可以修改的
        长度:字符串的长度,其实并不是字面量的长度,而是字节数组的长度,只是大多数时候都是ANSCII字符,刚好能用一个字节表示,所以恰好与字面量长度相等,求字符串长度使用内置函数len
        拷贝:类似数组切片的拷贝方式,字符串拷贝其实是字节切片拷贝,使用内置函数copy,也可以使用strings.clone函数
        拼接:字符串的拼接使用+操作符,也可以转换为字节切片再进行添加元素,可以使用strings.Builder
    c.数组Array
        数组是定长的数据结构,长度被指定后就不能被改变
        切割数组的格式为arr[startIndex:endIndex],切割的区间为左闭右开
        在数组初始化时,需要注意的是,长度必须为一个常量表达式,否则将无法通过编译,const length = 5,var nums [length]int // 常量
        ---------------------------------------------------------------------------------------------------------
        全局:
        var arr0 [5]int = [5]int{1, 2, 3}
        var arr1 = [5]int{1, 2, 3, 4, 5}
        var arr2 = [...]int{1, 2, 3, 4, 5, 6}
        var str = [5]string{3: "hello world", 4: "tom"}
        局部:
        a := [3]int{1, 2}           // 未初始化元素值为 0。
        b := [...]int{1, 2, 3, 4}   // 通过初始化值确定数组长度。
        c := [5]int{2: 100, 4: 200} // 使用索引号初始化元素。
        d := [...]struct {
            name string
            age  uint8
        }{
            {"user1", 10}, // 可省略元素类型。
            {"user2", 20}, // 别忘了最后一行的逗号。
        }
    d.切片Slice:动态数组
        切片是不定长的,切片在容量不够时会自行扩容
        ---------------------------------------------------------------------------------------------------------
        var nums []int // 值
        nums := []int{1, 2, 3} // 值
        nums := make([]int, 0, 0) // 值      make函数接收三个参数:类型,长度,容量
        nums := new([]int) // 指针
    e.字典Map
        在Go中,map的实现是基于哈希桶(也是一种哈希表),所以也是无序的
        在Go语言中的 map 是无序的,键是唯一的,每个键只能映射到一个值。
        ---------------------------------------------------------------------------------------------------------
        sync.Map
        在go1.21之前,想要清空map,就只能对每一个map的key进行delete;但是go1.21更新了clear函数,就不用再进行之前的操作了,只需要一个clear就可以清空
        map对于不存的键其返回值是对应类型的零值,并且在访问map的时候其实有两个返回值,第一个返回值对应类型的值,第二个返回值一个布尔值,代表键是否存在
    f.堆container/heap
        在Go语言中,container/heap 包提供了一个堆(Heap)数据结构的实现,支持优先队列的功能。
        堆是一种特殊的完全二叉树,满足堆性质:最大堆的每个节点的值都大于等于其子节点的值,
        而最小堆的每个节点的值都小于等于其子节点的值。
    g.列表container/list
        在Go语言中,List 通常指的是一个双向链表,它由 container/list 包提供。
        List 是一种链式数据结构,允许在两端高效地插入和删除元素
    h.环形缓冲区container/ring
        在Go语言中,container/ring 包提供了一个环形缓冲区(Ring Buffer)数据结构的实现。
        环形缓冲区是一种循环使用的缓冲区,当写入超出其大小时,会覆盖最早的数据。
        它适用于需要固定大小且高效管理数据的场景,如流处理、生产者-消费者问题等。
        ---------------------------------------------------------------------------------------------------------
        container/ring 包中的 Ring 类型实现了一个双向链表,其中每个节点都有一个指向下一个节点和上一个节点的指针。
        最重要的特性是它的“环形”结构,即链表的尾节点指向头节点,头节点指向尾节点。
    i.指针Pointer
        区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。
        要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。
        ---------------------------------------------------------------------------------------------------------
        Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。
        传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。
        Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据地址取值)。
        ---------------------------------------------------------------------------------------------------------
        Go保留了指针,在一定程度上保证了性能,同时为了更好的GC和安全考虑,又限制了指针的使用。
    j.结构体Struct:替代类
        结构体可以存储一组不同类型的数据,是一种复合类型。Go抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,
        Go并非是一个传统OOP的语言,但是Go依旧有着OOP的影子,通过结构体和方法也可以模拟出一个类。
        type Programmer struct {
            Name     string
            Age      int
            Job      string
            Language []string
        }

02.常用信息2
    a.数组(Array)
        概述: 固定长度的序列,存储相同类型的元素。
        特点: 一旦定义长度就不能改变,适用于少量且固定数量的数据存储。
        示例:
        var arr [5]int
        arr[0] = 10
        fmt.Println(arr)
    b.切片(Slice)
        概述: 动态长度的数组,可以基于数组或其他切片创建。
        特点: 切片是 Go 中最常用的容器类型,支持动态增长,且容量可以超过原始数组。
        示例:
        s := []int{1, 2, 3, 4}
        s = append(s, 5)
        fmt.Println(s)
    c.字典(Map)
        概述: 基于键值对的数据结构,键是唯一的。
        特点: 非常高效的查找、插入、删除操作,适用于需要快速访问和存储数据的场景。
        示例:
        m := map[string]int{"apple": 5, "banana": 7}
        m["cherry"] = 9
        fmt.Println(m)
    d.通道(Channel)
        概述: Go 中用于在不同 Goroutine 之间传递数据的通信机制。
        特点: 支持同步和异步通信,常用于并发编程。
        示例:
        ch := make(chan int)
        go func() {
            ch <- 1
        }()
        value := <-ch
        fmt.Println(value)
    e.集合(Set)(Go语言中没有内置集合类型,但可以使用 map 模拟)
        概述: 存储唯一元素的无序集合。
        特点: 适用于不允许重复元素的场景。
        示例:
        set := make(map[string]struct{})
        set["apple"] = struct{}{}
        set["banana"] = struct{}{}
        fmt.Println(set)
    f.堆栈(Stack)(通过切片或自定义类型实现)
        概述: 后进先出(LIFO)数据结构。
        特点: 适用于递归计算或历史记录的场景。
        示例:
        var stack []int
        stack = append(stack, 1)    // Push
        top := stack[len(stack)-1]  // Peek
        stack = stack[:len(stack)-1]// Pop
        fmt.Println(top)
    g.队列(Queue)(通过切片或自定义类型实现)
        概述: 先进先出(FIFO)数据结构。
        特点: 适用于按顺序处理任务的场景。
        示例:
        var queue []int
        queue = append(queue, 1)    // Enqueue
        front := queue[0]           // Peek
        queue = queue[1:]           // Dequeue
        fmt.Println(front)
    h.双端队列(Deque)(可以通过切片或第三方库实现)
        概述: 可以在两端插入和删除元素的队列。
        特点: 适用于需要从两端处理数据的场景。
    i.环形缓冲区(Circular Buffer)(可以通过切片或自定义类型实现)
        概述: 固定大小的缓冲区,当写入数据超过缓冲区大小时会覆盖最早的数据。
        特点: 适用于需要固定内存使用的场景,如流处理。
    j.链表(Linked List)(通过自定义类型实现)
        概述: 一种由节点组成的数据结构,每个节点包含数据和指向下一个节点的指针。
        特点: 动态调整大小,适用于频繁插入和删除操作的场景。
    k.树(Tree)(通过自定义类型或第三方库实现)
        概述: 一种分层数据结构,由节点组成,常见的有二叉树、B树等。
        特点: 适用于需要快速查找、有层次结构的数据。
    l.图(Graph)(通过自定义类型或第三方库实现)
        概述: 一种由节点和边组成的数据结构,适用于表示网络、关系等。
        特点: 适用于表示复杂关系和路径查找的场景。
    m.稀疏数组(Sparse Array)(通过自定义类型或第三方库实现)
        概述: 一种主要存储非零元素的数组结构,节省内存空间。
        特点: 适用于数据稀疏的场景,如图像处理中的矩阵。
    n.位集合(Bit Set)(通过自定义类型或第三方库实现)
        概述: 使用位表示集合中的元素,适合大规模布尔值集合的存储和操作。
        特点: 高效的内存使用,适用于布尔矩阵或大规模数据筛选。
    o.优先队列(Priority Queue)(可以通过堆或第三方库实现)
        概述: 一种在插入数据时按优先级排序的队列。
        特点: 适用于需要动态维护最小或最大元素的场景。

2.2 数字Math

00.汇总
    在Go语言中,处理数字和进行各种计算的库和工具非常丰富,
    涵盖了基本的算术运算、高级数学函数、随机数生成、科学计算、统计分析、分布模拟等多个领域。

01.Go 标准库中的数字处理库
    a.math
        提供了基本的数学函数,如三角函数、对数函数、指数函数、平方根、绝对值等。
        常用函数:
        math.Abs(x float64) float64:计算绝对值。
        math.Sqrt(x float64) float64:计算平方根。
        math.Pow(x, y float64) float64:计算x的y次幂。
        math.Sin(x float64) float64、math.Cos(x float64) float64、math.Tan(x float64) float64:计算三角函数。
        math.Log(x float64) float64:计算自然对数。
        math.Exp(x float64) float64:计算e的x次方。
        math.Ceil(x float64) float64、math.Floor(x float64) float64:向上/向下取整。
    b.math/big
        支持任意精度的整数和浮点数运算。
        常用类型:
        big.Int:任意精度的整数。
        big.Float:任意精度的浮点数。
        big.Rat:有理数。
    c.math/rand
        用于生成伪随机数。
        常用函数:
        rand.Intn(n int) int:返回介于[0, n)之间的随机整数。
        rand.Float64() float64:返回介于[0.0, 1.0)之间的随机浮点数。
        rand.Seed(seed int64):设置随机数生成器的种子。
    d.math/cmplx
        提供了复数的数学运算,如复数的求模、求角度、计算指数等。
        常用函数:
        cmplx.Abs(x complex128) float64:计算复数的模。
        cmplx.Phase(x complex128) float64:计算复数的相角。
        cmplx.Sqrt(x complex128) complex128:计算复数的平方根。
    e.strconv
        提供了数字和字符串之间的相互转换。
        常用函数:
        strconv.Atoi(s string) (int, error):将字符串转换为整数。
        strconv.Itoa(i int) string:将整数转换为字符串。
        strconv.ParseFloat(s string, bitSize int) (float64, error):将字符串转换为浮点数。
        strconv.FormatFloat(f float64, fmt byte, prec, bitSize int) string:将浮点数格式化为字符串。

02.常用的第三方库
    a.gonum
        是一个功能强大的数值计算库,支持线性代数、微积分、概率和统计、优化等多种数值运算。
        子模块:
        gonum/mat:矩阵操作与线性代数。
        gonum/stat:统计与概率分布。
        gonum/diff:微分与积分。
        gonum/optimize:优化算法。
    b.decimal
        提供任意精度的十进制浮点数运算。
        常用方法:
        NewFromFloat(f float64) Decimal:从float64创建一个Decimal对象。
        Add(d Decimal) Decimal、Sub(d Decimal) Decimal:执行加法和减法运算。
        Mul(d Decimal) Decimal、Div(d Decimal) Decimal:执行乘法和除法运算。
    c.gmp
        Go语言的一个库,用于与GMP库(GNU多精度计算库)进行绑定,实现大整数的高效计算。
    d.stats
        提供基础的统计计算功能,如均值、方差、标准差、回归分析等。
    e.numgo
        类似于Python的NumPy,支持多维数组和矩阵操作,适合科学计算和数据分析。

2.3 字符串String1

00.汇总
    在Go语言中,string 是一种内建的数据类型,表示一段不可变的字节序列。
    虽然字符串不可变,但可以通过字符串的下标操作符来访问其中的字节,并且可以对字符串进行多种操作。

01.字符串基本操作
    长度操作        len(s string) int:返回字符串的字节长度。
    访问操作        s[i]:访问字符串中第i个字节。
    字符串切片      s[start:end]:返回从start到end-1的子字符串。

02.字符串连接
    +:使用+操作符可以连接两个字符串。
    strings.Join(a []string, sep string) string:将字符串切片a中的所有元素连接成一个字符串,以sep为分隔符。
    使用strings.Builder

03.字符串比较
    ==:检查两个字符串是否相等。
    !=:检查两个字符串是否不相等。
    strings.Compare(a, b string) int:按字典顺序比较两个字符串。

04.字符串包含和查找
    strings.Contains(s, substr string) bool:检查字符串s是否包含子字符串substr。
    strings.ContainsAny(s, chars string) bool:检查字符串s是否包含chars中的任一字符。
    strings.ContainsRune(s string, r rune) bool:检查字符串s是否包含指定的Unicode字符r。
    strings.Index(s, substr string) int:返回子字符串substr在字符串s中首次出现的索引。
    strings.IndexAny(s, chars string) int:返回chars中任一字符在字符串s中首次出现的索引。
    strings.IndexByte(s string, c byte) int:返回字节c在字符串s中首次出现的索引。
    strings.IndexRune(s string, r rune) int:返回Unicode字符r在字符串s中首次出现的索引。
    strings.LastIndex(s, substr string) int:返回子字符串substr在字符串s中最后一次出现的索引。
    strings.LastIndexAny(s, chars string) int:返回chars中任一字符在字符串s中最后一次出现的索引。
    strings.LastIndexByte(s string, c byte) int:返回字节c在字符串s中最后一次出现的索引。

05.字符串前缀和后缀检查
    strings.HasPrefix(s, prefix string) bool:检查字符串s是否以prefix为前缀。
    strings.HasSuffix(s, suffix string) bool:检查字符串s是否以suffix为后缀。

06.字符串修剪
    strings.TrimSpace(s string) string:去掉字符串s开头和结尾的空白字符。
    strings.Trim(s, cutset string) string:去掉字符串s开头和结尾的cutset中的字符。
    strings.TrimLeft(s, cutset string) string:去掉字符串s开头的cutset中的字符。
    strings.TrimRight(s, cutset string) string:去掉字符串s结尾的cutset中的字符。
    strings.TrimPrefix(s, prefix string) string:去掉字符串s的前缀prefix。
    strings.TrimSuffix(s, suffix string) string:去掉字符串s的后缀suffix。

07.字符串替换
    strings.Replace(s, old, new string, n int) string:将字符串s中前n个old子字符串替换为new。
    strings.ReplaceAll(s, old, new string) string:将字符串s中所有old子字符串替换为new。

08.字符串分割
    strings.Split(s, sep string) []string:按分隔符sep将字符串s分割为子字符串切片。
    strings.SplitAfter(s, sep string) []string:按分隔符sep将字符串s分割为子字符串切片,分隔符也会包含在结果中。
    strings.SplitN(s, sep string, n int) []string:按分隔符sep将字符串s分割为最多n个子字符串。
    strings.SplitAfterN(s, sep string, n int) []string:按分隔符sep将字符串s分割为最多n个子字符串,分隔符也会包含在结果中。

09.字符串重复
    strings.Repeat(s string, count int) string:返回字符串s重复count次的结果。

10.字符串大小写转换
    strings.ToLower(s string) string:将字符串s转换为小写。
    strings.ToUpper(s string) string:将字符串s转换为大写。
    strings.ToTitle(s string) string:将字符串s转换为标题格式。

11.字符串处理
    strings.Fields(s string) []string:将字符串s按照空白字符分割成若干个子字符串。
    strings.FieldsFunc(s string, f func(rune) bool) []string:根据用户定义的分割函数f将字符串s分割成若干个子字符串。
    strings.Map(mapping func(rune) rune, s string) string:根据用户定义的mapping函数,将字符串s中的字符映射为新字符串。
    strings.Count(s, substr string) int:返回子字符串substr在字符串s中出现的次数。

12.字符串转换
    strconv.Itoa(i int) string:将整数i转换为字符串。
    strconv.Atoi(s string) (int, error):将字符串s转换为整数。
    strconv.ParseFloat(s string, bitSize int) (float64, error):将字符串s解析为浮点数。

13.字符串构建
    strings.Builder:用于高效地构建字符串。

14.其他操作
    strings.Cut(s, sep string) (before, after string, found bool):将字符串s按sep分割为两个子字符串并返回,found表示是否找到分隔符。
    strings.EqualFold(s, t string) bool:忽略大小写比较两个字符串是否相等。

15.正则表达式相关操作
    regexp.MatchString(pattern, s string) (bool, error):判断字符串s是否匹配正则表达式pattern。
    regexp.Compile(pattern string) (*regexp.Regexp, error):编译正则表达式pattern并返回Regexp对象,提供丰富的正则表达式操作方法。

2.4 字符串String2

00.总结
    在Go语言中,string 是一种内建的数据类型,表示一段不可变的字节序列。
    在Go中,字符串本质上是一个不可变的只读的字节数组,也是一片连续的内存空间
    字面量:普通字符串由""双引号表示,支持转义,不支持多行书写
    访问:因为字符串本质是字节数组,所以字符串的访问形式跟数组切片完全一致,例如访问字符串第一个元素,输出是字节而不是字符
    转换:字符串可以转换为字节切片,而字节切片或字节数组也可以转换为字符串,字符串的内容是只读的不可变的,无法修改,但是字节切片是可以修改的
    长度:字符串的长度,其实并不是字面量的长度,而是字节数组的长度,只是大多数时候都是ANSCII字符,刚好能用一个字节表示,所以恰好与字面量长度相等,求字符串长度使用内置函数len
    拷贝:类似数组切片的拷贝方式,字符串拷贝其实是字节切片拷贝,使用内置函数copy,也可以使用strings.clone函数
    拼接:字符串的拼接使用+操作符,也可以转换为字节切片再进行添加元素,可以使用strings.Builder

01.访问
    a.因为字符串本质是字节数组,所以字符串的访问形式跟数组切片完全一致,例如访问字符串第一个元素
        func main() {
           str := "this is a string"
           fmt.Println(str[0])
        }
        输出是字节而不是字符
        116
    b.切割字符串
        func main() {
           str := "this is a string"
           fmt.Println(string(str[0:4]))
        }

        this
    c.尝试修改字符串元素
        func main() {
           str := "this is a string"
           str[0] = 'a' // 无法通过编译
           fmt.Println(str)
        }
        main.go:7:2: cannot assign to str[0] (value of type byte)
    d.虽然没法修改字符串,但是可以覆盖
        func main() {
           str := "this is a string"
           str = "that is a string"
           fmt.Println(str)
        }
        that is a string

02.转换
    a.字符串可以转换为字节切片,而字节切片或字节数组也可以转换为字符串,例子如下:
        func main() {
           str := "this is a string"
           // 显式类型转换为字节切片
           bytes := []byte(str)
           fmt.Println(bytes)
           // 显式类型转换为字符串
           fmt.Println(string(bytes))
        }
    b.字符串的内容是只读的不可变的,无法修改,但是字节切片是可以修改的。
        func main() {
            str := "this is a string"
            fmt.Println(&str)
            bytes := []byte(str)
            // 修改字节切片
            bytes = append(bytes, 96, 97, 98, 99)
            // 赋值给原字符串
            str = string(bytes)
            fmt.Println(str)
        }
    c.注意
        将字符串转换成字节切片以后,两者之间毫无关联,因为Go会新分配一片内存空间给字节切片,
        再将字符串的内存复制过去,对字节切片进行修改不会对原字符串产生任何影响,这么做是为了内存安全。
        在这种情况下,如果要转换的字符串或字节切片很大,那么性能开销就会很高。
        不过你也可以通过unsafe库来实现无复制转换,不过背后的安全问题需要自己承担,
        -----------------------------------------------------------------------------------------------------
        比如下面的例子,b1和s1的地址是一样的。
        func main() {
            s1 := "hello world"
            b1 := unsafe.Slice(unsafe.StringData(s1), len(s1))
            fmt.Printf("%p %p", unsafe.StringData(s1), unsafe.SliceData(b1))
        }
        0xe27bb2 0xe27bb2

03.长度
    a.字符串的长度,其实并不是字面量的长度,而是字节数组的长度,只是大多数时候都是ANSCII字符,
      刚好能用一个字节表示,所以恰好与字面量长度相等,求字符串长度使用内置函数len,例子如下:
        func main() {
           str := "this is a string" // 看起来长度是16
           str2 := "这是一个字符串" // 看起来长度是7
           fmt.Println(len(str), len(str2))
        }
        16 21
    b.看起来中文字符串比英文字符串短,但是实际求得的长度却比英文字符串长。
      这是因为在unicode编码中,一个汉字在大多数情况下占3个字节,一个英文字符只占一个字节,
      通过输出字符串第一个元素可以看出结果:
        func main() {
           str := "this is a string"
           str2 := "这是一个字符串"
           fmt.Println(string(str[0]))
           fmt.Println(string(str2[0]))
           fmt.Println(string(str2[0:3]))
        }
        t // 字母t
        è // 意大利语
        这 // 中文汉字

04.拷贝
    a.类似数组切片的拷贝方式,字符串拷贝其实是字节切片拷贝,使用内置函数copy
        func main() {
           var dst, src string
           src = "this is a string"
           desBytes := make([]byte, len(src))
           copy(desBytes, src)
           dst = string(desBytes)
           fmt.Println(src, dst)
        }
    b.也可以使用strings.clone函数,但其实内部实现都差不多
        func main() {
           var dst, src string
           src = "this is a string"
           dst = strings.Clone(src)
           fmt.Println(src, dst)
        }

05.拼接
    a.字符串的拼接使用+操作符
        func main() {
           str := "this is a string"
           str = str + " that is a int"
           fmt.Println(str)
        }
    b.也可以转换为字节切片再进行添加元素
        func main() {
           str := "this is a string"
           bytes := []byte(str)
           bytes = append(bytes, "that is a int"...)
           str = string(bytes)
           fmt.Println(str)
        }
    c.以上两种拼接方式性能都很差,一般情况下可以使用,但如果对应性能有更高要求,可以使用strings.Builder
        func main() {
           builder := strings.Builder{}
           builder.WriteString("this is a string ")
           builder.WriteString("that is a int")
           fmt.Println(builder.String())
        }
        this is a string that is a int

06.遍历
    在本文开头就已经提到过,Go中的字符串就是一个只读的字节切片,
    也就是说字符串的组成单位是字节而不是字符。这种情况经常会在遍历字符串时遇到,例如下方的代码
    ---------------------------------------------------------------------------------------------------------
    func main() {
        str := "hello world!"
        for i := 0; i < len(str); i++ {
            fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
        }
    }
    例子中分别输出了字节的十进制形式和十六进制形式。
    104,68,h
    101,65,e
    108,6c,l
    108,6c,l
    111,6f,o
    32,20,
    119,77,w
    111,6f,o
    114,72,r
    108,6c,l
    100,64,d
    33,21,!
    ---------------------------------------------------------------------------------------------------------
    由于例子中的字符都是属于ASCII字符,只需要一个字节就能表示,所以结果恰巧每一个字节对应一个字符。
    但如果包含非ASCII字符结果就不同了,如下
    func main() {
        str := "hello 世界!"
        for i := 0; i < len(str); i++ {
            fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
        }
    }
    ---------------------------------------------------------------------------------------------------------
    通常情况下,一个中文字符会占用3个字节,所以就可能会看到以下结果
    104,68,h
    101,65,e
    108,6c,l
    108,6c,l
    111,6f,o
    32,20,
    228,e4,ä
    184,b8,¸
    150,96,
    231,e7,ç
    149,95,
    140,8c,
    33,21,!
    ---------------------------------------------------------------------------------------------------------
    按照字节来遍历会把中文字符拆开,这显然会出现乱码。Go字符串是明确支持utf8的,
    应对这种情况就需要用到rune类型,在使用for range进行遍历时,其默认的遍历单位类型就是一个rune,例如下方代码
    func main() {
       str := "hello 世界!"
       for _, r := range str {
          fmt.Printf("%d,%x,%s\n", r, r, string(r))
       }
    }
    输出如下
    104,68,h
    101,65,e
    108,6c,l
    108,6c,l
    111,6f,o
    32,20,
    19990,4e16,世
    30028,754c,界
    33,21,!
    ---------------------------------------------------------------------------------------------------------
    rune本质上是int32的类型别名,unicode字符集的范围位于0x0000 - 0x10FFFF之间,
    最大也只有三个字节,合法的UTF8编码最大字节数只有4个字节,所以使用int32来存储是理所当然,
    上述例子中将字符串转换成[]rune再遍历也是一样的道理,如下
    func main() {
       str := "hello 世界!"
       runes := []rune(str)
       for i := 0; i < len(runes); i++ {
          fmt.Println(string(runes[i]))
       }
    }
    ---------------------------------------------------------------------------------------------------------
    还可以使用uft8包下的工具,例如
    func main() {
        str := "hello 世界!"
        for i, w := 0, 0; i < len(str); i += w {
            r, width := utf8.DecodeRuneInString(str[i:])
            fmt.Println(string(r))
            w = width
        }
    }
    这两个例子的输出都是相同的。

2.5 数组Array

00.总结
    a.数组的声明与初始化
        声明数组
        初始化数组
        使用短变量声明
    b.访问与修改数组元素
        访问数组元素
        修改数组元素
    c.遍历数组
        使用 for 循环遍历
        使用 range 循环遍历
    d.数组的长度与容量
        获取数组长度
        获取数组容量
    e.数组的复制
        复制数组(浅复制)
    f.数组的比较
        数组相等性比较
    g.多维数组
        声明多维数组
        访问与修改多维数组元素
        遍历多维数组
    h.数组切片操作
        切片(数组的子集)
        切片数组的一部分
    i.数组排序
        使用sort包对数组排序

01.Golang Array和以往认知的数组有很大不同。
    1.数组:是同一种数据类型的固定长度的序列。
    2.数组定义:var a [len]int,比如:var a [5]int,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。
    3.长度是数组类型的一部分,因此,var a[5] int和var a[10]int是不同的类型。
    4.数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1
        for i := 0; i < len(a); i++ {
        }
        for index, v := range a {
        }
    5.访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic
    6.数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
    7.支持 "=="、"!=" 操作符,因为内存总是被初始化过的。
    8.指针数组 [n]*T,数组指针 *[n]T。

02.数组初始化
    a.一维数组:
        a.定义
            全局:
            var arr0 [5]int = [5]int{1, 2, 3}
            var arr1 = [5]int{1, 2, 3, 4, 5}
            var arr2 = [...]int{1, 2, 3, 4, 5, 6}
            var str = [5]string{3: "hello world", 4: "tom"}
            局部:
            a := [3]int{1, 2}           // 未初始化元素值为 0。
            b := [...]int{1, 2, 3, 4}   // 通过初始化值确定数组长度。
            c := [5]int{2: 100, 4: 200} // 使用索引号初始化元素。
            d := [...]struct {
                name string
                age  uint8
            }{
                {"user1", 10}, // 可省略元素类型。
                {"user2", 20}, // 别忘了最后一行的逗号。
            }
        b.定义
            数组在声明是长度只能是一个常量,不能是变量
            // 正确示例
            var a [5]int

            // 错误示例
            l := 1
            var b [l]int
            -------------------------------------------------------------------------------------------------
            先来初始化一个长度为5的整型数组
            var nums [5]int
            -------------------------------------------------------------------------------------------------
            也可以用元素初始化
            nums := [5]int{1, 2, 3}
            -------------------------------------------------------------------------------------------------
            还可以通过new函数获得一个指针
            nums := new([5]int)
            以上几种方式都会给nums分配一片固定大小的内存,区别只是最后一种得到的值是指针。
        c.长度必须为一个常量表达式
            在数组初始化时,需要注意的是,长度必须为一个常量表达式,否则将无法通过编译,
            常量表达式即表达式的最终结果是一个常量,错误例子如下:
            length := 5 // 这是一个变量
            var nums [length]int
            -------------------------------------------------------------------------------------------------
            length是一个变量,因此无法用于初始化数组长度,如下是正确示例:
            const length = 5
            var nums [length]int // 常量
            var nums2 [length + 1]int // 常量表达式
            var nums3 [(1 + 2 + 3) * 5]int // 常量表达式
            var nums4 [5]int // 最常用的
        d.使用
            只要有数组名和下标,就可以访问数组中对应的元素。
            fmt.Println(nums[0])
            -------------------------------------------------------------------------------------------------
            同样的也可以修改数组元素
            nums[0] = 1
            -------------------------------------------------------------------------------------------------
            还可以通过内置函数len来访问数组元素的数量
            len(nums)
            -------------------------------------------------------------------------------------------------
            内置函数cap来访问数组容量,数组的容量等于数组长度,容量对于切片才有意义。
            cap(nums)
        e.切割
            切割数组的格式为arr[startIndex:endIndex],切割的区间为左闭右开,例子如下:
            nums := [5]int{1, 2, 3, 4, 5}
            nums[1:] // 子数组范围[1,5) -> [2 3 4 5]
            nums[:5] // 子数组范围[0,5) -> [1 2 3 4 5]
            nums[2:3] // 子数组范围[2,3) -> [3]
            nums[1:3] // 子数组范围[1,3) -> [2 3]
        f.代码:
            package main

            import (
                "fmt"
            )

            var arr0 [5]int = [5]int{1, 2, 3}
            var arr1 = [5]int{1, 2, 3, 4, 5}
            var arr2 = [...]int{1, 2, 3, 4, 5, 6}
            var str = [5]string{3: "hello world", 4: "tom"}

            func main() {
                a := [3]int{1, 2}           // 未初始化元素值为 0。
                b := [...]int{1, 2, 3, 4}   // 通过初始化值确定数组长度。
                c := [5]int{2: 100, 4: 200} // 使用引号初始化元素。
                d := [...]struct {
                    name string
                    age  uint8
                }{
                    {"user1", 10}, // 可省略元素类型。
                    {"user2", 20}, // 别忘了最后一行的逗号。
                }
                fmt.Println(arr0, arr1, arr2, str)
                fmt.Println(a, b, c, d)
            }
            -------------------------------------------------------------------------------------------------
            输出结果:
            [1 2 3 0 0] [1 2 3 4 5] [1 2 3 4 5 6] [   hello world tom]
            [1 2 0] [1 2 3 4] [0 0 100 0 200] [{user1 10} {user2 20}]
    b.多维数组
        a.介绍
            全局
            var arr0 [5][3]int
            var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
            局部:
            a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
            b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。
        b.代码:
            package main

            import (
                "fmt"
            )

            var arr0 [5][3]int
            var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}

            func main() {
                a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
                b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。
                fmt.Println(arr0, arr1)
                fmt.Println(a, b)
            }
            -------------------------------------------------------------------------------------------------
            输出结果:
            [[0 0 0] [0 0 0] [0 0 0] [0 0 0] [0 0 0]] [[1 2 3] [7 8 9]]
            [[1 2 3] [4 5 6]] [[1 1] [2 2] [3 3]]
        d.值拷贝行为会造成性能问题,通常会建议使用 slice,或数组指针。
            package main

            import (
                "fmt"
            )

            func test(x [2]int) {
                fmt.Printf("x: %p\n", &x)
                x[1] = 1000
            }

            func main() {
                a := [2]int{}
                fmt.Printf("a: %p\n", &a)

                test(a)
                fmt.Println(a)
            }
            -------------------------------------------------------------------------------------------------
            输出结果:
            a: 0xc42007c010
            x: 0xc42007c030
            [0 0]
        e.内置函数 len 和 cap 都返回数组长度 (元素数量)。
            package main

            func main() {
                a := [2]int{}
                println(len(a), cap(a))
            }
            -------------------------------------------------------------------------------------------------
            输出结果:
            2 2
    c.多维数组遍历:
        package main

        import (
            "fmt"
        )

        func main() {

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

            for k1, v1 := range f {
                for k2, v2 := range v1 {
                    fmt.Printf("(%d,%d)=%d ", k1, k2, v2)
                }
                fmt.Println()
            }
        }
        -----------------------------------------------------------------------------------------------------
        输出结果:
        (0,0)=1 (0,1)=2 (0,2)=3
        (1,0)=7 (1,1)=8 (1,2)=9

03.数组拷贝和传参
    package main

    import "fmt"

    func printArr(arr *[5]int) {
        arr[0] = 10
        for i, v := range arr {
            fmt.Println(i, v)
        }
    }

    func main() {
        var arr1 [5]int
        printArr(&arr1)
        fmt.Println(arr1)
        arr2 := [...]int{2, 4, 6, 8, 10}
        printArr(&arr2)
        fmt.Println(arr2)
    }

2.6 切片Slice:动态数组

00.总结
    a.特性
        slice并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案
        1.切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
        2.切片的长度可以改变,因此,切片是一个可变的数组。
        3.切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。
        4.cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array),其中array是slice引用的数组。
        5.切片的定义:var 变量名 []类型,比如 var str []string  var arr []int。
        6.如果 slice == nil,那么 len、cap 结果都等于 0。
    b.定义
        你可以声明一个未指定大小的数组来定义切片:
        var identifier []type
        切片不需要说明长度。
        -----------------------------------------------------------------------------------------------------
        或使用 make() 函数来创建切片:
        var slice1 []type = make([]type, len)
        也可以简写为
        slice1 := make([]type, len)
        -----------------------------------------------------------------------------------------------------
        也可以指定容量,其中 capacity 为可选参数。
        make([]T, length, capacity)
        这里 len 是数组的长度并且也是切片的初始长度。
    c.定义
        var nums []int // 值
        nums := []int{1, 2, 3} // 值
        nums := make([]int, 0, 0) // 值      make函数接收三个参数:类型,长度,容量
        nums := new([]int) // 指针
    d.定义
        s :=[] int {1,2,3 }
        直接初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3。
        -----------------------------------------------------------------------------------------------------
        s := arr[:]
        初始化切片 s,是数组 arr 的引用。
        -----------------------------------------------------------------------------------------------------
        s := arr[startIndex:endIndex]
        将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片。
        -----------------------------------------------------------------------------------------------------
        s := arr[startIndex:]
        默认 endIndex 时将表示一直到arr的最后一个元素。
        -----------------------------------------------------------------------------------------------------
        s := arr[:endIndex]
        默认 startIndex 时将表示从 arr 的第一个元素开始。
        -----------------------------------------------------------------------------------------------------
        s1 := s[startIndex:endIndex]
        通过切片 s 初始化切片 s1。
        -----------------------------------------------------------------------------------------------------
        s :=make([]int,len,cap)
        通过内置函数 make() 初始化切片s,[]int 标识为其元素类型为 int 的切片。
    e.插入元素
        切片元素的插入也是需要结合append函数来使用,现有切片如下,
        nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
        -----------------------------------------------------------------------------------------------------
        从头部插入元素
        nums = append([]int{-1, 0}, nums...)
        fmt.Println(nums) // [-1 0 1 2 3 4 5 6 7 8 9 10]
        -----------------------------------------------------------------------------------------------------
        从中间下标i插入元素
        nums = append(nums[:i+1], append([]int{999, 999}, nums[i+1:]...)...)
        fmt.Println(nums) // i=3,[1 2 3 4 999 999 5 6 7 8 9 10]
        -----------------------------------------------------------------------------------------------------
        从尾部插入元素,就是append最原始的用法
        nums = append(nums, 99, 100)
        fmt.Println(nums) // [1 2 3 4 5 6 7 8 9 10 99 100]
    f.删除元素
        切片元素的删除需要结合append函数来使用,现有如下切片
        nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
        -----------------------------------------------------------------------------------------------------
        从头部删除n个元素
        nums = nums[n:]
        fmt.Println(nums) //n=3 [4 5 6 7 8 9 10]
        -----------------------------------------------------------------------------------------------------
        从尾部删除n个元素
        nums = nums[:len(nums)-n]
        fmt.Println(nums) //n=3 [1 2 3 4 5 6 7]
        -----------------------------------------------------------------------------------------------------
        从中间指定下标i位置开始删除n个元素
        nums = append(nums[:i], nums[i+n:]...)
        fmt.Println(nums)// i=2,n=3,[1 2 6 7 8 9 10]
        -----------------------------------------------------------------------------------------------------
        删除所有元素
        nums = nums[:0]
        fmt.Println(nums) // []
    g.拷贝
        切片在拷贝时需要确保目标切片有足够的长度,例如
        func main() {
            dest := make([]int, 0)
            src := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
            fmt.Println(src, dest)
            fmt.Println(copy(dest, src))
            fmt.Println(src, dest)
        }
    h.遍历
        切片的遍历与数组完全一致,
        -----------------------------------------------------------------------------------------------------
        for循环
        func main() {
           slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
           for i := 0; i < len(slice); i++ {
              fmt.Println(slice[i])
           }
        }
        -----------------------------------------------------------------------------------------------------
        for range循环
        func main() {
            slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
            for index, val := range slice {
                fmt.Println(index, val)
            }
        }
    i.clear
        在go1.21新增了clear内置函数,clear会将切片内所有的值置为零值,
        package main

        import (
            "fmt"
        )

        func main() {
            s := []int{1, 2, 3, 4}
            clear(s)
            fmt.Println(s)
        }
        输出
        [0 0 0 0]
        -----------------------------------------------------------------------------------------------------
        如果想要清空切片,可以
        func main() {
            s := []int{1, 2, 3, 4}
            s = s[:0:0]
            fmt.Println(s)
        }
        限制了切割后的容量,这样可以避免覆盖原切片的后续元素。
    j.其他操作
        a.切片的声明与初始化
            使用 make 函数声明切片
            使用字面量初始化切片
            使用现有数组或切片创建新切片
            空切片与零值切片
        b.访问与修改切片元素
            访问切片元素
            修改切片元素
        c.切片的追加与扩展
            使用 append 函数追加元素
            追加多个元素
            追加另一个切片
            切片的容量扩展
        d.切片的复制
            使用 copy 函数复制切片
        e.切片的长度与容量
            获取切片长度
            获取切片容量
        f.切片的遍历
            使用 for 循环遍历
            使用 range 循环遍历
        g.切片的切割与重新切片
            从现有切片中切割出新切片
            动态调整切片范围
            零值切片切割
        h.切片的比较
            使用手动比较切片内容
        i.切片排序
            使用 sort 包对切片排序
            自定义排序规则
        j.切片与数组的关系
            将切片转换为数组
            从数组创建切片

01.创建切片的各种方式
    package main

    import "fmt"

    func main() {
       //1.声明切片
       var s1 []int
       if s1 == nil {
          fmt.Println("是空")
       } else {
          fmt.Println("不是空")
       }
       // 2.:=
       s2 := []int{}
       // 3.make()
       var s3 []int = make([]int, 0)
       fmt.Println(s1, s2, s3)
       // 4.初始化赋值
       var s4 []int = make([]int, 0, 0)
       fmt.Println(s4)
       s5 := []int{1, 2, 3}
       fmt.Println(s5)
       // 5.从数组切片
       arr := [5]int{1, 2, 3, 4, 5}
       var s6 []int
       // 前包后不包
       s6 = arr[1:4]
       fmt.Println(s6)
    }

02.切片初始化
    a.介绍
        全局:
        var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
        var slice0 []int = arr[start:end]
        var slice1 []int = arr[:end]
        var slice2 []int = arr[start:]
        var slice3 []int = arr[:]
        var slice4 = arr[:len(arr)-1]      //去掉切片的最后一个元素
        局部:
        arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
        slice5 := arr[start:end]
        slice6 := arr[:end]
        slice7 := arr[start:]
        slice8 := arr[:]
        slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素
    b.代码
        package main

        import (
            "fmt"
        )

        var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
        var slice0 []int = arr[2:8]
        var slice1 []int = arr[0:6]        //可以简写为 var slice []int = arr[:end]
        var slice2 []int = arr[5:10]       //可以简写为 var slice[]int = arr[start:]
        var slice3 []int = arr[0:len(arr)] //var slice []int = arr[:]
        var slice4 = arr[:len(arr)-1]      //去掉切片的最后一个元素
        func main() {
            fmt.Printf("全局变量:arr %v\n", arr)
            fmt.Printf("全局变量:slice0 %v\n", slice0)
            fmt.Printf("全局变量:slice1 %v\n", slice1)
            fmt.Printf("全局变量:slice2 %v\n", slice2)
            fmt.Printf("全局变量:slice3 %v\n", slice3)
            fmt.Printf("全局变量:slice4 %v\n", slice4)
            fmt.Printf("-----------------------------------\n")
            arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
            slice5 := arr[2:8]
            slice6 := arr[0:6]         //可以简写为 slice := arr[:end]
            slice7 := arr[5:10]        //可以简写为 slice := arr[start:]
            slice8 := arr[0:len(arr)]  //slice := arr[:]
            slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素
            fmt.Printf("局部变量: arr2 %v\n", arr2)
            fmt.Printf("局部变量: slice5 %v\n", slice5)
            fmt.Printf("局部变量: slice6 %v\n", slice6)
            fmt.Printf("局部变量: slice7 %v\n", slice7)
            fmt.Printf("局部变量: slice8 %v\n", slice8)
            fmt.Printf("局部变量: slice9 %v\n", slice9)
        }
        -----------------------------------------------------------------------------------------------------
        输出结果:
        全局变量:arr [0 1 2 3 4 5 6 7 8 9]
        全局变量:slice0 [2 3 4 5 6 7]
        全局变量:slice1 [0 1 2 3 4 5]
        全局变量:slice2 [5 6 7 8 9]
        全局变量:slice3 [0 1 2 3 4 5 6 7 8 9]
        全局变量:slice4 [0 1 2 3 4 5 6 7 8]
        -----------------------------------
        局部变量: arr2 [9 8 7 6 5 4 3 2 1 0]
        局部变量: slice5 [2 3 4 5 6 7]
        局部变量: slice6 [0 1 2 3 4 5]
        局部变量: slice7 [5 6 7 8 9]
        局部变量: slice8 [0 1 2 3 4 5 6 7 8 9]
        局部变量: slice9 [0 1 2 3 4 5 6 7 8]

03.通过make来创建切片
    a.介绍
        var slice []type = make([]type, len)
        slice  := make([]type, len)
        slice  := make([]type, len, cap)
    b.代码
        package main

        import (
            "fmt"
        )

        var slice0 []int = make([]int, 10)
        var slice1 = make([]int, 10)
        var slice2 = make([]int, 10, 10)

        func main() {
            fmt.Printf("make全局slice0 :%v\n", slice0)
            fmt.Printf("make全局slice1 :%v\n", slice1)
            fmt.Printf("make全局slice2 :%v\n", slice2)
            fmt.Println("--------------------------------------")
            slice3 := make([]int, 10)
            slice4 := make([]int, 10)
            slice5 := make([]int, 10, 10)
            fmt.Printf("make局部slice3 :%v\n", slice3)
            fmt.Printf("make局部slice4 :%v\n", slice4)
            fmt.Printf("make局部slice5 :%v\n", slice5)
        }
        -----------------------------------------------------------------------------------------------------
        输出结果:
        make全局slice0 :[0 0 0 0 0 0 0 0 0 0]
        make全局slice1 :[0 0 0 0 0 0 0 0 0 0]
        make全局slice2 :[0 0 0 0 0 0 0 0 0 0]
        --------------------------------------
        make局部slice3 :[0 0 0 0 0 0 0 0 0 0]
        make局部slice4 :[0 0 0 0 0 0 0 0 0 0]
        make局部slice5 :[0 0 0 0 0 0 0 0 0 0]

04.用append内置函数操作切片(切片追加)
    a.示例1
        package main

        import (
            "fmt"
        )

        func main() {

            var a = []int{1, 2, 3}
            fmt.Printf("slice a : %v\n", a)
            var b = []int{4, 5, 6}
            fmt.Printf("slice b : %v\n", b)
            c := append(a, b...)
            fmt.Printf("slice c : %v\n", c)
            d := append(c, 7)
            fmt.Printf("slice d : %v\n", d)
            e := append(d, 8, 9, 10)
            fmt.Printf("slice e : %v\n", e)

        }
        -----------------------------------------------------------------------------------------------------
        输出结果:

            slice a : [1 2 3]
            slice b : [4 5 6]
            slice c : [1 2 3 4 5 6]
            slice d : [1 2 3 4 5 6 7]
            slice e : [1 2 3 4 5 6 7 8 9 10]
    b.示例2:append向 slice 尾部添加数据,返回新的 slice 对象。
        package main

        import (
            "fmt"
        )

        func main() {

            s1 := make([]int, 0, 5)
            fmt.Printf("%p\n", &s1)

            s2 := append(s1, 1)
            fmt.Printf("%p\n", &s2)

            fmt.Println(s1, s2)

        }
        -----------------------------------------------------------------------------------------------------
        输出结果:
        0xc42000a060
        0xc42000a080
        [] [1]

05.超出原 slice.cap 限制,就会重新分配底层数组,即便原数组并未填满。
    a.示例
        package main

        import (
            "fmt"
        )

        func main() {

            data := [...]int{0, 1, 2, 3, 4, 10: 0}
            s := data[:2:3]

            s = append(s, 100, 200) // 一次 append 两个值,超出 s.cap 限制。

            fmt.Println(s, data)         // 重新分配底层数组,与原数组无关。
            fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。

        }
        -----------------------------------------------------------------------------------------------------
        输出结果:
        [0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0]
        0xc4200160f0 0xc420070060
    b.总结
        从输出结果可以看出,append 后的 s 重新分配了底层数组,并复制数据。
        如果只追加一个值,则不会超过 s.cap 限制,也就不会重新分配。
        通常以 2 倍容量重新分配底层数组。在大批量添加数据时,建议一次性分配足够大的空间,以减少内存分配和数据复制开销。
        或初始化足够长的 len 属性,改用索引号进行操作。及时释放不再使用的 slice 对象,避免持有过期数组,造成 GC 无法回收。

06.slice中cap重新分配规律
    a.示例
        package main

        import (
            "fmt"
        )

        func main() {

            s := make([]int, 0, 1)
            c := cap(s)

            for i := 0; i < 50; i++ {
                s = append(s, i)
                if n := cap(s); n > c {
                    fmt.Printf("cap: %d -> %d\n", c, n)
                    c = n
                }
            }

        }
        -----------------------------------------------------------------------------------------------------
        输出结果:
        cap: 1 -> 2
        cap: 2 -> 4
        cap: 4 -> 8
        cap: 8 -> 16
        cap: 16 -> 32
        cap: 32 -> 64

07.切片拷贝
    a.示例
        package main

        import (
            "fmt"
        )

        func main() {

            s1 := []int{1, 2, 3, 4, 5}
            fmt.Printf("slice s1 : %v\n", s1)
            s2 := make([]int, 10)
            fmt.Printf("slice s2 : %v\n", s2)
            copy(s2, s1)
            fmt.Printf("copied slice s1 : %v\n", s1)
            fmt.Printf("copied slice s2 : %v\n", s2)
            s3 := []int{1, 2, 3}
            fmt.Printf("slice s3 : %v\n", s3)
            s3 = append(s3, s2...)
            fmt.Printf("appended slice s3 : %v\n", s3)
            s3 = append(s3, 4, 5, 6)
            fmt.Printf("last slice s3 : %v\n", s3)

        }
        -----------------------------------------------------------------------------------------------------
        输出结果:
        slice s1 : [1 2 3 4 5]
        slice s2 : [0 0 0 0 0 0 0 0 0 0]
        copied slice s1 : [1 2 3 4 5]
        copied slice s2 : [1 2 3 4 5 0 0 0 0 0]
        slice s3 : [1 2 3]
        appended slice s3 : [1 2 3 1 2 3 4 5 0 0 0 0 0]
        last slice s3 : [1 2 3 1 2 3 4 5 0 0 0 0 0 4 5 6]
    b.copy:函数 copy 在两个 slice 间复制数据,复制长度以 len 小的为准。两个 slice 可指向同一底层数组,允许元素区间重叠。
        package main
        import (
            "fmt"
        )

        func main() {

            data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
            fmt.Println("array data : ", data)
            s1 := data[8:]
            s2 := data[:5]
            fmt.Printf("slice s1 : %v\n", s1)
            fmt.Printf("slice s2 : %v\n", s2)
            copy(s2, s1)
            fmt.Printf("copied slice s1 : %v\n", s1)
            fmt.Printf("copied slice s2 : %v\n", s2)
            fmt.Println("last array data : ", data)

        }
        输出结果:
        array data :  [0 1 2 3 4 5 6 7 8 9]
        slice s1 : [8 9]
        slice s2 : [0 1 2 3 4]
        copied slice s1 : [8 9]
        copied slice s2 : [8 9 2 3 4]
        last array data :  [8 9 2 3 4 5 6 7 8 9]
        应及时将所需数据 copy 到较小的 slice,以便释放超大号底层数组内存。

08.slice遍历
    package main

    import (
        "fmt"
    )

    func main() {

        data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
        slice := data[:]
        for index, value := range slice {
            fmt.Printf("inde : %v , value : %v\n", index, value)
        }

    }
    ---------------------------------------------------------------------------------------------------------
    输出结果:
    inde : 0 , value : 0
    inde : 1 , value : 1
    inde : 2 , value : 2
    inde : 3 , value : 3
    inde : 4 , value : 4
    inde : 5 , value : 5
    inde : 6 , value : 6
    inde : 7 , value : 7
    inde : 8 , value : 8
    inde : 9 , value : 9

09.切片resize(调整大小)
    package main

    import (
        "fmt"
    )

    func main() {
        var a = []int{1, 3, 4, 5}
        fmt.Printf("slice a : %v , len(a) : %v\n", a, len(a))
        b := a[1:2]
        fmt.Printf("slice b : %v , len(b) : %v\n", b, len(b))
        c := b[0:3]
        fmt.Printf("slice c : %v , len(c) : %v\n", c, len(c))
    }
    ---------------------------------------------------------------------------------------------------------
    输出结果:
    slice a : [1 3 4 5] , len(a) : 4
    slice b : [3] , len(b) : 1
    slice c : [3 4 5] , len(c) : 3

10.字符串和切片(string and slice)
    string底层就是一个byte的数组,因此,也可以进行切片操作。

    package main

    import (
        "fmt"
    )

    func main() {
        str := "hello world"
        s1 := str[0:5]
        fmt.Println(s1)

        s2 := str[6:]
        fmt.Println(s2)
    }
    输出结果:
    hello
    world
    ---------------------------------------------------------------------------------------------------------
    string本身是不可变的,因此要改变string中字符。需要如下操作: 英文字符串:

    package main

    import (
        "fmt"
    )

    func main() {
        str := "Hello world"
        s := []byte(str) //中文字符需要用[]rune(str)
        s[6] = 'G'
        s = s[:8]
        s = append(s, '!')
        str = string(s)
        fmt.Println(str)
    }
    输出结果:
    Hello Go!

11.含有中文字符串
    package main

    import (
        "fmt"
    )

    func main() {
        str := "你好,世界!hello world!"
        s := []rune(str)
        s[3] = '够'
        s[4] = '浪'
        s[12] = 'g'
        s = s[:14]
        str = string(s)
        fmt.Println(str)
    }
    输出结果:
    你好,够浪!hello go
    ---------------------------------------------------------------------------------------------------------
    golang slice data[:6:8] 两个冒号的理解
    常规slice , data[6:8],从第6位到第8位(返回6, 7),长度len为2, 最大可扩充长度cap为4(6-9)
    另一种写法: data[:6:8] 每个数字前都有个冒号, slice内容为data从0到第6位,长度len为6,最大扩充项cap设置为8
    a[x:y:z] 切片内容 [x:y] 切片长度: y-x 切片容量:z-x
    package main

    import (
        "fmt"
    )

    func main() {
        slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
        d1 := slice[6:8]
        fmt.Println(d1, len(d1), cap(d1))
        d2 := slice[:6:8]
        fmt.Println(d2, len(d2), cap(d2))
    }
    数组or切片转字符串:
    strings.Replace(strings.Trim(fmt.Sprint(array_or_slice), "[]"), " ", ",", -1)

2.7 字典Map1

00.总结
    a.创建和初始化
        声明和创建一个空的 map
        var m map[string]int
        m = make(map[string]int)
        -----------------------------------------------------------------------------------------------------
        创建并初始化 map
        m := map[string]int{"a": 1, "b": 2}
    b.添加和更新元素
        添加或更新键值对
        m["key"] = 42
    c.删除元素
        删除键值对
        delete(m, "key")
    d.访问元素
        通过键访问值
        value := m["key"]
        -----------------------------------------------------------------------------------------------------
        检查键是否存在
        value, exists := m["key"]
        if exists {
            fmt.Println("Key exists with value:", value)
        } else {
            fmt.Println("Key does not exist")
        }
    e.遍历 map
        使用 for 循环遍历 map
        for key, value := range m {
            fmt.Println(key, value)
        }
    f.获取 map 的长度
        获取 map 中键值对的数量
        length := len(m)
    g.清空 map
        清空 map 的所有元素
        for key := range m {
            delete(m, key)
        }
    h.其他操作
        创建一个新的 map 并拷贝
        m2 := make(map[string]int)
        for k, v := range m {
            m2[k] = v
        }

00.介绍
    在Go语言中,map 是一个内置的数据结构,用于存储键值对(key-value pairs)。
    map 允许通过键来快速访问和操作对应的值。Go语言中的 map 是无序的,键是唯一的,每个键只能映射到一个值。
    而最小堆的每个节点的值都小于等于其子节点的值。
    ---------------------------------------------------------------------------------------------------------
    一般来说,映射表数据结构实现通常有两种,哈希表(hash table)和搜索树(search tree),
    区别在于前者无序,后者有序。在Go中,map的实现是基于哈希桶(也是一种哈希表),所以也是无序的
    ---------------------------------------------------------------------------------------------------------
    可以使用内建函数 make 或使用 map 关键字来定义 Map:
    /* 使用 make 函数 */
    map_variable := make(map[KeyType]ValueType, initialCapacity)
    其中 KeyType 是键的类型,ValueType 是值的类型,initialCapacity 是可选的参数,用于指定 Map 的初始容量。Map 的容量是指 Map 中可以保存的键值对的数量,当 Map 中的键值对数量达到容量时,Map 会自动扩容。如果不指定 initialCapacity,Go 语言会根据实际情况选择一个合适的值。
    ---------------------------------------------------------------------------------------------------------
    实例
    // 创建一个空的 Map
    m := make(map[string]int)
    // 创建一个初始容量为 10 的 Map
    m := make(map[string]int, 10)
    ---------------------------------------------------------------------------------------------------------
    也可以使用字面量创建 Map:
    // 使用字面量创建 Map
    m := map[string]int{
        "apple": 1,
        "banana": 2,
        "orange": 3,
    }
    ---------------------------------------------------------------------------------------------------------
    获取元素:
    // 获取键值对
    v1 := m["apple"]
    v2, ok := m["pear"]  // 如果键不存在,ok 的值为 false,v2 的值为该类型的零值
    ---------------------------------------------------------------------------------------------------------
    修改元素:
    // 修改键值对
    m["apple"] = 5
    ---------------------------------------------------------------------------------------------------------
    获取 Map 的长度:
    // 获取 Map 的长度
    len := len(m)
    ---------------------------------------------------------------------------------------------------------
    遍历 Map:
    // 遍历 Map
    for k, v := range m {
        fmt.Printf("key=%s, value=%d\n", k, v)
    }
    ---------------------------------------------------------------------------------------------------------
    删除元素:
    // 删除键值对
    delete(m, "banana")

01.初始化
    在Go中,map的键类型必须是可比较的,比如string ,int是可比较的,而[]int是不可比较的,也就无法作为map的键。初始化一个map有两种方法,第一种是字面量,格式如下:
    map[keyType]valueType{}

    mp := map[int]string{
       0: "a",
       1: "a",
       2: "a",
       3: "a",
       4: "a",
    }

    mp := map[string]int{
       "a": 0,
       "b": 22,
       "c": 33,
    }
    ---------------------------------------------------------------------------------------------------------
    第二种方法是使用内置函数make,对于map而言,接收两个参数,分别是类型与初始容量,例子如下:
    mp := make(map[string]int, 8)
    mp := make(map[string][]int, 10)

    map是引用类型,零值或未初始化的map可以访问,但是无法存放元素,所以必须要为其分配内存。
    func main() {
       var mp map[string]int
       mp["a"] = 1
       fmt.Println(mp)
    }
    panic: assignment to entry in nil map

02.访问
    访问一个map的方式就像通过索引访问一个数组一样。
    func main() {
        mp := map[string]int{
            "a": 0,
            "b": 1,
            "c": 2,
            "d": 3,
        }
        fmt.Println(mp["a"])
        fmt.Println(mp["b"])
        fmt.Println(mp["d"])
        fmt.Println(mp["f"])
    }
    0
    1
    3
    0
    ---------------------------------------------------------------------------------------------------------
    通过代码可以观察到,即使map中不存在"f"这一键值对,但依旧有返回值。
    map对于不存的键其返回值是对应类型的零值,并且在访问map的时候其实有两个返回值,
    第一个返回值对应类型的值,第二个返回值一个布尔值,代表键是否存在,例如:
    func main() {
       mp := map[string]int{
          "a": 0,
          "b": 1,
          "c": 2,
          "d": 3,
       }
       if val, exist := mp["f"]; exist {
          fmt.Println(val)
       } else {
          fmt.Println("key不存在")
       }
    }
    ---------------------------------------------------------------------------------------------------------
    对map求长度
    func main() {
       mp := map[string]int{
          "a": 0,
          "b": 1,
          "c": 2,
          "d": 3,
       }
       fmt.Println(len(mp))
    }

03.存值
    map存值的方式也类似数组存值一样,例如:
    func main() {
       mp := make(map[string]int, 10)
       mp["a"] = 1
       mp["b"] = 2
       fmt.Println(mp)
    }
    ---------------------------------------------------------------------------------------------------------
    存值时使用已存在的键会覆盖原有的值
    func main() {
       mp := make(map[string]int, 10)
       mp["a"] = 1
       mp["b"] = 2
       if _, exist := mp["b"]; exist {
          mp["b"] = 3
       }
       fmt.Println(mp)
    }
    ---------------------------------------------------------------------------------------------------------
    但是也存在一个特殊情况,那就是键为math.NaN()时
    func main() {
        mp := make(map[float64]string, 10)
        mp[math.NaN()] = "a"
        mp[math.NaN()] = "b"
        mp[math.NaN()] = "c"
        _, exist := mp[math.NaN()]
        fmt.Println(exist)
        fmt.Println(mp)
    }
    false
    map[NaN:c NaN:a NaN:b]
    ---------------------------------------------------------------------------------------------------------
    通过结果可以观察到相同的键值并没有覆盖,反而还可以存在多个,也无法判断其是否存在,也就无法正常取值。
    因为NaN是IEE754标准所定义的,其实现是由底层的汇编指令UCOMISD完成,这是一个无序比较双精度浮点数的指令,
    该指令会考虑到NaN的情况,因此结果就是任何数字都不等于NaN,NaN也不等于自身,这也造成了每次哈希值都不相同。
    关于这一点社区也曾激烈讨论过,但是官方认为没有必要去修改,所以应当尽量避免使用NaN作为map的键。

04.删除
    func delete(m map[Type]Type1, key Type)
    ---------------------------------------------------------------------------------------------------------
    删除一个键值对需要用到内置函数delete,例如
    func main() {
       mp := map[string]int{
          "a": 0,
          "b": 1,
          "c": 2,
          "d": 3,
       }
       fmt.Println(mp)
       delete(mp, "a")
       fmt.Println(mp)
    }
    map[a:0 b:1 c:2 d:3]
    map[b:1 c:2 d:3]
    ---------------------------------------------------------------------------------------------------------
    需要注意的是,如果值为NaN,甚至没法删除该键值对。
    func main() {
       mp := make(map[float64]string, 10)
       mp[math.NaN()] = "a"
       mp[math.NaN()] = "b"
       mp[math.NaN()] = "c"
       fmt.Println(mp)
       delete(mp, math.NaN())
       fmt.Println(mp)
    }
    map[NaN:c NaN:a NaN:b]
    map[NaN:c NaN:a NaN:b]

05.遍历
    通过for range可以遍历map,例如
    func main() {
       mp := map[string]int{
          "a": 0,
          "b": 1,
          "c": 2,
          "d": 3,
       }
       for key, val := range mp {
          fmt.Println(key, val)
       }
    }
    c 2
    d 3
    a 0
    b 1
    ---------------------------------------------------------------------------------------------------------
    可以看到结果并不是有序的,也印证了map是无序存储。值得一提的是,NaN虽然没法正常获取,但是可以通过遍历访问到,例如
    func main() {
       mp := make(map[float64]string, 10)
       mp[math.NaN()] = "a"
       mp[math.NaN()] = "b"
       mp[math.NaN()] = "c"
       for key, val := range mp {
          fmt.Println(key, val)
       }
    }
    NaN a
    NaN c
    NaN b

06.清空
    在go1.21之前,想要清空map,就只能对每一个map的key进行delete
    func main() {
        m := map[string]int{
            "a": 1,
            "b": 2,
        }
        for k, _ := range m {
            delete(m, k)
        }
        fmt.Println(m)
    }
    ---------------------------------------------------------------------------------------------------------
    但是go1.21更新了clear函数,就不用再进行之前的操作了,只需要一个clear就可以清空
    func main() {
        m := map[string]int{
            "a": 1,
            "b": 2,
        }
        clear(m)
        fmt.Println(m)
    }
    输出
    map[]

07.Set
    Set是一种无序的,不包含重复元素的集合,Go中并没有提供类似的数据结构实现,但是map的键正是无序且不能重复的,所以也可以使用map来替代set。
    func main() {
        set := make(map[int]struct{}, 10)
        for i := 0; i < 10; i++ {
            set[rand.Intn(100)] = struct{}{}
        }
        fmt.Println(set)
    }
    map[0:{} 18:{} 25:{} 40:{} 47:{} 56:{} 59:{} 81:{} 87:{}]

08.注意
    map并不是一个并发安全的数据结构,Go团队认为大多数情况下map的使用并不涉及高并发的场景,
    引入互斥锁会极大的降低性能,map内部有读写检测机制,如果冲突会触发fatal error。
    例如下列情况有非常大的可能性会触发fatal。
    ---------------------------------------------------------------------------------------------------------
    func main() {

       group.Add(10)
       // map
       mp := make(map[string]int, 10)
       for i := 0; i < 10; i++ {
          go func() {
             // 写操作
             for i := 0; i < 100; i++ {
                mp["helloworld"] = 1
             }
             // 读操作
             for i := 0; i < 10; i++ {
                fmt.Println(mp["helloworld"])
             }
             group.Done()
          }()
       }
       group.Wait()
    }
    fatal error: concurrent map writes
    ---------------------------------------------------------------------------------------------------------
    在这种情况下,需要使用sync.Map来替代。

2.8 字典Map2

00.介绍
    map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。

01.map定义
    Go语言中 map的定义语法如下
        map[KeyType]ValueType
    其中,
        KeyType:表示键的类型。
        ValueType:表示键对应的值的类型。
    map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:
        make(map[KeyType]ValueType, [cap])
    其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。

02.map基本使用
    map中的数据都是成对出现的,map的基本使用示例代码如下:
    func main() {
        scoreMap := make(map[string]int, 8)
        scoreMap["张三"] = 90
        scoreMap["小明"] = 100
        fmt.Println(scoreMap)
        fmt.Println(scoreMap["小明"])
        fmt.Printf("type of a:%T\n", scoreMap)
    }
    输出:
    map[小明:100 张三:90]
    100
    type of a:map[string]int
    ---------------------------------------------------------------------------------------------------------
    map也支持在声明的时候填充元素,例如:
    func main() {
        userInfo := map[string]string{
            "username": "pprof.cn",
            "password": "123456",
        }
        fmt.Println(userInfo) //
    }

03.判断某个键是否存在
    Go语言中有个判断map中键是否存在的特殊写法,格式如下:
    value, ok := map[key]
    ---------------------------------------------------------------------------------------------------------
    举个例子:
    func main() {
        scoreMap := make(map[string]int)
        scoreMap["张三"] = 90
        scoreMap["小明"] = 100
        // 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
        v, ok := scoreMap["张三"]
        if ok {
            fmt.Println(v)
        } else {
            fmt.Println("查无此人")
        }
    }

04.map的遍历
    Go语言中使用for range遍历map。
    func main() {
        scoreMap := make(map[string]int)
        scoreMap["张三"] = 90
        scoreMap["小明"] = 100
        scoreMap["王五"] = 60
        for k, v := range scoreMap {
            fmt.Println(k, v)
        }
    }
    ---------------------------------------------------------------------------------------------------------
    但我们只想遍历key的时候,可以按下面的写法:
    func main() {
        scoreMap := make(map[string]int)
        scoreMap["张三"] = 90
        scoreMap["小明"] = 100
        scoreMap["王五"] = 60
        for k := range scoreMap {
            fmt.Println(k)
        }
    }
    注意: 遍历map时的元素顺序与添加键值对的顺序无关。

05.使用delete()函数删除键值对
    使用delete()内建函数从map中删除一组键值对,delete()函数的格式如下:
        delete(map, key)
    其中,
        map:表示要删除键值对的map
        key:表示要删除的键值对的键
    示例代码如下:
    func main(){
        scoreMap := make(map[string]int)
        scoreMap["张三"] = 90
        scoreMap["小明"] = 100
        scoreMap["王五"] = 60
        delete(scoreMap, "小明")//将小明:100从map中删除
        for k,v := range scoreMap{
            fmt.Println(k, v)
        }
    }

06.按照指定顺序遍历map
    func main() {
        rand.Seed(time.Now().UnixNano()) //初始化随机数种子

        var scoreMap = make(map[string]int, 200)

        for i := 0; i < 100; i++ {
            key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串
            value := rand.Intn(100)          //生成0~99的随机整数
            scoreMap[key] = value
        }
        //取出map中的所有key存入切片keys
        var keys = make([]string, 0, 200)
        for key := range scoreMap {
            keys = append(keys, key)
        }
        //对切片进行排序
        sort.Strings(keys)
        //按照排序后的key遍历map
        for _, key := range keys {
            fmt.Println(key, scoreMap[key])
        }
    }

07.元素为map类型的切片
    下面的代码演示了切片中的元素为map类型时的操作:
    func main() {
        var mapSlice = make([]map[string]string, 3)
        for index, value := range mapSlice {
            fmt.Printf("index:%d value:%v\n", index, value)
        }
        fmt.Println("after init")
        // 对切片中的map元素进行初始化
        mapSlice[0] = make(map[string]string, 10)
        mapSlice[0]["name"] = "王五"
        mapSlice[0]["password"] = "123456"
        mapSlice[0]["address"] = "红旗大街"
        for index, value := range mapSlice {
            fmt.Printf("index:%d value:%v\n", index, value)
        }
    }

08.值为切片类型的map
    下面的代码演示了map中值为切片类型的操作:
    func main() {
        var sliceMap = make(map[string][]string, 3)
        fmt.Println(sliceMap)
        fmt.Println("after init")
        key := "中国"
        value, ok := sliceMap[key]
        if !ok {
            value = make([]string, 0, 2)
        }
        value = append(value, "北京", "上海")
        sliceMap[key] = value
        fmt.Println(sliceMap)
    }

2.9 堆container/heap

00.总结
    a.堆的创建和初始化
        创建一个实现 heap.Interface 的类型
        初始化堆
    b.添加元素
        使用 heap.Push 向堆中添加元素
    c.删除元素
        使用 heap.Pop 从堆中删除元素
    d.堆操作
        获取堆的根元素
        遍历堆
    e.堆的调整
        手动调整堆(不常用)

01.介绍
    在Go语言中,container/heap 包提供了一个堆(Heap)数据结构的实现,支持优先队列的功能。
    堆是一种特殊的完全二叉树,满足堆性质:最大堆的每个节点的值都大于等于其子节点的值,
    而最小堆的每个节点的值都小于等于其子节点的值。

2.10 列表container/list

00.总结
    a.创建和初始化
        创建一个新的链表
        插入节点到链表
    b.添加元素
        在链表头部添加元素
        在链表尾部添加元素
        在指定位置插入元素
    c.删除元素
        从链表头部删除元素
        从链表尾部删除元素
        删除指定元素
    d.访问和遍历
        获取链表的头节点和尾节点
        遍历链表元素
        查找链表中是否包含某个元素
    e.链表操作
        获取链表的长度
        清空链表

01.介绍
    在Go语言中,List 通常指的是一个双向链表,它由 container/list 包提供。
    List 是一种链式数据结构,允许在两端高效地插入和删除元素

02.对比
    a.Java 中的 List
        a.介绍
            在 Java 中,List 是一个接口,提供了集合的操作方法。
            List 接口有多个实现类,其中最常用的实现类是 ArrayList 和 LinkedList:
        b.ArrayList:
            ArrayList 使用动态数组来存储元素,不是链表,因此不涉及链表的单向或双向问题。
        c.LinkedList:
            LinkedList 实现了双向链表。每个节点都有指向前一个和下一个节点的引用。
            因此,LinkedList 是双向的。你可以从任意一个节点向前或向后遍历。
    b.Go 中的 list
        a.介绍
            在 Go 语言中,container/list 包提供了一个双向链表的实现。以下是相关的详细说明:
        b.container/list:
            Go 的 container/list 包实现了一个双向链表。每个节点都有指向前一个节点和后一个节点的指针。
            你可以从链表的任意节点向前或向后遍历。
    c.对比总结
        Java List 接口:不是特定的链表类型;ArrayList 是基于动态数组的,而 LinkedList 是双向链表。
        Go container/list:实现了一个双向链表。
        -----------------------------------------------------------------------------------------------------
        java ArrayList 类似 go 切片Slice:动态数组

2.11 环形缓冲区container/ring

00.总结
    a.创建和初始化
        创建一个新的环形缓冲区
        初始化环形缓冲区
    b.添加元素
        在环形缓冲区中添加元素
    c.删除元素
        从环形缓冲区中删除元素
    d.访问和遍历
        访问环形缓冲区的元素
        遍历环形缓冲区的元素
    e.调整和操作
        移动到环形缓冲区的下一个节点
        移动到环形缓冲区的上一个节点
        获取环形缓冲区的大小和内容

01.介绍
    在Go语言中,container/ring 包提供了一个环形缓冲区(Ring Buffer)数据结构的实现。
    环形缓冲区是一种循环使用的缓冲区,当写入超出其大小时,会覆盖最早的数据。
    它适用于需要固定大小且高效管理数据的场景,如流处理、生产者-消费者问题等。
    ---------------------------------------------------------------------------------------------------------
    container/ring 包中的 Ring 类型实现了一个双向链表,其中每个节点都有一个指向下一个节点和上一个节点的指针。
    最重要的特性是它的“环形”结构,即链表的尾节点指向头节点,头节点指向尾节点。

2.12 指针Pointer1

01.创建
    Go保留了指针,在一定程度上保证了性能,同时为了更好的GC和安全考虑,又限制了指针的使用。
    ---------------------------------------------------------------------------------------------------------
    关于指针有两个常用的操作符,一个是取地址符&,另一个是解引用符*。对一个变量进行取地址,会返回对应类型的指针,例如:
    func main() {
       num := 2
       p := &num
       fmt.Println(p)
    }
    指针存储的是变量num的地址
    0xc00001c088
    ---------------------------------------------------------------------------------------------------------
    解引用符则有两个用途,第一个是访问指针所指向的元素,也就是解引用,例如
    func main() {
        num := 2
        p := &num
        rawNum := *p
        fmt.Println(rawNum)
    }
    ---------------------------------------------------------------------------------------------------------
    p是一个指针,对指针类型解引用就能访问到指针所指向的元素。还有一个用途就是声明一个指针,例如:
    func main() {
       var numPtr *int
       fmt.Println(numPtr)
    }
    <nil>
    ---------------------------------------------------------------------------------------------------------
    *int即代表该变量的类型是一个int类型的指针,不过指针不能光声明,还得初始化,需要为其分配内存,否则就是一个空指针,
    无法正常使用。要么使用取地址符将其他变量的地址赋值给该指针,要么就使用内置函数new手动分配,例如:
    func main() {
       var numPtr *int
       numPtr = new(int)
       fmt.Println(numPtr)
    }
    更多的是使用短变量
    func main() {
       numPtr := new(int)
       fmt.Println(numPtr)
    }
    ---------------------------------------------------------------------------------------------------------
    new函数只有一个参数那就是类型,并返回一个对应类型的指针,函数会为该指针分配内存,并且指针指向对应类型的零值,例如:
    func main() {
       fmt.Println(*new(string))
       fmt.Println(*new(int))
       fmt.Println(*new([5]int))
       fmt.Println(*new([]float64))
    }
    0
    [0 0 0 0 0]
    []

02.禁止指针运算
    在Go中是不支持指针运算的,也就是说指针无法偏移,先来看一段C++代码:
    int main() {
        int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
        int *p = &arr[0];
        cout << &arr << endl
             << p << endl
             << p + 1 << endl
             << &arr[1] << endl;
    }
    0x31d99ff880
    0x31d99ff880
    0x31d99ff884
    0x31d99ff884
    ---------------------------------------------------------------------------------------------------------
    可以看出数组的地址与数字第一个元素的地址一致,并且对指针加一运算后,其指向的元素为数组第二个元素。
    Go中的数组也是如此,不过区别在于指针无法偏移,例如
    func main() {
       arr := [5]int{0, 1, 2, 3, 4}
       p := &arr
       println(&arr[0])
       println(p)
       // 试图进行指针运算
       p++
       fmt.Println(p)
    }
    这样的程序将无法通过编译,报错如下
    main.go:10:2: invalid operation: p++ (non-numeric type *[5]int)

03.new和make
    在前面的几节已经很多次提到过内置函数new和make,两者有点类似,但也有不同,下面复习下。
    ---------------------------------------------------------------------------------------------------------
    func new(Type) *Type
    返回值是类型指针
    接收参数是类型
    专用于给指针分配内存空间
    ---------------------------------------------------------------------------------------------------------
    func make(t Type, size ...IntegerType) Type
    返回值是值,不是指针
    接收的第一个参数是类型,不定长参数根据传入类型的不同而不同
    专用于给切片,映射表,通道分配内存。
    ---------------------------------------------------------------------------------------------------------
    下面是一些例子:
    new(int) // int指针
    new(string) // string指针
    new([]int) // 整型切片指针
    make([]int, 10, 100) // 长度为10,容量100的整型切片
    make(map[string]int, 10) // 容量为10的映射表
    make(chan int, 10) // 缓冲区大小为10的通道

2.13 指针Pointer2

00.总结
    在Go语言中,指针(pointer)是一个非常基础但重要的概念。指针允许你直接操作变量的内存地址,
    从而可以间接地访问和修改变量的值。指针的操作包括声明、获取、解引用、修改等。
    区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。
    要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。

01.声明和初始化指针
    a.声明一个指针变量
        var p *int
    b.声明并初始化一个指针变量
        var a int = 10
        var p *int = &a

02.获取指针
    获取变量的地址
    var a int = 10
    var p *int = &a

03.解引用指针
    a.访问指针指向的值
        var a int = 10
        var p *int = &a
        value := *p  // value 是 10
    b.修改指针指向的值
    var a int = 10
    var p *int = &a
    *p = 20  // a 现在是 20

04.指针算术
    Go语言不支持指针算术(如 C 语言中的指针加减操作),但可以进行以下操作:
    通过 uintptr 类型进行指针的整数转换
    var a int = 10
    var p *int = &a
    addr := uintptr(unsafe.Pointer(p))

05.使用 new 和 make
    a.使用 new 函数创建指针
        p := new(int)  // p 是一个 *int 类型的指针,指向一个初始化为零值的 int
    b.使用 make 函数创建切片、映射或通道,不用于创建指针
    s := make([]int, 10)  // 创建一个切片,不返回指针
    m := make(map[string]int)  // 创建一个映射,不返回指针
    ch := make(chan int)  // 创建一个通道,不返回指针

06.空指针
    定义和检查空指针
    var p *int
    if p == nil {
        fmt.Println("p is nil")
    }

07.指针与结构体
    通过指针访问结构体字段
    type Person struct {
        Name string
        Age  int
    }
    func main() {
        p := &Person{Name: "Alice", Age: 30}
        p.Age = 31  // 通过指针修改结构体字段
        fmt.Println(p.Name, p.Age)
    }

08.指针与函数
    a.将指针作为函数参数
        func increment(n *int) {
            *n++
        }

        func main() {
            a := 10
            increment(&a)
            fmt.Println(a)  // 输出 11
        }
    b.函数返回指针
        func createPointer() *int {
            a := 10
            return &a
        }

        func main() {
            p := createPointer()
            fmt.Println(*p)  // 输出 10
        }

09.指针与接口
    将指针赋值给接口
    type Stringer interface {
        String() string
    }

    type Person struct {
        Name string
    }

    func (p *Person) String() string {
        return p.Name
    }

    func main() {
        p := &Person{Name: "Alice"}
        var s Stringer = p
        fmt.Println(s.String())  // 输出 Alice
    }

2.14 指针Pointer3

00.指针
    区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。
    要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。

01.Go语言中的指针
    Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。
    传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。
    Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据地址取值)。
    a.指针地址和指针类型
        每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。
        Go语言中使用&字符放在变量前面对变量进行“取地址”操作。
        Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等。
        -----------------------------------------------------------------------------------------------------
        取变量指针的语法如下:
            ptr := &v    // v的类型为T
        其中:
            v:代表被取地址的变量,类型为T
            ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。
        举个例子:
            func main() {
                a := 10
                b := &a
                fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
                fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
                fmt.Println(&b)                    // 0xc00000e018
            }

02.指针取值
    a.示例
        在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。
        func main() {
            //指针取值
            a := 10
            b := &a // 取变量a的地址,将指针保存到b中
            fmt.Printf("type of b:%T\n", b)
            c := *b // 指针取值(根据指针去内存取值)
            fmt.Printf("type of c:%T\n", c)
            fmt.Printf("value of c:%v\n", c)
        }
        输出如下:
        type of b:*int
        type of c:int
        value of c:10
        总结:取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。
    b.变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
        1.对变量进行取地址(&)操作,可以获得这个变量的指针变量。
        2.指针变量的值是指针地址。
        3.对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
    c.指针传值示例:
        func modify1(x int) {
            x = 100
        }
        func modify2(x *int) {
            *x = 100
        }
        func main() {
            a := 10
            modify1(a)
            fmt.Println(a) // 10
            modify2(&a)
            fmt.Println(a) // 100
        }

03.空指针
    当一个指针被定义后没有分配到任何变量时,它的值为 nil
    空指针的判断
    package main

    import "fmt"

    func main() {
        var p *string
        fmt.Println(p)
        fmt.Printf("p的值是%s/n", p)
        if p != nil {
            fmt.Println("非空")
        } else {
            fmt.Println("空值")
        }
    }

04.new和make
    我们先来看一个例子:
    func main() {
        var a *int
        *a = 100
        fmt.Println(*a)

        var b map[string]int
        b["测试"] = 100
        fmt.Println(b)
    }
    执行上面的代码会引发panic,为什么呢?
    在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。
    而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。
    要分配内存,就引出来今天的new和make。 Go语言中new和make是内建的两个函数,主要用来分配内存

05.new
    new是一个内置的函数,它的函数签名如下:
    func new(Type) *Type
    其中,
    1.Type表示类型,new函数只接受一个参数,这个参数是一个类型
    2.*Type表示类型指针,new函数返回一个指向该类型内存地址的指针。
    ---------------------------------------------------------------------------------------------------------
    new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:
    func main() {
        a := new(int)
        b := new(bool)
        fmt.Printf("%T\n", a) // *int
        fmt.Printf("%T\n", b) // *bool
        fmt.Println(*a)       // 0
        fmt.Println(*b)       // false
    }
    ---------------------------------------------------------------------------------------------------------
    本节开始的示例代码中var a *int只是声明了一个指针变量a但是没有初始化,
    指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。
    应该按照如下方式使用内置的new函数对a进行初始化之后就可以正常对其赋值了:
    func main() {
        var a *int
        a = new(int)
        *a = 10
        fmt.Println(*a)
    }

06.make
    make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,
    而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:
    func make(t Type, size ...IntegerType) Type
    ---------------------------------------------------------------------------------------------------------
    make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。
    这个我们在上一章中都有说明,关于channel我们会在后续的章节详细说明。
    ---------------------------------------------------------------------------------------------------------
    本节开始的示例中var b map[string]int只是声明变量b是一个map类型的变量,
    需要像下面的示例代码一样使用make函数进行初始化操作之后,才能对其进行键值对赋值:
    func main() {
        var b map[string]int
        b = make(map[string]int, 10)
        b["测试"] = 100
        fmt.Println(b)
    }

07.new与make的区别
    1.二者都是用来做内存分配的。
    2.make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
    3.而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

08.指针小练习
    程序定义一个int变量num的地址并打印
    将num的地址赋给指针ptr,并通过ptr去修改num的值
    package main

    import "fmt"

    func main() {
        var a int
        fmt.Println(&a)
        var p *int
        p = &a
        *p = 20
        fmt.Println(a)
    }

2.15 结构体Struct1:替代类

00.汇总
    在 Go 语言中,结构体(struct)是一种用于定义数据类型的复合数据结构,它可以包含多个字段(field),
    每个字段都有一个名称和类型。结构体常用于将不同的数据组合在一起,创建复杂的数据模型。

01.定义结构体
    a.定义一个简单结构体
        type Person struct {
            Name string
            Age  int
        }
    b.定义嵌套结构体
        type Address struct {
            City    string
            Country string
        }
        type Person struct {
            Name    string
            Age     int
            Address Address
        }

02.创建结构体实例
    a.使用字段名初始化结构体
        p := Person{Name: "Alice", Age: 30}
    b.使用结构体字面量创建结构体
        p := Person{"Alice", 30}
    c.使用 new 函数创建结构体的指针
        p := new(Person)
        p.Name = "Alice"
        p.Age = 30

03.访问和修改结构体字段
    a.访问字段
        fmt.Println(p.Name) // 访问 Name 字段
    b.修改字段
        p.Age = 31 // 修改 Age 字段

04.方法与结构体
    a.为结构体定义方法
        func (p *Person) Greet() string {
            return "Hello, my name is " + p.Name
        }
    b.调用结构体的方法
        fmt.Println(p.Greet())

05.结构体嵌套
    a.定义嵌套结构体
        type Address struct {
            City    string
            Country string
        }
        type Person struct {
            Name    string
            Age     int
            Address Address
        }
        // 创建嵌套结构体实例
        p := Person{
            Name: "Alice",
            Age:  30,
            Address: Address{
                City:    "Wonderland",
                Country: "Fiction",
            },
        }
    b.访问嵌套结构体的字段
        fmt.Println(p.Address.City) // 访问嵌套结构体的字段

06.结构体的比较
    比较结构体是否相等
    p1 := Person{Name: "Alice", Age: 30}
    p2 := Person{Name: "Alice", Age: 30}
    fmt.Println(p1 == p2) // 输出: true

07.结构体标签
    a.使用结构体标签
        type Person struct {
            Name string `json:"name"`
            Age  int    `json:"age"`
        }
    b.获取结构体标签
        t := reflect.TypeOf(Person{})
        field, _ := t.FieldByName("Name")
        fmt.Println(field.Tag) // 输出: json:"name"

08.结构体与接口
    将结构体赋值给接口
    type Stringer interface {
        String() string
    }

    type Person struct {
        Name string
    }

    func (p Person) String() string {
        return p.Name
    }

    var s Stringer = Person{Name: "Alice"}
    fmt.Println(s.String()) // 输出: Alice

09.结构体的复制
    复制结构体
    p1 := Person{Name: "Alice", Age: 30}
    p2 := p1 // 复制结构体
    p2.Age = 31
    fmt.Println(p1.Age) // 输出: 30
    fmt.Println(p2.Age) // 输出: 31

10.结构体的零值
    获取结构体的零值
    var p Person
    fmt.Println(p) // 输出: { 0}

2.16 结构体Struct2:替代类

01.声明
    结构体的声明非常简单,例子如下:
    type Person struct {
       name string
       age int
    }
    ---------------------------------------------------------------------------------------------------------
    结构体本身以及其内部的字段都遵守大小写命名的暴露方式。对于一些类型相同的相邻字段,可以不需要重复声明类型,如下:
    type Rectangle struct {
        height, width, area int
        color               string
    }
    ---------------------------------------------------------------------------------------------------------
    提示:在声明结构体字段时,字段名不能与方法名重复

02.实例化
    a.普通创建方法
        Go不存在构造方法,大多数情况下采用如下的方式来实例化结构体,初始化的时候就像map一样指定字段名称再初始化字段值
        programmer := Programmer{
           Name:     "jack",
           Age:      19,
           Job:      "coder",
           Language: []string{"Go", "C++"},
        }
        -----------------------------------------------------------------------------------------------------
        不过也可以省略字段名称,当省略字段名称时,就必须初始化所有字段,通常不建议使用这种方式,因为可读性很糟糕。
        programmer := Programmer{
           "jack",
           19,
           "coder",
           []string{"Go", "C++"}}
        -----------------------------------------------------------------------------------------------------
        如果实例化过程比较复杂,你也可以编写一个函数来实例化结构体,就像下面这样,你也可以把它理解为一个构造函数
        type Person struct {
            Name    string
            Age     int
            Address string
            Salary  float64
        }
        func NewPerson(name string, age int, address string, salary float64) *Person {
            return &Person{Name: name, Age: age, Address: address, Salary: salary}
        }
        不过Go并不支持函数与方法重载,所以你无法为同一个函数或方法定义不同的参数。
        如果你想以多种方式实例化结构体,要么创建多个构造函数,要么建议使用options模式。
    b.选项模式
        选项模式是Go语言中一种很常见的设计模式,可以更为灵活的实例化结构体,拓展性强,并且不需要改变构造函数的函数签名。假设有下面这样一个结构体
        type Person struct {
            Name     string
            Age      int
            Address  string
            Salary   float64
            Birthday string
        }
        -----------------------------------------------------------------------------------------------------
        声明一个PersonOptions类型,它接受一个*Person类型的参数,它必须是指针,因为我们要在闭包中对Person赋值。
        type PersonOptions func(p *Person)
        -----------------------------------------------------------------------------------------------------
        接下来创建选项函数,它们一般是With开头,它们的返回值就是一个闭包函数。
        func WithName(name string) PersonOptions {
            return func(p *Person) {
                p.Name = name
            }
        }

        func WithAge(age int) PersonOptions {
            return func(p *Person) {
                p.Age = age
            }
        }

        func WithAddress(address string) PersonOptions {
            return func(p *Person) {
                p.Address = address
            }
        }

        func WithSalary(salary float64) PersonOptions {
            return func(p *Person) {
                p.Salary = salary
            }
        }
        -----------------------------------------------------------------------------------------------------
        实际声明的构造函数签名如下,它接受一个可变长PersonOptions类型的参数。
        func NewPerson(options ...PersonOptions) *Person {
            // 优先应用options
            p := &Person{}
            for _, option := range options {
                option(p)
            }

            // 默认值处理
            if p.Age < 0 {
                p.Age = 0
            }
            ......

            return p
        }
        -----------------------------------------------------------------------------------------------------
        这样一来对于不同实例化的需求只需要一个构造函数即可完成,只需要传入不同的Options函数即可
        func main() {
            pl := NewPerson(
                WithName("John Doe"),
                WithAge(25),
                WithAddress("123 Main St"),
                WithSalary(10000.00),
            )

            p2 := NewPerson(
                WithName("Mike jane"),
                WithAge(30),
            )
        }
        -----------------------------------------------------------------------------------------------------
        函数式选项模式在很多开源项目中都能看见,gRPC Server的实例化方式也是采用了该设计模式。
        函数式选项模式只适合于复杂的实例化,如果参数只有简单几个,建议还是用普通的构造函数来解决。

03.组合
    在Go中,结构体之间的关系是通过组合来表示的,可以显式组合,也可以匿名组合,
    后者使用起来更类似于继承,但本质上没有任何变化。例如:
    ---------------------------------------------------------------------------------------------------------
    显式组合的方式
    type Person struct {
       name string
       age  int
    }

    type Student struct {
       p      Person
       school string
    }

    type Employee struct {
       p   Person
       job string
    }
    ---------------------------------------------------------------------------------------------------------
    在使用时需要显式的指定字段p
    student := Student{
       p:      Person{name: "jack", age: 18},
       school: "lili school",
    }
    fmt.Println(student.p.name)
    ---------------------------------------------------------------------------------------------------------
    而匿名组合可以不用显式的指定字段
    type Person struct {
        name string
        age  int
    }

    type Student struct {
        Person
        school string
    }

    type Employee struct {
        Person
        job string
    }
    ---------------------------------------------------------------------------------------------------------
    匿名字段的名称默认为类型名,调用者可以直接访问该类型的字段和方法,但除了更加方便以外与第一种方式没有任何的区别。
    student := Student{
       Person: Person{name: "jack",age: 18},
       school: "lili school",
    }
    fmt.Println(student.name)

04.指针
    对于结构体指针而言,不需要解引用就可以直接访问结构体的内容,例子如下:
    p := &Person{
       name: "jack",
       age:  18,
    }
    fmt.Println(p.age,p.name)
    在编译的时候会转换为(*p).name ,(*p).age,其实还是需要解引用,不过在编码的时候可以省去,算是一种语法糖。

05.标签
    结构体标签是一种元编程的形式,结合反射可以做出很多奇妙的功能,格式如下
    `key1:"val1" key2:"val2"`
    ---------------------------------------------------------------------------------------------------------
    标签是一种键值对的形式,使用空格进行分隔。结构体标签的容错性很低,如果没能按照正确的格式书写结构体,那么将会导致无法正常读取,但是在编译时却不会有任何的报错,下方是一个使用示例。
    type Programmer struct {
        Name     string `json:"name"`
        Age      int `yaml:"age"`
        Job      string `toml:"job"`
        Language []string `properties:"language"`
    }
    结构体标签最广泛的应用就是在各种序列化格式中的别名定义,标签的使用需要结合反射才能完整发挥出其功能。

06.内存对齐
    Go结构体字段的内存分布遵循内存对齐的规则,这么做可以减少CPU访问内存的次数,相应的占用的内存要多一些,属于空间换时间的一种手段。
    ---------------------------------------------------------------------------------------------------------
    假设有如下结构体
    type Num struct {
        A int64
        B int32
        C int16
        D int8
        E int32
    }

    已知这些类型的占用字节数
    int64占8个字节
    int32占4个字节
    int16占2字节
    int8占一个字节

    整个结构体的内存占用似乎是8+4+2+1+4=19个字节吗,当然不是这样,根据内存对齐规则而言,
    结构体的内存占用长度至少是最大字段的整数倍,不足的则补齐。该结构体中最大的是int64占用8个字节
    所以实际上是占用24个字节,其中有5个字节是无用的。
    ---------------------------------------------------------------------------------------------------------
    再来看下面这个结构体
    type Num struct {
        A int8
        B int64
        C int8
    }
    明白了上面的规则后,可以很快的理解它的内存占用也是24个字节,尽管它只有三个字段,足足浪费了14个字节。
    ---------------------------------------------------------------------------------------------------------
    但是我们可以调整字段,改成如下的顺序
    type Num struct {
        A int8
        C int8
        B int64
    }
    如此一来就占用的内存就变为了16字节,浪费了6个字节,减少了8个字节的内存浪费。
    ---------------------------------------------------------------------------------------------------------
    从理论上来说,让结构体中的字段按照合理的顺序分布,可以减少其内存占用。不过实际编码过程中,
    并没有必要的理由去这样做,它不一定能在减少内存占用这方面带来实质性的提升,
    但一定会提高开发人员的血压和心智负担,尤其是在业务中一些结构体的字段数可能多大几十个或者数百个,所以仅做了解即可。

07.空结构体
    空结构体没有字段,不占用内存空间,我们可以通过unsafe.SizeOf函数来计算占用的字节大小
    func main() {
       type Empty struct {}
       fmt.Println(unsafe.Sizeof(Empty{}))
    }
    输出
    0
    空结构体的使用场景有很多,比如之前提到过的,作为map的值类型,可以将map作为set来进行使用,又或者是作为通道的类型,表示仅做通知类型的通道。

2.17 结构体Struct3:替代类

00.介绍
    Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

01.类型别名和自定义类型
    a.自定义类型
        在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型,
        Go语言中可以使用type关键字来定义自定义类型。
        -----------------------------------------------------------------------------------------------------
        自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:
        //将MyInt定义为int类型
        type MyInt int
        通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性。
    b.类型别名
        类型别名是Go1.9版本添加的新功能。
        类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
        type TypeAlias = Type
        -----------------------------------------------------------------------------------------------------
        我们之前见过的rune和byte就是类型别名,他们的定义如下:
        type byte = uint8
        type rune = int32
    c.类型定义和类型别名的区别
        类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。
        //类型定义
        type NewInt int

        //类型别名
        type MyInt = int

        func main() {
            var a NewInt
            var b MyInt

            fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
            fmt.Printf("type of b:%T\n", b) //type of b:int
        }
        结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

02.结构体
    a.介绍
        Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,
        这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,
        这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。
        -----------------------------------------------------------------------------------------------------
        Go语言中通过struct来实现面向对象。
    b.结构体的定义
        使用type和struct关键字来定义结构体,具体代码格式如下:
        type 类型名 struct {
            字段名 字段类型
            字段名 字段类型
            …
        }
        -----------------------------------------------------------------------------------------------------
        其中:
        1.类型名:标识自定义结构体的名称,在同一个包内不能重复。
        2.字段名:表示结构体字段名。结构体中的字段名必须唯一。
        3.字段类型:表示结构体字段的具体类型。
        -----------------------------------------------------------------------------------------------------
        举个例子,我们定义一个Person(人)结构体,代码如下:
        type person struct {
            name string
            city string
            age  int8
        }
        -----------------------------------------------------------------------------------------------------
        同样类型的字段也可以写在一行,
        type person1 struct {
            name, city string
            age        int8
        }
        -----------------------------------------------------------------------------------------------------
        这样我们就拥有了一个person的自定义类型,它有name、city、age三个字段,分别表示姓名、城市和年龄。
        这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。
        -----------------------------------------------------------------------------------------------------
        语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。
        比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型
    c.结构体实例化
        只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
        结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。
        var 结构体实例 结构体类型
    d.基本实例化
        type person struct {
            name string
            city string
            age  int8
        }

        func main() {
            var p1 person
            p1.name = "pprof.cn"
            p1.city = "北京"
            p1.age = 18
            fmt.Printf("p1=%v\n", p1)  //p1={pprof.cn 北京 18}
            fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"pprof.cn", city:"北京", age:18}
        }
        我们通过.来访问结构体的字段(成员变量),例如p1.name和p1.age等。

03.匿名结构体
    a.介绍
        在定义一些临时数据结构等场景下还可以使用匿名结构体。
        package main

        import (
            "fmt"
        )

        func main() {
            var user struct{Name string; Age int}
            user.Name = "pprof.cn"
            user.Age = 18
            fmt.Printf("%#v\n", user)
        }
    b.创建指针类型结构体
        我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:
        var p2 = new(person)
        fmt.Printf("%T\n", p2)     //*main.person
        fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}
        从打印的结果中我们可以看出p2是一个结构体指针。
        -----------------------------------------------------------------------------------------------------
        需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员。
        var p2 = new(person)
        p2.name = "测试"
        p2.age = 18
        p2.city = "北京"
        fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"测试", city:"北京", age:18}
    c.取结构体的地址实例化
        使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。
        p3 := &person{}
        fmt.Printf("%T\n", p3)     //*main.person
        fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
        p3.name = "博客"
        p3.age = 30
        p3.city = "成都"
        fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"博客", city:"成都", age:30}
        p3.name = "博客"其实在底层是(*p3).name = "博客",这是Go语言帮我们实现的语法糖。
    d.结构体初始化
        type person struct {
            name string
            city string
            age  int8
        }

        func main() {
            var p4 person
            fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0}
        }
    e.使用键值对初始化
        使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。
        p5 := person{
            name: "pprof.cn",
            city: "北京",
            age:  18,
        }
        fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"pprof.cn", city:"北京", age:18}
        -----------------------------------------------------------------------------------------------------
        也可以对结构体指针进行键值对初始化,例如:
        p6 := &person{
            name: "pprof.cn",
            city: "北京",
            age:  18,
        }
        fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"pprof.cn", city:"北京", age:18}
        -----------------------------------------------------------------------------------------------------
        当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
        p7 := &person{
            city: "北京",
        }
        fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"北京", age:0}
    f.使用值的列表初始化
        初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
        p8 := &person{
            "pprof.cn",
            "北京",
            18,
        }
        fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"pprof.cn", city:"北京", age:18}
        -----------------------------------------------------------------------------------------------------
        使用这种格式初始化时,需要注意:
        1.必须初始化结构体的所有字段。
        2.初始值的填充顺序必须与字段在结构体中的声明顺序一致。
        3.该方式不能和键值初始化方式混用。
    g.结构体内存布局
        type test struct {
            a int8
            b int8
            c int8
            d int8
        }
        n := test{
            1, 2, 3, 4,
        }
        fmt.Printf("n.a %p\n", &n.a)
        fmt.Printf("n.b %p\n", &n.b)
        fmt.Printf("n.c %p\n", &n.c)
        fmt.Printf("n.d %p\n", &n.d)
        输出:
        n.a 0xc0000a0060
        n.b 0xc0000a0061
        n.c 0xc0000a0062
        n.d 0xc0000a0063
    h.面试题
        type student struct {
            name string
            age  int
        }

        func main() {
            m := make(map[string]*student)
            stus := []student{
                {name: "pprof.cn", age: 18},
                {name: "测试", age: 23},
                {name: "博客", age: 28},
            }

            for _, stu := range stus {
                m[stu.name] = &stu
            }
            for k, v := range m {
                fmt.Println(k, "=>", v.name)
            }
        }
    i.构造函数
        Go语言的结构体没有构造函数,我们可以自己实现。
        例如,下方的代码就实现了一个person的构造函数。
        因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
        -----------------------------------------------------------------------------------------------------
        func newPerson(name, city string, age int8) *person {
            return &person{
                name: name,
                city: city,
                age:  age,
            }
        }
        -----------------------------------------------------------------------------------------------------
        调用构造函数
        p9 := newPerson("pprof.cn", "测试", 90)
        fmt.Printf("%#v\n", p9)
    j.方法和接收者
        Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。
        接收者的概念就类似于其他语言中的this或者 self。
        -----------------------------------------------------------------------------------------------------
        方法的定义格式如下:
        func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
            函数体
        }
        -----------------------------------------------------------------------------------------------------
        其中,
        1.接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。
        2.接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
        3.方法名、参数列表、返回参数:具体格式与函数定义相同。
        -----------------------------------------------------------------------------------------------------
        举个例子:
        //Person 结构体
        type Person struct {
            name string
            age  int8
        }

        //NewPerson 构造函数
        func NewPerson(name string, age int8) *Person {
            return &Person{
                name: name,
                age:  age,
            }
        }

        //Dream Person做梦的方法
        func (p Person) Dream() {
            fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
        }

        func main() {
            p1 := NewPerson("测试", 25)
            p1.Dream()
        }
        方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
    k.指针类型的接收者
        指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,
        在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self。
        例如我们为Person添加一个SetAge方法,来修改实例变量的年龄。
        -----------------------------------------------------------------------------------------------------
        // SetAge 设置p的年龄
        // 使用指针接收者
        func (p *Person) SetAge(newAge int8) {
            p.age = newAge
        }
        -----------------------------------------------------------------------------------------------------
        调用该方法:
        func main() {
            p1 := NewPerson("测试", 25)
            fmt.Println(p1.age) // 25
            p1.SetAge(30)
            fmt.Println(p1.age) // 30
        }
    l.值类型的接收者
        当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。
        在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
        -----------------------------------------------------------------------------------------------------
        // SetAge2 设置p的年龄
        // 使用值接收者
        func (p Person) SetAge2(newAge int8) {
            p.age = newAge
        }

        func main() {
            p1 := NewPerson("测试", 25)
            p1.Dream()
            fmt.Println(p1.age) // 25
            p1.SetAge2(30) // (*p1).SetAge2(30)
            fmt.Println(p1.age) // 25
        }
    m.什么时候应该使用指针类型接收者
        1.需要修改接收者中的值
        2.接收者是拷贝代价比较大的大对象
        3.保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
    n.任意类型添加方法
        在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。
        举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
        -----------------------------------------------------------------------------------------------------
        //MyInt 将int定义为自定义MyInt类型
        type MyInt int

        //SayHello 为MyInt添加一个SayHello的方法
        func (m MyInt) SayHello() {
            fmt.Println("Hello, 我是一个int。")
        }
        func main() {
            var m1 MyInt
            m1.SayHello() //Hello, 我是一个int。
            m1 = 100
            fmt.Printf("%#v  %T\n", m1, m1) //100  main.MyInt
        }
        注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
    o.结构体的匿名字段
        结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
        //Person 结构体Person类型
        type Person struct {
            string
            int
        }

        func main() {
            p1 := Person{
                "pprof.cn",
                18,
            }
            fmt.Printf("%#v\n", p1)        //main.Person{string:"pprof.cn", int:18}
            fmt.Println(p1.string, p1.int) //pprof.cn 18
        }
        匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
    p.嵌套结构体
        一个结构体中可以嵌套包含另一个结构体或结构体指针。
        //Address 地址结构体
        type Address struct {
            Province string
            City     string
        }

        //User 用户结构体
        type User struct {
            Name    string
            Gender  string
            Address Address
        }

        func main() {
            user1 := User{
                Name:   "pprof",
                Gender: "女",
                Address: Address{
                    Province: "黑龙江",
                    City:     "哈尔滨",
                },
            }
            fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
        }
    q.嵌套匿名结构体
        //Address 地址结构体
        type Address struct {
            Province string
            City     string
        }

        //User 用户结构体
        type User struct {
            Name    string
            Gender  string
            Address //匿名结构体
        }

        func main() {
            var user2 User
            user2.Name = "pprof"
            user2.Gender = "女"
            user2.Address.Province = "黑龙江"    //通过匿名结构体.字段名访问
            user2.City = "哈尔滨"                //直接访问匿名结构体的字段名
            fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
        }
        当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。
    r.嵌套结构体的字段名冲突
        嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。
        //Address 地址结构体
        type Address struct {
            Province   string
            City       string
            CreateTime string
        }

        //Email 邮箱结构体
        type Email struct {
            Account    string
            CreateTime string
        }

        //User 用户结构体
        type User struct {
            Name   string
            Gender string
            Address
            Email
        }

        func main() {
            var user3 User
            user3.Name = "pprof"
            user3.Gender = "女"
            // user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
            user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
            user3.Email.CreateTime = "2000"   //指定Email结构体中的CreateTime
        }
    s.结构体的“继承”
        Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。
        //Animal 动物
        type Animal struct {
            name string
        }

        func (a *Animal) move() {
            fmt.Printf("%s会动!\n", a.name)
        }

        //Dog 狗
        type Dog struct {
            Feet    int8
            *Animal //通过嵌套匿名结构体实现继承
        }

        func (d *Dog) wang() {
            fmt.Printf("%s会汪汪汪~\n", d.name)
        }

        func main() {
            d1 := &Dog{
                Feet: 4,
                Animal: &Animal{ //注意嵌套的是结构体指针
                    name: "乐乐",
                },
            }
            d1.wang() //乐乐会汪汪汪~
            d1.move() //乐乐会动!
        }
    t.结构体字段的可见性
        结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
    u.结构体与JSON序列化
        JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。
        同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,
        键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。
        -----------------------------------------------------------------------------------------------------
        //Student 学生
        type Student struct {
            ID     int
            Gender string
            Name   string
        }

        //Class 班级
        type Class struct {
            Title    string
            Students []*Student
        }

        func main() {
            c := &Class{
                Title:    "101",
                Students: make([]*Student, 0, 200),
            }
            for i := 0; i < 10; i++ {
                stu := &Student{
                    Name:   fmt.Sprintf("stu%02d", i),
                    Gender: "男",
                    ID:     i,
                }
                c.Students = append(c.Students, stu)
            }
            //JSON序列化:结构体-->JSON格式的字符串
            data, err := json.Marshal(c)
            if err != nil {
                fmt.Println("json marshal failed")
                return
            }
            fmt.Printf("json:%s\n", data)
            //JSON反序列化:JSON格式的字符串-->结构体
            str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
            c1 := &Class{}
            err = json.Unmarshal([]byte(str), c1)
            if err != nil {
                fmt.Println("json unmarshal failed!")
                return
            }
            fmt.Printf("%#v\n", c1)
        }
    v.结构体标签(Tag)
        Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。
        Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
        `key1:"value1" key2:"value2"`
        结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。 注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
        -----------------------------------------------------------------------------------------------------
        例如我们为Student结构体的每个字段定义json序列化时使用的Tag:
        //Student 学生
        type Student struct {
            ID     int    `json:"id"` //通过指定tag实现json序列化该字段时的key
            Gender string //json序列化是默认使用字段名作为key
            name   string //私有不能被json包访问
        }

        func main() {
            s1 := Student{
                ID:     1,
                Gender: "女",
                name:   "pprof",
            }
            data, err := json.Marshal(s1)
            if err != nil {
                fmt.Println("json marshal failed!")
                return
            }
            fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"女"}
        }
    w.小练习:
        猜一下下列代码运行的结果是什么
        package main

        import "fmt"

        type student struct {
            id   int
            name string
            age  int
        }

        func demo(ce []student) {
            //切片是引用传递,是可以改变值的
            ce[1].age = 999
            // ce = append(ce, student{3, "xiaowang", 56})
            // return ce
        }
        func main() {
            var ce []student  //定义一个切片类型的结构体
            ce = []student{
                student{1, "xiaoming", 22},
                student{2, "xiaozhang", 33},
            }
            fmt.Println(ce)
            demo(ce)
            fmt.Println(ce)
        }
    x.删除map类型的结构体
        package main

        import "fmt"

        type student struct {
            id   int
            name string
            age  int
        }

        func main() {
            ce := make(map[int]student)
            ce[1] = student{1, "xiaolizi", 22}
            ce[2] = student{2, "wang", 23}
            fmt.Println(ce)
            delete(ce, 2)
            fmt.Println(ce)
        }

3 函数

3.1 汇总

01.常用信息1
    a.总结
        在Go中,函数是一等公民,函数是Go最基础的组成部分,也是Go的核心。
        Go的理念便是:如果参数不一样那就是两个不同的函数,那么就不应该取一样的名字,函数重载会让代码变得混淆和难以理解。
        值得一提的是,Go中的函数参数是传值传递,即在传递参数时会拷贝实参的值。如果你觉得在传递切片或map时会复制大量的内存,我只能告诉你大可不必担心,如果你想知道细节的话可以去实现原理章节看看。
        不管具名返回值如何声明,永远都是以return关键字后的值为最高优先级。
        匿名函数只能在函数内部存在,匿名函数可以简单理解为没有名称的函数,例如
        闭包(Closure)这一概念,在一些语言中又被称为Lamda表达式,经常与匿名函数一起使用,函数 + 环境引用 = 闭包。看一个例子:
        defer关键字描述的一个匿名函数会在函数返回之前执行。
    b.声明
        函数的声明格式如下
        func 函数名([参数列表]) [返回值] {
            函数体
        }
        ---------------------------------------------------------------------------------------------------------
        函数可以直接通过func关键字来声明,也可以声明为一个字面量,也可以作为一个类型。
        // 直接声明
        func DoSomething() {

        }

        // 字面量
        var doSomthing func()

        // 类型
        type DoAnything func()
        ---------------------------------------------------------------------------------------------------------
        函数签名由函数名称,参数列表,返回值组成,下面是一个完整的例子
        func Sum(a, b int) int {
           return a + b
        }
        ---------------------------------------------------------------------------------------------------------
        函数名称Sum,有两个int类型的参数a,b,返回值类型为int。
        还有一个非常重要的点,即Go中的函数不支持重载,像下面的代码就无法通过编译
        type Person struct {
            Name    string
            Age     int
            Address string
            Salary  float64
        }

        func NewPerson(name string, age int, address string, salary float64) *Person {
            return &Person{Name: name, Age: age, Address: address, Salary: salary}
        }

        func NewPerson(name string) *Person {
            return &Person{Name: name}
        }
        ---------------------------------------------------------------------------------------------------------
        Go的理念便是:如果参数不一样那就是两个不同的函数,那么就不应该取一样的名字,函数重载会让代码变得混淆和难以理解。
        这种理念是否正确见仁见智,至少在Go中你可以仅通过函数名就知道它是干什么的。
    c.参数
        Go中的参数名可以不带名称,一般这种是在接口或函数类型声明时才会用到,不过为了可读性一般还是建议尽量给参数加上名称
        type ExWriter func(io.Writer) error

        type Writer interface {
            ExWrite([]byte) (int, error)
        }
        ---------------------------------------------------------------------------------------------------------
        对于类型相同的参数而言,可以只需要声明一次类型,不过条件是它们必须相邻
        func Log(format string, a1, a2 any) {
            ...
        }
        ---------------------------------------------------------------------------------------------------------
        变长参数可以接收0个或多个值,必须声明在参数列表的末尾,最典型的例子就是fmt.Printf函数。
        func Printf(format string, a ...any) (n int, err error) {
            return Fprintf(os.Stdout, format, a...)
        }
        ---------------------------------------------------------------------------------------------------------
        值得一提的是,Go中的函数参数是传值传递,即在传递参数时会拷贝实参的值。如果你觉得在传递切片或map时会复制大量的内存,我只能告诉你大可不必担心,如果你想知道细节的话可以去实现原理章节看看。
    d.返回值
        下面是一个简单的函数返回值的例子,Sum函数返回一个int类型的值。
        func Sum(a, b int) int {
           return a + b
        }
        ---------------------------------------------------------------------------------------------------------
        当函数没有返回值时,不需要void,不带返回值即可。
        func ErrPrintf(format string, a ...any) {
            _, _ = fmt.Fprintf(os.Stderr, format, a...)
        }
        ---------------------------------------------------------------------------------------------------------
        Go允许函数有多个返回值,此时就需要用括号将返回值围起来。
        func Div(a, b float64) (float64, error) {
            if a == 0 {
                return math.NaN(), errors.New("0不能作为被除数")
            }
            return a / b, nil
        }
        ---------------------------------------------------------------------------------------------------------
        Go也支持具名返回值,不能与参数名重复,使用具名返回值时,return关键字可以不需要指定返回哪些值。
        func Sum(a, b int) (ans int) {
            ans = a + b
            return
        }
        ---------------------------------------------------------------------------------------------------------
        和参数一样,当有多个同类型的具名返回值时,可以省略掉重复的类型声明
        func SumAndMul(a, b int) (c, d int) {
            c = a + b
            d = a * b
            return
        }
        ---------------------------------------------------------------------------------------------------------
        不管具名返回值如何声明,永远都是以return关键字后的值为最高优先级。
        func SumAndMul(a, b int) (c, d int) {
            c = a + b
            d = a * b
            // c,d将不会被返回
            return a + b, a * b
        }
    e.匿名函数
        匿名函数只能在函数内部存在,匿名函数可以简单理解为没有名称的函数,例如
        func main() {
           func(a, b int) int {
              return a + b
           }(1, 2)
        }
        ---------------------------------------------------------------------------------------------------------
        或者当函数参数是一个函数类型时,这时名称不再重要,可以直接传递一个匿名函数
        func main() {
            DoSum(1, 2, func(a int, b int) int {
                return a + b
            })
        }
        func DoSum(a, b int, f func(int, int) int) int {
            return f(a, b)
        }
    f.闭包
        闭包(Closure)这一概念,在一些语言中又被称为Lamda表达式,经常与匿名函数一起使用,函数 + 环境引用 = 闭包。看一个例子:
        func main() {
            sum := Sum(1, 2)
            fmt.Println(sum(3, 4))
            fmt.Println(sum(5, 6))
        }

        func Sum(a, b int) func(int, int) int {
            return func(int, int) int {
                return a + b
            }
        }
        3
        3
        ---------------------------------------------------------------------------------------------------------
        在上述代码中,无论传入什么数字,输出结果都是3,稍微修改一下代码
        func main() {
           sum := Sum(5)
           fmt.Println(sum(1, 2))
           fmt.Println(sum(1, 2))
           fmt.Println(sum(1, 2))
        }

        func Sum(sum int) func(int, int) int {
           return func(a, b int) int {
              sum += a + b
              return sum
           }
        }
        8
        11
        14
        ---------------------------------------------------------------------------------------------------------
        匿名函数引用了参数sum,即便Sum函数已经执行完毕,虽然已经超出了它的生命周期,但是对其返回的函数传入参数,
        依旧可以成功的修改其值,这一个过程就是闭包。事实上参数sum已经逃逸到了堆上,只要其返回值函数的生命周期没有结束,
        就不会被回收掉。
        ---------------------------------------------------------------------------------------------------------
        利用这一特性,可以非常简单的实现一个求费波那契数列的函数,代码如下
        func main() {
            fib := Fib(1)
            for i := 0; i < 10; i++ {
                fmt.Println(fib())
            }
        }

        func Fib(n int) func() int {
            if n < 1 {
                n = 1
            }
            a, b := n, n
            return func() int {
                a, b = b, a+b
                return a
            }
        }
        输出为
        1
        2
        3
        5
        8
        13
        21
        34
        55
        89
    g.延迟调用
        defer关键字描述的一个匿名函数会在函数返回之前执行。
        func main() {
            Do()
        }

        func Do() {
            defer func() {
                fmt.Println("1")
            }()
            fmt.Println("2")
        }
        2
        1
        ---------------------------------------------------------------------------------------------------------
        当有多个defer语句时,会按照后进先出的顺序执行。
        func main() {
           Do()
        }
        func Do() {
           defer func() {
              fmt.Println("1")
           }()
           defer func() {
              fmt.Println("2")
           }()
           defer func() {
              fmt.Println("3")
           }()
           defer func() {
              fmt.Println("4")
           }()
           fmt.Println("2")
           defer func() {
              fmt.Println("5")
           }()
        }
        2
        5
        4
        3
        2
        1
        延迟调用通常用于释放文件资源,关闭连接等操作,另一个常用的写法是用于捕获panic。

02.常用信息2
    a.基本函数定义
        func add(a int, b int) int {
            return a + b
        }
        参数和返回值:函数可以接受参数并返回值。参数类型和返回类型必须明确指定。
    b.多返回值
        func divide(a int, b int) (int, int) {
            return a / b, a % b
        }
        多返回值:Go 允许函数返回多个值,常用于返回结果和错误。
    c.命名返回值
        func swap(a, b int) (x int, y int) {
            x = b
            y = a
            return
        }
        命名返回值:你可以在函数定义中为返回值命名,这样可以直接返回这些变量。
    d.可变参数函数
        func sum(nums ...int) int {
            total := 0
            for _, num := range nums {
                total += num
            }
            return total
        }
        可变参数:使用 ... 语法来表示一个可变数量的参数,允许传递任意数量的参数。
    e.匿名函数
        func main() {
            f := func(a, b int) int {
                return a + b
            }
            fmt.Println(f(1, 2))
        }
        匿名函数:Go 支持匿名函数,这些函数没有名字,可以赋值给变量或直接调用。
    f.闭包
        func adder() func(int) int {
            sum := 0
            return func(x int) int {
                sum += x
                return sum
            }
        }
        func main() {
            pos, neg := adder(), adder()
            fmt.Println(pos(1))  // 1
            fmt.Println(pos(2))  // 3
            fmt.Println(neg(-1)) // -1
        }
        闭包:匿名函数可以捕获其外部环境中的变量,这种机制称为闭包。
    g.方法
        type Rectangle struct {
            Width, Height int
        }
        func (r Rectangle) Area() int {
            return r.Width * r.Height
        }
        方法:方法是与类型绑定的函数。它们可以定义在结构体、接口或自定义类型上。
    h.指针接收器方法
        func (r *Rectangle) Scale(factor int) {
            r.Width *= factor
            r.Height *= factor
        }
        指针接收器方法:使用指针接收器定义的方法可以修改接收器的值。
    i.递归函数
        func factorial(n int) int {
            if n == 0 {
                return 1
            }
            return n * factorial(n-1)
        }
        递归:函数可以调用自身,常用于处理递归问题。
    j.延迟执行(defer)
        func main() {
            defer fmt.Println("This is deferred")
            fmt.Println("This is executed first")
        }
        defer 关键字:延迟函数的执行直到包含它的函数返回时执行,常用于资源清理。
    k.错误处理
        func divide(a, b int) (int, error) {
            if b == 0 {
                return 0, fmt.Errorf("cannot divide by zero")
            }
            return a / b, nil
        }
        错误处理:Go 通过返回值的方式处理错误,而不是通过异常。
    l.高阶函数
        func applyOperation(a, b int, op func(int, int) int) int {
            return op(a, b)
        }
        func add(a, b int) int {
            return a + b
        }
        func main() {
            fmt.Println(applyOperation(2, 3, add))
        }
        高阶函数:函数可以作为参数传递给另一个函数,也可以作为返回值返回。
    m.内置函数
        len 和 cap:用于获取切片、数组、通道等的长度和容量。
        make:用于创建切片、映射和通道。
        new:用于分配内存,并返回指向新分配的零值的指针。
        copy:用于复制切片。
        append:用于向切片添加元素。
        panic 和 recover:用于处理运行时错误。
    n.类型转换函数
        var a int = 42
        var b float64 = float64(a)
        类型转换:将一个类型转换为另一个类型。
    o.内联函数
        func max(a, b int) int {
            if a > b {
                return a
            }
            return b
        }
        内联函数:Go 的编译器可以在某些情况下将函数内联,以提高性能(但这个过程是编译器自动进行的,开发者不能强制控制)。
    p.函数的类型
        type Operation func(int, int) int
        func main() {
            var op Operation = add
            fmt.Println(op(3, 4))
        }
        函数类型:可以将函数类型定义为一种类型,这样可以更灵活地处理函数。
    q.函数变量
        func main() {
            f := func(a, b int) int {
                return a + b
            }
            fmt.Println(f(1, 2))
        }
        函数变量:可以将函数赋值给变量,从而在程序的其他地方调用该函数。
    r.方法表达式
        type MyInt int
        func (m MyInt) Add(n int) int {
            return int(m) + n
        }
        func main() {
            var num MyInt = 5
            f := MyInt.Add
            fmt.Println(f(num, 3)) // 8
        }
        方法表达式:允许将方法赋值给变量,并且在调用时传递接收器。
    s.接口与函数
        type Adder interface {
            Add(a, b int) int
        }
        type myAdder struct{}
        func (myAdder) Add(a, b int) int {
            return a + b
        }
        func main() {
            var a Adder = myAdder{}
            fmt.Println(a.Add(3, 4))
        }
        接口与函数:函数可以实现接口,从而使得不同类型的函数表现一致。
    t.内嵌函数
        func outerFunc() {
            innerFunc := func() {
                fmt.Println("Hello from inner function")
            }
            innerFunc()
        }
        内嵌函数:在函数内部定义另一个函数,可以将相关的功能封装在一起。
    u.包级函数
        // utils.go
        package utils
        func Add(a, b int) int {
            return a + b
        }
        // main.go
        package main
        import (
            "fmt"
            "path/to/your/project/utils"
        )
        func main() {
            fmt.Println(utils.Add(1, 2))
        }
        包级函数:函数可以定义在包中并被导出(首字母大写),以便在其他包中使用。
    v.尾递归优化
        Go 的编译器不支持尾递归优化,但你可以通过重写递归函数为循环来优化性能。
        func factorial(n int) int {
            result := 1
            for n > 0 {
                result *= n
                n--
            }
            return result
        }
    w.延迟执行多个函数
        func main() {
            defer fmt.Println("First deferred")
            defer fmt.Println("Second deferred")
            defer fmt.Println("Third deferred")
        }
        多个延迟函数:延迟执行的函数以后进先出(LIFO)的顺序执行。
    x.Panic 和 Recover
        func mayPanic() {
            defer func() {
                if r := recover(); r != nil {
                    fmt.Println("Recovered from:", r)
                }
            }()
            panic("Something went wrong")
        }
        func main() {
            mayPanic()
            fmt.Println("Continuing execution")
        }
        Panic 和 Recover:panic 用于引发运行时错误,recover 用于从 panic 状态中恢复。
    y.函数返回多个值及其应用
    func divide(a, b int) (int, int, error) {
        if b == 0 {
            return 0, 0, fmt.Errorf("division by zero")
        }
        return a / b, a % b, nil
    }
    func main() {
        quotient, remainder, err := divide(10, 2)
        if err != nil {
            fmt.Println("Error:", err)
        } else {
            fmt.Println("Quotient:", quotient, "Remainder:", remainder)
        }
    }
    多个返回值及其应用:Go 的多返回值机制非常适合处理需要返回多个结果或错误的场景。

3.2 函数定义

01.golang函数特点:
    无需声明原型。
    支持不定 变参。
    支持多返回值。
    支持命名返回参数。
    支持匿名函数和闭包。
    函数也是一种类型,一个函数可以赋值给变量。
    ---------------------------------------------------------------------------------------------------------
    不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。
    不支持 重载 (overload)
    不支持 默认参数 (default parameter)。

02.函数声明:
    函数声明包含一个函数名,参数列表,返回值列表和函数体。如果函数没有返回值,则返回列表可以省略。
    函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。
    函数可以没有参数或接受多个参数。
    注意类型在变量名之后 。
    当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。
    函数可以返回任意数量的返回值。
    使用关键字 func 定义函数,左大括号依旧不能另起一行。
    ---------------------------------------------------------------------------------------------------------
    func test(x, y int, s string) (int, string) {
        // 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。
        n := x + y
        return n, fmt.Sprintf(s, n)
    }
    ---------------------------------------------------------------------------------------------------------
    函数是第一类对象,可作为参数传递。建议将复杂签名定义为函数类型,以便于阅读。
    package main

    import "fmt"

    func test(fn func() int) int {
        return fn()
    }
    // 定义函数类型。
    type FormatFunc func(s string, x, y int) string

    func format(fn FormatFunc, s string, x, y int) string {
        return fn(s, x, y)
    }

    func main() {
        s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。

        s2 := format(func(s string, x, y int) string {
            return fmt.Sprintf(s, x, y)
        }, "%d, %d", 10, 20)

        println(s1, s2)
    }
    输出结果:
    100 10, 20
    ---------------------------------------------------------------------------------------------------------
    有返回值的函数,必须有明确的终止语句,否则会引发编译错误。
    你可能会偶尔遇到没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义了函数标识符。
    package math
    func Sin(x float64) float //implemented in assembly language

3.3 参数

01.函数参数
    函数定义时指出,函数定义时有参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。
    但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:
    ---------------------------------------------------------------------------------------------------------
    值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
    func swap(x, y int) int {
           ... ...
    }
    ---------------------------------------------------------------------------------------------------------
    引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
    package main

    import (
        "fmt"
    )

    /* 定义相互交换值的函数 */
    func swap(x, y *int) {
        var temp int
        temp = *x /* 保存 x 的值 */
        *x = *y   /* 将 y 值赋给 x */
        *y = temp /* 将 temp 值赋给 y*/

    }

    func main() {
        var a, b int = 1, 2
        /*
            调用 swap() 函数
            &a 指向 a 指针,a 变量的地址
            &b 指向 b 指针,b 变量的地址
        */
        swap(&a, &b)

        fmt.Println(a, b)
    }
    输出结果:
    2 1

02.函数参数
    a.介绍
        在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
        注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
        注意2:map、slice、chan、指针、interface默认以引用的方式传递。
        不定参数传值 就是函数的参数不是固定的,后面的类型是固定的。(可变参数)
        Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。
    b.示例
        在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上“…”即可。
        func myfunc(args ...int) {    //0个或多个参数
        }
        func add(a int, args…int) int {    //1个或多个参数
        }
        func add(a int, b int, args…int) int {    //2个或多个参数
        }
        注意:其中args是一个slice,我们可以通过arg[index]依次访问所有参数,通过len(arg)来判断传递参数的个数.
    c.示例
        任意类型的不定参数: 就是函数的参数和每个参数的类型都不是固定的。
        用interface{}传递任意类型数据是Go语言的惯例用法,而且interface{}是类型安全的。
        func myfunc(args ...interface{}) {
        }
        -----------------------------------------------------------------------------------------------------
        代码:
        package main

        import (
            "fmt"
        )

        func test(s string, n ...int) string {
            var x int
            for _, i := range n {
                x += i
            }

            return fmt.Sprintf(s, x)
        }

        func main() {
            println(test("sum: %d", 1, 2, 3))
        }
        输出结果:
        sum: 6
        -----------------------------------------------------------------------------------------------------
        使用 slice 对象做变参时,必须展开。(slice...)
        package main

        import (
            "fmt"
        )

        func test(s string, n ...int) string {
            var x int
            for _, i := range n {
                x += i
            }

            return fmt.Sprintf(s, x)
        }

        func main() {
            s := []int{1, 2, 3}
            res := test("sum: %d", s...)    // slice... 展开slice
            println(res)
        }

3.4 返回值

01.函数返回值
    "_"标识符,用来忽略函数的某个返回值
    Go 的返回值可以被命名,并且就像在函数体开头声明的变量那样使用。
    返回值的名称应当具有一定的意义,可以作为文档使用。
    没有参数的 return 语句返回各个返回变量的当前值。这种用法被称作“裸”返回。
    直接返回语句仅应当用在像下面这样的短函数中。在长的函数中它们会影响代码的可读性。
    ---------------------------------------------------------------------------------------------------------
    package main

    import (
        "fmt"
    )

    func add(a, b int) (c int) {
        c = a + b
        return
    }

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

        return
    }

    func main() {
        var a, b int = 1, 2
        c := add(a, b)
        sum, avg := calc(a, b)
        fmt.Println(a, b, c, sum, avg)
    }
    输出结果:
    1 2 3 3 1

02.示例
    Golang返回值不能用容器对象接收多返回值。只能用多个变量,或 "_" 忽略。
    package main

    func test() (int, int) {
        return 1, 2
    }

    func main() {
        // s := make([]int, 2)
        // s = test()   // Error: multiple-value test() in single-value context

        x, _ := test()
        println(x)
    }
    输出结果:
    1
    ---------------------------------------------------------------------------------------------------------
    多返回值可直接作为其他函数调用实参。
    package main

    func test() (int, int) {
        return 1, 2
    }

    func add(x, y int) int {
        return x + y
    }

    func sum(n ...int) int {
        var x int
        for _, i := range n {
            x += i
        }

        return x
    }

    func main() {
        println(add(test()))
        println(sum(test()))
    }
    输出结果:
    3
    3
    ---------------------------------------------------------------------------------------------------------
    命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。
    package main

    func add(x, y int) (z int) {
        z = x + y
        return
    }

    func main() {
        println(add(1, 2))
    }
    输出结果:
    3
    ---------------------------------------------------------------------------------------------------------
    命名返回参数可被同名局部变量遮蔽,此时需要显式返回。
    func add(x, y int) (z int) {
        { // 不能在一个级别,引发 "z redeclared in this block" 错误。
            var z = x + y
            // return   // Error: z is shadowed during return
            return z // 必须显式返回。
        }
    }
    ---------------------------------------------------------------------------------------------------------
    命名返回参数允许 defer 延迟调用通过闭包读取和修改。
    package main

    func add(x, y int) (z int) {
        defer func() {
            z += 100
        }()

        z = x + y
        return
    }

    func main() {
        println(add(1, 2))
    }
    输出结果:
    103
    ---------------------------------------------------------------------------------------------------------
    显式 return 返回前,会先修改命名返回参数。
    package main

    func add(x, y int) (z int) {
        defer func() {
            println(z) // 输出: 203
        }()

        z = x + y
        return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (return)
    }

    func main() {
        println(add(1, 2)) // 输出: 203
    }
    输出结果:
    203
    203

3.5 匿名函数

01.匿名函数
    匿名函数是指不需要定义函数名的一种函数实现方式。1958年LISP首先采用匿名函数。
    在Go里面,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数。
    匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
    ---------------------------------------------------------------------------------------------------------
    package main

    import (
        "fmt"
        "math"
    )

    func main() {
        getSqrt := func(a float64) float64 {
            return math.Sqrt(a)
        }
        fmt.Println(getSqrt(4))
    }
    输出结果:
    2
    ---------------------------------------------------------------------------------------------------------
    上面先定义了一个名为getSqrt 的变量,初始化该变量时和之前的变量初始化有些不同,使用了func,func是定义函数的,
    可是这个函数和上面说的函数最大不同就是没有函数名,也就是匿名函数。这里将一个函数当做一个变量一样的操作。

02.匿名函数
    Golang匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送。
    ---------------------------------------------------------------------------------------------------------
    package main

    func main() {
        // --- function variable ---
        fn := func() { println("Hello, World!") }
        fn()

        // --- function collection ---
        fns := [](func(x int) int){
            func(x int) int { return x + 1 },
            func(x int) int { return x + 2 },
        }
        println(fns[0](100))

        // --- function as field ---
        d := struct {
            fn func() string
        }{
            fn: func() string { return "Hello, World!" },
        }
        println(d.fn())

        // --- channel of function ---
        fc := make(chan func() string, 2)
        fc <- func() string { return "Hello, World!" }
        println((<-fc)())
    }
    输出结果:
    Hello, World!
    101
    Hello, World!
    Hello, World!

3.6 闭包、递归

00.闭包详解
    闭包的应该都听过,但到底什么是闭包呢?
    闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。
    “官方”的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
    维基百科讲,闭包(Closure),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
    看着上面的描述,会发现闭包和匿名函数似乎有些像。可是可能还是有些云里雾里的。因为跳过闭包的创建过程直接理解闭包的定义是非常困难的。目前在JavaScript、Go、PHP、Scala、Scheme、Common Lisp、Smalltalk、Groovy、Ruby、 Python、Lua、objective c、Swift 以及Java8以上等语言中都能找到对闭包不同程度的支持。通过支持闭包的语法可以发现一个特点,他们都有垃圾回收(GC)机制。 javascript应该是普及度比较高的编程语言了,通过这个来举例应该好理解写。看下面的代码,只要关注script里方法的定义和调用就可以了。
    ---------------------------------------------------------------------------------------------------------
    <!DOCTYPE html>
    <html lang="zh">
    <head>
        <title></title>
    </head>
    <body>
    </body>
    </html>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js" type="text/javascript"></script>
    <script>
    function a(){
        var i=0;
        function b(){
            console.log(++i);
            document.write("<h1>"+i+"</h1>");
        }
        return b;
    }

    $(function(){
        var c=a();
        c();
        c();
        c();
        //a(); //不会有信息输出
        document.write("<h1>=============</h1>");
        var c2=a();
        c2();
        c2();
    });
    </script>
    ---------------------------------------------------------------------------------------------------------
    这段代码有两个特点:
    函数b嵌套在函数a内部 函数a返回函数b 这样在执行完var c=a()后,变量c实际上是指向了函数b(),再执行函数c()后就会显示i的值,第一次为1,第二次为2,第三次为3,以此类推。 其实,这段代码就创建了一个闭包。因为函数a()外的变量c引用了函数a()内的函数b(),就是说:
    当函数a()的内部函数b()被函数a()外的一个变量引用的时候,就创建了一个闭包。 在上面的例子中,由于闭包的存在使得函数a()返回后,a中的i始终存在,这样每次执行c(),i都是自加1后的值。 从上面可以看出闭包的作用就是在a()执行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回a()所占用的资源,因为a()的内部函数b()的执行需要依赖a()中的变量i。
    在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。不过,变量的生存期是可以很长,在一次函数调用期间所创建所生成的值在下次函数调用时仍然存在。正因为这一特点,闭包可以用来完成信息隐藏,并进而应用于需要状态表达的某些编程范型中。 下面来想象另一种情况,如果a()返回的不是函数b(),情况就完全不同了。因为a()执行完后,b()没有被返回给a()的外界,只是被a()所引用,而此时a()也只会被b()引 用,因此函数a()和b()互相引用但又不被外界打扰(被外界引用),函数a和b就会被GC回收。所以直接调用a();是页面并没有信息输出。
    下面来说闭包的另一要素引用环境。c()跟c2()引用的是不同的环境,在调用i++时修改的不是同一个i,因此两次的输出都是1。函数a()每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。这和c()和c()的调用顺序都是无关的。

01.Go的闭包
    Go语言是支持闭包的,这里只是简单地讲一下在Go语言中闭包是如何实现的。
    下面我来将之前的JavaScript的闭包例子用Go来实现。
    ---------------------------------------------------------------------------------------------------------
    package main

    import (
        "fmt"
    )

    func a() func() int {
        i := 0
        b := func() int {
            i++
            fmt.Println(i)
            return i
        }
        return b
    }

    func main() {
        c := a()
        c()
        c()
        c()

        a() //不会输出i
    }
    输出结果:
    1
    2
    3
    可以发现,输出和之前的JavaScript的代码是一致的。具体的原因和上面的也是一样的,这说明Go语言是支持闭包的。
    ---------------------------------------------------------------------------------------------------------
    闭包复制的是原对象指针,这就很容易解释延迟引用现象。
    package main

    import "fmt"

    func test() func() {
        x := 100
        fmt.Printf("x (%p) = %d\n", &x, x)

        return func() {
            fmt.Printf("x (%p) = %d\n", &x, x)
        }
    }

    func main() {
        f := test()
        f()
    }
    输出:
    x (0xc42007c008) = 100
    x (0xc42007c008) = 100
    ---------------------------------------------------------------------------------------------------------
    在汇编层 ,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址、闭包对象指针。
    当调 匿名函数时,只需以某个寄存器传递该对象即可。
    FuncVal { func_address, closure_var_pointer ... }
    ---------------------------------------------------------------------------------------------------------
    外部引用函数参数局部变量
    package main

    import "fmt"

    // 外部引用函数参数局部变量
    func add(base int) func(int) int {
        return func(i int) int {
            base += i
            return base
        }
    }

    func main() {
        tmp1 := add(10)
        fmt.Println(tmp1(1), tmp1(2))
        // 此时tmp1和tmp2不是一个实体了
        tmp2 := add(100)
        fmt.Println(tmp2(1), tmp2(2))
    }
    ---------------------------------------------------------------------------------------------------------
    返回2个闭包
    package main

    import "fmt"

    // 返回2个函数类型的返回值
    func test01(base int) (func(int) int, func(int) int) {
        // 定义2个函数,并返回
        // 相加
        add := func(i int) int {
            base += i
            return base
        }
        // 相减
        sub := func(i int) int {
            base -= i
            return base
        }
        // 返回
        return add, sub
    }

    func main() {
        f1, f2 := test01(10)
        // base一直是没有消
        fmt.Println(f1(1), f2(2))
        // 此时base是9
        fmt.Println(f1(3), f2(4))
    }

02.Go语言递归函数
    递归,就是在运行的过程中调用自己。 一个函数调用自己,就叫做递归函数。
    构成递归需具备的条件:
    1.子问题须与原始问题为同样的事,且更为简单。
    2.不能无限制地调用本身,须有个出口,化简为非递归状况处理。
    ---------------------------------------------------------------------------------------------------------
    数字阶乘
    一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。自然数n的阶乘写作n!。1808年,基斯顿·卡曼引进这个表示法。
    package main

    import "fmt"

    func factorial(i int) int {
        if i <= 1 {
            return 1
        }
        return i * factorial(i-1)
    }

    func main() {
        var i int = 7
        fmt.Printf("Factorial of %d is %d\n", i, factorial(i))
    }
    输出结果:
    Factorial of 7 is 5040
    ---------------------------------------------------------------------------------------------------------
    斐波那契数列(Fibonacci)
    这个数列从第3项开始,每一项都等于前两项之和。
    package main

    import "fmt"

    func fibonaci(i int) int {
        if i == 0 {
            return 0
        }
        if i == 1 {
            return 1
        }
        return fibonaci(i-1) + fibonaci(i-2)
    }

    func main() {
        var i int
        for i = 0; i < 10; i++ {
            fmt.Printf("%d\n", fibonaci(i))
        }
    }
    输出结果:
        0
        1
        1
        2
        3
        5
        8
        13
        21
        34

3.7 延迟调用

01.Golang延迟调用:
    defer特性:
        1. 关键字 defer 用于注册延迟调用。
        2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
        3. 多个defer语句,按先进后出的方式执行。
        4. defer语句中的变量,在defer声明时就决定了。
    defer用途:
        1. 关闭文件句柄
        2. 锁资源释放
        3. 数据库连接释放
    ---------------------------------------------------------------------------------------------------------
    go语言 defer
    go 语言的defer功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱。
    defer 是先进后出
    这个很自然,后面的语句会依赖前面的资源,因此如果先前面的资源先释放了,后面的语句就没法执行了。
    ---------------------------------------------------------------------------------------------------------
    package main

    import "fmt"

    func main() {
        var whatever [5]struct{}

        for i := range whatever {
            defer fmt.Println(i)
        }
    }
    输出结果:
    4
    3
    2
    1
    0
    ---------------------------------------------------------------------------------------------------------
    defer 碰上闭包
    package main

    import "fmt"

    func main() {
        var whatever [5]struct{}
        for i := range whatever {
            defer func() { fmt.Println(i) }()
        }
    }
    输出结果:
    4
    4
    4
    4
    4
    其实go说的很清楚,我们一起来看看go spec如何说的
    Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usualand saved anew but the actual function is not invoked.
    也就是说函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成4,所以输出全都是4.
    ---------------------------------------------------------------------------------------------------------
    defer f.Close
    这个大家用的都很频繁,但是go语言编程举了一个可能一不小心会犯错的例子.

    package main

    import "fmt"

    type Test struct {
        name string
    }

    func (t *Test) Close() {
        fmt.Println(t.name, " closed")
    }
    func main() {
        ts := []Test{{"a"}, {"b"}, {"c"}}
        for _, t := range ts {
            defer t.Close()
        }
    }
    输出结果:
    c  closed
    c  closed
    c  closed
    这个输出并不会像我们预计的输出c b a,而是输出c c c
    可是按照前面的go spec中的说明,应该输出c b a才对啊.
    ---------------------------------------------------------------------------------------------------------
    那我们换一种方式来调用一下.

    package main

    import "fmt"

    type Test struct {
        name string
    }

    func (t *Test) Close() {
        fmt.Println(t.name, " closed")
    }
    func Close(t Test) {
        t.Close()
    }
    func main() {
        ts := []Test{{"a"}, {"b"}, {"c"}}
        for _, t := range ts {
            defer Close(t)
        }
    }
    输出结果:
    c  closed
    b  closed
    a  closed
    这个时候输出的就是c b a
    ---------------------------------------------------------------------------------------------------------
    当然,如果你不想多写一个函数,也很简单,可以像下面这样,同样会输出c b a
    看似多此一举的声明
    package main

    import "fmt"

    type Test struct {
        name string
    }

    func (t *Test) Close() {
        fmt.Println(t.name, " closed")
    }
    func main() {
        ts := []Test{{"a"}, {"b"}, {"c"}}
        for _, t := range ts {
            t2 := t
            defer t2.Close()
        }
    }
    输出结果:
    c  closed
    b  closed
    a  closed
    通过以上例子,结合Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usualand saved anew but the actual function is not invoked.这句话。可以得出下面的结论:
    defer后面的语句在执行的时候,函数调用的参数会被保存起来,但是不执行。也就是复制了一份。但是并没有说struct这里的this指针如何处理,通过这个例子可以看出go语言并没有把这个明确写出来的this指针当作参数来看待。
    ---------------------------------------------------------------------------------------------------------
    多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
    package main

    func test(x int) {
        defer println("a")
        defer println("b")

        defer func() {
            println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
        }()

        defer println("c")
    }

    func main() {
        test(0)
    }
    输出结果:
    c
    b
    a
    panic: runtime error: integer divide by zero
    ---------------------------------------------------------------------------------------------------------
    *延迟调用参数在注册时求值或复制,可用指针或闭包 "延迟" 读取。
    package main

    func test() {
        x, y := 10, 20

        defer func(i int) {
            println("defer:", i, y) // y 闭包引用
        }(x) // x 被复制

        x += 10
        y += 100
        println("x =", x, "y =", y)
    }

    func main() {
        test()
    }
    输出结果:
    x = 20 y = 120
    defer: 10 120
    ---------------------------------------------------------------------------------------------------------
    *滥用 defer 可能会导致性能问题,尤其是在一个 "大循环" 里。
    package main

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

    var lock sync.Mutex

    func test() {
        lock.Lock()
        lock.Unlock()
    }

    func testdefer() {
        lock.Lock()
        defer lock.Unlock()
    }

    func main() {
        func() {
            t1 := time.Now()

            for i := 0; i < 10000; i++ {
                test()
            }
            elapsed := time.Since(t1)
            fmt.Println("test elapsed: ", elapsed)
        }()
        func() {
            t1 := time.Now()

            for i := 0; i < 10000; i++ {
                testdefer()
            }
            elapsed := time.Since(t1)
            fmt.Println("testdefer elapsed: ", elapsed)
        }()

    }
    输出结果:
    test elapsed:  223.162µs
    testdefer elapsed:  781.304µs

02.defer陷阱
    defer 与 closure
    package main

    import (
        "errors"
        "fmt"
    )

    func foo(a, b int) (i int, err error) {
        defer fmt.Printf("first defer err %v\n", err)
        defer func(err error) { fmt.Printf("second defer err %v\n", err) }(err)
        defer func() { fmt.Printf("third defer err %v\n", err) }()
        if b == 0 {
            err = errors.New("divided by zero!")
            return
        }

        i = a / b
        return
    }

    func main() {
        foo(2, 0)
    }
    输出结果:
    third defer err divided by zero!
    second defer err <nil>
    first defer err <nil>
    解释:如果 defer 后面跟的不是一个 closure 最后执行的时候我们得到的并不是最新的值。
    ---------------------------------------------------------------------------------------------------------
    defer 与 return
    package main

    import "fmt"

    func foo() (i int) {

        i = 0
        defer func() {
            fmt.Println(i)
        }()

        return 2
    }

    func main() {
        foo()
    }
    输出结果:
    2
    解释:在有具名返回值的函数中(这里具名返回值为 i),执行 return 2 的时候实际上已经将 i 的值重新赋值为 2。所以defer closure 输出结果为 2 而不是 1。
    ---------------------------------------------------------------------------------------------------------
    defer nil 函数
    package main

    import (
        "fmt"
    )

    func test() {
        var run func() = nil
        defer run()
        fmt.Println("runs")
    }

    func main() {
        defer func() {
            if err := recover(); err != nil {
                fmt.Println(err)
            }
        }()
        test()
    }
    输出结果:
    runs
    runtime error: invalid memory address or nil pointer dereference
    解释:名为 test 的函数一直运行至结束,然后 defer 函数会被执行且会因为值为 nil 而产生 panic 异常。然而值得注意的是,run() 的声明是没有问题,因为在test函数运行完成后它才会被调用。
    ---------------------------------------------------------------------------------------------------------
    在错误的位置使用 defer
    当 http.Get 失败时会抛出异常。

    package main

    import "net/http"

    func do() error {
        res, err := http.Get("http://www.google.com")
        defer res.Body.Close()
        if err != nil {
            return err
        }

        // ..code...

        return nil
    }

    func main() {
        do()
    }
    输出结果:
    panic: runtime error: invalid memory address or nil pointer dereference
    因为在这里我们并没有检查我们的请求是否成功执行,当它失败的时候,我们访问了 Body 中的空变量 res ,因此会抛出异常
    ---------------------------------------------------------------------------------------------------------
    解决方案
    总是在一次成功的资源分配下面使用 defer ,对于这种情况来说意味着:当且仅当 http.Get 成功执行时才使用 defer

    package main

    import "net/http"

    func do() error {
        res, err := http.Get("http://xxxxxxxxxx")
        if res != nil {
            defer res.Body.Close()
        }

        if err != nil {
            return err
        }

        // ..code...

        return nil
    }

    func main() {
        do()
    }
    在上述的代码中,当有错误的时候,err 会被返回,否则当整个函数返回的时候,会关闭 res.Body 。
    解释:在这里,你同样需要检查 res 的值是否为 nil ,这是 http.Get 中的一个警告。通常情况下,出错的时候,返回的内容应为空并且错误会被返回,可当你获得的是一个重定向 error 时, res 的值并不会为 nil ,但其又会将错误返回。上面的代码保证了无论如何 Body 都会被关闭,如果你没有打算使用其中的数据,那么你还需要丢弃已经接收的数据。
    -------------------------------------------------------------------------------------------------------------
    不检查错误
    在这里,f.Close() 可能会返回一个错误,可这个错误会被我们忽略掉

    package main

    import "os"

    func do() error {
        f, err := os.Open("book.txt")
        if err != nil {
            return err
        }

        if f != nil {
            defer f.Close()
        }

        // ..code...

        return nil
    }

    func main() {
        do()
    }
    ---------------------------------------------------------------------------------------------------------
    改进一下
    package main

    import "os"

    func do() error {
        f, err := os.Open("book.txt")
        if err != nil {
            return err
        }

        if f != nil {
            defer func() {
                if err := f.Close(); err != nil {
                    // log etc
                }
            }()
        }

        // ..code...

        return nil
    }

    func main() {
        do()
    }
    ---------------------------------------------------------------------------------------------------------
    再改进一下
    通过命名的返回变量来返回 defer 内的错误。
    package main

    import "os"

    func do() (err error) {
        f, err := os.Open("book.txt")
        if err != nil {
            return err
        }

        if f != nil {
            defer func() {
                if ferr := f.Close(); ferr != nil {
                    err = ferr
                }
            }()
        }

        // ..code...

        return nil
    }

    func main() {
        do()
    }
    ---------------------------------------------------------------------------------------------------------
    释放相同的资源
    如果你尝试使用相同的变量释放不同的资源,那么这个操作可能无法正常执行。
    package main

    import (
        "fmt"
        "os"
    )

    func do() error {
        f, err := os.Open("book.txt")
        if err != nil {
            return err
        }
        if f != nil {
            defer func() {
                if err := f.Close(); err != nil {
                    fmt.Printf("defer close book.txt err %v\n", err)
                }
            }()
        }

        // ..code...

        f, err = os.Open("another-book.txt")
        if err != nil {
            return err
        }
        if f != nil {
            defer func() {
                if err := f.Close(); err != nil {
                    fmt.Printf("defer close another-book.txt err %v\n", err)
                }
            }()
        }

        return nil
    }

    func main() {
        do()
    }
    输出结果: defer close book.txt err close ./another-book.txt: file already closed
    ---------------------------------------------------------------------------------------------------------
    当延迟函数执行时,只有最后一个变量会被用到,因此,f 变量 会成为最后那个资源 (another-book.txt)。而且两个 defer 都会将这个资源作为最后的资源来关闭
    解决方案:
    package main

    import (
        "fmt"
        "io"
        "os"
    )

    func do() error {
        f, err := os.Open("book.txt")
        if err != nil {
            return err
        }
        if f != nil {
            defer func(f io.Closer) {
                if err := f.Close(); err != nil {
                    fmt.Printf("defer close book.txt err %v\n", err)
                }
            }(f)
        }

        // ..code...

        f, err = os.Open("another-book.txt")
        if err != nil {
            return err
        }
        if f != nil {
            defer func(f io.Closer) {
                if err := f.Close(); err != nil {
                    fmt.Printf("defer close another-book.txt err %v\n", err)
                }
            }(f)
        }

        return nil
    }

    func main() {
        do()
    }

3.8 异常处理

00.介绍
    Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。
    异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
    panic:
        1、内置函数
        2、假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
        3、返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
        4、直到goroutine整个退出,并报告错误
    recover:
        1、内置函数
        2、用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
        3、一般的调用建议
            a). 在defer函数中,通过recever来终止一个goroutine的panicking过程,从而恢复正常代码的执行
            b). 可以获取通过panic传递的error
    注意:
        1.利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
        2.recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
        3.多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。

02.异常处理
    package main

    func main() {
        test()
    }

    func test() {
        defer func() {
            if err := recover(); err != nil {
                println(err.(string)) // 将 interface{} 转型为具体类型。
            }
        }()

        panic("panic error!")
    }
    输出结果:
        panic error!
    ---------------------------------------------------------------------------------------------------------
    由于 panic、recover 参数类型为 interface{},因此可抛出任何类型对象。
        func panic(v interface{})
        func recover() interface{}
    向已关闭的通道发送数据会引发panic
    package main

    import (
        "fmt"
    )

    func main() {
        defer func() {
            if err := recover(); err != nil {
                fmt.Println(err)
            }
        }()

        var ch chan int = make(chan int, 10)
        close(ch)
        ch <- 1
    }
    输出结果:
    send on closed channel
    ---------------------------------------------------------------------------------------------------------
    延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。
    package main

    import "fmt"

    func test() {
        defer func() {
            fmt.Println(recover())
        }()

        defer func() {
            panic("defer panic")
        }()

        panic("test panic")
    }

    func main() {
        test()
    }
    输出:
    defer panic
    ---------------------------------------------------------------------------------------------------------
    捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。
    package main

    import "fmt"

    func test() {
        defer func() {
            fmt.Println(recover()) //有效
        }()
        defer recover()              //无效!
        defer fmt.Println(recover()) //无效!
        defer func() {
            func() {
                println("defer inner")
                recover() //无效!
            }()
        }()

        panic("test panic")
    }

    func main() {
        test()
    }
    输出:
    defer inner
    <nil>
    test panic
    ---------------------------------------------------------------------------------------------------------
    使用延迟匿名函数或下面这样都是有效的。
    package main

    import (
        "fmt"
    )

    func except() {
        fmt.Println(recover())
    }

    func test() {
        defer except()
        panic("test panic")
    }

    func main() {
        test()
    }
    输出结果:
    test panic
    ---------------------------------------------------------------------------------------------------------
    如果需要保护代码 段,可将代码块重构成匿名函数,如此可确保后续代码被执 。
    package main

    import "fmt"

    func test(x, y int) {
        var z int

        func() {
            defer func() {
                if recover() != nil {
                    z = 0
                }
            }()
            panic("test panic")
            z = x / y
            return
        }()

        fmt.Printf("x / y = %d\n", z)
    }

    func main() {
        test(2, 1)
    }
    输出结果:
    x / y = 0
    ---------------------------------------------------------------------------------------------------------
    除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。
    type error interface {
        Error() string
    }
    ---------------------------------------------------------------------------------------------------------
    标准库 errors.New 和 fmt.Errorf 函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。
    package main

    import (
        "errors"
        "fmt"
    )

    var ErrDivByZero = errors.New("division by zero")

    func div(x, y int) (int, error) {
        if y == 0 {
            return 0, ErrDivByZero
        }
        return x / y, nil
    }

    func main() {
        defer func() {
            fmt.Println(recover())
        }()
        switch z, err := div(10, 0); err {
        case nil:
            println(z)
        case ErrDivByZero:
            panic(err)
        }
    }
    输出结果:
    division by zero
    ---------------------------------------------------------------------------------------------------------
    Go实现类似 try catch 的异常处理
    package main

    import "fmt"

    func Try(fun func(), handler func(interface{})) {
        defer func() {
            if err := recover(); err != nil {
                handler(err)
            }
        }()
        fun()
    }

    func main() {
        Try(func() {
            panic("test panic")
        }, func(err interface{}) {
            fmt.Println(err)
        })
    }
    输出结果:
        test panic
    ---------------------------------------------------------------------------------------------------------
    如何区别使用 panic 和 error 两种方式?
    惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。

3.9 单元测试

01.go test工具
    Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。
    go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。
    ---------------------------------------------------------------------------------------------------------
    在*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
    类型  格式  作用
    测试函数    函数名前缀为Test  测试程序的一些逻辑行为是否正确
    基准函数    函数名前缀为Benchmark 测试函数的性能
    示例函数    函数名前缀为Example   为文档提供示例文档
    go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
    ---------------------------------------------------------------------------------------------------------
    Golang单元测试对文件名和方法名,参数都有很严格的要求。
    1、文件名必须以xx_test.go命名
    2、方法必须是Test[^a-z]开头
    3、方法参数必须 t *testing.T
    4、使用go test执行单元测试
    ---------------------------------------------------------------------------------------------------------
    go test的参数解读:
    go test是go语言自带的测试工具,其中包含的是两类,单元测试和性能测试
    通过go help test可以看到go test的使用说明:
    格式形如: go test [-c] [-i] [build flags] [packages] [flags for test binary]
    参数解读:
    -c : 编译go test成为可执行的二进制文件,但是不运行测试。
    -i : 安装测试包依赖的package,但是不运行测试。
    ---------------------------------------------------------------------------------------------------------
    关于build flags,调用go help build,这些是编译运行过程中需要使用到的参数,一般设置为空
    关于packages,调用go help packages,这些是关于包的管理,一般设置为空
    关于flags for test binary,调用go help testflag,这些是go test过程中经常使用到的参数
    ---------------------------------------------------------------------------------------------------------
    -test.v : 是否输出全部的单元测试用例(不管成功或者失败),默认没有加上,所以只输出失败的单元测试用例。
    -test.run pattern: 只跑哪些单元测试用例
    -test.bench patten: 只跑那些性能测试用例
    -test.benchmem : 是否在性能测试的时候输出内存情况
    -test.benchtime t : 性能测试运行的时间,默认是1s
    -test.cpuprofile cpu.out : 是否输出cpu性能分析文件
    -test.memprofile mem.out : 是否输出内存性能分析文件
    -test.blockprofile block.out : 是否输出内部goroutine阻塞的性能分析文件
    -test.memprofilerate n : 内存性能分析的时候有一个分配了多少的时候才打点记录的问题。这个参数就是设置打点的内存分配间隔,也就是profile中一个sample代表的内存大小。默认是设置为512 * 1024的。如果你将它设置为1,则每分配一个内存块就会在profile中有个打点,那么生成的profile的sample就会非常多。如果你设置为0,那就是不做打点了。你可以通过设置memprofilerate=1和GOGC=off来关闭内存回收,并且对每个内存块的分配进行观察。
    -test.blockprofilerate n: 基本同上,控制的是goroutine阻塞时候打点的纳秒数。默认不设置就相当于-test.blockprofilerate=1,每一纳秒都打点记录一下
    -test.parallel n : 性能测试的程序并行cpu数,默认等于GOMAXPROCS。
    -test.timeout t : 如果测试用例运行时间超过t,则抛出panic
    -test.cpu 1,2,4 : 程序运行在哪些CPU上面,使用二进制的1所在位代表,和nginx的nginx_worker_cpu_affinity是一个道理
    -test.short : 将那些运行时间较长的测试用例运行时间缩短
    ---------------------------------------------------------------------------------------------------------
    目录结构:
        test
          |
           —— calc.go
          |
           —— calc_test.go

02.测试函数的格式
    每个测试函数必须导入testing包,测试函数的基本格式(签名)如下:
    func TestName(t *testing.T){
        // ...
    }
    ---------------------------------------------------------------------------------------------------------
    测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头,举几个例子:\
    func TestAdd(t *testing.T){ ... }
    func TestSum(t *testing.T){ ... }
    func TestLog(t *testing.T){ ... }
    ---------------------------------------------------------------------------------------------------------1
    其中参数t用于报告测试失败和附加的日志信息。 testing.T的拥有的方法如下:
    func (c *T) Error(args ...interface{})
    func (c *T) Errorf(format string, args ...interface{})
    func (c *T) Fail()
    func (c *T) FailNow()
    func (c *T) Failed() bool
    func (c *T) Fatal(args ...interface{})
    func (c *T) Fatalf(format string, args ...interface{})
    func (c *T) Log(args ...interface{})
    func (c *T) Logf(format string, args ...interface{})
    func (c *T) Name() string
    func (t *T) Parallel()
    func (t *T) Run(name string, f func(t *T)) bool
    func (c *T) Skip(args ...interface{})
    func (c *T) SkipNow()
    func (c *T) Skipf(format string, args ...interface{})
    func (c *T) Skipped() bool

03.测试函数示例
    就像细胞是构成我们身体的基本单位,一个软件程序也是由很多单元组件构成的。单元组件可以是函数、结构体、方法和最终用户可能依赖的任意东西。总之我们需要确保这些组件是能够正常运行的。单元测试是一些利用各种方法测试单元组件的程序,它会将结果与预期输出进行比较。
    接下来,我们定义一个split的包,包中定义了一个Split函数,具体实现如下:
    ---------------------------------------------------------------------------------------------------------
    // split/split.go

    package split

    import "strings"

    // split package with a single split function.

    // Split slices s into all substrings separated by sep and
    // returns a slice of the substrings between those separators.
    func Split(s, sep string) (result []string) {
        i := strings.Index(s, sep)

        for i > -1 {
            result = append(result, s[:i])
            s = s[i+1:]
            i = strings.Index(s, sep)
        }
        result = append(result, s)
        return
    }
    ---------------------------------------------------------------------------------------------------------
    在当前目录下,我们创建一个split_test.go的测试文件,并定义一个测试函数如下:
    // split/split_test.go

    package split

    import (
        "reflect"
        "testing"
    )

    func TestSplit(t *testing.T) { // 测试函数名必须以Test开头,必须接收一个*testing.T类型参数
        got := Split("a:b:c", ":")         // 程序输出的结果
        want := []string{"a", "b", "c"}    // 期望的结果
        if !reflect.DeepEqual(want, got) { // 因为slice不能比较直接,借助反射包中的方法比较
            t.Errorf("excepted:%v, got:%v", want, got) // 测试失败输出错误提示
        }
    }
    ---------------------------------------------------------------------------------------------------------
    此时split这个包中的文件如下:
    split $ ls -l
    total 16
    -rw-r--r--  1 pprof staff  408  4 29 15:50 split.go
    -rw-r--r--  1 pprof  staff  466  4 29 16:04 split_test.go
    ---------------------------------------------------------------------------------------------------------
    在split包路径下,执行go test命令,可以看到输出结果如下:
    split $ go test
    PASS
    ok      github.com/pprof/studygo/code_demo/test_demo/split       0.005s
    ---------------------------------------------------------------------------------------------------------
    一个测试用例有点单薄,我们再编写一个测试使用多个字符切割字符串的例子,在split_test.go中添加如下测试函数:
    func TestMoreSplit(t *testing.T) {
        got := Split("abcd", "bc")
        want := []string{"a", "d"}
        if !reflect.DeepEqual(want, got) {
            t.Errorf("excepted:%v, got:%v", want, got)
        }
    }
    ---------------------------------------------------------------------------------------------------------
    再次运行go test命令,输出结果如下:
    split $ go test
    --- FAIL: TestMultiSplit (0.00s)
        split_test.go:20: excepted:[a d], got:[a cd]
    FAIL
    exit status 1
    FAIL    github.com/pprof/studygo/code_demo/test_demo/split       0.006s
    ---------------------------------------------------------------------------------------------------------
    这一次,我们的测试失败了。我们可以为go test命令添加-v参数,查看测试函数名称和运行时间:
    split $ go test -v
    === RUN   TestSplit
    --- PASS: TestSplit (0.00s)
    === RUN   TestMoreSplit
    --- FAIL: TestMoreSplit (0.00s)
        split_test.go:21: excepted:[a d], got:[a cd]
    FAIL
    exit status 1
    FAIL    github.com/pprof/studygo/code_demo/test_demo/split       0.005s
    ---------------------------------------------------------------------------------------------------------
    这一次我们能清楚的看到是TestMoreSplit这个测试没有成功。 还可以在go test命令后添加-run参数,它对应一个正则表达式,只有函数名匹配上的测试函数才会被go test命令执行。
    split $ go test -v -run="More"
    === RUN   TestMoreSplit
    --- FAIL: TestMoreSplit (0.00s)
        split_test.go:21: excepted:[a d], got:[a cd]
    FAIL
    exit status 1
    FAIL    github.com/pprof/studygo/code_demo/test_demo/split       0.006s
    ---------------------------------------------------------------------------------------------------------
    现在我们回过头来解决我们程序中的问题。很显然我们最初的split函数并没有考虑到sep为多个字符的情况,我们来修复下这个Bug:
    package split

    import "strings"

    // split package with a single split function.

    // Split slices s into all substrings separated by sep and
    // returns a slice of the substrings between those separators.
    func Split(s, sep string) (result []string) {
        i := strings.Index(s, sep)

        for i > -1 {
            result = append(result, s[:i])
            s = s[i+len(sep):] // 这里使用len(sep)获取sep的长度
            i = strings.Index(s, sep)
        }
        result = append(result, s)
        return
    }
    ---------------------------------------------------------------------------------------------------------
    这一次我们再来测试一下,我们的程序。注意,当我们修改了我们的代码之后不要仅仅执行那些失败的测试函数,我们应该完整的运行所有的测试,保证不会因为修改代码而引入了新的问题。
    split $ go test -v
    === RUN   TestSplit
    --- PASS: TestSplit (0.00s)
    === RUN   TestMoreSplit
    --- PASS: TestMoreSplit (0.00s)
    PASS
    ok      github.com/pprof/studygo/code_demo/test_demo/split       0.006s
    这一次我们的测试都通过

3.10 压力测试

01.Go怎么写测试用例
    开发程序其中很重要的一点是测试,我们如何保证代码的质量,如何保证每个函数是可运行,运行结果是正确的,又如何保证写出来的代码性能是好的,我们知道单元测试的重点在于发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让线上的程序能够在高并发的情况下还能保持稳定。本小节将带着这一连串的问题来讲解Go语言中如何来实现单元测试和性能测试。
    Go语言中自带有一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试,testing框架和其他语言中的测试框架类似,你可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例,那么接下来让我们一一来看一下怎么写。
    另外建议安装gotests插件自动生成测试代码:
    go get -u -v github.com/cweill/gotests/...

02.如何编写测试用例
    由于go test命令只能在一个相应的目录下执行所有文件,所以我们接下来新建一个项目目录gotest,这样我们所有的代码和测试代码都在这个目录下。
    接下来我们在该目录下面创建两个文件:gotest.go和gotest_test.go
    ---------------------------------------------------------------------------------------------------------
    gotest.go:这个文件里面我们是创建了一个包,里面有一个函数实现了除法运算:
    package gotest

    import (
        "errors"
    )

    func Division(a, b float64) (float64, error) {
        if b == 0 {
            return 0, errors.New("除数不能为0")
        }

        return a / b, nil
    }
    ---------------------------------------------------------------------------------------------------------
    gotest_test.go:这是我们的单元测试文件,但是记住下面的这些原则:
    文件名必须是_test.go结尾的,这样在执行go test的时候才会执行到相应的代码
    你必须import testing这个包
    所有的测试用例函数必须是Test开头
    测试用例会按照源代码中写的顺序依次执行
    测试函数TestXxx()的参数是testing.T,我们可以使用该类型来记录错误或者是测试状态
    测试格式:func TestXxx (t *testing.T),Xxx部分可以为任意的字母数字的组合,但是首字母不能是小写字母[a-z],例如Testintdiv是错误的函数名。
    函数中通过调用testing.T的Error, Errorf, FailNow, Fatal, FatalIf方法,说明测试不通过,调用Log方法用来记录测试的信息。
    ---------------------------------------------------------------------------------------------------------
    下面是我们的测试用例的代码:
    package gotest

    import (
        "testing"
    )

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

    func Test_Division_2(t *testing.T) {
        t.Error("就是不通过")
    }
    ---------------------------------------------------------------------------------------------------------
    我们在项目目录下面执行go test,就会显示如下信息:
    --- FAIL: Test_Division_2 (0.00 seconds)
        gotest_test.go:16: 就是不通过
    FAIL
    exit status 1
    FAIL    gotest    0.013s
    ---------------------------------------------------------------------------------------------------------
    从这个结果显示测试没有通过,因为在第二个测试函数中我们写死了测试不通过的代码t.Error,那么我们的第一个函数执行的情况怎么样呢?默认情况下执行go test是不会显示测试通过的信息的,我们需要带上参数go test -v,这样就会显示如下信息:
    === RUN Test_Division_1
    --- PASS: Test_Division_1 (0.00 seconds)
        gotest_test.go:11: 第一个测试通过了
    === RUN Test_Division_2
    --- FAIL: Test_Division_2 (0.00 seconds)
        gotest_test.go:16: 就是不通过
    FAIL
    exit status 1
    FAIL    gotest    0.012s
    ---------------------------------------------------------------------------------------------------------
    上面的输出详细的展示了这个测试的过程,我们看到测试函数1Test_Division_1测试通过,而测试函数2Test_Division_2测试失败了,最后得出结论测试不通过。接下来我们把测试函数2修改成如下代码:
    func Test_Division_2(t *testing.T) {
        if _, e := Division(6, 0); e == nil { //try a unit test on function
            t.Error("Division did not work as expected.") // 如果不是如预期的那么就报错
        } else {
            t.Log("one test passed.", e) //记录一些你期望记录的信息
        }
    }
    ---------------------------------------------------------------------------------------------------------
    然后我们执行go test -v,就显示如下信息,测试通过了:
    === RUN Test_Division_1
    --- PASS: Test_Division_1 (0.00 seconds)
        gotest_test.go:11: 第一个测试通过了
    === RUN Test_Division_2
    --- PASS: Test_Division_2 (0.00 seconds)
        gotest_test.go:20: one test passed. 除数不能为0
    PASS
    ok      gotest    0.013s

03.如何编写压力测试
    压力测试用来检测函数(方法)的性能,和编写单元功能测试的方法类似,此处不再赘述,但需要注意以下几点:
    压力测试用例必须遵循如下格式,其中XXX可以是任意字母数字的组合,但是首字母不能是小写字母
    func BenchmarkXXX(b *testing.B) { ... }
    go test不会默认执行压力测试的函数,如果要执行压力测试需要带上参数-test.bench,语法:-test.bench="test_name_regex",例如go test -test.bench=".*"表示测试全部的压力测试函数
    ---------------------------------------------------------------------------------------------------------
    在压力测试用例中,请记得在循环体内使用testing.B.N,以使测试可以正常的运行 文件名也必须以_test.go结尾
    下面我们新建一个压力测试文件webbench_test.go,代码如下所示:
    package gotest

    import (
        "testing"
    )

    func Benchmark_Division(b *testing.B) {
        for i := 0; i < b.N; i++ { //use b.N for looping
            Division(4, 5)
        }
    }

    func Benchmark_TimeConsumingFunction(b *testing.B) {
        b.StopTimer() //调用该函数停止压力测试的时间计数

        //做一些初始化的工作,例如读取文件数据,数据库连接之类的,
        //这样这些时间不影响我们测试函数本身的性能

        b.StartTimer() //重新开始时间
        for i := 0; i < b.N; i++ {
            Division(4, 5)
        }
    }
    ---------------------------------------------------------------------------------------------------------
    我们执行命令go test webbench_test.go -test.bench=".*",可以看到如下结果:
    Benchmark_Division-4                         500000000          7.76 ns/op         456 B/op          14 allocs/op
    Benchmark_TimeConsumingFunction-4            500000000          7.80 ns/op         224 B/op           4 allocs/op
    PASS
    ok      gotest    9.364s
    上面的结果显示我们没有执行任何TestXXX的单元测试函数,显示的结果只执行了压力测试函数,第一条显示了Benchmark_Division执行了500000000次,每次的执行平均时间是7.76纳秒,第二条显示了Benchmark_TimeConsumingFunction执行了500000000,每次的平均执行时间是7.80纳秒。最后一条显示总共的执行时间。

4 方法

4.1 汇总

01.常用信息1
    a.方法与函数的区别在于,方法拥有接收者,而函数没有,且只有自定义类型能够拥有方法。
        先来看一个例子。
        type IntSlice []int

        func (i IntSlice) Get(index int) int {
            return i[index]
        }
        func (i IntSlice) Set(index, val int) {
            i[index] = val
        }

        func (i IntSlice) Len() int {
            return len(i)
        }
        先声明了一个类型IntSlice,其底层类型为[]int,再声明了三个方法Get,Set和Len,
        方法的长相与函数并无太大的区别,只是多了一小段(i IntSlice) 。
        i就是接收者,IntSlice就是接收者的类型,接收者就类似于其他语言中的this或self,只不过在Go中需要显示的指明。
        ---------------------------------------------------------------------------------------------------------
        func main() {
           var intSlice IntSlice
           intSlice = []int{1, 2, 3, 4, 5}
           fmt.Println(intSlice.Get(0))
           intSlice.Set(0, 2)
           fmt.Println(intSlice)
           fmt.Println(intSlice.Len())
        }
        方法的使用就类似于调用一个类的成员方法,先声明,再初始化,再调用。
    b.值接收者
        接收者也分两种类型,值接收者和指针接收者,先看一个例子
        type MyInt int

        func (i MyInt) Set(val int) {
           i = MyInt(val) // 修改了,但是不会造成任何影响
        }

        func main() {
           myInt := MyInt(1)
           myInt.Set(2)
           fmt.Println(myInt)
        }
        ---------------------------------------------------------------------------------------------------------
        上述代码运行过后,会发现myInt的值依旧是1,并没有被修改成2。方法在被调用时,会将接收者的值传入方法中,
        上例的接收者就是一个值接收者,可以简单的看成一个形参,而修改一个形参的值,并不会对方法外的值造成任何影响,
        那么如果通过指针调用会如何呢?
        func main() {
            myInt := MyInt(1)
            (&myInt).Set(2)
            fmt.Println(myInt)
        }
        遗憾的是,这样的代码依旧不能修改内部的值,为了能够匹配上接收者的类型,Go会将其解引用,解释为(*(&myInt)).Set(2)。
    c.指针接收者
        稍微修改了一下,就能正常修改myInt的值。
        type MyInt int

        func (i *MyInt) Set(val int) {
           *i = MyInt(val)
        }

        func main() {
           myInt := MyInt(1)
           myInt.Set(2)
           fmt.Println(myInt)
        }
        现在的接收者就是一个指针接收者,虽然myInt是一个值类型,在通过值类型调用指针接收者的方法时,
        Go会将其解释为(&myint).Set(2)。所以方法的接收者为指针时,不管调用者是不是指针,都可以修改内部的值。
        ---------------------------------------------------------------------------------------------------------
        函数的参数传递过程中,是值拷贝的,如果传递的是一个整型,那就拷贝这个整型,如果是一个切片,那就拷贝这个切片,
        但如果是一个指针,就只需要拷贝这个指针,显然传递一个指针比起传递一个切片所消耗的资源更小,接收者也不例外,
        值接收者和指针接收者也是同样的道理。在大多数情况下,都推荐使用指针接收者,不过两者并不应该混合使用,
        要么都用,要么就都不用,看下面一个例子。
        type Animal interface {
           Run()
        }

        type Dog struct {
        }

        func (d *Dog) Run() {
           fmt.Println("Run")
        }

        func main() {
           var an Animal
           an = Dog{}
           // an = &Dog{} 正确方式
           an.Run()
        }
        ---------------------------------------------------------------------------------------------------------
        这一段代码将会无法通过编译,编译器将会输出如下错误
        cannot use Dog{} (value of type Dog) as type Animal in assignment:
        Dog does not implement Animal (Run method has pointer receiver)
        ---------------------------------------------------------------------------------------------------------
        翻译过来就是,无法使用Dog{}初始化Animal类型的变量,因为Dog没有实现Animal ,
        解决办法有两种,一是将指针接收者改为值接收者,二是将Dog{}改为&Dog{},接下来逐个讲解。
        type Dog struct {
        }

        func (d Dog) Run() { // 改为了值接收者
           fmt.Println("Run")
        }

        func main() { // 可以正常运行
           var an Animal
           an = Dog{}
           // an = &Dog{} 同样可以
           an.Run()
        }
        在原来的代码中,Run 方法的接收者是*Dog ,自然而然实现Animal接口的就是Dog指针,而不是Dog结构体,这是两个不同的类型,
        所以编译器就会认为Dog{}并不是Animal的实现,因此无法赋值给变量an,所以第二种解决办法就是赋值Dog指针给变量an。
        不过在使用值接收者时,Dog指针依然可以正常赋值给animal,这是因为Go会在适当情况下对指针进行解引用,
        因为通过指针可以找到Dog结构体,但是反过来的话,无法通过Dog结构体找到Dog指针。
        如果单纯的在结构体中混用值接收者和指针接收者的话无伤大雅,但是和接口一起使用后,就会出现错误,
        倒不如无论何时要么都用值接收者,要么就都用指针接收者,形成一个良好的规范,也可以减少后续维护的负担。
        ---------------------------------------------------------------------------------------------------------
        还有一种情况,就是当值接收者是可寻址的时候,Go会自动的插入指针运算符来进行调用,例如切片是可寻址,依旧可以通过值接收者来修改其内部值。比如下面这个代码
        type Slice []int

        func (s Slice) Set(i int, v int) {
            s[i] = v
        }

        func main() {
            s := make(Slice, 1)
            s.Set(0, 1)
            fmt.Println(s)
        }
        输出
        [1]
        ---------------------------------------------------------------------------------------------------------
        但这样会引发另一个问题,如果对其添加元素的话,情况就不同了。看下面的例子
        type Slice []int

        func (s Slice) Set(i int, v int) {
            s[i] = v
        }

        func (s Slice) Append(a int) {
            s = append(s, a)
        }

        func main() {
            s := make(Slice, 1, 2)
            s.Set(0, 1)
            s.Append(2)
            fmt.Println(s)
        }
        [1]
        ---------------------------------------------------------------------------------------------------------
        它的输出还是和之前一样,append函数是有返回值的,向切片添加完元素后必须覆盖原切片,尤其是在扩容后,在方法中对值接收者修改并不会产生任何影响,这也就导致了例子中的结果,改成指针接收者就正常了。
        type Slice []int

        func (s *Slice) Set(i int, v int) {
            (*s)[i] = v
        }

        func (s *Slice) Append(a int) {
            *s = append(*s, a)
        }

        func main() {
            s := make(Slice, 1, 2)
            s.Set(0, 1)
            s.Append(2)
            fmt.Println(s)
        }
        输出
        [1 2]

02.常用信息2
    a.方法的定义
        接收者类型:方法的接收者可以是值类型或指针类型。接收者类型必须是当前包内定义的命名类型(例如结构体)。
        接收者名称:接收者参数可以任意命名,通常使用简短的名称(如 t、u、p 等),也可以省略参数名。
        方法签名:方法的定义与函数类似,可以包含参数和返回值。
        func (receiver ReceiverType) MethodName(parameters) (returns) {
            // method body
        }
    b.方法的调用
        值类型调用:如果方法的接收者是值类型,可以通过值类型实例或指针类型实例来调用。
        指针类型调用:如果方法的接收者是指针类型,必须使用指针类型实例来调用,但值类型实例也会被自动转换为指针类型来调用。
        v := ValueType{}
        v.MethodName()   // 值类型调用
        p := &ValueType{}
        p.MethodName()   // 指针类型调用
    c.方法集
        值接收者的方法集:包括所有接收者为值类型的方法。
        指针接收者的方法集:包括接收者为指针类型的方法。指针接收者的方法集包含值接收者的方法集。
    d.接收者类型
        值接收者:方法操作的是接收者的副本,不会修改原始对象。适用于方法不需要修改对象状态。
        指针接收者:方法操作的是接收者的原始对象的指针,可以修改对象的状态。适用于方法需要修改对象状态或避免复制大对象。
    e.方法与函数的区别
        方法:与特定类型关联,必须指定接收者。可以使用类型实例或指针调用。
        函数:独立于任何类型,可以直接调用,不需要接收者。
    f.方法重载
        不支持重载:Go 不支持方法重载,方法名在同一类型中必须唯一。
    g.嵌入
        结构体嵌入:通过嵌入(embedding),一个结构体可以包含另一个结构体的方法。嵌入结构体的方法可以直接被调用。
        type A struct{}
        func (a A) MethodA() {}

        type B struct {
            A  // 嵌入结构体
        }
    h.方法值与方法表达式
        方法值:可以将方法绑定到实例,并作为函数传递。
        var f = u.Notify
        f()  // 调用绑定到实例 u 的 Notify 方法
        ---------------------------------------------------------------------------------------------------------
        方法表达式:创建方法的引用,不需要具体实例。
        var f = (*User).Notify
    i.自定义方法接口
    接口实现:一个类型实现了接口中的方法,则它符合该接口类型的要求。可以通过接口定义方法签名,并实现这些方法。
    type Notifier interface {
        Notify()
    }
    type User struct{}
    func (u User) Notify() {}

4.2 方法定义

01.方法定义
    a.介绍
        Golang 方法总是绑定对象实例,并隐式将实例作为第一实参 (receiver)。
        • 只能为当前包内命名类型定义方法。
        • 参数 receiver 可任意命名。如方法中未曾使用 ,可省略参数名。
        • 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。
        • 不支持方法重载,receiver 只是参数签名的组成部分。
        • 可用实例 value 或 pointer 调用全部方法,编译器自动转换。
        一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。
        所有给定类型的方法属于该类型的方法集。
    b.方法定义:
        func (recevier type) methodName(参数列表)(返回值列表){}
        参数和返回值可以省略
        -------------------------------------------------------------------------------------------------
        package main

        type Test struct{}

        // 无参数、无返回值
        func (t Test) method0() {

        }

        // 单参数、无返回值
        func (t Test) method1(i int) {

        }

        // 多参数、无返回值
        func (t Test) method2(x, y int) {

        }

        // 无参数、单返回值
        func (t Test) method3() (i int) {
            return
        }

        // 多参数、多返回值
        func (t Test) method4(x, y int) (z int, err error) {
            return
        }

        // 无参数、无返回值
        func (t *Test) method5() {

        }

        // 单参数、无返回值
        func (t *Test) method6(i int) {

        }

        // 多参数、无返回值
        func (t *Test) method7(x, y int) {

        }

        // 无参数、单返回值
        func (t *Test) method8() (i int) {
            return
        }

        // 多参数、多返回值
        func (t *Test) method9(x, y int) (z int, err error) {
            return
        }

        func main() {}
        -----------------------------------------------------------------------------------------------------
        下面定义一个结构体类型和该类型的一个方法:

        package main

        import (
            "fmt"
        )

        //结构体
        type User struct {
            Name  string
            Email string
        }

        //方法
        func (u User) Notify() {
            fmt.Printf("%v : %v \n", u.Name, u.Email)
        }
        func main() {
            // 值类型调用方法
            u1 := User{"golang", "golang@golang.com"}
            u1.Notify()
            // 指针类型调用方法
            u2 := User{"go", "go@go.com"}
            u3 := &u2
            u3.Notify()
        }
        输出结果:
        golang : golang@golang.com
        go : go@go.com
        -----------------------------------------------------------------------------------------------------
        解释:首先我们定义了一个叫做 User 的结构体类型,然后定义了一个该类型的方法叫做 Notify,
        该方法的接受者是一个 User 类型的值。要调用 Notify 方法我们需要一个 User 类型的值或者指针。
        -----------------------------------------------------------------------------------------------------
        在这个例子中当我们使用指针时,Go 调整和解引用指针使得调用可以被执行。
        注意,当接受者不是一个指针时,该方法操作对应接受者的值的副本
        (意思就是即使你使用了指针调用函数,但是函数的接受者是值类型,所以函数内部操作还是对副本的操作,而不是指针操作。
        -----------------------------------------------------------------------------------------------------
        我们修改 Notify 方法,让它的接受者使用指针类型:
        package main

        import (
            "fmt"
        )

        //结构体
        type User struct {
            Name  string
            Email string
        }

        //方法
        func (u *User) Notify() {
            fmt.Printf("%v : %v \n", u.Name, u.Email)
        }
        func main() {
            // 值类型调用方法
            u1 := User{"golang", "golang@golang.com"}
            u1.Notify()
            // 指针类型调用方法
            u2 := User{"go", "go@go.com"}
            u3 := &u2
            u3.Notify()
        }
        输出结果:
        golang : golang@golang.com
        go : go@go.com
        -----------------------------------------------------------------------------------------------------
        注意:当接受者是指针时,即使用值类型调用那么函数内部也是对指针的操作。
        方法不过是一种特殊的函数,只需将其还原,就知道 receiver T 和 *T 的差别。
        package main

        import "fmt"

        type Data struct {
            x int
        }

        func (self Data) ValueTest() { // func ValueTest(self Data);
            fmt.Printf("Value: %p\n", &self)
        }

        func (self *Data) PointerTest() { // func PointerTest(self *Data);
            fmt.Printf("Pointer: %p\n", self)
        }

        func main() {
            d := Data{}
            p := &d
            fmt.Printf("Data: %p\n", p)

            d.ValueTest()   // ValueTest(d)
            d.PointerTest() // PointerTest(&d)

            p.ValueTest()   // ValueTest(*p)
            p.PointerTest() // PointerTest(p)
        }
        输出:
        Data: 0xc42007c008
        Value: 0xc42007c018
        Pointer: 0xc42007c008
        Value: 0xc42007c020
        Pointer: 0xc42007c008

02.普通函数与方法的区别
    1.对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然。
    2.对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以。
    ---------------------------------------------------------------------------------------------------------
    package main

    //普通函数与方法的区别(在接收者分别为值类型和指针类型的时候)

    import (
        "fmt"
    )

    //1.普通函数
    //接收值类型参数的函数
    func valueIntTest(a int) int {
        return a + 10
    }

    //接收指针类型参数的函数
    func pointerIntTest(a *int) int {
        return *a + 10
    }

    func structTestValue() {
        a := 2
        fmt.Println("valueIntTest:", valueIntTest(a))
        //函数的参数为值类型,则不能直接将指针作为参数传递
        //fmt.Println("valueIntTest:", valueIntTest(&a))
        //compile error: cannot use &a (type *int) as type int in function argument

        b := 5
        fmt.Println("pointerIntTest:", pointerIntTest(&b))
        //同样,当函数的参数为指针类型时,也不能直接将值类型作为参数传递
        //fmt.Println("pointerIntTest:", pointerIntTest(&b))
        //compile error:cannot use b (type int) as type *int in function argument
    }

    //2.方法
    type PersonD struct {
        id   int
        name string
    }

    //接收者为值类型
    func (p PersonD) valueShowName() {
        fmt.Println(p.name)
    }

    //接收者为指针类型
    func (p *PersonD) pointShowName() {
        fmt.Println(p.name)
    }

    func structTestFunc() {
        //值类型调用方法
        personValue := PersonD{101, "hello world"}
        personValue.valueShowName()
        personValue.pointShowName()

        //指针类型调用方法
        personPointer := &PersonD{102, "hello golang"}
        personPointer.valueShowName()
        personPointer.pointShowName()

        //与普通函数不同,接收者为指针类型和值类型的方法,指针类型和值类型的变量均可相互调用
    }

    func main() {
        structTestValue()
        structTestFunc()
    }
    输出结果:
    valueIntTest: 12
    pointerIntTest: 15
    hello world
    hello world
    hello golang
    hello golang

4.3 匿名字段

01.Golang匿名字段 :可以像字段成员那样访问匿名字段方法,编译器负责查找。
    package main

    import "fmt"

    type User struct {
        id   int
        name string
    }

    type Manager struct {
        User
    }

    func (self *User) ToString() string { // receiver = &(Manager.User)
        return fmt.Sprintf("User: %p, %v", self, self)
    }

    func main() {
        m := Manager{User{1, "Tom"}}
        fmt.Printf("Manager: %p\n", &m)
        fmt.Println(m.ToString())
    }
    输出结果:
    Manager: 0xc42000a060
    User: 0xc42000a060, &{1 Tom}

02.通过匿名字段,可获得和继承类似的复用能力。依据编译器查找次序,只需在外层定义同名方法,就可以实现 "override"。
    package main

    import "fmt"

    type User struct {
        id   int
        name string
    }

    type Manager struct {
        User
        title string
    }

    func (self *User) ToString() string {
        return fmt.Sprintf("User: %p, %v", self, self)
    }

    func (self *Manager) ToString() string {
        return fmt.Sprintf("Manager: %p, %v", self, self)
    }

    func main() {
        m := Manager{User{1, "Tom"}, "Administrator"}

        fmt.Println(m.ToString())

        fmt.Println(m.User.ToString())
    }
    输出结果:
    Manager: 0xc420074180, &{{1 Tom} Administrator}
    User: 0xc420074180, &{1 Tom}

4.4 方法集

01.方法集
    Golang方法集 :每个类型都有与之关联的方法集,这会影响到接口实现规则。
    • 类型 T 方法集包含全部 receiver T 方法。
    • 类型 *T 方法集包含全部 receiver T + *T 方法。
    • 如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
    • 如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T + *T 方法。
    • 不管嵌入 T 或 *T,*S 方法集总是包含 T + *T 方法。
    用实例 value 和 pointer 调用方法 (含匿名字段) 不受方法集约束,编译器总是查找全部方法,并自动转换 receiver 实参。
    ---------------------------------------------------------------------------------------------------------
    Go 语言中内部类型方法集提升的规则:
    类型 T 方法集包含全部 receiver T 方法。
    ---------------------------------------------------------------------------------------------------------
    package main

    import (
        "fmt"
    )

    type T struct {
        int
    }

    func (t T) test() {
        fmt.Println("类型 T 方法集包含全部 receiver T 方法。")
    }

    func main() {
        t1 := T{1}
        fmt.Printf("t1 is : %v\n", t1)
        t1.test()
    }
    输出结果:
    t1 is : {1}
    ---------------------------------------------------------------------------------------------------------
    类型 T 方法集包含全部 receiver T 方法。
    类型 *T 方法集包含全部 receiver T + *T 方法。

    package main

    import (
        "fmt"
    )

    type T struct {
        int
    }

    func (t T) testT() {
        fmt.Println("类型 *T 方法集包含全部 receiver T 方法。")
    }

    func (t *T) testP() {
        fmt.Println("类型 *T 方法集包含全部 receiver *T 方法。")
    }

    func main() {
        t1 := T{1}
        t2 := &t1
        fmt.Printf("t2 is : %v\n", t2)
        t2.testT()
        t2.testP()
    }
    输出结果:
    t2 is : &{1}
    类型 *T 方法集包含全部 receiver T 方法。
    类型 *T 方法集包含全部 receiver *T 方法。
    ---------------------------------------------------------------------------------------------------------
    给定一个结构体类型 S 和一个命名为 T 的类型,方法提升像下面规定的这样被包含在结构体方法集中:
    如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
    这条规则说的是当我们嵌入一个类型,嵌入类型的接受者为值类型的方法将被提升,可以被外部类型的值和指针调用。
    package main

    import (
        "fmt"
    )

    type S struct {
        T
    }

    type T struct {
        int
    }

    func (t T) testT() {
        fmt.Println("如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。")
    }

    func main() {
        s1 := S{T{1}}
        s2 := &s1
        fmt.Printf("s1 is : %v\n", s1)
        s1.testT()
        fmt.Printf("s2 is : %v\n", s2)
        s2.testT()
    }
    输出结果:
    s1 is : {{1}}
    如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
    s2 is : &{{1}}
    如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
    ---------------------------------------------------------------------------------------------------------
    如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T + *T 方法。
    这条规则说的是当我们嵌入一个类型的指针,嵌入类型的接受者为值类型或指针类型的方法将被提升,可以被外部类型的值或者指针调用。
    package main

    import (
        "fmt"
    )

    type S struct {
        T
    }

    type T struct {
        int
    }

    func (t T) testT() {
        fmt.Println("如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T 方法")
    }
    func (t *T) testP() {
        fmt.Println("如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 *T 方法")
    }

    func main() {
        s1 := S{T{1}}
        s2 := &s1
        fmt.Printf("s1 is : %v\n", s1)
        s1.testT()
        s1.testP()
        fmt.Printf("s2 is : %v\n", s2)
        s2.testT()
        s2.testP()
    }
    输出结果:
    s1 is : {{1}}
    如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T 方法
    如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 *T 方法
    s2 is : &{{1}}
    如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T 方法
    如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 *T 方法

4.5 表达式

01.表达式
    Golang 表达式 :根据调用者不同,方法分为两种表现形式:
    instance.method(args...) ---> <type>.func(instance, args...)
    ---------------------------------------------------------------------------------------------------------
    前者称为 method value,后者 method expression。
    两者都可像普通函数那样赋值和传参,区别在于 method value 绑定实例,而 method expression 则须显式传参。
    ---------------------------------------------------------------------------------------------------------
    package main

    import "fmt"

    type User struct {
        id   int
        name string
    }

    func (self *User) Test() {
        fmt.Printf("%p, %v\n", self, self)
    }

    func main() {
        u := User{1, "Tom"}
        u.Test()

        mValue := u.Test
        mValue() // 隐式传递 receiver

        mExpression := (*User).Test
        mExpression(&u) // 显式传递 receiver
    }
    输出结果:
    0xc42000a060, &{1 Tom}
    0xc42000a060, &{1 Tom}
    0xc42000a060, &{1 Tom}
    -----------------------------------------------------------------------------------------------------
    需要注意,method value 会复制 receiver。
    package main

    import "fmt"

    type User struct {
        id   int
        name string
    }

    func (self User) Test() {
        fmt.Println(self)
    }

    func main() {
        u := User{1, "Tom"}
        mValue := u.Test // 立即复制 receiver,因为不是指针类型,不受后续修改影响。

        u.id, u.name = 2, "Jack"
        u.Test()

        mValue()
    }
    输出结果
    {2 Jack}
    {1 Tom}
    ---------------------------------------------------------------------------------------------------------
    在汇编层面,method value 和闭包的实现方式相同,实际返回 FuncVal 类型对象。
    FuncVal { method_address, receiver_copy }
    ---------------------------------------------------------------------------------------------------------
    可依据方法集转换 method expression,注意 receiver 类型的差异。
    package main

    import "fmt"

    type User struct {
        id   int
        name string
    }

    func (self *User) TestPointer() {
        fmt.Printf("TestPointer: %p, %v\n", self, self)
    }

    func (self User) TestValue() {
        fmt.Printf("TestValue: %p, %v\n", &self, self)
    }

    func main() {
        u := User{1, "Tom"}
        fmt.Printf("User: %p, %v\n", &u, u)

        mv := User.TestValue
        mv(u)

        mp := (*User).TestPointer
        mp(&u)

        mp2 := (*User).TestValue // *User 方法集包含 TestValue。签名变为 func TestValue(self *User)。实际依然是 receiver value copy。
        mp2(&u)
    }
    输出:
    User: 0xc42000a060, {1 Tom}
    TestValue: 0xc42000a0a0, {1 Tom}
    TestPointer: 0xc42000a060, &{1 Tom}
    TestValue: 0xc42000a100, {1 Tom}
    ---------------------------------------------------------------------------------------------------------
    将方法 "还原" 成函数,就容易理解下面的代码了。
    package main

    type Data struct{}

    func (Data) TestValue() {}

    func (*Data) TestPointer() {}

    func main() {
        var p *Data = nil
        p.TestPointer()

        (*Data)(nil).TestPointer() // method value
        (*Data).TestPointer(nil)   // method expression

        // p.TestValue()            // invalid memory address or nil pointer dereference

        // (Data)(nil).TestValue()  // cannot convert nil to type Data
        // Data.TestValue(nil)      // cannot use nil as type Data in function argument
    }

4.6 自定义error

01.系统抛
    package main

    import "fmt"

    // 系统抛
    func test01() {
       a := [5]int{0, 1, 2, 3, 4}
       a[1] = 123
       fmt.Println(a)
       //a[10] = 11
       index := 10
       a[index] = 10
       fmt.Println(a)
    }

    func getCircleArea(radius float32) (area float32) {
       if radius < 0 {
          // 自己抛
          panic("半径不能为负")
       }
       return 3.14 * radius * radius
    }

    func test02() {
       getCircleArea(-5)
    }

    //
    func test03() {
       // 延时执行匿名函数
       // 延时到何时?(1)程序正常结束   (2)发生异常时
       defer func() {
          // recover() 复活 恢复
          // 会返回程序为什么挂了
          if err := recover(); err != nil {
             fmt.Println(err)
          }
       }()
       getCircleArea(-5)
       fmt.Println("这里有没有执行")
    }

    func test04()  {
       test03()
       fmt.Println("test04")
    }

    func main() {
       test04()
    }

02.返回异常
    package main

    import (
       "errors"
       "fmt"
    )

    func getCircleArea(radius float32) (area float32, err error) {
       if radius < 0 {
          // 构建个异常对象
          err = errors.New("半径不能为负")
          return
       }
       area = 3.14 * radius * radius
       return
    }

    func main() {
       area, err := getCircleArea(-5)
       if err != nil {
          fmt.Println(err)
       } else {
          fmt.Println(area)
       }
    }

03.自定义error:
    package main

    import (
        "fmt"
        "os"
        "time"
    )

    type PathError struct {
        path       string
        op         string
        createTime string
        message    string
    }

    func (p *PathError) Error() string {
        return fmt.Sprintf("path=%s \nop=%s \ncreateTime=%s \nmessage=%s", p.path,
            p.op, p.createTime, p.message)
    }

    func Open(filename string) error {

        file, err := os.Open(filename)
        if err != nil {
            return &PathError{
                path:       filename,
                op:         "read",
                message:    err.Error(),
                createTime: fmt.Sprintf("%v", time.Now()),
            }
        }

        defer file.Close()
        return nil
    }

    func main() {
        err := Open("/Users/5lmh/Desktop/go/src/test.txt")
        switch v := err.(type) {
        case *PathError:
            fmt.Println("get path error,", v)
        default:

        }

    }
    输出结果:
    get path error, path=/Users/pprof/Desktop/go/src/test.txt
    op=read
    createTime=2018-04-05 11:25:17.331915 +0800 CST m=+0.000441790
    message=open /Users/pprof/Desktop/go/src/test.txt: no such file or directory

5 面向对象

5.1 接口1

01.定义接口
    接口通过 type 关键字定义,包含一组方法签名。例如:
    type Stringer interface {
        String() string
    }

02.实现接口
    实现接口是隐式的,只要类型实现了接口中定义的所有方法,就算实现了该接口。例如:
    type Person struct {
        Name string
    }
    func (p Person) String() string {
        return p.Name
    }

03.接口类型的零值
    接口类型的零值是 nil,表示接口变量没有指向任何具体实现。例如:
    var s Stringer
    fmt.Println(s) // 输出: <nil>

04.接口赋值
    接口可以被赋值为实现了该接口的类型的实例。例如:
    var s Stringer
    s = Person{Name: "Alice"}
    fmt.Println(s.String()) // 输出: Alice

05.类型断言
    类型断言用于检查接口的具体类型并进行转换。例如:
    var s Stringer = Person{Name: "Bob"}
    p, ok := s.(Person)
    if ok {
        fmt.Println(p.Name) // 输出: Bob
    }

06.类型切换
    类型切换用于基于类型进行不同的处理。例如:
    func printInfo(i interface{}) {
        switch v := i.(type) {
        case int:
            fmt.Println("int:", v)
        case string:
            fmt.Println("string:", v)
        default:
            fmt.Println("unknown type")
        }
    }

07.空接口
    空接口(interface{})可以接受任何类型,是最常用的通用类型。例如:
    var i interface{}
    i = 42
    i = "hello"

08.接口的嵌入
    接口可以嵌入其他接口,从而扩展其方法集。例如:
    type Reader interface {
        Read(p []byte) (n int, err error)
    }
    type StringReader interface {
        Reader
        String() string
    }

09.接口作为参数
    函数可以接受接口作为参数,这使得函数可以处理多种类型的值。例如:
    func print(s Stringer) {
        fmt.Println(s.String())
    }

10.接口作为返回值
    函数可以返回接口类型,这允许函数返回不同的具体类型。例如:
    func newStringer() Stringer {
        return Person{Name: "Charlie"}
    }

11.接口与类型组合
    接口可以与其他类型组合,形成更复杂的接口。例如:
    type ReadWriter interface {
        Reader
        Writer
    }

12.接口的零值和空值
    接口的零值为 nil,这表示接口没有具体的实现。例如:
    var r Reader
    fmt.Println(r == nil) // 输出: true

13.接口和空接口的转换
    空接口 interface{} 可以用来转换为具体的接口类型或其它类型。例如:
    var i interface{} = "hello"
    str, ok := i.(string)
    if ok {
        fmt.Println(str) // 输出: hello
    }

14. 接口与反射
    接口可以与反射(reflect 包)一起使用来动态处理类型。例如:
    import "reflect"

    func printType(i interface{}) {
        t := reflect.TypeOf(i)
        fmt.Println(t)
    }

15. 接口的具体实现检查
    使用 reflect 包来检查接口变量的具体实现。例如:
    func checkType(i interface{}) {
        t := reflect.TypeOf(i)
        fmt.Println("Type:", t.Name())
    }

5.2 接口2

01.概念
    Go关于接口的发展历史有一个分水岭,在Go1.17及以前,官方在参考手册中对于接口的定义为:一组方法的集合。
    接口实现的定义:当一个类型的方法集是一个接口的方法集的超集时,且该类型的值可以由该接口类型的变量存储,那么称该类型实现了该接口。
    ---------------------------------------------------------------------------------------------------------
    不过在Go1.18时,关于接口的定义发生了变化,接口定义为:一组类型的集合。
    接口实现的定义:当一个类型位于一个接口的类型集内,且该类型的值可以由该接口类型的变量存储,那么称该类型实现了该接口。并且还给出了如下的额外定义。
    ---------------------------------------------------------------------------------------------------------
    当如下情况时,可以称类型T实现了接口I
    T不是一个接口,并且是接口I类型集中的一个元素
    T是一个接口,并且T的类型集是接口I类型集的一个子集
    如果T实现了一个接口,那么T的值也实现了该接口。
    ---------------------------------------------------------------------------------------------------------
    Go在1.18最大的变化就是加入了泛型,新接口定义就是为了泛型而服务的,不过一点也不影响之前接口的使用,同时接口也分为了两类,
    基本接口(Basic Interface):只包含方法集的接口就是基本接口
    通用接口(General Interface):只要包含类型集的接口就是通用接口
    ---------------------------------------------------------------------------------------------------------
    什么是方法集,方法集就是一组方法的集合,同样的,类型集就是一组类型的集合。

02.基本接口
    a.声明
        先来看看接口长什么样子。
        type Person interface {
            Say(string) string
            Walk(int)
        }
        这是一个Person接口,有两个对外暴露的方法Walk和Say,在接口里,函数的参数名变得不再重要,当然如果想加上参数名和返回值名也是允许的。
    b.初始化
        仅仅只有接口是无法被初始化的,因为它仅仅只是一组规范,并没有具体的实现,不过可以被声明。
        func main() {
           var person Person
           fmt.Println(person)
        }
        输出
         <nil>
    c.实现1
        先来看一个例子,一个建筑公司想一种特殊规格的起重机,于是给出了起重机的特殊规范和图纸,
        并指明了起重机应该有起重和吊货的功能,建筑公司并不负责造起重机,只是给出了一个规范,这就叫接口,
        于是公司A接下了订单,根据自家公司的独门技术造出了绝世起重机并交给了建筑公司,建筑公司不在乎是用什么技术实现的,
        也不在乎什么绝世起重机,只要能够起重和吊货就行,仅仅只是当作一台普通起重机来用,根据规范提供具体的功能,这就叫实现。
        只根据接口的规范来使用功能,屏蔽其内部实现,这就叫面向接口编程。过了一段时间,绝世起重机出故障了,公司A也跑路了,
        于是公司B依据规范造了一台更厉害的巨无霸起重机,由于同样具有起重和吊货的功能,可以与绝世起重机无缝衔接,并不影响建筑进度,
        建筑得以顺利完成,内部实现改变而功能不变,不影响之前的使用,可以随意替换,这就是面向接口编程的好处。
        -----------------------------------------------------------------------------------------------------
        // 起重机接口
        type Crane interface {
            JackUp() string
            Hoist() string
        }

        // 起重机A
        type CraneA struct {
            work int //内部的字段不同代表内部细节不一样
        }

        func (c CraneA) Work() {
            fmt.Println("使用技术A")
        }
        func (c CraneA) JackUp() string {
            c.Work()
            return "jackup"
        }

        func (c CraneA) Hoist() string {
            c.Work()
            return "hoist"
        }

        // 起重机B
        type CraneB struct {
            boot string
        }

        func (c CraneB) Boot() {
            fmt.Println("使用技术B")
        }

        func (c CraneB) JackUp() string {
            c.Boot()
            return "jackup"
        }

        func (c CraneB) Hoist() string {
            c.Boot()
            return "hoist"
        }

        type ConstructionCompany struct {
            Crane Crane // 只根据Crane类型来存放起重机
        }

        func (c *ConstructionCompany) Build() {
            fmt.Println(c.Crane.JackUp())
            fmt.Println(c.Crane.Hoist())
            fmt.Println("建筑完成")
        }

        func main() {
            // 使用起重机A
            company := ConstructionCompany{CraneA{}}
            company.Build()
            fmt.Println()
            // 更换起重机B
            company.Crane = CraneB{}
            company.Build()
        }
        -----------------------------------------------------------------------------------------------------
        使用技术A
        jackup
        使用技术A
        hoist
        建筑完成

        使用技术B
        jackup
        使用技术B
        hoist
        建筑完成
        -----------------------------------------------------------------------------------------------------
        上面例子中,可以观察到接口的实现是隐式的,也对应了官方对于基本接口实现的定义:方法集是接口方法集的超集,
        所以在Go中,实现一个接口不需要implements关键字显式的去指定要实现哪一个接口,只要是实现了一个接口的全部方法,
        那就是实现了该接口。有了实现之后,就可以初始化接口了,建筑公司结构体内部声明了一个Crane类型的成员变量,
        可以保存所有实现了Crane接口的值,由于是Crane 类型的变量,所以能够访问到的方法只有JackUp 和Hoist,
        内部的其他方法例如Work和Boot都无法访问。
    d.实现2
        之前提到过任何自定义类型都可以拥有方法,那么根据实现的定义,任何自定义类型都可以实现接口,下面举几个比较特殊的例子。
        type Person interface {
           Say(string) string
           Walk(int)
        }

        type Man interface {
           Exercise()
           Person
        }
        -------------------------------------------------------------------------------------------------------------
        Man接口方法集是Person的超集,所以Man也实现了接口Person,不过这更像是一种"继承"。
        type Number int

        func (n Number) Say(s string) string {
            return "bibibibibi"
        }

        func (n Number) Walk(i int) {
            fmt.Println("can not walk")
        }
        -----------------------------------------------------------------------------------------------------
        类型Number的底层类型是int,虽然这放在其他语言中看起来很离谱,但Number的方法集确实是Person 的超集,所以也算实现。
        type Func func()

        func (f Func) Say(s string) string {
            f()
            return "bibibibibi"
        }

        func (f Func) Walk(i int) {
            f()
            fmt.Println("can not walk")
        }

        func main() {
            var function Func
            function = func() {
                fmt.Println("do somthing")
            }
            function()
        }
        同样的,函数类型也可以实现接口。

03.空接口
    type Any interface{

    }
    ---------------------------------------------------------------------------------------------------------
    Any接口内部没有方法集合,根据实现的定义,所有类型都是Any接口的的实现,因为所有类型的方法集都是空集的超集,所以Any接口可以保存任何类型的值。
    func main() {
        var anything Any

        anything = 1
        println(anything)
        fmt.Println(anything)

        anything = "something"
        println(anything)
        fmt.Println(anything)

        anything = complex(1, 2)
        println(anything)
        fmt.Println(anything)

        anything = 1.2
        println(anything)
        fmt.Println(anything)

        anything = []int{}
        println(anything)
        fmt.Println(anything)

        anything = map[string]int{}
        println(anything)
        fmt.Println(anything)
    }
    输出
    (0xe63580,0xeb8b08)
    1
    (0xe63d80,0xeb8c48)
    something
    (0xe62ac0,0xeb8c58)
    (1+2i)
    (0xe62e00,0xeb8b00)
    1.2
    (0xe61a00,0xc0000080d8)
    []
    (0xe69720,0xc00007a7b0)
    map[]
    ---------------------------------------------------------------------------------------------------------
    通过输出会发现,两种输出的结果不一致,其实接口内部可以看成是一个由(val,type)组成的元组,type是具体类型,在调用方法时会去调用具体类型的具体值。
    interface{}
    ---------------------------------------------------------------------------------------------------------
    这也是一个空接口,不过是一个匿名空接口,在开发时通常会使用匿名空接口来表示接收任何类型的值,例子如下
    func main() {
       DoSomething(map[int]string{})
    }

    func DoSomething(anything interface{}) interface{} {
       return anything
    }
    ---------------------------------------------------------------------------------------------------------
    在后续的更新中,官方提出了另一种解决办法,为了方便起见,可以使用any来替代interace{},两者是完全等价的,因为前者仅仅只是一个类型别名,如下
    type any = interface{}
    ---------------------------------------------------------------------------------------------------------
    在比较空接口时,会对其底层类型进行比较,如果类型不匹配的话则为false,其次才是值的比较,例如
    func main() {
        var a interface{}
        var b interface{}
        a = 1
        b = "1"
        fmt.Println(a == b)
        a = 1
        b = 1
        fmt.Println(a == b)
    }
    输出为
    false
    true
    ---------------------------------------------------------------------------------------------------------
    如果底层的类型是不可比较的,那么会panic,对于Go而言,内置数据类型是否可比较的情况如下
    类型        可比较   依据
    数字类型     是      值是否相等
    字符串类型   是      值是否相等
    数组类型     是      数组的全部元素是否相等
    切片类型     否      不可比较
    结构体       是      字段值是否全部相等
    map类型      否      不可比较
    通道         是      地址是否相等
    指针         是      指针存储的地址是否相等
    接口         是      底层所存储的数据是否相等
    ---------------------------------------------------------------------------------------------------------
    在Go中有一个专门的接口类型用于代表所有可比较类型,即comparable
    type comparable interface{ comparable }
    如果尝试对不可比较的类型进行比较,则会panic

04.通用接口
    通用接口就是为了泛型服务的,只要掌握了泛型,就掌握了通用接口,请移步泛型

5.3 接口3

01.接口
    a.接口类型
        在Go语言中接口(interface)是一种类型,一种抽象的类型。
        interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。
        为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。
    b.为什么要使用接口
        type Cat struct{}

        func (c Cat) Say() string { return "喵喵喵" }

        type Dog struct{}

        func (d Dog) Say() string { return "汪汪汪" }

        func main() {
            c := Cat{}
            fmt.Println("猫:", c.Say())
            d := Dog{}
            fmt.Println("狗:", d.Say())
        }
        上面的代码中定义了猫和狗,然后它们都会叫,你会发现main函数中明显有重复的代码,如果我们后续再加上猪、青蛙等动物的话,我们的代码还会一直重复下去。那我们能不能把它们当成“能叫的动物”来处理呢?
        像类似的例子在我们编程过程中会经常遇到:
        比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢?
        比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢?
        比如销售、行政、程序员都能计算月薪,我们能不能把他们当成“员工”来处理呢?
        Go语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一种抽象的类型。当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。
    c.接口的定义
        a.Go语言提倡面向接口编程。
            接口是一个或多个方法签名的集合。
            任何类型的方法集中只要拥有该接口'对应的全部方法'签名。
            就表示它 "实现" 了该接口,无须在该类型上显式声明实现了哪个接口。
            这称为Structural Typing。
            所谓对应方法,是指有相同名称、参数列表 (不包括参数名) 以及返回值。
            当然,该类型还可以有其他方法。
            -------------------------------------------------------------------------------------------------
            接口只有方法声明,没有实现,没有数据字段。
            接口可以匿名嵌入其他接口,或嵌入到结构中。
            对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针。
            只有当接口存储的类型和对象都为nil时,接口才等于nil。
            接口调用不会做receiver的自动转换。
            接口同样支持匿名字段方法。
            接口也可实现类似OOP中的多态。
            空接口可以作为任何类型数据的容器。
            一个类型可实现多个接口。
            接口命名习惯以 er 结尾。
        b.每个接口由数个方法组成,接口的定义格式如下:
            type 接口类型名 interface{
                方法名1( 参数列表1 ) 返回值列表1
                方法名2( 参数列表2 ) 返回值列表2
                …
            }
            其中:
            1.接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
            2.方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
            3.参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
        c.举个例子:
            type writer interface{
                Write([]byte) error
            }
            当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。
    d.实现接口的条件
        一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。
        我们来定义一个Sayer接口:
        // Sayer 接口
        type Sayer interface {
            say()
        }
        定义dog和cat两个结构体:

        type dog struct {}

        type cat struct {}
        因为Sayer接口里只有一个say方法,所以我们只需要给dog和cat 分别实现say方法就可以实现Sayer接口了。

        // dog实现了Sayer接口
        func (d dog) say() {
            fmt.Println("汪汪汪")
        }

        // cat实现了Sayer接口
        func (c cat) say() {
            fmt.Println("喵喵喵")
        }
        接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。
    e.接口类型变量
        那实现了接口有什么用呢?
        接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,Sayer类型的变量能够存储dog和cat类型的变量。
        func main() {
            var x Sayer // 声明一个Sayer类型的变量x
            a := cat{}  // 实例化一个cat
            b := dog{}  // 实例化一个dog
            x = a       // 可以把cat实例直接赋值给x
            x.say()     // 喵喵喵
            x = b       // 可以把dog实例直接赋值给x
            x.say()     // 汪汪汪
        }
    f.值接收者和指针接收者实现接口的区别
        使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?接下来我们通过一个例子看一下其中的区别。
        我们有一个Mover接口和一个dog结构体。
        type Mover interface {
            move()
        }
        type dog struct {}
    g.值接收者实现接口
        func (d dog) move() {
            fmt.Println("狗会动")
        }
        此时实现接口的是dog类型:

        func main() {
            var x Mover
            var wangcai = dog{} // 旺财是dog类型
            x = wangcai         // x可以接收dog类型
            var fugui = &dog{}  // 富贵是*dog类型
            x = fugui           // x可以接收*dog类型
            x.move()
        }
        从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui。
    h.指针接收者实现接口
        同样的代码我们再来测试一下使用指针接收者有什么区别:

        func (d *dog) move() {
            fmt.Println("狗会动")
        }
        func main() {
            var x Mover
            var wangcai = dog{} // 旺财是dog类型
            x = wangcai         // x不可以接收dog类型
            var fugui = &dog{}  // 富贵是*dog类型
            x = fugui           // x可以接收*dog类型
        }
        此时实现Mover接口的是*dog类型,所以不能给x传入dog类型的wangcai,此时x只能存储*dog类型的值。
    i.下面的代码是一个比较好的面试题
        请问下面的代码是否能通过编译?
        type People interface {
            Speak(string) string
        }

        type Student struct{}

        func (stu *Stduent) Speak(think string) (talk string) {
            if think == "sb" {
                talk = "你是个大帅比"
            } else {
                talk = "您好"
            }
            return
        }

        func main() {
            var peo People = Student{}
            think := "bitch"
            fmt.Println(peo.Speak(think))
        }

02.类型与接口的关系
    a.一个类型实现多个接口
        一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。
        例如,狗可以叫,也可以动。我们就分别定义Sayer接口和Mover接口,如下: Mover接口。
        // Sayer 接口
        type Sayer interface {
            say()
        }

        // Mover 接口
        type Mover interface {
            move()
        }
        dog既可以实现Sayer接口,也可以实现Mover接口。

        type dog struct {
            name string
        }

        // 实现Sayer接口
        func (d dog) say() {
            fmt.Printf("%s会叫汪汪汪\n", d.name)
        }

        // 实现Mover接口
        func (d dog) move() {
            fmt.Printf("%s会动\n", d.name)
        }

        func main() {
            var x Sayer
            var y Mover

            var a = dog{name: "旺财"}
            x = a
            y = a
            x.say()
            y.move()
        }
    b.多个类型实现同一接口
        Go语言中不同的类型还可以实现同一接口 首先我们定义一个Mover接口,它要求必须由一个move方法。
        // Mover 接口
        type Mover interface {
            move()
        }
        例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系:

        type dog struct {
            name string
        }

        type car struct {
            brand string
        }

        // dog类型实现Mover接口
        func (d dog) move() {
            fmt.Printf("%s会跑\n", d.name)
        }

        // car类型实现Mover接口
        func (c car) move() {
            fmt.Printf("%s速度70迈\n", c.brand)
        }
        这个时候我们在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move方法就可以了。

        func main() {
            var x Mover
            var a = dog{name: "旺财"}
            var b = car{brand: "保时捷"}
            x = a
            x.move()
            x = b
            x.move()
        }
        上面的代码执行结果如下:

            旺财会跑
            保时捷速度70迈
        并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。

        // WashingMachine 洗衣机
        type WashingMachine interface {
            wash()
            dry()
        }

        // 甩干器
        type dryer struct{}

        // 实现WashingMachine接口的dry()方法
        func (d dryer) dry() {
            fmt.Println("甩一甩")
        }

        // 海尔洗衣机
        type haier struct {
            dryer //嵌入甩干器
        }

        // 实现WashingMachine接口的wash()方法
        func (h haier) wash() {
            fmt.Println("洗刷刷")
        }
    c.接口嵌套
        接口与接口间可以通过嵌套创造出新的接口。

        // Sayer 接口
        type Sayer interface {
            say()
        }

        // Mover 接口
        type Mover interface {
            move()
        }

        // 接口嵌套
        type animal interface {
            Sayer
            Mover
        }
        嵌套得到的接口的使用与普通接口一样,这里我们让cat实现animal接口:

        type cat struct {
            name string
        }

        func (c cat) say() {
            fmt.Println("喵喵喵")
        }

        func (c cat) move() {
            fmt.Println("猫会动")
        }

        func main() {
            var x animal
            x = cat{name: "花花"}
            x.move()
            x.say()
        }

03.空接口
    a.空接口的定义
        空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。

        空接口类型的变量可以存储任意类型的变量。

        func main() {
            // 定义一个空接口x
            var x interface{}
            s := "pprof.cn"
            x = s
            fmt.Printf("type:%T value:%v\n", x, x)
            i := 100
            x = i
            fmt.Printf("type:%T value:%v\n", x, x)
            b := true
            x = b
            fmt.Printf("type:%T value:%v\n", x, x)
        }
    b.空接口的应用
        空接口作为函数的参数
        使用空接口实现可以接收任意类型的函数参数。

        // 空接口作为函数参数
        func show(a interface{}) {
            fmt.Printf("type:%T value:%v\n", a, a)
        }
        空接口作为map的值
        使用空接口实现可以保存任意值的字典。

        // 空接口作为map值
            var studentInfo = make(map[string]interface{})
            studentInfo["name"] = "李白"
            studentInfo["age"] = 18
            studentInfo["married"] = false
            fmt.Println(studentInfo)
    c.类型断言
        空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?

        接口值
        一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。

        我们来看一个具体的例子:

        var w io.Writer
        w = os.Stdout
        w = new(bytes.Buffer)
        w = nil
        想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
            x.(T)
        其中:
            x:表示类型为interface{}的变量
            T:表示断言x可能是的类型。
        该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。

        举个例子:
        func main() {
            var x interface{}
            x = "pprof.cn"
            v, ok := x.(string)
            if ok {
                fmt.Println(v)
            } else {
                fmt.Println("类型断言失败")
            }
        }
        上面的示例中如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现:

        func justifyType(x interface{}) {
            switch v := x.(type) {
            case string:
                fmt.Printf("x is a string,value is %v\n", v)
            case int:
                fmt.Printf("x is a int is %v\n", v)
            case bool:
                fmt.Printf("x is a bool is %v\n", v)
            default:
                fmt.Println("unsupport type!")
            }
        }
        因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。
        关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

5.4 类型1

01.静态强类型
    Go是一个静态强类型语言,静态指的是Go所有变量的类型早在编译期间就已经确定了,在程序的生命周期都不会再发生改变,
    尽管Go中的短变量声明有点类似动态语言的写法,但其变量类型是由编译器自行推断的,最根本的区别在于它的类型一旦推断出来
    后不会再发生变化,动态语言则完全相反。所以下面的代码完全无法通过编译,因为a是int类型的变量,不能赋值字符串。
    func main() {
        var a int = 64
        a = "64"
        fmt.Println(a) // cannot use "64" (untyped string constant) as int value in assignment
    }
    ---------------------------------------------------------------------------------------------------------
    强类型则指的是在程序中执行严格的类型检查,如果出现类型不匹配的情况时,会立即告诉程序员不应该这么做,
    而不是像动态语言一样去尝试推断可能的结果。所以下面的代码无法通过编译,因为两者类型不同,无法进行运算。
    func main() {
        fmt.Println(1 + "1") // invalid operation: 1 + "1" (mismatched types untyped int and untyped string)
    }

02.类型后置
    Go为什么要把类型声明放在后面而不是前面,很大程度上是从C语言吸取了教训,拿官方的一个例子展示效果,这是一个函数指针
    int (*(*fp)(int (*)(int, int), int))(int, int)
    ---------------------------------------------------------------------------------------------------------
    说实话不认真看很难知道这是一个什么类型,在Go中类似的写法如下
    f func(func(int,int) int, int) func(int, int) int
    Go的声明方式始终遵循名字在前面,类型在后面的原则,从左往右读,大概第一眼就可以知道这是一个函数,
    且返回值为func(int,int) int。当类型变得越来越复杂时,类型后置在可读性上要好得多,
    Go在许多层面的设计都是为了可读性而服务的。

03.类型声明
    在Go中通过类型声明,可以声明一个自定义名称的新类型,声明一个新类型通常需要一个类型名称以及一个基础类型,简单的例子如下:
    type MyInt int64
    ---------------------------------------------------------------------------------------------------------
    在上述类型声明中,通过type关键字声明了一个基础类型为int64名为MyInt的类型。在Go中,每一个新声明的类型都必须有一个与之对应的基础类型,且类型名称不建议与已有的内置标识符重复。
    type MyInt int64
    type MyFloat64 float64
    type MyMap map[string]int
    // 可以通过编译,但是不建议使用,这会覆盖原有的类型
    type int int64
    ---------------------------------------------------------------------------------------------------------
    通过类型声明的类型都是新类型,不同的类型无法进行运算,即便基础类型是相同的。
    type MyFloat64 float64
    var f1 MyFloat64
    var f float64
    f1 = 0.2
    f = 0.1
    fmt.Println(f1 + f)
    invalid operation: f1 + f (mismatched types MyFloat64 and float64)

04.类型别名
    类型别名与类型声明则不同,类型别名仅仅只是一个别名,并不是创建了一个新的类型,简单的例子如下:
    type Int = int
    ---------------------------------------------------------------------------------------------------------
    两者是都是同一个类型,仅仅叫的名字不同,所以也就可以进行运算,所以下例自然也就可以通过编译。
    type Int = int
    var a Int = 1
    var b int = 2
    fmt.Println(a + b)
    3
    ---------------------------------------------------------------------------------------------------------
    类型别名对于一些特别复杂的类型有很大的用处,例如现在有一个类型map[string]map[string]int,这是一个二维map,
    现有一个函数参数是map[string]map[string]int类型,如下
    func PrintMyMap(mymap map[string]map[string]int) {
       fmt.Println(mymap)
    }
    ---------------------------------------------------------------------------------------------------------
    这种情况下,就没有必要使用类型声明了,因为前者是声明了一个新的类型,无法作为该函数的参数,使用类型别名后的例子如下
    type TwoDMap = map[string]map[string]int

    func PrintMyMap(mymap TwoDMap) {
       fmt.Println(mymap)
    }
    使用类型别名后看起来会简洁一些。
    ---------------------------------------------------------------------------------------------------------
    提示:内置类型any就是interface{}的类型别名,两者完全等价,仅仅叫法不一样。

05.类型转换
    在Go中,只存在显式的类型转换,不存在隐式类型转换,因此不同类型的变量无法进行运算,无法作为参数传递。类型转换适用的前提是知晓被转换变量的类型和要转换成的目标类型,例子如下:
    type MyFloat64 float64

    var f1 MyFloat64
    var f float64
    f1 = 0.2
    f = 0.1
    fmt.Println(float64(f1) + f)

    0.30000000000000004
    ---------------------------------------------------------------------------------------------------------
    通过显式的将MyFloat64 转换为float64类型,才能进行加法运算。类型转换的另一个前提是:被转换类型必须是可以被目标类型
    代表的(Representability),例如int可以被int64类型所代表,也可以被float64类型代表,所以它们之间可以进行显式的类型
    转换,但是int类型无法被string和bool类型代表,因为也就无法进行类型转换。
    ---------------------------------------------------------------------------------------------------------
    即便两个类型可以相互代表,类型转换的结果也不总是正确的,看下面的一个例子:
    var num1 int8 = 1
    var num2 int32 = 512
    fmt.Println(int32(num1), int8(num2))
    1 0
    ---------------------------------------------------------------------------------------------------------
    num1被正确的转换为了int32类型,但是num2并没有。这是一个典型的数值溢出问题,int32能够表示31位整数,
    int8只能表示7位整数,高精度整数在向低精度整数转换时会抛弃高位保留低位,因此num1的转换结果就是0。
    在数字的类型转换中,通常建议小转大,而不建议大转小。
    在使用类型转换时,对于一些类型需要避免歧义,例子如下:
    *Point(p) // 等价于 *(Point(p))
    (*Point)(p)  // 将p转换为类型 *Point
    <-chan int(c)    // 等价于 <-(chan int(c))
    (<-chan int)(c)  // 将c转换为类型  <-chan int
    (func())(x)      // 将x转换为类型 func()
    (func() int)(x)  // 将x转换为类型 func() int

06.类型断言
    类型断言通常用于判断某一接口类型的变量是否属于某一个类型,示例如下
    var b int = 1
    var a interface{} = b
    if intVal, ok := a.(int); ok {
       fmt.Println(intVal)
    } else {
       fmt.Println("error type")
    }
    1
    ---------------------------------------------------------------------------------------------------------
    由于interface{}是空接口类型,空接口类型可以代表所有的类型,但是int类型无法代表interface{}类型,
    所以无法使用类型转换。而类型断言就可以判断其底层类型是否为想要的类型,类型断言语句有两个返回值,
    一个是类型转换过后的值,另一个是转换结果的布尔值。

07.类型判断
    在Go中,switch语句还支持一种特殊的写法,通过这种写法可以根据不同的case做出不同的逻辑处理,使用的前提是入参必须是接口类型,示例如下:
    var a interface{} = 2
    switch a.(type) {
        case int: fmt.Println("int")
        case float64: fmt.Println("float")
        case string: fmt.Println("string")
    }
    int

5.5 类型2

00.go不支持隐式转换类型
    package main
    import "fmt"

    func main() {
        var a int64 = 3
        var b int32
        b = a
        fmt.Printf("b 为 : %d", b)
    }
    ---------------------------------------------------------------------------------------------------------
    此时会报错
    cannot use a (type int64) as type int32 in assignment
    cannot use b (type int32) as type string in argument to fmt.Printf
    ---------------------------------------------------------------------------------------------------------
    但是如果改成 b = int32(a) 就不会报错了:
    package main
    import "fmt"

    func main() {
        var a int64 = 3
        var b int32
        b = int32(a)
        fmt.Printf("b 为 : %d", b)
    }

01.数据转换
    类型转换用于将一种数据类型的变量转换为另外一种类型的变量。
    Go 语言类型转换基本格式如下:
    type_name(expression)
    type_name 为类型,expression 为表达式。

02.数值类型转换
    将整型转换为浮点型:
    var a int = 10
    var b float64 = float64(a)
    ---------------------------------------------------------------------------------------------------------
    以下实例中将整型转化为浮点型,并计算结果,将结果赋值给浮点型变量:
    package main

    import "fmt"

    func main() {
       var sum int = 17
       var count int = 5
       var mean float32

       mean = float32(sum)/float32(count)
       fmt.Printf("mean 的值为: %f\n",mean)
    }
    以上实例执行输出结果为:
    mean 的值为: 3.400000

03.字符串类型转换
    将一个字符串转换成另一个类型,可以使用以下语法:
    var str string = "10"
    var num int
    num, _ = strconv.Atoi(str)
    以上代码将字符串变量 str 转换为整型变量 num。
    注意,strconv.Atoi 函数返回两个值,第一个是转换后的整型值,第二个是可能发生的错误,我们可以使用空白标识符 _ 来忽略这个错误
    ---------------------------------------------------------------------------------------------------------
    a.以下实例将字符串转换为整数
        package main

        import (
            "fmt"
            "strconv"
        )

        func main() {
            str := "123"
            num, err := strconv.Atoi(str)
            if err != nil {
                fmt.Println("转换错误:", err)
            } else {
                fmt.Printf("字符串 '%s' 转换为整数为:%d\n", str, num)
            }
        }
        以上实例执行输出结果为:
        字符串 '123' 转换为整数为:123
    b.以下实例将整数转换为字符串:
        package main

        import (
            "fmt"
            "strconv"
        )

        func main() {
            num := 123
            str := strconv.Itoa(num)
            fmt.Printf("整数 %d  转换为字符串为:'%s'\n", num, str)
        }
        以上实例执行输出结果为:
        整数 123  转换为字符串为:'123'
    c.以下实例将字符串转换为浮点数:
        package main

        import (
            "fmt"
            "strconv"
        )

        func main() {
            str := "3.14"
            num, err := strconv.ParseFloat(str, 64)
            if err != nil {
                fmt.Println("转换错误:", err)
            } else {
                fmt.Printf("字符串 '%s' 转为浮点型为:%f\n", str, num)
            }
        }
        以上实例执行输出结果为:
        字符串 '3.14' 转为浮点型为:3.140000
    d.以下实例将浮点数转换为字符串:
        package main

        import (
            "fmt"
            "strconv"
        )

        func main() {
            num := 3.14
            str := strconv.FormatFloat(num, 'f', 2, 64)
            fmt.Printf("浮点数 %f 转为字符串为:'%s'\n", num, str)
        }
        以上实例执行输出结果为:
        浮点数 3.140000 转为字符串为:'3.14'
    e.string转int、int转string
        package main
        import (
          "fmt"
          "strconv"
        )

        func main() {
          // string to int
          aStr := "100"
          bInt, err := strconv.Atoi(aStr)

          if err == nil {
            fmt.Printf("aStr:%T %s,bInt:%T %d", aStr, aStr, bInt, bInt)
          } else {
            fmt.Printf("err:%s", err)
          }

          // int to string
          cInt := 200
          dStr := strconv.Itoa(cInt)

          fmt.Printf("cInt:%T %d,dStr:%T %s", cInt, cInt, dStr, dStr)
        }

03.接口类型转换有两种情况:类型断言和类型转换。
    a.类型断言
        类型断言用于将接口类型转换为指定类型,其语法为:
        value.(type)
        或者
        value.(T)
        其中 value 是接口类型的变量,type 或 T 是要转换成的类型。
        如果类型断言成功,它将返回转换后的值和一个布尔值,表示转换是否成功。
        -----------------------------------------------------------------------------------------------------
        package main

        import "fmt"

        func main() {
            var i interface{} = "Hello, World"
            str, ok := i.(string)
            if ok {
                fmt.Printf("'%s' is a string\n", str)
            } else {
                fmt.Println("conversion failed")
            }
        }
        以上实例中,我们定义了一个接口类型变量 i,并将它赋值为字符串 "Hello, World"。然后,我们使用类型断言将 i 转换为字符串类型,并将转换后的值赋值给变量 str。最后,我们使用 ok 变量检查类型转换是否成功,如果成功,我们打印转换后的字符串;否则,我们打印转换失败的消息。
    b.类型转换
        类型转换用于将一个接口类型的值转换为另一个接口类型,其语法为:
        T(value)
        T 是目标接口类型,value 是要转换的值。
        在类型转换中,我们必须保证要转换的值和目标接口类型之间是兼容的,否则编译器会报错。
        -----------------------------------------------------------------------------------------------------
        package main

        import "fmt"

        // 定义一个接口 Writer
        type Writer interface {
            Write([]byte) (int, error)
        }

        // 实现 Writer 接口的结构体 StringWriter
        type StringWriter struct {
            str string
        }

        // 实现 Write 方法
        func (sw *StringWriter) Write(data []byte) (int, error) {
            sw.str += string(data)
            return len(data), nil
        }

        func main() {
            // 创建一个 StringWriter 实例并赋值给 Writer 接口变量
            var w Writer = &StringWriter{}

            // 将 Writer 接口类型转换为 StringWriter 类型
            sw := w.(*StringWriter)

            // 修改 StringWriter 的字段
            sw.str = "Hello, World"

            // 打印 StringWriter 的字段值
            fmt.Println(sw.str)
        }
        -----------------------------------------------------------------------------------------------------
        定义接口和结构体:
            Writer 接口定义了 Write 方法。
            StringWriter 结构体实现了 Write 方法。
        类型转换:
            将 StringWriter 实例赋值给 Writer 接口变量 w。
            使用 w.(*StringWriter) 将 Writer 接口类型转换为 StringWriter 类型。
        访问字段:
            修改 StringWriter 的字段 str,并打印其值。

04.空接口类型
    空接口 interface{} 可以持有任何类型的值。在实际应用中,空接口经常被用来处理多种类型的值。
    package main

    import (
        "fmt"
    )

    func printValue(v interface{}) {
        switch v := v.(type) {
        case int:
            fmt.Println("Integer:", v)
        case string:
            fmt.Println("String:", v)
        default:
            fmt.Println("Unknown type")
        }
    }

    func main() {
        printValue(42)
        printValue("hello")
        printValue(3.14)
    }

5.6 泛型

00.背景
    最初的Go是没有泛型这一说法的,但自从诞生以来,社区关于Go呼声最高的事情就是希望加入泛型。
    终于Go在1.18版本加入了对泛型的支持,不过有一点怪。

01.示例
    在开始之前,先来看一个简单的例子。
    func Sum(a, b int) int {
       return a + b
    }
    ---------------------------------------------------------------------------------------------------------
    这是一个功能十分简单的函数,作用就是将两个int类型的整数相加并返回结果,倘若想要传入两个float64类型的浮点数求和的话,显然是不可以的,因为类型不匹配。一种解决办法就是再定义一个新的函数,如下
    func SumFloat64(a, b float64) float64 {
        return a + b
    }
    ---------------------------------------------------------------------------------------------------------
    那么问题来了,如果开发一个数学工具包,计算所有数字类型的两数之和,难道要每一个类型都要编写一个函数吗?显然是不太可能的,或者也可以使用any类型加反射来判断,如下
    func SumAny(a, b any) (any, error) {
        tA, tB := reflect.ValueOf(a), reflect.ValueOf(b)
        if tA.Kind() != tB.Kind() {
            return nil, errors.New("disMatch type")
        }

        switch tA.Kind() {
        case reflect.Int:
        case reflect.Int32:
            ...
        }
    }
    ---------------------------------------------------------------------------------------------------------
    但是这样写会显得十分复杂,而且性能低下。但是Sum函数的逻辑都是一模一样的,都只不过是将两个数相加而已,这时候就需要用到了泛型,所以为什么需要泛型,泛型是为了解决执行逻辑与类型无关的问题,这类问题不关心给出的类型是什么,只需要完成对应的操作就足够。所以泛型的写法如下
    func Sum[T int | float64](a, b T) T {
       return a + b
    }
    类型形参:T就是一个类型形参,形参具体是什么类型取决于传进来什么类型
    类型约束:int | float64构成了一个类型约束,这个类型约束内规定了哪些类型是允许的,约束了类型形参的类型范围
    类型实参:Sum[int](1,2),手动指定了int类型,int就是类型实参。
    ---------------------------------------------------------------------------------------------------------
    第一种用法,显式的指明使用哪种类型,如下
    Sum[int](2012, 2022)
    第二种用法,不指定类型,让编译器自行推断,如下
    Sum(3.1415926, 1.114514)
    看到这里后,应该对为什么要使用泛型,以及泛型解决了哪种问题有了一个大概的了解。将泛型引入项目后,开发上确实会比较方便,随之而来的是项目复杂度的增加,毫无节制的使用泛型会使得代码难以维护,所以应该在正确的地方使用泛型,而不是为了泛型而泛型。

02.泛型结构
    a.介绍
        这是一个泛型切片,类型约束为int | int32 | int64
        type GenericSlice[T int | int32 | int64] []T
        -----------------------------------------------------------------------------------------------------
        这里使用时就不能省略掉类型实参
        GenericSlice[int]{1, 2, 3}
        -----------------------------------------------------------------------------------------------------
        这是一个泛型哈希表,键的类型必须是可比较的,所以使用comparable接口,值的类型约束为V int | string | byte
        type GenericMap[K comparable, V int | string | byte] map[K]V

        使用
        gmap1 := GenericMap[int, string]{1: "hello world"}
        gmap2 := make(GenericMap[string, byte], 0)
        -----------------------------------------------------------------------------------------------------
        这是一个泛型结构体,类型约束为T int | string
        type GenericStruct[T int | string] struct {
           Name string
           Id   T
        }

        使用
        GenericStruct[int]{
           Name: "jack",
           Id:   1024,
        }
        GenericStruct[string]{
           Name: "Mike",
           Id:   "1024",
        }
        -----------------------------------------------------------------------------------------------------
        这是一个泛型切片形参的例子
        type Company[T int | string, S []T] struct {
           Name  string
           Id    T
           Stuff S
        }

        //也可以如下
        type Company[T int | string, S []int | string] struct {
            Name  string
            Id    T
            Stuff S
        }

        使用
        Company[int, []int]{
           Name:  "lili",
           Id:    1,
           Stuff: []int{1},
        }
        -----------------------------------------------------------------------------------------------------
        提示
        在泛型结构体中,更推荐这种写法
        type Company[T int | string, S int | string] struct {
            Name  string
            Id    T
            Stuff []S
        }
        SayAble是一个泛型接口,Person实现了该接口。


        type SayAble[T int | string] interface {
           Say() T
        }

        type Person[T int | string] struct {
           msg T
        }

        func (p Person[T]) Say() T {
           return p.msg
        }

        func main() {
            var s SayAble[string]
            s = Person[string]{"hello world"}
            fmt.Println(s.Say())
        }
    b.泛型结构注意点
        泛型不能作为一个类型的基本类型
        以下写法是错误的,泛型形参T是不能作为基础类型的
        type GenericType[T int | int32 | int64] T
        虽然下列的写法是允许的,不过毫无意义而且可能会造成数值溢出的问题,虽然并不推荐
        type GenericType[T int | int32 | int64] int
        -----------------------------------------------------------------------------------------------------
        泛型类型无法使用类型断言
        对泛型类型使用类型断言将会无法通过编译,泛型要解决的问题是类型无关的,如果一个问题需要根据不同类型做出不同的逻辑,那么就根本不应该使用泛型,应该使用interface{}或者any。
        func Sum[T int | float64](a, b T) T {
           ints,ok := a.(int) // 不被允许
           switch a.(type) { // 不被允许
           case int:
           case bool:
              ...
           }
           return a + b
        }
        -----------------------------------------------------------------------------------------------------
        匿名结构不支持泛型
        匿名结构体是不支持泛型的,如下的代码将无法通过编译
        testStruct := struct[T int | string] {
           Name string
           Id T
        }[int]{
           Name: "jack",
           Id: 1
        }
        -----------------------------------------------------------------------------------------------------
        匿名函数不支持自定义泛型
        以下两种写法都将无法通过编译
        var sum[T int | string] func (a, b T) T
        sum := func[T int | string](a,b T) T{
            ...
        }
        但是可以使用已有的泛型类型,例如闭包中
        func Sum[T int | float64](a, b T) T {
            sub := func(c, d T) T {
                return c - d
            }
            return sub(a,b) + a + b
        }
        -----------------------------------------------------------------------------------------------------
        不支持泛型方法
        方法是不能拥有泛型形参的,但是receiver可以拥有泛型形参。如下的代码将会无法通过编译
        type GenericStruct[T int | string] struct {
           Name string
           Id   T
        }
        func (g GenericStruct[T]) name[S int | float64](a S) S {
           return a
        }

03.类型集
    a.介绍
        a.介绍
            在1.18以后,接口的定义变为了类型集(type set),含有类型集的接口又称为General interfaces即通用接口。
            An interface type defines a type set
            类型集主要用于类型约束,不能用作类型声明,既然是集合,就会有空集,并集,交集,接下来将会讲解这三种情况。
        b.并集
            接口类型SignedInt是一个类型集,有符号整数类型的并集就是SignedInt,反过来SignedInt就是它们的超集。
            type SignedInt interface {
               int8 | int16 | int | int32 | int64
            }

            基本数据类型如此,对待其它通用接口也是如此
            type SignedInt interface {
                int8 | int16 | int | int32 | int64
            }
            type UnSignedInt interface {
                uint8 | uint16 | uint32 | uint64
            }
            type Integer interface {
                SignedInt | UnSignedInt
            }
        c.交集
            非空接口的类型集是其所有元素的类型集的交集,翻译成人话就是:如果一个接口包含多个非空类型集,那么该接口就是这些类型集的交集,例子如下
            type SignedInt interface {
               int8 | int16 | int | int32 | int64
            }
            type Integer interface {
               int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64
            }
            type Number interface {
                SignedInt
                Integer
            }
            例子中的交集肯定就是SignedInt,
            func Do[T Number](n T) T {
               return n
            }

            Do[int](2)
            DO[uint](2) //无法通过编译
            d.空集
            空集就是没有交集,例子如下,下面例子中的Integer就是一个类型空集。
            type SignedInt interface {
                int8 | int16 | int | int32 | int64
            }

            type UnsignedInt interface {
                uint8 | uint16 | uint | uint32 | uint64
            }

            type Integer interface {
                SignedInt
                UnsignedInt
            }
            因为无符号整数和有符号整数两个肯定没有交集,所以交集就是个空集,下方例子中不管传什么类型都无法通过编译。
            Do[Integer](1)
            Do[Integer](-100)
        e.空接口
            空接口与空集并不同,空接口是所有类型集的集合,即包含所有类型。
            func Do[T interface{}](n T) T {
               return n
            }

            func main() {
               Do[struct{}](struct{}{})
               Do[any]("abc")
            }
        f.底层类型
            当使用type关键字声明了一个新的类型时,即便其底层类型包含在类型集内,当传入时也依旧会无法通过编译。
            type Int interface {
               int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64
            }

            type TinyInt int8

            func Do[T Int](n T) T {
               return n
            }

            func main() {
               Do[TinyInt](1) // 无法通过编译,即便其底层类型属于Int类型集的范围内
            }
            -------------------------------------------------------------------------------------------------
            有两种解决办法,第一种是往类型集中并入该类型,但是这毫无意义,因为TinyInt与int8底层类型就是一致的,所以就有了第二种解决办法。
            type Int interface {
               int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64 | TinyInt
            }
            使用~符号,来表示底层类型,如果一个类型的底层类型属于该类型集,那么该类型就属于该类型集,如下所示
            -------------------------------------------------------------------------------------------------
            type Int interface {
               ~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64
            }
            修改过后就可以通过编译了。
            func main() {
               Do[TinyInt](1) // 可以通过编译,因为TinyInt在类型集Int内
            }
    b.类型集注意点
        a.带有方法集的接口无法并入类型集
            只要是带有方法集的接口,不论是基本接口,泛型接口,又或者是通用接口,都无法并入类型集中,同样的也无法在类型约束中并入。以下两种写法都是错误的,都无法通过编译。
            type Integer interface {
                Sum(int, int) int
                Sub(int, int) int
            }

            type SignedInt interface {
               int8 | int16 | int | int32 | int64 | Integer
            }

            func Do[T Integer | float64](n T) T {
                return n
            }
        b.类型集无法当作类型实参使用
            只要是带有类型集的接口,都无法当作类型实参。
            type SignedInt interface {
                int8 | int16 | int | int32 | int64
            }

            func Do[T SignedInt](n T) T {
               return n
            }

            func main() {
               Do[SignedInt](1) // 无法通过编译
            }
        c.类型集中的交集问题
            对于非接口类型,类型并集中不能有交集,例如下例中的TinyInt与~int8有交集。
            type Int interface {
               ~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64 | TinyInt // 无法通过编译
            }

            type TinyInt int8
            但是对于接口类型的话,就允许有交集,如下例
            type Int interface {
               ~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64 | TinyInt // 可以通过编译
            }

            type TinyInt interface {
                int8
            }
        d.类型集不能直接或间接的并入自身
            以下示例中,Floats 直接的并入了自身,而Double又并入了Floats,所以又间接的并入了自身。
            type Floats interface {  // 代码无法通过编译
               Floats | Double
            }

            type Double interface {
               Floats
            }
        e.comparable接口无法并入类型集,同样的,也无法并入类型约束中,所以基本上都是单独使用。
            func Do[T comparable | Integer](n T) T { //无法通过编译
               return n
            }

            type Number interface { // 无法通过编译
                Integer | comparable
            }

            type Comparable interface { // 可以通过编译但是毫无意义
                comparable
            }

04.使用
    a.队列
        下面用泛型实现一个简单的队列,首先声明队列类型,队列中的元素类型可以是任意的,所以类型约束为any
        type Queue[T any] []T
        -----------------------------------------------------------------------------------------------------
        总共只有四个方法Pop ,Peek,Push,Size,代码如下。
        type Queue[T any] []T

        func (q *Queue[T]) Push(e T) {
            *q = append(*q, e)
        }

        func (q *Queue[T]) Pop(e T) (_ T) {
            if q.Size() > 0 {
                res := q.Peek()
                *q = (*q)[1:]
                return res
            }
            return
        }

        func (q *Queue[T]) Peek() (_ T) {
            if q.Size() > 0 {
                return (*q)[0]
            }
            return
        }

        func (q *Queue[T]) Size() int {
            return len(*q)
        }
        -----------------------------------------------------------------------------------------------------
        在Pop和Peek方法中,可以看到返回值是_ T,这是具名返回值的使用方式,但是又采用了下划线_表示这是匿名的,
        这并非多此一举,而是为了表示泛型零值。由于采用了泛型,当队列为空时,需要返回零值,但由于类型未知,
        不可能返回具体的类型,借由上面的那种方式就可以返回泛型零值。也可以声明泛型变量的方式来解决零值问题,
        对于一个泛型变量,其默认的值就是该类型的零值,如下
        func (q *Queue[T]) Pop(e T) T {
            var res T
            if q.Size() > 0 {
                res = q.Peek()
                *q = (*q)[1:]
                return res
            }
            return res
        }
    b.堆
        上面队列的例子,由于对元素没有任何的要求,所以类型约束为any。但堆就不一样了,堆是一种特殊的数据结构,
        它可以在O(1)的时间内判断最大或最小值,所以它对元素有一个要求,那就是必须是可以排序的类型,
        但内置的可排序类型只有数字和字符串,并且go的泛型约束不允许存在带方法的接口,所以在堆的初始化时,
        需要传入一个自定义的比较器,比较器由使用者提供,比较器也必须使用泛型,如下
        type Comparator[T any] func(a, b T) int
        -----------------------------------------------------------------------------------------------------
        下面是一个简单的二项最小堆的实现,先声明泛型结构体,依旧采用any进行约束,这样可以存放任意类型
        type Comparator[T any] func(a, b T) int

        type BinaryHeap[T any] struct {
            s []T
            c Comparator[T]
        }
        -----------------------------------------------------------------------------------------------------
        几个方法实现
        func (heap *BinaryHeap[T]) Peek() (_ T) {
            if heap.Size() > 0 {
                return heap.s[0]
            }
            return
        }

        func (heap *BinaryHeap[T]) Pop() (_ T) {
            size := heap.Size()
            if size > 0 {
                res := heap.s[0]
                heap.s[0], heap.s[size-1] = heap.s[size-1], heap.s[0]
                heap.s = heap.s[:size-1]
                heap.down(0)
                return res
            }
            return
        }

        func (heap *BinaryHeap[T]) Push(e T) {
            heap.s = append(heap.s, e)
            heap.up(heap.Size() - 1)
        }

        func (heap *BinaryHeap[T]) up(i int) {
            if heap.Size() == 0 || i < 0 || i >= heap.Size() {
                return
            }
            for parentIndex := i>>1 - 1; parentIndex >= 0; parentIndex = i>>1 - 1 {
                // greater than or equal to
                if heap.compare(heap.s[i], heap.s[parentIndex]) >= 0 {
                    break
                }
                heap.s[i], heap.s[parentIndex] = heap.s[parentIndex], heap.s[i]
                i = parentIndex
            }
        }

        func (heap *BinaryHeap[T]) down(i int) {
            if heap.Size() == 0 || i < 0 || i >= heap.Size() {
                return
            }
            size := heap.Size()
            for lsonIndex := i<<1 + 1; lsonIndex < size; lsonIndex = i<<1 + 1 {
                rsonIndex := lsonIndex + 1

                if rsonIndex < size && heap.compare(heap.s[rsonIndex], heap.s[lsonIndex]) < 0 {
                    lsonIndex = rsonIndex
                }

                // less than or equal to
                if heap.compare(heap.s[i], heap.s[lsonIndex]) <= 0 {
                    break
                }
                heap.s[i], heap.s[lsonIndex] = heap.s[lsonIndex], heap.s[i]
                i = lsonIndex
            }
        }

        func (heap *BinaryHeap[T]) Size() int {
            return len(heap.s)
        }
        -----------------------------------------------------------------------------------------------------
        使用起来如下
        type Person struct {
            Age  int
            Name string
        }

        func main() {
            heap := NewHeap[Person](10, func(a, b Person) int {
                return cmp.Compare(a.Age, b.Age)
            })
            heap.Push(Person{Age: 10, Name: "John"})
            heap.Push(Person{Age: 18, Name: "mike"})
            heap.Push(Person{Age: 9, Name: "lili"})
            heap.Push(Person{Age: 32, Name: "miki"})
            fmt.Println(heap.Peek())
            fmt.Println(heap.Pop())
            fmt.Println(heap.Peek())
        }
        输出
        {9 lili}
        {9 lili}
        {10 John}
        -----------------------------------------------------------------------------------------------------
        有泛型的加持,原本不可排序的类型传入比较器后也可以使用堆了,这样做肯定比以前使用interface{}来进行类型转换和断言要优雅和方便很多。

05.小结
    go的一大特点就是编译速度非常快,编译快是因为编译期做的优化少,泛型的加入会导致编译器的工作量增加,工作更加复杂,
    这必然会导致编译速度变慢,事实上当初go1.18刚推出泛型的时候确实导致编译更慢了,go团队既想加入泛型又不想太拖累编译速度,
    开发者用的顺手,编译器就难受,反过来编译器轻松了(最轻松的当然是直接不要泛型),开发者就难受了,
    现如今的泛型就是这两者之间妥协后的产物。

5.7 错误处理

00.错误
    在Go中的异常有三种级别:
    error:部分流程出错,需要处理
    panic:很严重的问题,程序应该在处理完问题后立即退出
    fatal:非常致命的问题,程序应该立即退出
    ---------------------------------------------------------------------------------------------------------
    准确的来说,Go并没有异常,更多的是通过错误来体现,同样的,Go中也并没有try-catch-finally这种语句,
    Go创始人希望能够将错误可控,他们不希望干什么事情都需要嵌套一堆try-catch,
    所以大多数情况会将其作为函数的返回值来返回,例如下方代码例子:
    ---------------------------------------------------------------------------------------------------------
    func main() {
        // 打开一个文件
        if file, err := os.Open("README.txt"); err != nil {
            fmt.Println(err)
            return
        }
        fmt.Println(file.Name())
    }
    ---------------------------------------------------------------------------------------------------------
    这段代码的意图很明显,打开一个名为README.txt的文件,如果打开失败函数将会返回一个错误,输出错误信息,
    如果错误为nil的话那么就是打开成功,输出文件名。
    ---------------------------------------------------------------------------------------------------------
    看起来似乎是要比try-catch简洁一些,那如果有特别多的函数调用,将会到处都充斥着if err != nil 这种判断语句,
    比如下面的例子,这是一个计算文件哈希值的demo,在这一小段代码中总共出现了三次if err != nil。
    func main() {
        sum, err := checksum("main.go")
        if err != nil {
            fmt.Println(err)
            return
        }
        fmt.Println(sum)
    }
    func checksum(path string) (string, error) {
        file, err := os.Open(path)
        if err != nil {
            return "", err
        }
        defer file.Close()

        hash := sha256.New()
        _, err = io.Copy(hash, file)
        if err != nil {
            return "", err
        }

        var hexSum [64]byte
        sum := hash.Sum(nil)
        hex.Encode(hexSum[:], sum)

        return string(hexSum[:]), nil
    }
    ---------------------------------------------------------------------------------------------------------
    正因如此,外界对于Go最诟病的点就在错误处理上,Go源代码里if err != nil就占了相当一部分。
    Rust同样也是返回错误值,但没有人会去说它这一点,因为它通过语法糖的方式解决了这类问题,
    与Rust相比之下,Go的语法糖不能说很多,只能说是几乎没有。
    ---------------------------------------------------------------------------------------------------------
    不过我们看待事物要辩证的来看,凡事都是有好有坏的,Go的错误处理的优点有几个
    心智负担小:有错误就处理,不处理就返回
    可读性:因为处理的方式非常简单,大部分情况下都很容易读懂代码
    易于调试:每一个错误都是通过函数调用的返回值产生的,可以一层一层往回找到,很少会出现突然冒出一个错误却不知道是从哪里来的这种情况
    ---------------------------------------------------------------------------------------------------------
    不过缺点也不少
    错误中没有堆栈信息(需要第三方包解决或者自己封装)
    丑陋,重复代码多(看个人喜好)
    自定义错误是通过var来声明的,它是一个变量而不是常量(确实不应该)
    变量遮蔽问题
    ---------------------------------------------------------------------------------------------------------
    社区中有关于Go错误处理的提案和讨论自从Go诞生以来就从未停止过,有这么一句玩笑话:如果你能接受Go的错误处理,那么你就是一个合格的Gopher了。

01.error
    a.介绍
        error属于是一种正常的流程错误,它的出现是可以被接受的,大多数情况下应该对其进行处理,当然也可以忽略不管,
        error的严重级别不足以停止整个程序的运行。error本身是一个预定义的接口,该接口下只有一个方法Error(),
        该方法的返回值是字符串,用于输出错误信息。
        type error interface {
           Error() string
        }
        error在历史上也有过大改,在1.13版本时Go团队推出了链式错误,且提供了更加完善的错误检查机制,接下来都会一一介绍。
    b.创建
        创建一个error有以下几种方法,第一种是使用errors包下的New函数。
        err := errors.New("这是一个错误")
        -----------------------------------------------------------------------------------------------------
        第二种是使用fmt包下的Errorf函数,可以得到一个格式化参数的error。
        err := fmt.Errorf("这是%d个格式化参数的的错误", 1)
        -----------------------------------------------------------------------------------------------------
        下面是一个完整的例子
        func sumPositive(i, j int) (int, error) {
           if i <= 0 || j <= 0 {
              return -1, errors.New("必须是正整数")
           }
           return i + j, nil
        }
        -----------------------------------------------------------------------------------------------------
        大部分情况,为了更好的维护性,一般都不会临时创建error,而是会将常用的error当作全局变量使用,例如下方节选自os\erros.go文件的代码
        var (
            ErrInvalid = fs.ErrInvalid // "invalid argument"

            ErrPermission = fs.ErrPermission // "permission denied"
            ErrExist      = fs.ErrExist      // "file already exists"
            ErrNotExist   = fs.ErrNotExist   // "file does not exist"
            ErrClosed     = fs.ErrClosed     // "file already closed"

            ErrNoDeadline       = errNoDeadline()       // "file type does not support deadline"
            ErrDeadlineExceeded = errDeadlineExceeded() // "i/o timeout"
        )
        可以看到它们都是被var定义的变量
    c.自定义错误
        通过实现Error()方法,可以很轻易的自定义error,例如erros包下的errorString就是一个很简单的实现。
        func New(text string) error {
           return &errorString{text}
        }

        // errorString结构体
        type errorString struct {
           s string
        }

        func (e *errorString) Error() string {
           return e.s
        }
        因为errorString实现太过于简单,表达能力不足,所以很多开源库包括官方库都会选择自定义error,以满足不同的错误需求。
    d.传递
        在一些情况中,调用者调用的函数返回了一个错误,但是调用者本身不负责处理错误,于是也将错误作为返回值返回,抛给上一层调用者,这个过程叫传递,错误在传递的过程中可能会层层包装,当上层调用者想要判断错误的类型来做出不同的处理时,可能会无法判别错误的类别或者误判,而链式错误正是为了解决这种情况而出现的。
        type wrapError struct {
           msg string
           err error
        }

        func (e *wrapError) Error() string {
           return e.msg
        }

        func (e *wrapError) Unwrap() error {
           return e.err
        }
        -----------------------------------------------------------------------------------------------------
        wrappError同样实现了error接口,也多了一个方法Unwrap,用于返回其内部对于原error的引用,
        层层包装下就形成了一条错误链表,顺着链表上寻找,很容易就能找到原始错误。由于该结构体并不对外暴露,
        所以只能使用fmt.Errorf函数来进行创建,例如
        -----------------------------------------------------------------------------------------------------
        err := errors.New("这是一个原始错误")
        wrapErr := fmt.Errorf("错误,%w", err)
        使用时,必须使用%w格式动词,且参数只能是一个有效的error。
    e.处理
        错误处理中的最后一步就是如何处理和检查错误,errors包提供了几个方便函数用于处理错误。
        func Unwrap(err error) error
        -----------------------------------------------------------------------------------------------------
        errors.Unwrap()函数用于解包一个错误链,其内部实现也很简单
        func Unwrap(err error) error {
           u, ok := err.(interface { // 类型断言,是否实现该方法
              Unwrap() error
           })
           if !ok { //没有实现说明是一个基础的error
              return nil
           }
           return u.Unwrap() // 否则调用Unwrap
        }
        -----------------------------------------------------------------------------------------------------
        解包后会返回当前错误链所包裹的错误,被包裹的错误可能依旧是一个错误链,如果想要在错误链中找到对应的值或类型,
        可以递归进行查找匹配,不过标准库已经提供好了类似的函数。
        func Is(err, target error) bool
        -----------------------------------------------------------------------------------------------------
        errors.Is函数的作用是判断错误链中是否包含指定的错误,例子如下
        var originalErr = errors.New("this is an error")

        func wrap1() error { // 包裹原始错误
           return fmt.Errorf("wrapp error %w", wrap2())
        }

        func wrap2() error { // 原始错误
           return originalErr
        }

        func main() {
           err := wrap1()
           if errors.Is(err, originalErr) { // 如果使用if err == originalErr 将会是false
              fmt.Println("original")
           }
        }
        -----------------------------------------------------------------------------------------------------
        所以在判断错误时,不应该使用==操作符,而是应该使用errors.Is()。
        func As(err error, target any) bool
        -----------------------------------------------------------------------------------------------------
        errors.As()函数的作用是在错误链中寻找第一个类型匹配的错误,并将值赋值给传入的err。
        有些情况下需要将error类型的错误转换为具体的错误实现类型,以获得更详细的错误细节,
        而对一个错误链使用类型断言是无效的,因为原始错误是被结构体包裹起来的,这也是为什么需要As函数的原因。例子如下
        type TimeError struct { // 自定义error
           Msg  string
           Time time.Time //记录发生错误的时间
        }

        func (m TimeError) Error() string {
           return m.Msg
        }

        func NewMyError(msg string) error {
           return &TimeError{
              Msg:  msg,
              Time: time.Now(),
           }
        }

        func wrap1() error { // 包裹原始错误
           return fmt.Errorf("wrapp error %w", wrap2())
        }

        func wrap2() error { // 原始错误
           return NewMyError("original error")
        }

        func main() {
           var myerr *TimeError
           err := wrap1()
           // 检查错误链中是否有*TimeError类型的错误
           if errors.As(err, &myerr) { // 输出TimeError的时间
              fmt.Println("original", myerr.Time)
           }
        }
        -----------------------------------------------------------------------------------------------------
        target必须是指向error的指针,由于在创建结构体时返回的是结构体指针,所以error实际上*TimeError类型的,
        那么target就必须是**TimeError类型的。
        不过官方提供的errors包其实并不够用,因为它没有堆栈信息,不能定位,一般会比较推荐使用官方的另一个增强包
        github.com/pkg/errors
        -----------------------------------------------------------------------------------------------------
        例子
        import (
            "fmt"
            "github.com/pkg/errors"
        )

        func Do() error {
            return errors.New("error")
        }

        func main() {
            if err := Do(); err != nil {
                fmt.Printf("%+v", err)
            }
        }
        输出
        some unexpected error happened
        main.Do
                D:/WorkSpace/Code/GoLeran/golearn/main.go:9
        main.main
                D:/WorkSpace/Code/GoLeran/golearn/main.go:13
        runtime.main
                D:/WorkSpace/Library/go/root/go1.21.3/src/runtime/proc.go:267
        runtime.goexit
                D:/WorkSpace/Library/go/root/go1.21.3/src/runtime/asm_amd64.s:1650
        通过格式化输出,就可以看到堆栈信息了,默认情况下是不会输出堆栈的。这个包相当于是标准库errors包的加强版,同样都是官方写的,不知道为什么没有并入标准库。

02.panic
    a.介绍
        panic中文译为恐慌,表示十分严重的程序问题,程序需要立即停止来处理该问题,否则程序立即停止运行并输出堆栈信息,
        panic是Go是运行时异常的表达形式,通常在一些危险操作中会出现,主要是为了及时止损,从而避免造成更加严重的后果。
        不过panic在退出之前会做好程序的善后工作,同时panic也可以被恢复来保证程序继续运行。
        下方是一个向nil的map写入值的例子,肯定会触发panic
        func main() {
           var dic map[string]int
           dic["a"] = 'a'
        }
        panic: assignment to entry in nil map
        提示:只要任一协程发生panic,如果不将其捕获的话,整个程序都会崩溃
    b.创建
        显式的创建panic十分简单,使用内置函数panic即可,函数签名如下
        func panic(v any)
        -----------------------------------------------------------------------------------------------------
        panic函数接收一个类型为 any的参数v,当输出错误堆栈信息时,v也会被输出。使用例子如下
        func main() {
            initDataBase("", 0)
        }

        func initDataBase(host string, port int) {
            if len(host) == 0 || port == 0 {
                panic("非法的数据链接参数")
            }
            // ...其他的逻辑
        }
        -----------------------------------------------------------------------------------------------------
        当初始化数据库连接失败时,程序就不应该启动,因为没有数据库程序就运行的毫无意义,所以此处应该抛出panic
        panic: 非法的数据链接参数
    c.善后
        程序因为panic退出之前会做一些善后工作,例如执行defer语句。
        -----------------------------------------------------------------------------------------------------
        func main() {
           defer fmt.Println("A")
           defer fmt.Println("B")
           fmt.Println("C")
           panic("panic")
           defer fmt.Println("D")
        }
        输出为
        C
        B
        A
        panic: panic
        -----------------------------------------------------------------------------------------------------
        并且上游函数的defer语句同样会执行,例子如下
        func main() {
           defer fmt.Println("A")
           defer fmt.Println("B")
           fmt.Println("C")
           dangerOp()
           defer fmt.Println("D")
        }

        func dangerOp() {
           defer fmt.Println(1)
           defer fmt.Println(2)
           panic("panic")
           defer fmt.Println(3)
        }
        输出
        C
        2
        1
        B
        A
        panic: panic
        -----------------------------------------------------------------------------------------------------
        defer中也可以嵌套panic,下面是一个比较复杂的例子
        func main() {
            defer fmt.Println("A")
            defer func() {
                func() {
                    panic("panicA")
                    defer fmt.Println("E")
                }()
            }()
            fmt.Println("C")
            dangerOp()
            defer fmt.Println("D")
        }

        func dangerOp() {
            defer fmt.Println(1)
            defer fmt.Println(2)
            panic("panicB")
            defer fmt.Println(3)
        }
        -----------------------------------------------------------------------------------------------------
        defer中嵌套的panic 执行顺序依旧一致,发生panic时后续的逻辑将无法执行。
        C
        2
        1
        A
        panic: panicB
                panic: panicA
        综上所述,当发生panic时,会立即退出所在函数,并且执行当前函数的善后工作,例如defer,然后层层上抛,
        上游函数同样的也进行善后工作,直到程序停止运行。
        -----------------------------------------------------------------------------------------------------
        当子协程发生panic时,不会触发当前协程的善后工作,如果直到子协程退出都没有恢复panic,那么程序将会直接停止运行。
        var waitGroup sync.WaitGroup

        func main() {
            demo()
        }

        func demo() {
            waitGroup.Add(1)
            defer func() {
                fmt.Println("A")
            }()
            fmt.Println("C")
            go dangerOp()
            waitGroup.Wait() // 父协程阻塞等待子协程执行完毕
            defer fmt.Println("D")
        }
        func dangerOp() {
            defer fmt.Println(1)
            defer fmt.Println(2)
            panic("panicB")
            defer fmt.Println(3)
            waitGroup.Done()
        }
        输出为
        C
        2
        1
        panic: panicB
        -----------------------------------------------------------------------------------------------------
        可以看到demo()中的defer语句一个都没有执行,程序就直接退出了。需要注意的是,如果没有waitGroup来阻塞父协程的话,
        demo()的执行速度可能会快于子协程的执行速度,输出的结果就会变得非常有迷惑性,下面稍微修改一下代码
        func main() {
            demo()
        }

        func demo() {
            defer func() {
                // 父协程善后工作要花费20ms
                time.Sleep(time.Millisecond * 20)
                fmt.Println("A")
            }()
            fmt.Println("C")
            go dangerOp()
            defer fmt.Println("D")
        }
        func dangerOp() {
            // 子协程要执行一些逻辑,要花费1ms
            time.Sleep(time.Millisecond)
            defer fmt.Println(1)
            defer fmt.Println(2)
            panic("panicB")
            defer fmt.Println(3)
        }
        输出为
        C
        D
        2
        1
        panic: panicB
        在本例中,当子协程发生panic时,父协程早已完成的函数的执行,进入了善后工作,在执行最后一个defer时,
        碰巧遇到了子协程发生panic,所以程序就直接退出运行。
    c.恢复
        当发生panic时,使用内置函数recover()可以及时的处理并且保证程序继续运行,必须要在defer语句中运行,使用示例如下。
        func main() {
           dangerOp()
           fmt.Println("程序正常退出")
        }

        func dangerOp() {
           defer func() {
              if err := recover(); err != nil {
                 fmt.Println(err)
                 fmt.Println("panic恢复")
              }
           }()
           panic("发生panic")
        }
        -----------------------------------------------------------------------------------------------------
        调用者完全不知道dangerOp()函数内部发生了panic,程序执行剩下的逻辑后正常退出,所以输出如下
        发生panic
        panic恢复
        程序正常退出
        -----------------------------------------------------------------------------------------------------
        但事实上recover()的使用有许多隐含的陷阱。例如在defer中再次闭包使用recover。
        func main() {
            dangerOp()
            fmt.Println("程序正常退出")
        }

        func dangerOp() {
            defer func() {
                func() {
                    if err := recover(); err != nil {
                        fmt.Println(err)
                        fmt.Println("panic恢复")
                    }
                }()
            }()
            panic("发生panic")
        }
        -----------------------------------------------------------------------------------------------------
        闭包函数可以看作调用了一个函数,panic是向上传递而不是向下,自然闭包函数也就无法恢复panic,所以输出如下。
        panic: 发生panic
        -----------------------------------------------------------------------------------------------------
        除此之外,还有一种很极端的情况,那就是panic()的参数是nil。
        func main() {
           dangerOp()
           fmt.Println("程序正常退出")
        }

        func dangerOp() {
           defer func() {
              if err := recover(); err != nil {
                 fmt.Println(err)
                 fmt.Println("panic恢复")
              }
           }()
           panic(nil)
        }
        -----------------------------------------------------------------------------------------------------
        这种情况panic确实会恢复,但是不会输出任何的错误信息。
        输出
        程序正常退出
        -----------------------------------------------------------------------------------------------------
        总的来说recover函数有几个注意点
        1.必须在defer中使用
        2.多次使用也只会有一个能恢复panic
        3.闭包recover不会恢复外部函数的任何panic
        4.panic的参数禁止使用nil

03.fatal
    fatal是一种极其严重的问题,当发生fatal时,程序需要立刻停止运行,不会执行任何善后工作,通常情况下是调用os包下的Exit函数退出程序,如下所示
    func main() {
        dangerOp("")
    }

    func dangerOp(str string) {
        if len(str) == 0 {
            fmt.Println("fatal")
            os.Exit(1)
        }
        fmt.Println("正常逻辑")
    }
    输出
    fatal
    ---------------------------------------------------------------------------------------------------------
    fatal级别的问题一般很少会显式的去触发,大多数情况都是被动触发。