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", ¤t) // 这里获取的指针是 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/... 软件包。