1 项目开始

1.1 工具清单

00.汇总
    a.小型项目 / API 服务
        a.Web 框架
            Gin 或 Echo
        b.数据库
            MySQL 或 PostgreSQL
        c.ORM/数据访问
            GORM 或 SQLBoiler
        d.配置管理
            Viper
        e.日志
            Zap
        f.API 文档
            Swaggo
        g.认证
            JWT (如果需要)
        h.缓存
            Redis (如果需要)
    b.中大型项目 / 微服务
        a.Web 框架
            Gin, Iris 或 Fiber
        b.微服务框架
            Go-Micro, Kratos(字节跳动), Kitex(字节跳动)
        c.数据库
            a.主数据库
                PostgreSQL
            b.缓存/会话
                Redis
            c.搜索
                Elasticsearch
        d.ORM/数据访问
            GORM, Ent 或 SQLBoiler
        e.配置管理
            Viper + etcd / Consul (分布式配置)
        f.服务发现
            etcd, Consul 或 Kubernetes Service
        g.认证与授权
            JWT + OAuth2/OIDC + Casbin
        h.日志
            Zap + ELK/EFK Stack (集中式日志收集与分析)
        i.消息队列
            Kafka 或 RabbitMQ
        j.监控与告警
            Prometheus + Grafana
        k.链路追踪
            Jaeger 或 SkyWalking
        l.CI/CD
            GitLab CI, GitHub Actions 或 Jenkins
        m.容器编排
            Kubernetes

01.Web 框架 (Web Frameworks)
    a.Gin
        a.特点
            高性能、轻量级、基于 Radix 树的路由。API 设计优雅,易于上手。
        b.生态
            拥有丰富的中间件生态,如日志、恢复、CORS、限流等。
        c.适用场景
            构建 RESTful API、高性能的后端服务。是目前最流行的 Go Web 框架之一。
        d.示例代码
            ---
            package main

            import "github.com/gin-gonic/gin"

            func main() {
                r := gin.Default() // 默认包含 Logger 和 Recovery 中间件
                r.GET("/ping", func(c *gin.Context) {
                    c.JSON(200, gin.H{
                        "message": "pong",
                    })
                })
                r.Run() // 监听并在 0.0.0.0:8080 上启动服务
            }
            ---
    b.Echo
        a.特点
            与 Gin 类似,也是一个高性能、轻量级的框架。以其极简主义和优秀的性能著称。
        b.生态
            中间件系统灵活,支持自定义绑定器和渲染器。
        c.适用场景
            对性能要求极高的 API 服务,适合追求代码简洁性的开发者。
    c.Beego
        a.特点
            一个全栈式的 MVC 框架," batteries-included" 理念,内置了 ORM、日志、配置、模板引擎等几乎所有开发所需组件。
        b.生态
            提供了 bee 命令行工具,用于快速生成项目、热编译等,极大提升开发效率。
        c.适用场景
            快速开发中小型 Web 应用或 API,尤其适合团队中新手较多的情况。
    d.Iris
        a.特点
            功能极其丰富和强大,支持 MVC、WebSocket、GraphQL、HTTP/2、gRPC 等。性能也非常出色。
        b.生态
            自带大量功能,文档齐全,社区活跃。
        c.适用场景
            构建复杂的、功能全面的 Web 应用和 API。
    e.Fiber
        a.特点
            以 Node.js 的 Express.js 为灵感,API 设计非常相似,对 JavaScript 开发者非常友好。基于 fasthttp,性能极高。
        b.生态
            中间件和扩展易于集成。
        c.适用场景
            希望快速迁移 Express.js 经验到 Go 语言的开发者,或需要极致性能的微服务。
    f.Chi
        a.特点
            一个更轻量级、更贴近标准库 net/http 的路由和中间件框架。它不引入过多抽象,保持了高度的灵活性。
        b.生态
            设计简洁,易于与其他标准库兼容的工具集成。
        c.适用场景
            对框架侵入性有顾虑,希望保持代码对标准库兼容性的开发者,或构建小型、简单的服务。

02.数据库 (Databases)
    a.关系型数据库 (SQL)
        a.MySQL / MariaDB
            a.驱动
                github.com/go-sql-driver/mysql
            b.特点
                开源、成熟、社区庞大、文档丰富。适用于绝大多数需要关系型数据模型的场景。
        b.PostgreSQL
            a.驱动
                github.com/lib/pq
            b.特点
                功能强大,标准兼容性高,对 JSON/JSONB 支持极佳,适合存储半结构化数据。在复杂查询、事务和并发处理上表现出色。
        c.SQLite
            a.驱动
                github.com/mattn/go-sqlite3
            b.特点
                嵌入式数据库,零配置,数据存储在单一文件中。非常适合开发、测试、小型应用或作为应用中的本地缓存。
        d.SQL Server
            a.驱动
                github.com/denisenkom/go-mssqldb
            b.特点
                微软的企业级数据库,常用于 Windows 生态和企业内部系统。
    b.非关系型数据库 (NoSQL)
        a.MongoDB
            a.驱动/客户端
                go.mongodb.org/mongo-driver/mongo
            b.特点
                文档型数据库, schema 灵活,易于扩展。适合存储结构多变的数据,如用户画像、内容管理系统等。
        b.Redis
            a.客户端
                github.com/go-redis/redis/v8
            b.特点
                高性能的键值对存储,支持多种数据结构(字符串、哈希、列表、集合等)。常被用作缓存、会话存储、消息队列、计数器等。
        c.Elasticsearch
            a.客户端
                github.com/elastic/go-elasticsearch/v8
            b.特点
                基于 Lucene 的分布式搜索引擎,同时也是一个强大的文档数据库。擅长全文检索、日志分析和数据可视化。
        d.Cassandra
            a.驱动
                github.com/gocql/gocql
            b.特点
                分布式、高可用、宽列存储的数据库。专为处理海量数据和高写入吞吐量而设计。

03.ORM / 数据访问层 (ORM / Data Access Layer)
    a.GORM
        a.特点
            Go 生态中最流行的 ORM 库。功能全面,支持多种数据库,提供了优雅的 API 用于 CRUD、事务、关联查询、迁移等。
        b.示例代码
            ---
            // 定义模型
            type Product struct {
              gorm.Model
              Code  string
              Price uint
            }

            // 创建表
            db.AutoMigrate(&Product{})

            // 创建记录
            db.Create(&Product{Code: "D42", Price: 100})

            // 查询
            var product Product
            db.First(&product, 1) // 根据主键查询第一条记录
            db.First(&product, "code = ?", "D42") // 查询 code 为 D42 的记录
            ---
    b.XORM
        a.特点
            另一个成熟的 ORM 库,性能优秀,API 简洁明了。支持自动生成代码。
    c.Ent
        a.特点
            由 Facebook 开发的现代化 ORM 框架。基于代码生成,提供了完全类型安全的查询 API,非常适合构建复杂的数据模型和关联。学习曲线较陡,但长期维护性好。
    d.SQLBoiler
        a.特点
            一个基于数据库 schema 反向生成 Go 代码的工具。它生成的是类型安全的查询构建器,而非传统意义上的 ORM。性能极高,同时保留了 SQL 的灵活性。
    e.Raw SQL
        a.特点
            直接使用 database/sql 或类似 github.com/jmoiron/sqlx 的库手写 SQL。性能最好,控制力最强,但需要手动处理结果集映射和防止 SQL 注入。

04.配置管理 (Configuration)
    a.Viper
        a.特点
            功能强大且流行的配置解决方案。支持 JSON, YAML, TOML, HCL 等多种格式,能从文件、环境变量、远程配置中心(etcd, Consul)读取配置,并提供默认值和类型转换。
        b.适用场景
            绝大多数需要灵活配置的应用。
    b.Cobra
        a.特点
            用于构建强大的命令行应用。虽然不是专门的配置库,但常与 Viper 结合使用,处理命令行参数和子命令。
    c.go-envconfig
        a.特点
            一个轻量级库,用于将环境变量绑定到结构体上。非常适合在容器化(Docker, Kubernetes)环境中使用,遵循 "12-Factor App" 原则。
    d.dotenv
        a.特点
            加载 .env 文件中的环境变量到程序中。适合本地开发。github.com/joho/godotenv 是一个流行的实现。

05.认证与授权 (Authentication & Authorization)
    a.JWT (JSON Web Tokens)
        a.库
            github.com/golang-jwt/jwt/v5
        b.特点
            一种无状态的认证机制。用户登录后,服务器生成一个包含用户信息的加密 Token 返回给客户端,客户端后续请求携带此 Token 进行身份验证。非常适合前后端分离和微服务架构。
    b.OAuth2 / OIDC
        a.库
            golang.org/x/oauth2
        b.特点
            用于第三方登录(如 Google, GitHub, 微信)的开放标准。OIDC (OpenID Connect) 是基于 OAuth2 的身份层。
    c.Casbin
        a.特点
            一个强大的、通用的权限管理框架。支持多种访问控制模型,如 RBAC (基于角色)、ABAC (基于属性)、ACL (基于访问控制列表) 等。用于解决"谁在什么条件下可以访问什么资源"的问题。

