|
前言
整理了一份简要的手册,帮助大家高效的上手Go语言,主要是通过对比PHP和Go的不同点来强化理解,内容主要分为以下四部分:
语言层面差异
备注:下文基于PHP主流php-fpm模式。 对比项 | PHP | Go | 字符串表示 | 单引号(PSR) | 双引号 | 拼接字符串 | . | + | 语言版本兼容性 | 不好 | 向下兼容 | 代码风格 | 无官方标准,社区标准起步晚 | 自始至今官方统一标准,且提供工具 | 脚本语言 | 是 | 不是 | 强类型语言 | 不是(PHP7支持严格模式) | 是 | 是否支持垃圾回收 | 是 | 是 | 面向对象语言(OOP) | 神似 | 部分支持,核心是合成复用 | 是否支持继承 | 是 | 否(有合成复用) | 是否支持interface | 是 | 是 | 是否支持try...catch... | 是 | 否 | 是否支持包管理 | 是 | 是 | 是否支持跨平台 | 是 | 是 | 环境搭建成本 | 高 | 低 | 执行方式 | cli命令行模式、php-fpm模式(①) | 二进制 | 进程模型 | 多进程 | 单进程 | 原生是否支持创建TCP/UDP服务 | 是(支持不好,生产不可用) | 是 | 原生是否支持创建HTTP服务 | 是(支持不好,生产不可用) | 是 | 进程阻塞性 | 是 | 否 | 是否支持协程 | 否(②) | 是 | 并发能力(③) | 弱 | 极强 | 是否常驻内存运行 | 不是(④) | 是 | 引入文件方式 | require或者include对应文件 | import导入包 | 是否支持单元测试 | 是 | 是 | 是否支持基准测试(benchmark) | 否 | 是 | 是否支持性能分析 | 支持(xhprof/tideways) | 支持(pprof/dlv) | 性能分析工具使用成本 | 高(装扩展成本高) | 极低 | ①其他模式还有swoole等
②PHP的swoole协程框架等支持协程
③此处不考虑I/O多路复用,PHP的swoole协程框架等也支持协程并发
④PHP的swoole协程框架是常驻内存,cli命令行模式也可以常驻内存等 刚开始由PHP语言转Go语言的过程,重点是编程意识的转变,尤其是以下几点:
- 强类型
- 常驻内存运行
- 理解和使用指针
- 并发安全
- 资源及时释放或返还
基础语法差异
备注:下文基于PHP5.4+版本 常用基本类型对比
PHP类型比较少和简单,PHP常用数据类型有boolean布尔值、string字符串、int整型、float浮点型、array数组、object对象。
PHP常用数据类型和Go语言对应或者类似的类型做个对比,如下:
语言\类型 | boolean | string | int | float | array | object | PHP | bool | string | int | float | array(1,2,3)索引数组、array('1' => 1, '2' => 2, '3' => 3)关联数组 | 实例化类class | Go | bool | string | int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64 | float32、float64 | [length]type | 比较像struct | 除此之外Go还支持更丰富的类型:
类型 | slice切片(相当于PHP的索引数组) | map(相当于PHP的关联数组) | channel(管道,通过通信共享,不要通过共享来通信) | 指针(Go语言的值类型都有对应的指针类型) | byte(字节,对应uint8别名,可以表示Ascaii码) | rune(对应int32,可以表示unicode) | 等等 | 自定义类型,例如type userDefinedType int32 | 常用基本类型初始化方式对比
类型 | PHP | Go(定义变量带var关键字,或者不带直接使用语法糖:=) | boolean | $varStr = true; | var varStr bool = true
或者 var varStr = true
或者 varStr := true | string | $varStr = 'demo'; | var varStr string = ""
或者 varStr := ""(:=写法下面省略) | int32 | $varNum = 0; | var varInt32 int32 = 0 | int64 | 同上 | var varInt64 int64 = 0 | float32 | $varNum = 0.01; | var varFloat32 float32 = 0 | float64 | 同上 | var varFloat64 float64 = 0 | array | $varArray = array();
或者语法糖$varArray = []; | var varArray [6]int32 = [6]int32{} | slice(切片) | 同上,PHP叫索引数据 | var varSlice []int32 = []int32{}切片相对于数据会自动扩容 | map | $varMap = array('key' => 'value'); | var varMap map[string]int32 = map[string]int32{} | closure(闭包) | $varClosure = function() {}; | var varClosure func() = func() {} | channel | 无 | var varChannel chan string = make(chan string) 无缓存channel;
var varChannelBuffer chan string = make(chan string, 6)有缓存channel | PHP类的实例化和Go结构体的初始化的对比
PHP类的实例化
/*
定义class
*/
class ClassDemo {
// 私有属性
private $privateVar = "";
// 公有属性
public $publicVar = "";
// 构造函数
public function __construct()
{
// 实例化类时执行
}
// 私有方法
private function privateFun()
{
}
// 公有方法
public function publicFun()
{
}
}
// 实例化类ClassDemo 获取类ClassDemo的对象
$varObject = new ClassDemo(); // 对象(类)
Go结构体的初始化
// 包初始化时执行
func init() {
}
type StructDemo struct{
// 小写开头驼峰表示私有属性
// 不可导出
privateVar string
// 大写开头驼峰表示公有属性
// 可导出
PublicVar string
}
// 小写开头驼峰表示私有方法
// 结构体StructDemo的私有方法
func (demo *StructDemo) privateFun() error {
return nil
}
// 大写开头驼峰表示公有属性
// 结构体StructDemo的公有方法
func (demo *StructDemo) PublicFun() error {
return nil
}
// 初始化结构体StructDemo
// structDemo := &StructDemo{}
常用函数对比
常用函数描述 | PHP | Go | 数组长度 | count() | len() | 分割字符串为数组 | explode() | strings.Split(s string, sep string) []string | 转大写 | strtoupper() | strings.ToUpper(s string) string | 转小写 | strtolower() | strings.ToLower(s string) string | 去除空格 | trim() | strings.Trim(s, cutset string) string | json序列化 | json_encode() | json.Marshal(v interface{}) ([]byte, error) | json反序列化 | json_decode() | json.Unmarshal(data []byte, v interface{}) error | 序列化(不再建议使用) | serialize()、unserialize() | 包https://github.com/wulijun/go-php-serialize | md5 | md5() | 包crypto/md5 | 终端输出 | echo、var_dump等 | fmt.Println(a ...interface{}) | 各种类型互转 | intval()等 | 包strconv | 避坑指南
- 谨慎使用全局变量,全局变量不会像PHP一样,在完成一次请求之后被销毁
- 形参是slice、map类型的参数,注意值可被全局修改
- 资源使用完毕,记得释放资源或回收资源
- 不要依赖map遍历的顺序
- 不要并发写map
- 注意判断指针类型不为空nil,再操作
- Go语言不支持继承,但是有合成复用
1.谨慎使用全局变量,全局变量不会像PHP一样,在完成一次请求之后被销毁
package main
import (
"github.com/gin-gonic/gin"
)
// 全局变量不会像PHP一样,在完成一次请求之后被销毁
var GlobalVarDemo int32 = 0
// 模拟接口逻辑
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
GlobalVarDemo++
c.JSON(200, gin.H{
"message": GlobalVarDemo,
})
})
r.Run()
}
// 我们多次请求接口,可以很明显发现:全局变量不会像PHP一样,在完成一次请求之后被销毁。
// 但是PHP不一样,全局变量在完成一次请求之后会被自动销毁。
// curl "127.0.0.1:8080/ping"
// {"message":1}
// curl "127.0.0.1:8080/ping"
// {&#34;message&#34;:2} <------- 值在递增
// curl &#34;127.0.0.1:8080/ping&#34;
// {&#34;message&#34;:3} <------- 值在递增
2.形参是slice、map类型的参数,注意值可被全局修改
类似PHP的引用传递,Go里面都是值传递,具体原因下面说。 // 切片
package main
import &#34;fmt&#34;
func main() {
paramDemo := []int32{1}
fmt.Printf(&#34;main.paramDemo 1 %v, pointer: %p \n&#34;, paramDemo, &paramDemo)
// 浅拷贝
demo(paramDemo)
fmt.Printf(&#34;main.paramDemo 2 %v, pointer: %p \n&#34;, paramDemo, &paramDemo)
}
func demo(paramDemo []int32) ([]int32, error) {
fmt.Printf(&#34;main.demo.paramDemo pointer: %p \n&#34;, &paramDemo)
paramDemo[0] = 2
return paramDemo, nil
}
// main.paramDemo 1 [1], pointer: 0xc00000c048
// main.demo.paramDemo pointer: 0xc00000c078 <------- 内存地址不一样,发生了值拷贝
// main.paramDemo 2 [2] <------- 原值被修改
// main.paramDemo 1 [1], pointer: 0xc0000a6030
// main.demo.paramDemo pointer: 0xc0000a6060 <------- 内存地址不一样,发生了值拷贝
// main.paramDemo 2 [2], pointer: 0xc0000a6030 <------- 原值还是被修改了
//===========数组就没有这个问题===========
package main
import &#34;fmt&#34;
func main() {
paramDemo := [1]int32{1}
fmt.Println(&#34;main.paramDemo 1&#34;, paramDemo)
demo(paramDemo)
fmt.Println(&#34;main.paramDemo 2&#34;, paramDemo)
}
func demo(paramDemo [1]int32) ([1]int32, error) {
paramDemo[0] = 2
return paramDemo, nil
}
// [Running] go run &#34;.../demo/main.go&#34;
// main.paramDemo 1 [1]
// main.paramDemo 2 [1] <------- 值未被修改
//===========Map同样有这个问题===========
package main
import &#34;fmt&#34;
func main() {
paramDemo := map[string]string{
&#34;a&#34;: &#34;a&#34;,
}
fmt.Println(&#34;main.paramDemo 1&#34;, paramDemo)
demo(paramDemo)
fmt.Println(&#34;main.paramDemo 2&#34;, paramDemo)
}
func demo(paramDemo map[string]string) (map[string]string, error) {
paramDemo[&#34;a&#34;] = &#34;b&#34;
return paramDemo, nil
}
// [Running] go run &#34;.../demo/main.go&#34;
// main.paramDemo 1 map[a:a]
// main.paramDemo 2 map[a:b] <------- 值被修改
为什么? 答:Go语言都是值传递,浅复制过程,slice和map底层的类型是个结构体,实际存储值的类型是个指针。
// versions/1.13.8/src/runtime/slice.go
// slice源码结构体
type slice struct {
array unsafe.Pointer // 实际存储值的类型是个指针
len int
cap int
}
// versions/1.13.8/src/runtime/map.go
// map源码结构体
type hmap struct {
count int
flags uint8
B uint8
noverflow uint16
hash0 uint32
buckets unsafe.Pointer // 实际存储值的类型是个指针
oldbuckets unsafe.Pointer
nevacuate uintptr
extra *mapextra
}
怎么办? 答:深拷贝,开辟一块新内存,指针指向新内存地址,并把原有的值复制过去。如下:
package main
import &#34;fmt&#34;
func main() {
paramDemo := []int32{1}
fmt.Println(&#34;main.paramDemo 1&#34;, paramDemo)
// 初始化新空间
paramDemoCopy := make([]int32, len(paramDemo))
// 深拷贝
copy(paramDemoCopy, paramDemo)
demo(paramDemoCopy)
fmt.Println(&#34;main.paramDemo 2&#34;, paramDemo)
}
func demo(paramDemo []int32) ([]int32, error) {
paramDemo[0] = 2
return paramDemo, nil
}
// [Running] go run &#34;.../demo/main.go&#34;
// main.paramDemo 1 [1]
// main.paramDemo 2 [1]
3.资源使用完毕,记得释放资源或回收资源
package main
import (
&#34;github.com/gomodule/redigo/redis&#34;
)
var RedisPool *redis.Pool
func init() {
RedisPool = NewRedisPool()
}
func main() {
redisConn := RedisPool.Get()
// 记得defer释放资源
defer redisConn.Close()
}
func NewRedisPool() *redis.Pool {
// 略...
return &redis.Pool{}
}
为什么? 答:避免资源被无效的持有,浪费资源和增加了资源的连接数。其次如果是归还连接池也减少新建资源的开销。
- 资源连接数线性增长
- 如果一直持有,资源服务端也有超时时间
4.不要依赖map遍历的顺序
以往PHP的”Map“(关联数组)不管遍历多少次,元素的顺序都是稳定不变的,如下:
<?php
$demoMap = array(
&#39;a&#39; => &#39;a&#39;,
&#39;b&#39; => &#39;b&#39;,
&#39;c&#39; => &#39;c&#39;,
&#39;d&#39; => &#39;d&#39;,
&#39;e&#39; => &#39;e&#39;,
);
foreach ($demoMap as $v) {
var_dump(&#34;v {$v}&#34;);
}
// 第一次执行
[Running] php &#34;.../php/demo.php&#34;
string(3) &#34;v a&#34;
string(3) &#34;v b&#34;
string(3) &#34;v c&#34;
string(3) &#34;v d&#34;
string(3) &#34;v e&#34;
// 第N次执行
// 遍历结果的顺序都是稳定不变的
[Running] php &#34;.../php/demo.php&#34;
string(3) &#34;v a&#34;
string(3) &#34;v b&#34;
string(3) &#34;v c&#34;
string(3) &#34;v d&#34;
string(3) &#34;v e&#34;
但是Go语言里就不一样了,如下:
package main
import &#34;fmt&#34;
func main() {
var demoMap map[string]string = map[string]string{
&#34;a&#34;: &#34;a&#34;,
&#34;b&#34;: &#34;b&#34;,
&#34;c&#34;: &#34;c&#34;,
&#34;d&#34;: &#34;d&#34;,
&#34;e&#34;: &#34;e&#34;,
}
for _, v := range demoMap {
fmt.Println(&#34;v&#34;, v)
}
}
// 第一次执行
// [Running] go run &#34;.../demo/main.go&#34;
// v a
// v b
// v c
// v d
// v e
// 第二次执行
// 遍历结果,元素顺序发生了改变
// [Running] go run &#34;.../demo/main.go&#34;
// v e
// v a
// v b
// v c
// v d
为什么? 答:底层实现都是数组+类似拉链法。
1. hash函数无序写入
2. 成倍扩容
3. 等量扩容
都决定了map本来就是无序的,所以Go语言为了避免开发者依赖元素顺序,每次遍历的时候都是随机了一个索引起始值。然后PHP通过额外的内存空间维护了map元素的顺序。
5.不要并发写map
package main
import (
&#34;testing&#34;
)
func BenchmarkDemo(b *testing.B) {
var demoMap map[string]string = map[string]string{
&#34;a&#34;: &#34;a&#34;,
&#34;b&#34;: &#34;b&#34;,
}
// 模拟并发写map
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
demoMap[&#34;a&#34;] = &#34;aa&#34;
}
})
}
// BenchmarkDemo
// fatal error: concurrent map writes
// fatal error: concurrent map writes
为什么? 答:并发不安全,触发panic:“fatal error: concurrent map writes”。
// go version 1.13.8源码
// hashWriting 值为 4
if h.flags&hashWriting != 0 {
throw(&#34;concurrent map read and map write&#34;)
}
6.注意判断指针类型不为空nil,再操作
package main
import (
&#34;fmt&#34;
&#34;log&#34;
&#34;net/http&#34;
)
func main() {
resp, err := http.Get(&#34;https://www.example.com&#34;)
if resp.StatusCode != http.StatusOK || err != nil {
// 当 resp为nil时 会触发panic
// 当 resp.StatusCode != http.StatusOK 时err可能为nil 触发panic
log.Printf(&#34;err: %s&#34;, err.Error())
}
}
// [Running] go run &#34;.../demo/main.go&#34;
// panic: runtime error: invalid memory address or nil pointer dereference
package main
import (
&#34;fmt&#34;
&#34;log&#34;
&#34;net/http&#34;
)
func main() {
// 模拟请求业务code
resp, err := http.Get(&#34;https://www.example.com&#34;)
fmt.Println(resp, err)
if err != nil {
// 报错并记录异常日志
log.Printf(&#34;err: %s&#34;, err.Error())
return
}
// 模拟业务code不为成功的code
if resp != nil && resp.StatusCode != http.StatusOK {
// 报错并记录异常日志
}
}
7. Go语言不支持继承,但是有合成复用
abstract class AbstractClassDemo {
// 抽象方法
abstract public function demoFun();
// 公有方法
public function publicFun()
{
$this->demoFun();
}
}
class ClassDemo extends AbstractClassDemo {
public function demoFun()
{
var_dump(&#34;Demo&#34;);
}
}
(new ClassDemo())->demoFun();
// [Running] php &#34;.../php/demo.php&#34;
// string(4) &#34;Demo&#34;
package main
import (
&#34;fmt&#34;
)
//基础结构体
type Base struct {
}
// Base的DemoFun
func (b *Base) DemoFun() {
fmt.Println(&#34;Base&#34;)
}
func (b *Base) PublicFun() {
b.DemoFun()
}
type Demo struct {
// 合成复用Base
Base
}
// Demo的DemoFun
func (d *Demo) DemoFun() {
fmt.Println(&#34;Demo&#34;)
}
func main() {
// 执行
(&Demo{}).PublicFun()
}
// [Running] go run &#34;.../demo/main.go&#34;
// Base <------ 注意此处执行的是被合成复用的结构体的方法
进阶使用
- 热加载工具bee
- Goroutine并发控制之sync.WaitGroup包的使用
- 子Goroutine超时控制之context.Context包的使用
- 并发安全的map之sync.Map包的使用
- 减少GC压力之sync.Pool包的使用
- 减少缓存穿透利器之singleflight包的使用
- Channel的使用
- 单元测试&基准测试
- 性能分析
1.热加载工具bee
作用:以热加载方式运行Go代码,会监视代码的变动重新运行代码,提高开发效率。 使用:
安装
go get github.com/beego/bee/v2
热加载方式启动项目
SOAAGENT=10.40.24.126 bee run -main=main.go -runargs=&#34;start&#34;
2.Goroutine并发控制之sync.WaitGroup包的使用
作用:Goroutine可以等待,直到当前Goroutine派生的子Goroutine执行完成。 使用:
package main
import (
&#34;fmt&#34;
&#34;sync&#34;
&#34;time&#34;
)
func main() {
wg := &sync.WaitGroup{}
wg.Add(1)
go func(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println(&#34;子a 开始执行&#34;)
time.Sleep(5 * time.Second)
fmt.Println(&#34;子a 执行完毕&#34;)
}(wg)
wg.Add(1)
go func(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println(&#34;子b 开始执行&#34;)
time.Sleep(5 * time.Second)
fmt.Println(&#34;子b 执行完毕&#34;)
}(wg)
wg.Add(1)
go func(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println(&#34;子c 开始执行&#34;)
time.Sleep(5 * time.Second)
fmt.Println(&#34;子c 执行完毕&#34;)
}(wg)
fmt.Println(&#34;主 等待&#34;)
wg.Wait()
fmt.Println(&#34;主 退出&#34;)
}
// 第一次执行
// [Running] go run &#34;.../demo/main.go&#34;
// 子a 开始执行
// 子c 开始执行
// 子b 开始执行
// 主 等待 <------ 注意这里和下面打印的位置不一样,因为当前代码并发执行是没有保障执行顺序的
// 子b 执行完毕
// 子a 执行完毕
// 子c 执行完毕
// 主 退出
// 第一次执行
// [Running] go run &#34;.../demo/main.go&#34;
// 主 等待 <------ 注意这里和上面打印的位置不一样,因为当前代码并发执行是没有保障执行顺序的
// 子a 开始执行
// 子c 开始执行
// 子b 开始执行
// 子b 执行完毕
// 子c 执行完毕
// 子a 执行完毕
// 主 退出 <------ 主Goroutine一直等待直到子Goroutine都执行完毕
3.子Goroutine超时控制之context.Context包的使用
作用:Go语言第一形参通常都为context.Context类型,1. 传递上下文 2. 控制子Goroutine超时退出 3. 控制子Goroutine定时退出 使用:
package main
import (
&#34;context&#34;
&#34;fmt&#34;
&#34;time&#34;
)
func main() {
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
go func(ctx context.Context) {
execResult := make(chan bool)
// 模拟业务逻辑
go func(execResult chan<- bool) {
// 模拟处理超时
time.Sleep(6 * time.Second)
execResult <- true
}(execResult)
// 等待结果
select {
case <-ctx.Done():
fmt.Println(&#34;超时退出&#34;)
return
case <-execResult:
fmt.Println(&#34;处理完成&#34;)
return
}
}(ctx)
time.Sleep(10 * time.Second)
}
// [Running] go run &#34;.../demo/main.go&#34;
// 超时退出
4.并发安全的map之sync.Map包的使用
作用:并发安全的map,支持并发写。读多写少场景的性能好。 使用:
package main
import (
&#34;sync&#34;
&#34;testing&#34;
)
func BenchmarkDemo(b *testing.B) {
demoMap := &sync.Map{}
demoMap.Store(&#34;a&#34;, &#34;a&#34;)
demoMap.Store(&#34;b&#34;, &#34;b&#34;)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
demoMap.Store(&#34;a&#34;, &#34;aa&#34;)
}
})
}
// BenchmarkDemo
// BenchmarkDemo-4 6334993 203.8 ns/op 16 B/op 1 allocs/op
// PASS
// 没有panic
5.减少GC压力之sync.Pool包的使用
作用:复用对象,减少垃圾回收GC压力。 使用:
5.1 不使用sync.Pool代码示例
package main
import (
&#34;sync&#34;
&#34;testing&#34;
)
type Country struct {
ID int `json:&#34;id&#34;`
Name string `json:&#34;name&#34;`
}
type Province struct {
ID int `json:&#34;id&#34;`
Name string `json:&#34;name&#34;`
}
type City struct {
ID int `json:&#34;id&#34;`
Name string `json:&#34;name&#34;`
}
type County struct {
ID int `json:&#34;id&#34;`
Name string `json:&#34;name&#34;`
}
type Street struct {
ID int `json:&#34;id&#34;`
Name string `json:&#34;name&#34;`
}
// 模拟数据
// 地址信息对象
type AddressModule struct {
Consignee string `json:&#34;consignee&#34;`
Email string `json:&#34;email&#34;`
Mobile int64 `json:&#34;mobile&#34;`
Country *Country `json:&#34;country&#34;`
Province *Province `json:&#34;province&#34;`
City *City `json:&#34;city&#34;`
County *County `json:&#34;county&#34;`
Street *Street `json:&#34;street&#34;`
DetailedAddress string `json:&#34;detailed_address&#34;`
PostalCode string `json:&#34;postal_code&#34;`
AddressID int64 `json:&#34;address_id&#34;`
IsDefault bool `json:&#34;is_default&#34;`
Label string `json:&#34;label&#34;`
Longitude string `json:&#34;longitude&#34;`
Latitude string `json:&#34;latitude&#34;`
}
// 不使用sync.Pool
func BenchmarkDemo_NoPool(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// 直接初始化
addressModule := &AddressModule{}
addressModule.Consignee = &#34;&#34;
addressModule.Email = &#34;&#34;
addressModule.Mobile = 0
addressModule.Country = &Country{
ID: 0,
Name: &#34;&#34;,
}
addressModule.Province = &Province{
ID: 0,
Name: &#34;&#34;,
}
addressModule.City = &City{
ID: 0,
Name: &#34;&#34;,
}
addressModule.County = &County{
ID: 0,
Name: &#34;&#34;,
}
addressModule.Street = &Street{
ID: 0,
Name: &#34;&#34;,
}
addressModule.DetailedAddress = &#34;&#34;
addressModule.PostalCode = &#34;&#34;
addressModule.IsDefault = false
addressModule.Label = &#34;&#34;
addressModule.Longitude = &#34;&#34;
addressModule.Latitude = &#34;&#34;
// 下面这段代码没意义 只是为了不报语法错误
if addressModule == nil {
return
}
}
})
}
// 不使用sync.Pool执行结果
// goos: darwin
// goarch: amd64
// pkg: demo
// cpu: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
// BenchmarkDemo_NoPool-4 144146564 84.62 ns/op 120 B/op 5 allocs/op
// PASS
// ok demo 21.782s
不使用sync.Pool执行分析:火焰图&Top函数
可以很明显看见GC过程消耗了大量的CPU。


5.2 使用sync.Pool代码示例
// 使用sync.Pool
func BenchmarkDemo_Pool(b *testing.B) {
// 使用缓存池sync.Pool
demoPool := &sync.Pool{
// 定义初始化结构体的匿名函数
New: func() interface{} {
return &AddressModule{
Country: &Country{
ID: 0,
Name: &#34;&#34;,
},
Province: &Province{
ID: 0,
Name: &#34;&#34;,
},
City: &City{
ID: 0,
Name: &#34;&#34;,
},
County: &County{
ID: 0,
Name: &#34;&#34;,
},
Street: &Street{
ID: 0,
Name: &#34;&#34;,
},
}
},
}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// 从缓存池中获取对象
addressModule, _ := (demoPool.Get()).(*AddressModule)
// 下面这段代码没意义 只是为了不报语法错误
if addressModule == nil {
return
}
// 重置对象 准备归还对象到缓存池
addressModule.Consignee = &#34;&#34;
addressModule.Email = &#34;&#34;
addressModule.Mobile = 0
addressModule.Country.ID = 0
addressModule.Country.Name = &#34;&#34;
addressModule.Province.ID = 0
addressModule.Province.Name = &#34;&#34;
addressModule.County.ID = 0
addressModule.County.Name = &#34;&#34;
addressModule.Street.ID = 0
addressModule.Street.Name = &#34;&#34;
addressModule.DetailedAddress = &#34;&#34;
addressModule.PostalCode = &#34;&#34;
addressModule.IsDefault = false
addressModule.Label = &#34;&#34;
addressModule.Longitude = &#34;&#34;
addressModule.Latitude = &#34;&#34;
// 还对象到缓存池
demoPool.Put(addressModule)
}
})
}
// 使用sync.Pool执行结果
// goos: darwin
// goarch: amd64
// pkg: demo
// cpu: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
// BenchmarkDemo_Pool-4 988550808 12.41 ns/op 0 B/op 0 allocs/op
// PASS
// ok demo 14.215s
使用sync.Pool执行分析:火焰图&Top函数
runtime.mallocgc 已经在top里面看不见了


关于火焰图和Top函数的使用下面会讲到。
6.减少缓存穿透利器之singleflight包的使用
作用:缓存等穿透时减少请求数。 使用:
package main
import (
&#34;io/ioutil&#34;
&#34;net/http&#34;
&#34;sync&#34;
&#34;testing&#34;
&#34;golang.org/x/sync/singleflight&#34;
)
// 没有使用singleflight的代码示例
func TestDemo_NoSingleflight(t *testing.T) {
t.Parallel()
wg := sync.WaitGroup{}
// 模拟并发远程调用
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
resp, err := http.Get(&#34;http://example.com&#34;)
if err != nil {
t.Error(err)
return
}
_, err = ioutil.ReadAll(resp.Body)
if err != nil {
t.Error(err)
return
}
t.Log(&#34;log&#34;)
}()
}
wg.Wait()
}
// 使用singleflight的代码示例
func TestDemo_Singleflight(t *testing.T) {
t.Parallel()
singleGroup := singleflight.Group{}
wg := sync.WaitGroup{}
// 模拟并发远程调用
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 使用singleflight
res, err, shared := singleGroup.Do(&#34;cache_key&#34;, func() (interface{}, error) {
resp, err := http.Get(&#34;http://example.com&#34;)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
})
if err != nil {
t.Error(err)
return
}
_, _ = res.([]byte)
t.Log(&#34;log&#34;, shared, err)
}()
}
wg.Wait()
}
抓包域名http://example.com的请求:tcpdump host http://example.com
没有使用Singleflight一共发起了3次请求

使用Singleflight只发起了1次请求

7. Channel的使用
作用:不要通过共享内存来通信,要通过通信来实现共享内存。相当于管道。 使用:
package main
import (
&#34;fmt&#34;
&#34;time&#34;
)
// 响应公共结构体
type APIBase struct {
Code int32 `json:&#34;code&#34;`
Message string `json:&#34;message&#34;`
}
// 模拟接口A的响应结构体
type APIDemoA struct {
APIBase
Data APIDemoAData `json:&#34;data&#34;`
}
type APIDemoAData struct {
Title string `json:&#34;title&#34;`
}
// 模拟接口B的响应结构体
type APIDemoB struct {
APIBase
Data APIDemoBData `json:&#34;data&#34;`
}
type APIDemoBData struct {
SkuList []int64 `json:&#34;sku_list&#34;`
}
// 模拟接口逻辑
func main() {
// 创建接口A传输结果的通道
execAResult := make(chan APIDemoA)
// 创建接口B传输结果的通道
execBResult := make(chan APIDemoB)
// 并发调用接口A
go func(execAResult chan<- APIDemoA) {
// 模拟接口A远程调用过程
time.Sleep(2 * time.Second)
execAResult <- APIDemoA{}
}(execAResult)
// 并发调用接口B
go func(execBResult chan<- APIDemoB) {
// 模拟接口B远程调用过程
time.Sleep(1 * time.Second)
execBResult <- APIDemoB{}
}(execBResult)
var resultA APIDemoA
var resultB APIDemoB
i := 0
for {
if i >= 2 {
fmt.Println(&#34;退出&#34;)
break
}
select {
case resultA = <-execAResult: // 等待接口A的响应结果
i++
fmt.Println(&#34;resultA&#34;, resultA)
case resultB = <-execBResult: // 等待接口B的响应结果
i++
fmt.Println(&#34;resultB&#34;, resultB)
}
}
}
// [Running] go run &#34;.../demo/main.go&#34;
// resultB {{0 } {[]}}
// resultA {{0 } {}}
// 退出
8. 单元测试&基准测试
作用:开发阶段调试代码块、接口;对代码块、接口做基准测试,分析性能问题,包含CPU使用、内存使用等。可做对比测试。ci阶段检测代码质量减少bug。 使用:
8.1 单元测试
一个很简单的单元测试示例:
package main
import (
&#34;io/ioutil&#34;
&#34;net/http&#34;
&#34;testing&#34;
)
func TestDemo(t *testing.T) {
t.Parallel()
// 模拟调用接口
resp, err := http.Get(&#34;http://example.com?user_id=121212&#34;)
if err != nil {
t.Error(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Error(err)
return
}
t.Log(&#34;body&#34;, string(body))
}
// 执行
// go test -timeout 30s -run ^TestDemo$ demo -v -count=1
// === RUN TestDemo
// === PAUSE TestDemo
// === CONT TestDemo
// ......
// --- PASS: TestDemo (0.45s)
// PASS
// ok demo 1.130s
多个测试用例的单元测试示例:
package main
import (
&#34;fmt&#34;
&#34;io/ioutil&#34;
&#34;net/http&#34;
&#34;testing&#34;
)
type Req struct {
UserID int64
}
func TestDemo(t *testing.T) {
t.Parallel()
tests := []struct {
TestName string
*Req
}{
{
TestName: &#34;测试用例1&#34;,
Req: &Req{
UserID: 12121212,
},
},
{
TestName: &#34;测试用例2&#34;,
Req: &Req{
UserID: 829066,
},
},
}
for _, v := range tests {
t.Run(v.TestName, func(t *testing.T) {
// 模拟调用接口
url := fmt.Sprintf(&#34;http://example.com?user_id=%d&#34;, v.UserID)
resp, err := http.Get(url)
if err != nil {
t.Error(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Error(err)
return
}
t.Log(&#34;body&#34;, string(body), url)
})
}
}
// 执行
// go test -timeout 30s -run ^TestDemo$ demo -v -count=1
// === RUN TestDemo
// === PAUSE TestDemo
// === CONT TestDemo
// === RUN TestDemo/测试用例1
// ...
// === RUN TestDemo/测试用例2
// ...
// --- PASS: TestDemo (7.34s)
// --- PASS: TestDemo/测试用例1 (7.13s)
// --- PASS: TestDemo/测试用例2 (0.21s)
// PASS
// ok demo 7.984s
8.2 基准测试
简单的基准测试:
package main
import (
&#34;sync&#34;
&#34;testing&#34;
)
// 压力测试sync.Map
func BenchmarkSyncMap(b *testing.B) {
demoMap := &sync.Map{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
demoMap.Store(&#34;a&#34;, &#34;a&#34;)
for i := 0; i < 1000; i++ {
demoMap.Load(&#34;a&#34;)
}
}
})
}
// go test -benchmem -run=^$ -bench ^(BenchmarkSyncMap)$ demo -v -count=1 -cpuprofile=cpu.profile -memprofile=mem.profile -benchtime=10s
// goos: darwin
// goarch: amd64
// pkg: demo
// BenchmarkSyncMap
// BenchmarkSyncMap-4
// 570206 23047 ns/op 16 B/op 1 allocs/op
// PASS
// ok demo 13.623s
对比基准测试:
package main
import (
&#34;sync&#34;
&#34;testing&#34;
)
// 压力测试sync.Map
func BenchmarkSyncMap(b *testing.B) {
demoMap := &sync.Map{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
demoMap.Store(&#34;a&#34;, &#34;a&#34;)
for i := 0; i < 1000; i++ {
demoMap.Load(&#34;a&#34;)
}
}
})
}
// 用读写锁实现一个并发map
type ConcurrentMap struct {
value map[string]string
mutex sync.RWMutex
}
// 写
func (c *ConcurrentMap) Store(key string, val string) {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.value == nil {
c.value = map[string]string{}
}
c.value[key] = val
}
// 读
func (c *ConcurrentMap) Load(key string) string {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.value[key]
}
// 压力测试并发map
func BenchmarkConcurrentMap(b *testing.B) {
demoMap := &ConcurrentMap{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
demoMap.Store(&#34;a&#34;, &#34;a&#34;)
for i := 0; i < 1000; i++ {
demoMap.Load(&#34;a&#34;)
}
}
})
}
// go test -benchmem -run=^$ -bench . demo -v -count=1 -cpuprofile=cpu.profile -memprofile=mem.profile -benchtime=10s
// goos: darwin
// goarch: amd64
// pkg: demo
// BenchmarkSyncMap
// BenchmarkSyncMap-4 668082 15818 ns/op 16 B/op 1 allocs/op
// BenchmarkConcurrentMap
// BenchmarkConcurrentMap-4 171730 67888 ns/op 0 B/op 0 allocs/op
// PASS
// coverage: 0.0% of statements
// ok demo 23.823s
9.性能分析
作用:CPU分析、内存分析。通过可视化调用链路、可视化火焰图、TOP函数等快速定位代码问题、提升代码性能。
使用:
9.1 pprof的使用
9.1.1 基准测试场景
- 首先编写基准测试用例,复用上面sync.Map的用例:
package main
import (
&#34;sync&#34;
&#34;testing&#34;
)
// 压力测试sync.Map
func BenchmarkSyncMap(b *testing.B) {
demoMap := &sync.Map{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
demoMap.Store(&#34;a&#34;, &#34;a&#34;)
for i := 0; i < 1000; i++ {
demoMap.Load(&#34;a&#34;)
}
}
})
}
- 执行基准测试,生成cpu.profile文件和mem.profile 文件。命令如下
go test -benchmem -run=^-bench ^BenchmarkSyncMap−benchBenchmarkSyncMap demo -v -count=1 -cpuprofile=cpu.profile -memprofile=mem.profile -benchtime=10s 常用参数解释:
-benchmem: 输出内存指标
-run: 正则,指定需要test的方法
-bench: 正则,指定需要benchmark的方法
-v: 即使成功也输出打印结果和日志
-count: 执行次数
-cpuprofile: 输出cpu的profile文件
-memprofile: 输出内存的profile文件
-benchtime: 执行时间
更多参数请查看:
go help testflag
- 使用go tool自带的pprof工具分析测试结果。命令如下:
go tool pprof -http=:8000 cpu.profile 常用参数解释:
-http: 指定ip:port,启动web服务可视化查看分析,浏览器会自动打开页面 http://localhost:8000/ui/ 可视化选项菜单

火焰图

调用链路图

Top函数

9.1.2 Web服务场景
- 使用上面全局变量的代码示例,引入net/http/pprof包,并单独注册各端口获取pprof数据。
package main
import (
&#34;net/http&#34;
// 引入pprof包
// _代表只执行包内的init函数
_ &#34;net/http/pprof&#34;
&#34;github.com/gin-gonic/gin&#34;
)
// 全局变量不会像PHP一样,在完成一次请求之后被销毁
var GlobalVarDemo int32 = 0
// 模拟接口逻辑
func main() {
r := gin.Default()
r.GET(&#34;/ping&#34;, func(c *gin.Context) {
GlobalVarDemo++
c.JSON(200, gin.H{
&#34;message&#34;: GlobalVarDemo,
})
})
// 再开启一个端口获取pprof数据
go func() {
http.ListenAndServe(&#34;:8888&#34;, nil)
}()
// 启动web服务
r.Run()
}
- 访问链接 http://localhost:8888/debug/pprof/,可以看见相关profiles。

- 命令使用pprof工具,获取远程服务profile,命令如下:
go tool pprof -http=:8000 http://localhost:8888/debug/pprof/profile?seconds=5 备注:
执行上面命令的时候,可以使用压测工具模拟流量,比如命令:siege -c 50 -t 100 &#34;http://localhost:8080/ping&#34;
同样,我们得到了这个熟悉的页面:

9.2 trace工具的使用
作用:清晰查看每个逻辑处理器中Goroutine的执行过程,可以很直观看出Goroutine的阻塞消耗,包含网络阻塞、同步阻塞(锁)、系统调用阻塞、调度等待、GC执行耗时、GC STW(Stop The World)耗时。
9.2.1 基准测试场景
使用:
生成trace.out文件命令:
go test -benchmem -run=^$ -bench ^BenchmarkDemo_NoPool$ demo -v -count=1 -trace=trace.out
go test -benchmem -run=^$ -bench ^BenchmarkDemo_Pool$ demo -v -count=1 -trace=trace.out
分析trace.out文件命令:
go tool trace -http=127.0.0.1:8000 trace.out
没使用sync.Pool



使用sync.Pool

9.2.2 Web服务场景
使用:
同样引入包net/http/pprof
package main
import (
&#34;net/http&#34;
// 引入pprof包
// _代表只执行包内的init函数
_ &#34;net/http/pprof&#34;
&#34;github.com/gin-gonic/gin&#34;
)
// 全局变量不会像PHP一样,在完成一次请求之后被销毁
var GlobalVarDemo int32 = 0
// 模拟接口逻辑
func main() {
r := gin.Default()
r.GET(&#34;/ping&#34;, func(c *gin.Context) {
GlobalVarDemo++
c.JSON(200, gin.H{
&#34;message&#34;: GlobalVarDemo,
})
})
// 再开启一个端口获取pprof数据
go func() {
http.ListenAndServe(&#34;:8888&#34;, nil)
}()
// 启动web服务
r.Run()
}
启动服务后执行如下命令:
1.
生成trace.out文件命令:
curl http://localhost:8888/debug/pprof/trace?seconds=20 > trace.out
和上面命令同时执行,模拟请求,也可以用ab:
siege -c 50 -t 100 &#34;http://localhost:8080/ping&#34;
2. 分析trace.out文件命令:
go tool trace -http=127.0.0.1:8000 trace.out
快捷健:
w 放大
e 右移





9.3 dlv工具的使用
9.3.1 基准测试场景
作用:断点调试等。
安装:
go install github.com/go-delve/delve/cmd/dlv@latest 使用:
package main
import (
_ &#34;net/http/pprof&#34;
&#34;github.com/gin-gonic/gin&#34;
)
// 全局变量不会像PHP一样,在完成一次请求之后被销毁
var GlobalVarDemo int32 = 0
// 模拟接口逻辑
func main() {
r := gin.Default()
r.GET(&#34;/ping&#34;, func(c *gin.Context) {
GlobalVarDemo++
c.JSON(200, gin.H{
&#34;message&#34;: GlobalVarDemo,
})
})
r.Run()
}命令行执行命令:
dlv debug main.go 进入调试,常用调试命令:
- (list或l:输出代码):list main.go:16
- (break或b:断点命令):执行 break main.go:16 给行 GlobalVarDemo++打断点
- (continue或c:继续执行):continue
- (print或p:打印变量):print GlobalVarDemo
- (step或s:可以进入函数):step
更多命令请执行 help。
模拟请求:
curl http://localhost:8080/ping


9.3.2 Web服务场景
还是这个demo
package main
import (
&#34;github.com/gin-gonic/gin&#34;
)
// 全局变量不会像PHP一样,在完成一次请求之后被销毁
var GlobalVarDemo int32 = 0
// 模拟接口逻辑
func main() {
r := gin.Default()
r.GET(&#34;/ping&#34;, func(c *gin.Context) {
GlobalVarDemo++
c.JSON(200, gin.H{
&#34;message&#34;: GlobalVarDemo,
})
})
// 启动web服务
r.Run()
}
- 找到服务进程ID lsof -i :8080
- dlv调试进程 dlv attach 36968
- 进入调试模式,调试代码(和上面一样)
9.4(扩展) 逃逸分析
逃逸分析命令:go build -gcflags &#34;-m -l&#34; *.go package main
type Demo struct {
}
func main() {
DemoFun()
}
func DemoFun() *Demo {
demo := &Demo{}
return demo
}
// # command-line-arguments
// ./main.go:11:10: &Demo literal escapes to heap <------- 局部变量内存被分配到堆上
9.5(扩展) 汇编代码
直接生成汇编代码命令:go run -gcflags -S main.go # command-line-arguments
&#34;&#34;.main STEXT nosplit size=1 args=0x0 locals=0x0
0x0000 00000 (.../demo/main.go:6) TEXT &#34;&#34;.main(SB), NOSPLIT|ABIInternal, $0-0
0x0000 00000 (.../demo/main.go:6) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (.../demo/main.go:6) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (.../demo/main.go:6) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (<unknown line number>) RET
0x0000 c3
略......
获取生成汇编代码整个优化过程:GOSSAFUNC=main go build main.go dumped SSA to ./ssa.html <------- 生成的文件,浏览器打开此文件

总结
最后我们再总结下,从PHPer到Gopher的过程,我们重点需要关注的几点如下:
- PHP和Go常用代码块的对应关系
- 常驻内存
- 指针
- 并发
- 单元测试&基准测试
- 性能分析
|
|