06.日志 (Logging)
    a.Zap
        a.特点
            由 Uber 开发,以高性能和低内存分配著称的结构化日志库。API 设计优秀,支持不同的日志级别和输出格式。
        b.适用场景
            对性能要求高的生产环境。
    b.Logrus
        a.特点
            曾经是 Go 中最流行的日志库。API 友好,扩展性强,支持 hooks。虽然作者已宣布不再积极开发,但在现有项目中仍被广泛使用。
    c.ZeroLog
        a.特点
            追求极致性能和零分配的日志库。API 现代,使用起来感觉很流畅。
    d.go-kit/kit/log
        a.特点
            Go kit 微服务工具集的一部分,强调可组合性、可测试性和结构化日志。

07.缓存 (Caching)
    a.Redis
        a.特点
            如前所述,是最常用的分布式缓存解决方案。支持多种数据结构和过期策略。
    b.Memcached
        a.客户端
            github.com/bradfitz/gomemcache/memcache
        b.特点
            一个简单的分布式内存对象缓存系统。适合存储简单的键值对,性能高。
    c.Ristretto
        a.特点
            一个高性能的内存缓存库,适合单机应用。采用了 TinyLFU 淘汰策略,空间利用率高。
    d.BigCache
        a.特点
            也是一个高性能的内存缓存库,专为大数据量场景优化,减少 GC 压力。

08.消息队列 (Message Queues)
    a.RabbitMQ
        a.客户端
            github.com/streadway/amqp
        b.特点
            功能丰富,支持多种消息模式(如 Direct, Topic, Fanout, Headers),可靠性高。适合复杂的消息路由场景。
    b.Kafka
        a.客户端
            github.com/Shopify/sarama
        b.特点
            分布式流处理平台,高吞吐、高可用、持久化。非常适合处理海量日志、事件流和高并发的消息生产消费。
    c.NATS
        a.特点
            轻量级、高性能、云原生的消息系统。部署和使用简单,适合微服务间的实时通信。

09.API 文档 (API Documentation)
    a.Swagger / OpenAPI
        a.库
            github.com/swaggo/swag 和 github.com/swaggo/gin-swagger (针对 Gin)
        b.特点
            通过在代码注释中定义 API 规范,自动生成 Swagger UI 文档。是目前最主流的 API 文档方案。
    b.Go Doc
        a.特点
            Go 语言内置的文档工具。通过解析代码中的注释生成文档。适合库和服务的内部文档。

10.测试工具 (Testing Tools)
    a.标准库 testing
        Go 内置的测试框架,简洁而强大。
    b.Testify
        github.com/stretchr/testify 提供了断言、模拟(mock)等功能,极大地增强了测试能力。
    c.Ginkgo & Gomega
        github.com/onsi/ginkgo 和 github.com/onsi/gomega 提供了 BDD (行为驱动开发) 风格的测试框架,适合编写更具可读性的集成测试和端到端测试。
    d.Mockery
        github.com/vektra/mockery 用于自动生成接口的 mock 实现,方便进行单元测试。

1.2 框架清单

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协程池
        Gocron:代替crontab
        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.3 语言特性1

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.4 语言特性2

00.汇总
    01.注意 shadow 变量
    02.慎用 init 函数
    03.embed types 优缺点
    04.Functional Options Pattern 传递参数
    05.小心八进制整数
    06.float 的精度问题
    07.slice 相关注意点 slice 相关注意点
    08.注意 range
    09.注意 break 作用域
    10.defer
    11.string 相关
    12.interface 类型返回的非 nil 问题
    13.Error
    14.happens before 保证
    15.Context Values
    16.应多关注 goroutine 何时停止
    17.Channel
    18.string format 带来的 dead lock
    19.错误使用 sync.WaitGroup
    20.不要拷贝 sync 类型
    21.time.After 内存泄露
    22.HTTP body 忘记 Close 导致的泄露
    23.Cache line
    24.关于 False Sharing 造成的性能问题
    25.内存对齐
    26.逃逸分析
    27.byte slice 和 string 的转换优化
    28.容器中的 GOMAXPROCS

01.注意 shadow 变量
    a.示例代码
        在下面这段代码中,声明了一个 client 变量,然后使用 tracing 控制变量的初始化
        可能是因为没有声明 err 的缘故,使用的是 := 进行初始化,那么会导致外层的 client 变量永远是 nil。
        var client *http.Client
        if tracing {
            client, err := createClientWithTracing()
            if err != nil {
                return err
            }
            log.Println(client)
        } else {
            client, err := createDefaultClient()
            if err != nil {
                return err
            }
            log.Println(client)
        }
        这个例子实际上是很容易发生在我们实际的开发中,尤其需要注意
    b.解决方案
        如果是因为 err 没有初始化的缘故,我们在初始化的时候可以这么做:
        var client *http.Client
        var err error
        if tracing {
            client, err = createClientWithTracing()
        } else {
            ...
        }
        if err != nil { // 防止重复代码
            return err
        }
        或者内层的变量声明换一个变量名字,这样就不容易出错了
    c.使用工具分析代码是否有 shadow
        我们也可以使用工具分析代码是否有 shadow,先安装一下工具
        go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow
        -----------------------------------------------------------------------------------------------------
        然后使用 shadow 命令:
        go vet -vettool=C:\Users\luozhiyun\go\bin\shadow.exe .\main.go
        # command-line-arguments
        .\main.go:15:3: declaration of "client" shadows declaration at line 13
        .\main.go:21:3: declaration of "client" shadows declaration at line 13

02.慎用 init 函数
    a.init 函数会在全局变量之后被执行
        init 函数并不是最先被执行的,如果声明了 const 或全局变量,那么 init 函数会在它们之后执行
        ---
        package main
        import "fmt"
        var a = func() int {
            fmt.Println("a")
            return 0
        }()
        func init() {
            fmt.Println("init")
        }
        func main() {
            fmt.Println("main")
        }
        // output
        a
        init
        main
    b.init 初始化按解析的依赖关系顺序执行
        比如 main 包里面有 init 函数,依赖了 redis 包,main 函数执行了 redis 包的 Store 函数,恰好 redis 包里面也有 init 函数,那么执行顺序会是:
        还有一种情况,如果是使用 "import _ foo" 这种方式引入的,也是会先调用 foo 包中的 init 函数。
    c.扰乱单元测试
        比如我们在 init 函数中初始了一个全局的变量,但是单测中并不需要,那么实际上会增加单测得复杂度,比如:
        ---
        var db *sql.DB
        func init(){
            dataSourceName := os.Getenv("MYSQL_DATA_SOURCE_NAME")
            d, err := sql.Open("mysql", dataSourceName)
            if err != nil {
                log.Panic(err)
            }
            db = d
        }
        ---
        在上面这个例子中 init 函数初始化了一个 db 全局变量,那么在单测的时候也会初始化一个这样的变量,但是很多单测其实是很简单的,并不需要依赖这个东西。

03.embed types 优缺点
    a.示例代码
        embed types 指的是我们在 struct 里面定义的匿名的字段,如:
        ---
        type Foo struct {
            Bar
        }
        type Bar struct {
            Baz int
        }
        ---
        那么在上面这个例子中,我们可以通过 Foo.Baz 直接访问到成员变量,当然也可以通过 Foo.Bar.Baz 访问。
    b.优点
        这样在很多时候可以增加我们使用的便捷性,如果没有使用 embed types 那么可能需要很多代码,如下:
        ---
        type Logger struct {
            writeCloser io.WriteCloser
        }
        func (l Logger) Write(p []byte) (int, error) {
            return l.writeCloser.Write(p)
        }
        func (l Logger) Close() error {
            return l.writeCloser.Close()
        }
        func main() {
            l := Logger{writeCloser: os.Stdout}
            _, _ = l.Write([]byte("foo"))
            _ = l.Close()
        }
        ---
        如果使用了 embed types 我们的代码可以变得很简洁:
        ---
        type Logger struct {
            io.WriteCloser
        }
        func main() {
            l := Logger{WriteCloser: os.Stdout}
            _, _ = l.Write([]byte("foo"))
            _ = l.Close()
        }
        ---
    c.缺点
        但是同样它也有缺点,有些字段我们并不想 export ,但是 embed types 可能给我们带出去,例如:
        ---
        type InMem struct {
            sync.Mutex
            m map[string]int
        }
        func New() *InMem {
            return &InMem{m: make(map[string]int)}
        }
        ---
        Mutex 一般并不想 export, 只想在 InMem 自己的函数中使用,如:
        ---
        func (i *InMem) Get(key string) (int, bool) {
            i.Lock()
            v, contains := i.m[key]
            i.Unlock()
            return v, contains
        }
        ---
        但是这么写却可以让拿到 InMem 类型的变量都可以使用它里面的 Lock 方法:
        ---
        m := inmem.New()
        m.Lock() // ??
        ---

04.Functional Options Pattern 传递参数
    a.示例代码
        这种方法在很多 Go 开源库都有看到过使用,比如 zap、GRPC 等。
        它经常用在需要传递和初始化校验参数列表的时候使用,比如我们现在需要初始化一个 HTTP server,里面可能包含了 port、timeout 等等信息,但是参数列表很多,不能直接写在函数上,并且我们要满足灵活配置的要求,毕竟不是每个 server 都需要很多参数。
    b.解决方案
        设置一个不导出的 struct 叫 options,用来存放配置参数; 创建一个类型 type Option func(options *options) error,用这个类型来作为返回值;
        比如我们现在要给 HTTP server 里面设置一个 port 参数,那么我们可以这么声明一个 WithPort 函数,返回 Option 类型的闭包,当这个闭包执行的时候会将 options 的 port 填充进去:
        ---
        type options struct {
            port *int
        }
        type Option func(options *options) error
        func WithPort(port int) Option {
            // 所有的类型校验,赋值,初始化啥的都可以放到这个闭包里面做
            return func(options *options) error {
                if port < 0 {
                    return errors.New("port should be positive")
                }
                options.port = &port
                return nil
            }
        }
        ---
        假如我们现在有一个这样的 Option 函数集,除了上面的 port 以外,还可以填充 timeout 等。然后我们可以利用 NewServer 创建我们的 server:
        ---
        func NewServer(addr string, opts ...Option) (*http.Server, error) {
            var options options
            // 遍历所有的 Option
            for _, opt := range opts {
                // 执行闭包
                err := opt(&options)
                if err != nil {
                    return nil, err
                }
            }
            // 接下来可以填充我们的业务逻辑,比如这里设置默认的 port 等等
            var port int
            if options.port == nil {
                port = defaultHTTPPort
            } else {
                if *options.port == 0 {
                    port = randomPort()
                } else {
                    port = *options.port
                }
            }
            // ...
        }
        ---
        初始化 server:
        ---
        server, err := httplib.NewServer("localhost",
            httplib.WithPort(8080),
            httplib.WithTimeout(time.Second))
        ---
        这样写的话就比较灵活,如果只想生成一个简单的 server,我们的代码可以变得很简单:
        ---
        server, err := httplib.NewServer("localhost")
        ---

05.小心八进制整数
    a.示例代码
        比如下面例子:
        ---
        sum := 100 + 010
        fmt.Println(sum)
        ---
        你以为要输出 110,其实输出的是 108,因为在 Go 中以 0 开头的整数表示八进制。
    b.使用场景
        它经常用在处理 Linux 权限相关的代码上,如下面打开一个文件:
        ---
        file, err := os.OpenFile("foo", os.O_RDONLY, 0644)
        ---
        所以为了可读性,我们在用八进制的时候最好使用 "0o" 的方式表示,比如上面这段代码可以表示为:
        ---
        file, err := os.OpenFile("foo", os.O_RDONLY, 0o644)
        ---

06.float 的精度问题
    a.示例代码
        在 Go 中浮点数表示方式和其他语言一样,都是通过科学计数法表示,float 在存储中分为三部分:
        - 符号位(Sign): 0 代表正,1 代表为负
        - 指数位(Exponent): 用于存储科学计数法中的指数数据,并且采用移位存储
        - 尾数部分(Mantissa):尾数部分
        计算规则我就不在这里展示了,感兴趣的可以自己去查查,我这里说说这种计数法在 Go 里面会有哪些问题。
        ---
        func f1(n int) float64 {
            result := 10_000.
            for i := 0; i < n; i++ {
                result += 1.0001
            }
            return result
        }
        func f2(n int) float64 {
            result := 0.
            for i := 0; i < n; i++ {
                result += 1.0001
            }
            return result + 10_000.
        }
        ---
        在上面这段代码中,我们简单地做了一下加法:
        | n | Exact result | f1 | f2 |
        |---|--------------|----|----|
        | 10 | 10010.001 | 10010.001 | 10010.001 |
        | 1k | 11000.11 | 11000.11 | 11000.11 |
        | 1m | 1.01E+06 | 1.01E+06 | 1.01E+06 |
        可以看到 n 越大,误差就越大,并且 f2 的误差是小于 f1 的。
    b.乘法实验
        对于乘法我们可以做下面的实验:
        ---
        a := 100000.001
        b := 1.0001
        c := 1.0002
        fmt.Println(a * (b + c))
        fmt.Println(a*b + a*c)
        ---
        输出:
        ---
        200030.00200030004
        200030.0020003
        ---
        正确输出应该是 200030.0020003,所以它们实际上都有一定的误差,但是可以看到先乘再加精度丢失会更小。
    c.精确计算浮点
        如果想要准确计算浮点的话,可以尝试 "github.com/shopspring/…" 库,换成这个库我们再来计算一下:
        ---
        a := decimal.NewFromFloat(100000.001)
        b := decimal.NewFromFloat(1.0001)
        c := decimal.NewFromFloat(1.0002)
        fmt.Println(a.Mul(b.Add(c))) // 200030.0020003
        ---

07.slice 相关注意点
    a.区分 slice 的 length 和 capacity
        首先让我们初始化一个带有 length 和 capacity 的 slice:
        ---
        s := make([]int, 3, 6)
        ---
        在 make 函数里面,capacity 是可选的参数。上面这段代码我们创建了一个 length 是 3,capacity 是 6 的 slice,那么底层的数据结构是这样的:
        - slice 的底层实际上指向了一个数组。当然,由于我们的 length 是 3,所以这样设置 s[4] = 0 会 panic 的。需要使用 append 才能添加新元素。
        ---
        panic: runtime error: index out of range [4] with length 3
        ---
        当 append 超过 cap 大小的时候,slice 会自动帮我们扩容,在元素数量小于 1024 的时候每次会扩大一倍,当超过了 1024 个元素每次扩大 25%。
    b.使用 : 操作符创建新切片
        有时候我们会使用 : 操作符从另一个 slice 上面创建一个新切片:
        ---
        s1 := make([]int, 3, 6)
        s2 := s1[1:3]
        ---
        实际上这两个 slice 还是指向了底层同样的数组,构如下:
        - 由于指向了同一个数组,那么当我们改变第一个槽位的时候,比如 s1[1]=2,实际上两个 slice 的数据都会发生改变:
        - 但是当我们使用 append 的时候情况会有所不同:
        ---
        s2 = append(s2, 3)
        fmt.Println(s1) // [0 2 0]
        fmt.Println(s2) // [2 0 3]
        ---
        s1 的 len 并没有被改变,所以看到的还是 3 元素。
        还有一件比较有趣的细节是,如果再接着 append s1 那么第四个元素会被覆盖掉:
        ---
        s1 = append(s1, 4)
        fmt.Println(s1) // [0 2 0 4]
        fmt.Println(s2) // [2 0 4]
        ---
        我们再继续 append s2 直到 s2 发生扩容,这个时候会发现 s2 实际上和 s1 指向的不是同一个数组了:
        ---
        s2 = append(s2, 5, 6, 7)
        fmt.Println(s1) // [0 2 0 4]
        fmt.Println(s2) // [2 0 4 5 6 7]
        ---
    c.append 的意外效果
        除了上面这种情况,还有一种情况 append 会产生意想不到的效果:
        ---
        s1 := []int{1, 2, 3}
        s2 := s1[1:2]
        s3 := append(s2, 10)
        ---
        如果 print 它们应该是这样:
        ---
        s1=[1 2 10], s2=[2], s3=[2 10]
        ---
    d.slice 初始化
        对于 slice 的初始化实际上有很多种方式:
        ---
        func main() {
            var s []string
            log(1, s)
            s = []string(nil)
            log(2, s)
            s = []string{}
            log(3, s)
            s = make([]string, 0)
            log(4, s)
        }
        func log(i int, s []string) {
            fmt.Printf("%d: empty=%t\tnil=%t\n", i, len(s) == 0, s == nil)
        }
        ---
        输出:
        ---
        1: empty=true   nil=true
        2: empty=true   nil=true
        3: empty=true   nil=false
        4: empty=true   nil=false
        ---
        前两种方式会创建一个 nil 的 slice,后两种会进行初始化,并且这些 slice 的大小都为 0。
        对于 var s []string 这种方式来说,好处就是不用做任何的内存分配。比如下面场景可能可以节省一次内存分配:
        ---
        func f() []string {
            var s []string
            if foo() {
                s = append(s, "foo")
            }
            if bar() {
                s = append(s, "bar")
            }
            return s
        }
        ---
        对于 s := []string{} 这种方式来说,它比较适合初始化一个已知元素的 slice:
        ---
        s := []string{"foo", "bar", "baz"}
        ---
        如果没有这个需求其实用 var s []string 比较好,反正在使用的适合都是通过 append 添加元素, var s []string 还能节省一次内存分配。
        如果我们初始化了一个空的 slice, 那么最好是使用 len(xxx) == 0 来判断 slice 是不是空的,如果使用 nil 来判断可能会永远非空的情况,因为对于 s := []string{} 和 s = make([]string, 0) 这两种初始化都是非 nil 的。
        对于 []string(nil) 这种初始化的方式,使用场景很少,一种比较方便地使用场景是用它来进行 slice 的 copy:
        ---
        src := []int{0, 1, 2}
        dst := append([]int(nil), src...)
        ---
    e.copy slice
        使用 copy 函数 copy slice 的时候需要注意,上面这种情况实际上会 copy 失败,因为对 slice 来说是由 length 来控制可用数据,copy 并没有复制这个字段,要想 copy 我们可以这么做:
        ---
        src := []int{0, 1, 2}
        dst := make([]int, len(src))
        copy(dst, src)
        fmt.Println(dst) // [0 1 2]
        ---
        除此之外也可以用上面提到的:
        ---
        src := []int{0, 1, 2}
        dst := append([]int(nil), src...)
        ---
    f.slice capacity 内存释放问题
        先来看个例子:
        ---
        type Foo struct {
            v []byte
        }
        func keepFirstTwoElementsOnly(foos []Foo) []Foo {
            return foos[:2]
        }
        func main() {
            foos := make([]Foo, 1_000)
            printAlloc()
            for i := 0; i < len(foos); i++ {
                foos[i] = Foo{
                    v: make([]byte, 1024*1024),
                }
            }
            printAlloc()
            two := keepFirstTwoElementsOnly(foos)
            runtime.GC()
            printAlloc()
            runtime.KeepAlive(two)
        }
        ---
        上面这个例子中使用 printAlloc 函数来打印内存占用:
        ---
        func printAlloc() {
            var m runtime.MemStats
            runtime.ReadMemStats(&m)
            fmt.Printf("%d KB\n", m.Alloc/1024)
        }
        ---
        上面 foos 初始化了 1000 个容量的 slice ,里面 Foo struct 每个都持有 1M 内存的 slice,然后通过 keepFirstTwoElementsOnly 返回持有前两个元素的 Foo 切片,我们的想法是手动执行 GC 之后其他的 998 个 Foo 会被 GC 销毁,但是输出结果如下:
        ---
        387 KB
        1024315 KB
        1024319 KB
        ---
        实际上并没有,原因就是实际上 keepFirstTwoElementsOnly 返回的 slice 底层持有的数组是和 foos 持有的同一个:
        - 所以我们真的要只返回 slice 的前 2 个元素的话应该这样做:
        ---
        func keepFirstTwoElementsOnly(foos []Foo) []Foo {
            res := make([]Foo, 2)
            copy(res, foos)
            return res
        }
        ---
        不过上面这种方法会初始化一个新的 slice,然后将两个元素 copy 过去。不想进行多余的分配可以这么做:
        ---
        func keepFirstTwoElementsOnly(foos []Foo) []Foo {
            for i := 2; i < len(foos); i++ {
                foos[i].v = nil
            }
            return foos[:2]
        }
        ---

08.注意 range
    a.copy 的问题
        使用 range 的时候如果我们直接修改它返回的数据会不生效,因为返回的数据并不是原始数据:
        ---
        type account struct {
            balance float32
        }
        accounts := []account{
            {balance: 100.},
            {balance: 200.},
            {balance: 300.},
        }
        for _, a := range accounts {
            a.balance += 1000
        }
        ---
        如果像上面这么做,那么输出的 accounts 是:
        ---
        [{100} {200} {300}]
        ---
        所以我们想要改变 range 中的数据可以这么做:
        ---
        for i := range accounts {
            accounts[i].balance += 1000
        }
        ---
        range slice 的话也会 copy 一份:
        ---
        s := []int{0, 1, 2}
        for range s {
            s = append(s, 10)
        }
        ---
        这份代码在 range 的时候会 copy 一份,因此只会调用三次 append 后停止。
    b.指针问题
        比方我们想要 range slice 并将返回值存到 map 里面供后面业务使用,类似这样:
        ---
        type Customer struct {
            ID string
            Balance float64
        }
        test := []Customer{
            {ID: "1", Balance: 10},
            {ID: "2", Balance: -10},
            {ID: "3", Balance: 0},
        }
        var m map[string]*Customer
        for _, customer := range test {
            m[customer.ID] = &customer
        }
        ---
        但是这样遍历 map 里面存的并不是我们想要的,你会发现存的 value 都是最后一个:
        ---
        {"1":{"ID":"3","Balance":0},"2":{"ID":"3","Balance":0},"3":{"ID":"3","Balance":0}}
        ---
        这是因为当我们使用 range 遍历 slice 的时候,返回的 customer 变量实际上是一个固定的地址:
        ---
        for _, customer := range test {
            fmt.Printf("%p\n", &customer) // 我们想要获取这个指针的时候
        }
        ---
        输出:
        ---
        0x1400000e240
        0x1400000e240
        0x1400000e240
        ---
        这是因为迭代器会把数据都放入到 0x1400000e240 这块空间里面:
        - 所以我们可以这样在 range 里面获取指针:
        ---
        for _, customer := range test {
            current := customer // 使用局部变量
            fmt.Printf("%p\n", &current) // 这里获取的指针是 range copy 出来元素的指针
        }
        ---
        或者:
        ---
        for i := range test {
            current := &test[i] // 使用局部变量
            fmt.Printf("%p\n", current)
        }
        ---

09.注意 break 作用域
    a.示例代码
        比方说:
        ---
        for i := 0; i < 5; i++ {
            fmt.Printf("%d ", i)
            switch i {
            default:
            case 2:
                break
            }
        }
        ---
        上面这个代码本来想 break 停止遍历,实际上只是 break 了 switch 作用域,print 依然会打印:0,1,2,3,4。
    b.正确做法
        正确做法应该是通过 label 的方式 break:
        ---
        loop:
        for i := 0; i < 5; i++ {
            fmt.Printf("%d ", i)
            switch i {
            default:
            case 2:
                break loop
            }
        }
        ---
        有时候我们会没注意到自己的错误用法,比如下面:
        ---
        for {
            select {
            case <-ch:
                // Do something
            case <-ctx.Done():
                break
            }
        }
        ---
        上面这种写法会导致只跳出了 select,并没有终止 for 循环,正确写法应该这样:
        ---
        loop:
        for {
            select {
            case <-ch:
                // Do something
            case <-ctx.Done():
                break loop
            }
        }
        ---

10.defer
    a.注意 defer 的调用时机
        有时候我们会像下面一样使用 defer 去关闭一些资源:
        ---
        func readFiles(ch <-chan string) error {
            for path := range ch {
                file, err := os.Open(path)
                if err != nil {
                    return err
                }
                defer file.Close()
                // Do something with file
            }
            return nil
        }
        ---
        因为 defer 会在方法结束的时候调用,但是如果上面的 readFiles 函数永远没有 return,那么 defer 将永远不会被调用,从而造成内存泄露。并且 defer 写在 for 循环里面,编译器也无法做优化,会影响代码执行性能。
        为了避免这种情况,我们可以 wrap 一层:
        ---
        func readFiles(ch <-chan string) error {
            for path := range ch {
                if err := readFile(path); err != nil {
                    return err
                }
            }
            return nil
        }
        func readFile(path string) error {
            file, err := os.Open(path)
            if err != nil {
                return err
            }
            defer file.Close()
            // Do something with file
            return nil
        }
        ---
    b.注意 defer 的参数
        defer 声明时会先计算确定参数的值。
        ---
        func a() {
            i := 0
            defer notice(i) // 0
            i++
            return
        }
        func notice(i int) {
            fmt.Println(i)
        }
        ---
        在这个例子中,变量 i 在 defer 被调用的时候就已经确定了,而不是在 defer 执行的时候,所以上面的语句输出的是 0。
        所以我们想要获取这个变量的真实值,应该用引用:
        ---
        func a() {
            i := 0
            defer notice(&i) // 1
            i++
            return
        }
        ---
    c.defer 下的闭包
        ---
        func a() int {
            i := 0
            defer func() {
                fmt.Println(i + 1) // 12
            }()
            i++
            return i + 10
        }
        func TestA(t *testing.T) {
            fmt.Println(a()) // 11
        }
        ---
        如果换成闭包的话,实际上闭包中对变量 i 是通过指针传递的,所以可以读到真实的值。但是上面的例子中 a 函数返回的是 11 是因为执行顺序是:
        - 先计算(i+10)-> (call defer) -> (return)

11.string 相关
    a.迭代带来的问题
        在 Go 语言中,字符串是一种基本类型,默认是通过 utf8 编码的字符序列,当字符为 ASCII 码时则占用 1 个字节,其他字符根据需要占用 2-4 个字节,比如中文编码通常需要 3 个字节。
        那么我们在做 string 迭代的时候可能会产生意想不到的问题:
        ---
        s := "hêllo"
        for i := range s {
            fmt.Printf("position %d: %c\n", i, s[i])
        }
        fmt.Printf("len=%d\n", len(s))
        ---
        输出:
        ---
        position 0: h
        position 1: Ã
        position 3: l
        position 4: l
        position 5: o
        len=6
        ---
        上面的输出中发现第二个字符是 Ã,不是 ê,并且位置 2 的输出”消失“了,这其实就是因为 ê 在 utf8 里面实际上占用 2 个 byte:
        - shêllo[]byte(s)68c3 aa6c6c6f
        - 所以我们在迭代的时候 s[1] 等于 c3 这个 byte 等价 Ã 这个 utf8 值,所以输出的是 hÃllo 而不是 hêllo。
    b.解决方案
        那么根据上面的分析,我们就可以知道在迭代获取字符的时候不能只获取单个 byte,应该使用 range 返回的 value 值:
        ---
        s := "hêllo"
        for i, v := range s {
            fmt.Printf("position %d: %c\n", i, v)
        }
        ---
        或者我们可以把 string 转成 rune 数组,在 go 中 rune 代表 Unicode 码位,用它可以输出单个字符:
        ---
        s := "hêllo"
        runes := []rune(s)
        for i, _ := range runes {
            fmt.Printf("position %d: %c\n", i, runes[i])
        }
        ---
        输出:
        ---
        position 0: h
        position 1: ê
        position 2: l
        position 3: l
        position 4: o
        ---
    c.截断带来的问题
        在上面我们讲 slice 的时候也提到了,在对 slice 使用 : 操作符进行截断的时候,底层的数组实际上指向同一个,在 string 里面也需要注意这个问题,比如下面:
        ---
        func (s store) handleLog(log string) error {
            if len(log) < 36 {
                return errors.New("log is not correctly formatted")
            }
            uuid := log[:36]
            s.store(uuid)
            // Do something
        }
        ---
        这段代码用了 : 操作符进行截断,但是如果 log 这个对象很大,比如上面的 store 方法把 uuid 一直存在内存里,可能会造成底层的数组一直不释放,从而造成内存泄露。
        为了解决这个问题,我们可以先复制一份再处理:
        ---
        func (s store) handleLog(log string) error {
            if len(log) < 36 {
                return errors.New("log is not correctly formatted")
            }
            uuid := strings.Clone(log[:36]) // copy 一份
            s.store(uuid)
            // Do something
        }
        ---

12.interface 类型返回的非 nil 问题
    a.示例代码
        假如我们想要继承 error 接口实现一个自己的 MultiError:
        ---
        type MultiError struct {
            errs []string
        }
        func (m *MultiError) Add(err error) {
            m.errs = append(m.errs, err.Error())
        }
        func (m *MultiError) Error() string {
            return strings.Join(m.errs, ";")
        }
        ---
        然后在使用的时候返回 error,并且想通过 error 是否为 nil 判断是否有错误:
        ---
        func Validate(age int, name string) error {
            var m *MultiError
            if age < 0 {
                m = &MultiError{}
                m.Add(errors.New("age is negative"))
            }
            if name == "" {
                if m == nil {
                    m = &MultiError{}
                }
                m.Add(errors.New("name is nil"))
            }
            return m
        }
        func Test(t *testing.T) {
            if err := Validate(10, "a"); err != nil {
                t.Errorf("invalid")
            }
        }
        ---
        实际上 Validate 返回的 err 会总是为非 nil 的,也就是上面代码只会输出 invalid:
        ---
        invalid <nil>
        ---

13.Error
    a.error wrap
        对于 err 的 return 我们一般可以这么处理:
        ---
        err := xxx()
        if err != nil {
            return err
        }
        ---
        但是这样处理只是简单地将原始的错误抛出去了,无法知道当前处理的这段程序的上下文信息,这个时候我们可能会自定义个 error 结构体,继承 error 接口:
        ---
        err := xxx()
        if err != nil {
            return XXError{Err: err}
        }
        ---
        然后我们把上下文信息都加到 XXError 中,但是这样虽然可以添加一些上下文信息,但是每次都需要创建一个特定类型的 error 类会变得很麻烦,那么在 1.13 之后,我们可以使用 %w 进行 wrap。
        ---
        if err != nil {
            return fmt.Errorf("xxx failed: %w", err)
        }
        ---
        当然除了上面这种做法以外,我们还可以直接 %v 直接格式化我们的错误信息:
        ---
        if err != nil {
            return fmt.Errorf("xxx failed: %v", err)
        }
        ---
        这样做的缺点就是我们会丢失这个 err 的类型信息,如果不需要这个类型信息,只是想往上抛打印一些日志当然也无所谓。
    b.error Is & As
        因为我们的 error 可以会被 wrap 好几层,那么使用 == 是可能无法判断我们的 error 究竟是不是我们想要的特定的 error,那么可以用 errors.Is:
        ---
        var BaseErr = errors.New("base error")
        func main() {
            err1 := fmt.Errorf("wrap base: %w", BaseErr)
            err2 := fmt.Errorf("wrap err1: %w", err1)
            println(err2 == BaseErr)
            if !errors.Is(err2, BaseErr) {
                panic("err2 is not BaseErr")
            }
            println("err2 is BaseErr")
        }
        ---
        输出:
        ---
        false
        err2 is BaseErr
        ---
        在上面,我们通过 errors.Is 就可以判断出 err2 里面包含了 BaseErr 错误。errors.Is 里面会递归调用 Unwrap 方法拆包装,然后挨个使用 == 判断是否和指定类型的 error 相等。
        errors.As 主要用来做类型判断,原因也是和上面一样,error 被 wrap 之后我们通过 err.(type) 无法直接判断,errors.As 会用 Unwrap 方法拆包装,然后挨个判断类型。使用如下:
        ---
        type TypicalErr struct {
            e string
        }
        func (t TypicalErr) Error() string {
            return t.e
        }
        func main() {
            err := TypicalErr{"typical error"}
            err1 := fmt.Errorf("wrap err: %w", err)
            err2 := fmt.Errorf("wrap err1: %w", err1)
            var e TypicalErr
            if !errors.As(err2, &e) {
                panic("TypicalErr is not on the chain of err2")
            }
            println("TypicalErr is on the chain of err2")
            println(err == e)
        }
        ---
        输出:
        ---
        TypicalErr is on the chain of err2
        true
        ---
    c.处理 defer 中的 error
        比如下面代码,我们如果在调用 Close 的时候报错是没有处理的:
        ---
        func getBalance(db *sql.DB, clientID string) (
            float32, error) {
            rows, err := db.Query(query, clientID)
            if err != nil {
                return 0, err
            }
            defer rows.Close()
            // Use rows
        }
        ---
        那么也许我们可以在 defer 中打印一些 log,但是无法 return,defer 不接受一个 err 类型的返回值:
        ---
        defer func() {
            err := rows.Close()
            if err != nil {
                log.Printf("failed to close rows: %v", err)
            }
            return err // 无法通过编译
        }()
        ---
        那么我们可能想通过默认 err 返回值的方式将 defer 的 error 也返回了:
        ---
        func getBalance(db *sql.DB, clientID string) (balance float32, err error) {
            rows, err = db.Query(query, clientID)
            if err != nil {
                return 0, err
            }
            defer func() {
                err = rows.Close()
            }()
            // Use rows
        }
        ---
        上面代码看起来没问题,那么假如 Query 的时候和 Close 的时候同时发生异常呢?其中有一个 error 会被覆盖,那么我们可以根据自己的需求选择一个打印日志,另一个 error 返回:
        ---
        defer func() {
            closeErr := rows.Close()
            if err != nil {
                if closeErr != nil {
                    log.Printf("failed to close rows: %v", err)
                }
                return
            }
            err = closeErr
        }()
        ---

14.happens before 保证
    a.示例代码
        创建 goroutine 发生先于 goroutine 执行,所以下面这段代码先读一个变量,然后在 goroutine 中写变量不会发生 data race 问题:
        ---
        i := 0
        go func() {
            i++
        }()
        ---
        goroutine 退出没有任何 happen before 保证,例如下面代码会有 data race :
        ---
        i := 0
        go func() {
            i++
        }()
        fmt.Println(i)
        ---
    b.channel 操作
        channel 操作中 send 操作是 happens before receive 操作 :
        ---
        var c = make(chan int, 10)
        var a string
        func f() {
            a = "hello, world"
            c <- 0
        }
        func main() {
            go f()
            <-c
            print(a)
        }
        ---
        上面执行顺序应该是:
        - variable change -> channel send -> channel receive -> variable read
        - 上面能够保证一定输出 "hello, world"。
        close channel 是 happens before receive 操作,所以下面这个例子中也不会有 data race 问题:
        ---
        i := 0
        ch := make(chan struct{})
        go func() {
            <-ch
            fmt.Println(i)
        }()
        i++
        close(ch)
        ---
        在无缓冲的 channel 中 receive 操作是 happens before send 操作的,例如:
        ---
        var c = make(chan int)
        var a string
        func f() {
            a = "hello, world"
            <-c
        }
        func main() {
            go f()
            c <- 0
            print(a)
        }
        ---
        这里同样能保证输出 hello, world。

15.Context Values
    a.示例代码
        在 context 里面我们可以通过 key value 的形式传递一些信息:
        - context.WithValue 是从 parentCtx 创建,所以创建出来的 ctx 既包含了父类的上下文信息,也包含了当前新加的上下文。
        ---
        fmt.Println(ctx.Value("key"))
        ---
        使用的时候可以直接通过 Value 函数输出。那么其实就可以想到,如果 key 相同的话后面的值会覆盖前面的值的,所以在写 key 的时候可以自定义一个非导出的类型作为 key 来保证唯一:
        ---
        package provider
        type key string
        const myCustomKey key = "key"
        func f(ctx context.Context) {
            ctx = context.WithValue(ctx, myCustomKey, "foo")
            // ...
        }
        ---

16.应多关注 goroutine 何时停止
    a.示例代码
        很多同学觉得 goroutine 比较轻量,认为可以随意地启动 goroutine 去执行任何而不会有很大的性能损耗。这个观点基本没错,但是如果在 goroutine 启动之后因为代码问题导致它一直占用,没有停止,数量多了之后可能会造成内存泄漏。
        比如下面的例子:
        ---
        ch := foo()
        go func() {
            for v := range ch {
                // ...
            }
        }()
        ---
        如果在该 goroutine 中的 channel 一直没有关闭,那么这个 goroutine 就不会结束,会一直挂着占用一部分内存。
        还有一种情况是我们的主进程已经停止运行了,但是 goroutine 里面的任务还没结束就被主进程杀掉了,那么这样也可能造成我们的任务执行出问题,比如资源没有释放,抑或是数据还没处理完等等,如下:
        ---
        func main() {
            newWatcher()
            // Run the application
        }
        type watcher struct { /* Some resources */ }
        func newWatcher() {
            w := watcher{}
            go w.watch()
        }
        ---
        上面这段代码就可能出现主进程已经执行 over 了,但是 watch 函数还没跑完的情况,那么其实可以通过设置 stop 函数,让主进程执行完之后执行 stop 函数即可:
        ---
        func main() {
            w := newWatcher()
            defer w.close()
            // Run the application
        }
        func newWatcher() watcher {
            w := watcher{}
            go w.watch()
            return w
        }
        func (w watcher) close() {
            // Close the resources
        }
        ---

17.Channel
    a.select & channel
        select 和 channel 搭配起来往往有意想不到的效果,比如下面:
        ---
        for {
            select {
            case v := <-messageCh:
                fmt.Println(v)
            case <-disconnectCh:
                fmt.Println("disconnection, return")
                return
            }
        }
        ---
        上面代码中接受了 messageCh 和 disconnectCh 两个 channel 的数据,如果我们想先接受 messageCh 的数组再接受 disconnectCh 的数据,那么上面代码会产生 bug ,如:
        ---
        for i := 0; i < 10; i++ {
            messageCh <- i
        }
        disconnectCh <- struct{}{}
        ---
        我们想要上面的 select 先输出完 messageCh 里面的数据,然后再 return,实际上可能会输出:
        ---
        0
        1
        2
        3
        4
        disconnection, return
        ---
        这是因为 select 不像 switch 会依次匹配 case 分支,select 会随机执行下面的 case 分支,所以想要做到先消费 messageCh channel 数据,如果只有单个 goroutine 生产数据可以这样做:
        - 使用无缓冲的 messageCh channel,这样在发送数据的时候会一直等待,直到数据被消费了才会往下走,相当于是个同步模型了(无缓冲的 channel 是 receive happens before send);
        - 在 select 里面使用单个 channel,比如面的 demo 中我们可以定义一种特殊的 tag 来结束 channel,当读到这个特殊的 tag 的时候 return,这样就没必要用两个 channel 了。
        如果有多个 goroutine 生产数据,那么可以这样:
        ---
        for {
            select {
            case v := <-messageCh:
                fmt.Println(v)
            case <-disconnectCh:
                for {
                    select {
                    case v := <-messageCh:
                        fmt.Println(v)
                    default:
                        fmt.Println("disconnection, return")
                        return
                    }
                }
            }
        }
        ---
        在读取 disconnectCh 的时候里面再套一个循环读取 messageCh,读完了之后会调用 default 分支进行 return。
    b.不要使用 nil channel
        使用 nil channel 进行收发数据的时候会永远阻塞,例如发送数据:
        ---
        var ch chan int
        ch <- 0 // block
        ---
        接收数据:
        ---
        var ch chan int
        <-ch // block
        ---
    c.Channel 的 close 问题
        channel 在 close 之后仍然可以接收数据的,例如:
        ---
        ch1 := make(chan int, 1)
        close(ch1)
        for {
            v := <-ch1
            fmt.Println(v)
        }
        ---
        这段代码会一直 print 0。这会导致什么问题呢?比如我们想要将两个 channel 的数据汇集到另一个 channel 中:
        ---
        func merge(ch1, ch2 <-chan int) <-chan int {
            ch := make(chan int, 1)
            go func() {
                for {
                    select {
                    case v := <-ch1:
                        ch <- v
                    case v := <-ch2:
                        ch <- v
                    }
                }
                close(ch) // 永远运行不到
            }()
            return ch
        }
        ---
        由于 channel 被 close 了还可以接收到数据,所以上面代码中,即使 ch1 和 ch2 都被 close 了,也是运行不到 close(ch) 这段代码,并且还一直将 0 推入到 ch channel 中。所以为了感知到 channel 被关闭了,我们应该使用 channel 返回的两个参数:
        ---
        v, open := <-ch1
        fmt.Print(v, open) // open 返回 false 表示没有被关闭
        ---
        那么回到我们上面的例子中,就可以这样做:
        ---
        func merge(ch1, ch2 <-chan int) <-chan int {
            ch := make(chan int, 1)
            ch1Closed := false
            ch2Closed := false
            go func() {
                for {
                    select {
                    case v, open := <-ch1:
                        if !open { // 如果已经关闭
                            ch1Closed = true // 标记为 true
                            break
                        }
                        ch <- v
                    case v, open := <-ch2:
                        if !open { // 如果已经关闭
                            ch2Closed = true // 标记为 true
                            break
                        }
                        ch <- v
                    }
                    if ch1Closed && ch2Closed { // 都关闭了
                        close(ch) // 关闭 ch
                        return
                    }
                }
            }()
            return ch
        }
        ---
        通过两个标记以及返回的 open 变量就可以判断 channel 是否被关闭了,如果都关闭了,那么执行 close(ch)。

18.string format 带来的 dead lock
    a.示例代码
        如果类型定义了 String() 方法,它会被用在 fmt.Printf() 中生成默认的输出:等同于使用格式化描述符 %v 产生的输出。还有 fmt.Print() 和 fmt.Println() 也会自动使用 String() 方法。
        那么我们看看下面的例子:
        ---
        type Customer struct {
            mutex sync.RWMutex
            id string
            age int
        }
        func (c *Customer) UpdateAge(age int) error {
            c.mutex.Lock()
            defer c.mutex.Unlock()
            if age < 0 {
                return fmt.Errorf("age should be positive for customer %v", c)
            }
            c.age = age
            return nil
        }
        func (c *Customer) String() string {
            fmt.Println("enter string method")
            c.mutex.RLock()
            defer c.mutex.RUnlock()
            return fmt.Sprintf("id %s, age %d", c.id, c.age)
        }
        ---
        这个例子中,如果调用 UpdateAge 方法 age 小于 0 会调用 fmt.Errorf,格式化输出,这个时候 String() 方法里面也进行了加锁,那么这样会造成死锁。
        - mutex.Lock -> check age -> Format error -> call String() -> mutex.RLock
    b.解决方法
        解决方法也很简单,一个是缩小锁的范围,在 check age 之后再加锁,另一种方法是 Format error 的时候不要 Format 整个结构体,可以改成 Format id 就行了。

19.错误使用 sync.WaitGroup
    a.示例代码
        sync.WaitGroup 通常用在并发中等待 goroutines 任务完成,用 Add 方法添加计数器,当任务完成后需要调用 Done 方法让计数器减一。等待的线程会调用 Wait 方法等待,直到 sync.WaitGroup 内计数器为零。
        需要注意的是 Add 方法是怎么使用的,如下:
        ---
        wg := sync.WaitGroup{}
        var v uint64
        for i := 0; i < 3; i++ {
            go func() {
                wg.Add(1)
                atomic.AddUint64(&v, 1)
                wg.Done()
            }()
        }
        wg.Wait()
        fmt.Println(v)
        ---
        这样使用可能会导致 v 不一定等于 3,因为在 for 循环里面创建的 3 个 goroutines 不一定比外面的主线程先执行,从而导致在调用 Add 方法之前可能 Wait 方法就执行了,并且恰好 sync.WaitGroup 里面计数器是零,然后就通过了。
    b.正确做法
        正确的做法应该是在创建 goroutines 之前就将要创建多少个 goroutines 通过 Add 方法添加进去。

20.不要拷贝 sync 类型
    a.示例代码
        sync 包里面提供一些并发操作的类型,如 mutex、condition、wait group 等等,这些类型都不应该被拷贝之后使用。
        有时候我们在使用的时候拷贝是很隐秘的,比如下面:
        ---
        type Counter struct {
            mu sync.Mutex
            counters map[string]int
        }
        func (c Counter) Increment(name string) {
            c.mu.Lock()
            defer c.mu.Unlock()
            c.counters[name]++
        }
        func NewCounter() Counter {
            return Counter{counters: map[string]int{}}
        }
        func main() {
            counter := NewCounter()
            go counter.Increment("aa")
            go counter.Increment("bb")
        }
        ---
        receiver 是一个值类型,所以调用 Increment 方法的时候实际上拷贝了一份 Counter 里面的变量。这里我们可以将 receiver 改成一个指针,或者将 sync.Mutex 变量改成指针类型。
    b.注意事项
        所以如果:
        - receiver 是值类型;
        - 函数参数是 sync 包类型;
        - 函数参数的结构体里面包含了 sync 包类型;
        遇到这种情况需要注意检查一下,我们可以借用 go vet 来检测,比如上面如果并发调用了就可以检测出来:
        ---
        » go vet . bear@BEARLUO-MB7
        # github.com/cch123/gogctuner/main
        ./main.go:53:9: Increment passes lock by value: github.com/cch123/gogctuner/main.Counter contains sync.Mutex
        ---

21.time.After 内存泄露
    a.示例代码
        我们用一个简单的例子模拟一下:
        ---
        package main
        import (
            "fmt"
            "time"
        )
        // define a channel
        var chs chan int
        func Get() {
            for {
                select {
                case v := <- chs:
                    fmt.Printf("print:%v\n", v)
                case <- time.After(3 * time.Minute):
                    fmt.Printf("time.After:%v", time.Now().Unix())
                }
            }
        }
        func Put() {
            var i = 0
            for {
                i++
                chs <- i
            }
        }
        func main() {
            chs = make(chan int, 100)
            go Put()
            Get()
        }
        ---
        逻辑很简单就是先往 channel 里面存数据,然后不停地使用 for select case 语法从 channel 里面取数据,为了防止长时间取不到数据,所以在上面加了 time.After 定时器,这里只是简单打印一下。
        然后我没用 pprof 看一下内存占用:
        ---
        $ go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap
        ---
        发现不一会儿 Timer 的内存占用很高了。这是因为在计时器触发之前,垃圾收集器不会回收 Timer,但是在循环里面每次都调用 time.After 都会实例化一个一个新的定时器,并且这个定时器会在激活之后才会被清除。
    b.解决方案
        为了避免这种情况我们可以使用下面代码:
        ---
        func Get() {
            delay := time.NewTimer(3 * time.Minute)
            defer delay.Stop()
            for {
                delay.Reset(3 * time.Minute)
                select {
                case v := <- chs:
                    fmt.Printf("print:%v\n", v)
                case <- delay.C:
                    fmt.Printf("time.After:%v", time.Now().Unix())
                }
            }
        }
        ---

22.HTTP body 忘记 Close 导致的泄露
    a.示例代码
        ---
        type handler struct {
            client http.Client
            url string
        }
        func (h handler) getBody() (string, error) {
            resp, err := h.client.Get(h.url)
            if err != nil {
                return "", err
            }
            body, err := io.ReadAll(resp.Body)
            if err != nil {
                return "", err
            }
            return string(body), nil
        }
        ---
        上面这段代码看起来没什么问题,但是 resp 是 *http.Response 类型,里面包含了 Body io.ReadCloser 对象,它是一个 io 类,必须要正确关闭,否则是会产生资源泄露的。一般我们可以这么做:
        ---
        defer func() {
            err := resp.Body.Close()
            if err != nil {
                log.Printf("failed to close response: %v\n", err)
            }
        }()
        ---

23.Cache line
    a.示例代码
        目前在计算机中,主要有两大存储器 SRAM 和 DRAM。主存储器是由 DRAM 实现的,也就是我们常说的内存,在 CPU 里通常会有 L1、L2、L3 这样三层高速缓存是用 SRAM 实现的。
        当从内存中取单元到 cache 中时,会一次取一个 cacheline 大小的内存区域到 cache 中,然后存进相应的 cacheline 中,所以当你读取一个变量的时候,可能会把它相邻的变量也读取到 CPU 的缓存中(如果正好在一个 cacheline 中),因为有很大的几率你会继续访问相邻的变量,这样 CPU 利用缓存就可以加速对内存的访问。
        cacheline 大小通常有 32 bit,64 bit, 128 bit。拿我电脑的 64 bit 举例:
        ---
        cat /sys/devices/system/cpu/cpu1/cache/index0/coherency_line_size
        64
        ---
        我们设置两个函数,一个 index 加 2,一个 index 加 8:
        ---
        func sum2(s []int64) int64 {
            var total int64
            for i := 0; i < len(s); i += 2 {
                total += s[i]
            }
            return total
        }
        func sum8(s []int64) int64 {
            var total int64
            for i := 0; i < len(s); i += 8 {
                total += s[i]
            }
            return total
        }
        ---
        这看起来 sum8 处理的元素比 sum2 少四倍,那么性能应该也快四倍左右,书上说只快了 10%,但是我没测出来这个数据,无所谓了大家知道因为 cacheline 的存在,并且数据在 L1 缓存里面性能很高就行了。
    b.slice 类型的结构体和结构体里包含 slice
        然后再看看 slice 类型的结构体和结构体里包含 slice:
        ---
        type Foo struct {
            a int64
            b int64
        }
        func sumFoo(foos []Foo) int64 {
            var total int64
            for i := 0; i < len(foos); i++ {
                total += foos[i].a
            }
            return total
        }
        ---
        Foo 里面包含了两个字段 a 和 b, sumFoo 会遍历 Foo slice 将所有 a 字段加起来返回。
        ---
        type Bar struct {
            a []int64
            b []int64
        }
        func sumBar(bar Bar) int64 {
            var total int64
            for i := 0; i < len(bar.a); i++ {
                total += bar.a[i]
            }
            return total
        }
        ---
        Bar 里面是包含了 a,b 两个 slice,sumBar 会将 Bar 里面的 a 的元素和相加返回。我们同样用两个 benchmark 测试一下:
        ---
        func Benchmark_sumBar(b *testing.B) {
            s := Bar{
                a: make([]int64, 16),
                b: make([]int64, 16),
            }
            b.RunParallel(func(pb *testing.PB) {
                for pb.Next() {
                    sumBar(s)
                }
            })
        }
        func Benchmark_sumFoo(b *testing.B) {
            s := make([]Foo, 16)
            b.RunParallel(func(pb *testing.PB) {
                for pb.Next() {
                    sumFoo(s)
                }
            })
        }
        ---
        测试结果:
        ---
        # go test -gcflags "-N -l" -bench .
        Benchmark_sumBar-16 249029368 4.855 ns/op
        Benchmark_sumFoo-16 238571205 5.056 ns/op
        ---
        sumBar 会比 sumFoo 快一点的。这是因为对于 sumFoo 来说要读完整个数据才行,而对于 sumBar 来说只需要读前 16 bytes 读入到 cache line:

24.关于 False Sharing 造成的性能问题
    a.示例代码
        False Sharing 是由于多线程对于同一片内存进行并行读写操作的时候会造成内存缓存失效,而反复将数据载入缓存所造成的性能问题。
        因为现在 CPU 的缓存都是分级的,对于 L1 缓存来说是每个 Core 所独享的,那么就有可能面临缓存数据失效的问题。
        如果同一片数据被多个 Core 同时加载,那么它就是共享状态在共享状态下想要修改数据要先向所有的其他 CPU 核心广播一个请求,要求先把其他 CPU 核心里面的 cache ,都变成无效的状态,然后再更新当前 cache 里面的数据。
        CPU 核心里面的 cache 变成无效之后就不能使用了,需要重新加载,因为不同级别的缓存的速度是差异很大的,所以这其实性能影响还蛮大的,我们写个测试看看。
        ---
        type MyAtomic interface {
            IncreaseAllEles()
        }
        type Pad struct {
            a uint64
            _p1 [15]uint64
            b uint64
            _p2 [15]uint64
            c uint64
            _p3 [15]uint64
        }
        func (myatomic *Pad) IncreaseAllEles() {
            atomic.AddUint64(&myatomic.a, 1)
            atomic.AddUint64(&myatomic.b, 1)
            atomic.AddUint64(&myatomic.c, 1)
        }
        type NoPad struct {
            a uint64
            b uint64
            c uint64
        }
        func (myatomic *NoPad) IncreaseAllEles() {
            atomic.AddUint64(&myatomic.a, 1)
            atomic.AddUint64(&myatomic.b, 1)
            atomic.AddUint64(&myatomic.c, 1)
        }
        ---
        这里我定义了两个结构体 Pad 和 NoPad。然后我们定义一个 benchmark 进行多线程测试:
        ---
        func testAtomicIncrease(myatomic MyAtomic) {
            paraNum := 1000
            addTimes := 1000
            var wg sync.WaitGroup
            wg.Add(paraNum)
            for i := 0; i < paraNum; i++ {
                go func() {
                    for j := 0; j < addTimes; j++ {
                        myatomic.IncreaseAllEles()
                    }
                    wg.Done()
                }()
            }
            wg.Wait()
        }
        func BenchmarkNoPad(b *testing.B) {
            myatomic := &NoPad{}
            b.ResetTimer()
            testAtomicIncrease(myatomic)
        }
        func BenchmarkPad(b *testing.B) {
            myatomic := &Pad{}
            b.ResetTimer()
            testAtomicIncrease(myatomic)
        }
        ---
        结果可以看到快了 40% 左右:
        ---
        BenchmarkNoPad
        BenchmarkNoPad-10      1000000000           0.1360 ns/op
        BenchmarkPad
        BenchmarkPad-10        1000000000           0.08887 ns/op
        ---
        如果没有 pad 话,变量数据都会在一条 cache line 里面,这样如果其中一个线程修改了数据会导致另一个线程的 cache line 无效,需要重新加载:
        - 加了 padding 之后数据都不在同一个 cache line 上了,即使发生了修改 invalid 不是同一行数据也不需要重新加载。

25.内存对齐
    a.示例代码
        简而言之,现在的 CPU 访问内存的时候是一次性访问多个 bytes,比如 64 位架构一次访问 8bytes ,该处理器只能从地址为 8 的倍数的内存开始读取数据,所以要求数据在存放的时候首地址的值是 8 的倍数存放,这就是所谓的内存对齐。
        比如下面的例子中因为内存对齐的存在,所以下面的例子中 b 这个字段只能在后面另外找地址为 8 的倍数地址开始存放:
        - 除此之外还有一个零大小字段对齐的问题,如果结构体或数组类型不包含大小大于零的字段或元素,那么它的大小就为 0。比如 x [0]int8, 空结构体 struct{} 。当它作为字段时不需要对齐,但是作为结构体最后一个字段时需要对齐。我们拿空结构体来举个例子:
        ---
        type M struct {
            m int64
            x struct{}
        }
        type N struct {
            x struct{}
            n int64
        }
        func main() {
            m := M{}
            n := N{}
            fmt.Printf("as final field size:%d\nnot as final field size:%d\n", unsafe.Sizeof(m), unsafe.Sizeof(n))
        }
        ---
        输出:
        ---
        as final field size:16
        not as final field size:8
        ---
    b.注意事项
        当然,我们不可能手动去调整内存对齐,我们可以通过使用工具 fieldalignment:
        ---
        $ go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
        $ fieldalignment -fix .\main\my.go
        main\my.go:13:9: struct of size 24 could be 16
        ---

26.逃逸分析
    a.示例代码
        Go 是通过在编译器里做逃逸分析(escape analysis)来决定一个对象放栈上还是放堆上,不逃逸的对象放栈上,可能逃逸的放堆上。对于 Go 来说,我们可以通过下面指令来看变量是否逃逸:
        ---
        go run -gcflags '-m -l' main.go
        ---
        - -m 会打印出逃逸分析的优化策略,实际上最多总共可以用 4 个 -m,但是信息量较大,一般用 1 个就可以了。
        - -l 会禁用函数内联,在这里禁用掉内联能更好的观察逃逸情况,减少干扰。
    b.指针逃逸
        在函数中创建了一个对象,返回了这个对象的指针。这种情况下,函数虽然退出了,但是因为指针的存在,对象的内存不能随着函数结束而回收,因此只能分配在堆上。
        ---
        type Demo struct {
            name string
        }
        func createDemo(name string) *Demo {
            d := new(Demo) // 局部变量 d 逃逸到堆
            d.name = name
            return d
        }
        func main() {
            demo := createDemo("demo")
            fmt.Println(demo)
        }
        ---
        我们检测一下:
        ---
        go run -gcflags '-m -l'  .\main\main.go
        # command-line-arguments
        main\main.go:12:17: leaking param: name
        main\main.go:13:10: new(Demo) escapes to heap
        main\main.go:20:13: ... argument does not escape&{demo}
        ---
    c.interface{}/any 动态类型逃逸
        因为编译期间很难确定其参数的具体类型,也会发生逃逸,例如这样:
        ---
        func createDemo(name string) any {
            d := new(Demo) // 局部变量 d 逃逸到堆
            d.name = name
            return d
        }
        ---
    d.切片长度或容量没指定逃逸
        如果使用局部切片时,已知切片的长度或容量,请使用常量或数值字面量来定义,否则也会逃逸:
        ---
        func main() {
            number := 10
            s1 := make([]int, 0, number)
            for i := 0; i < number; i++ {
                s1 = append(s1, i)
            }
            s2 := make([]int, 0, 10)
            for i := 0; i < 10; i++ {
                s2 = append(s2, i)
            }
        }
        ---
        输出一下:
        ---
        go run -gcflags '-m -l'  main.go
        ./main.go:65:12: make([]int, 0, number) escapes to heap
        ./main.go:69:12: make([]int, 0, 10) does not escape
        ---
    e.闭包
        例如下面:Increase() 返回值是一个闭包函数,该闭包函数访问了外部变量 n,那变量 n 将会一直存在,直到 in 被销毁。很显然,变量 n 占用的内存不能随着函数 Increase() 的退出而回收,因此将会逃逸到堆上。
        ---
        func Increase() func() int {
            n := 0
            return func() int {
                n++
                return n
            }
        }
        func main() {
            in := Increase()
            fmt.Println(in()) // 1
            fmt.Println(in()) // 2
        }
        ---
        输出:
        ---
        go run -gcflags '-m -l'  main.go
        ./main.go:64:5: moved to heap: n
        ./main.go:65:12: func literal escapes to heap
        ---

27.byte slice 和 string 的转换优化
    a.示例代码
        直接通过强转 string(bytes) 或者 []byte(str) 会带来数据的复制,性能不佳,所以在追求极致性能场景使用 unsafe 包的方式直接进行转换来提升性能:
        ---
        // toBytes performs unholy acts to avoid allocations
        func toBytes(s string) []byte {
            return *(*[]byte)(unsafe.Pointer(&s))
        }
        // toString performs unholy acts to avoid allocations
        func toString(b []byte) string {
            return *(*string)(unsafe.Pointer(&b))
        }
        ---
    b.注意事项
        在 Go 1.12 中,增加了几个方法 String、StringData、Slice 和 SliceData,用来做这种性能转换。

28.容器中的 GOMAXPROCS
    a.说明
        自 Go 1.5 开始, Go 的 GOMAXPROCS 默认值已经设置为 CPU 的核数
        但是在 Docker 或 k8s 容器中 runtime.GOMAXPROCS() 获取的是 宿主机的 CPU 核数
        这样会导致 P 值设置过大,导致生成线程过多,会增加上下文切换的负担,导致严重的上下文切换,浪费 CPU
        所以可以使用 uber 的 automaxprocs 库,大致原理是读取 CGroup 值识别容器的 CPU quota
        计算得到实际核心数,并自动设置 GOMAXPROCS 线程数量
    b.代码
        import _ "go.uber.org/automaxprocs"
        func main() {
            // Your application logic here

1.5 版本特性

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/... 软件包。