措辞层面差异

备注:下文基于PHP主流php-fpm模式。

比拟项

PHP

Go

php转驼峰PHP转Go速学手册 Bootstrap

字符串表示

单引号(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

比较像struc

除此之外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 mainimport ("github.com/gin-gonic/gin")// 全局变量不会像PHP一样,在完成一次要求之后被销毁var GlobalVarDemo int32 = 0// 仿照接口逻辑func main() {r := gin.Default()r.GET("/ping", func(c gin.Context) {atomic.AddInt32(&GlobalVarDemo,1)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"// {"message":2} <------- 值在递增// curl "127.0.0.1:8080/ping"// {"message":3} <------- 值在递增
2. 形参是slice、map类型的参数,把稳值可被全局修正

类似PHP的引用通报,Go里面都是值通报,详细缘故原由下面说。

// 切片package mainimport "fmt"func main() {paramDemo := []int32{1}fmt.Printf("main.paramDemo 1 %v, pointer: %p \n", paramDemo, ¶mDemo)// 浅拷贝demo(paramDemo)fmt.Printf("main.paramDemo 2 %v, pointer: %p \n", paramDemo, ¶mDemo)}func demo(paramDemo []int32) ([]int32, error) {fmt.Printf("main.demo.paramDemo pointer: %p \n", ¶mDemo)paramDemo[0] = 2return 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 mainimport "fmt"func main() {paramDemo := [1]int32{1}fmt.Println("main.paramDemo 1", paramDemo)demo(paramDemo)fmt.Println("main.paramDemo 2", paramDemo)}func demo(paramDemo [1]int32) ([1]int32, error) {paramDemo[0] = 2return paramDemo, nil}// [Running] go run ".../demo/main.go"// main.paramDemo 1 [1]// main.paramDemo 2 [1] <------- 值未被修正//===========Map同样有这个问题===========package mainimport "fmt"func main() {paramDemo := map[string]string{"a": "a",}fmt.Println("main.paramDemo 1", paramDemo)demo(paramDemo)fmt.Println("main.paramDemo 2", paramDemo)}func demo(paramDemo map[string]string) (map[string]string, error) {paramDemo["a"] = "b"return paramDemo, nil}// [Running] go run ".../demo/main.go"// 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 mainimport "fmt"func main() {paramDemo := []int32{1}fmt.Println("main.paramDemo 1", paramDemo)// 初始化新空间paramDemoCopy := make([]int32, len(paramDemo))// 深拷贝copy(paramDemoCopy, paramDemo)demo(paramDemoCopy)fmt.Println("main.paramDemo 2", paramDemo)}func demo(paramDemo []int32) ([]int32, error) {paramDemo[0] = 2return paramDemo, nil}// [Running] go run ".../demo/main.go"// main.paramDemo 1 [1]// main.paramDemo 2 [1]3. 资源利用完毕,记得开释资源或回收资源

package mainimport ("github.com/gomodule/redigo/redis")var RedisPool redis.Poolfunc 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( 'a' => 'a','b' => 'b', 'c' => 'c', 'd' => 'd', 'e' => 'e',);foreach ($demoMap as $v) { var_dump("v {$v}");}// 第一次实行[Running] php ".../php/demo.php"string(3) "v a"string(3) "v b"string(3) "v c"string(3) "v d"string(3) "v e"// 第N次实行// 遍历结果的顺序都是稳定不变的[Running] php ".../php/demo.php"string(3) "v a"string(3) "v b"string(3) "v c"string(3) "v d"string(3) "v e"

但是Go措辞里就不一样了,如下:

package mainimport "fmt"func main() {var demoMap map[string]string = map[string]string{"a": "a","b": "b","c": "c","d": "d","e": "e",}for _, v := range demoMap {fmt.Println("v", v)}}// 第一次实行// [Running] go run ".../demo/main.go"// v a// v b// v c// v d// v e// 第二次实行// 遍历结果,元素顺序发生了改变// [Running] go run ".../demo/main.go"// v e// v a// v b// v c// v d

为什么?

答:底层实现都是数组+类似拉链法。
1. hash函数无序写入2. 成倍扩容3. 等量扩容都决定了map本来便是无序的,以是Go措辞为了避免开拓者依赖元素顺序,每次遍历的时候都是随机了一个索引起始值。
然后PHP通过额外的内存空间掩护了map元素的顺序。
5. 不要并发写map

package mainimport ("testing")func BenchmarkDemo(b testing.B) {var demoMap map[string]string = map[string]string{"a": "a","b": "b",}// 仿照并发写mapb.RunParallel(func(pb testing.PB) {for pb.Next() {demoMap["a"] = "aa"}})}// BenchmarkDemo// fatal error: concurrent map writes// fatal error: concurrent map writes

为什么?

答:并发不屈安,触发panic:“fatal error: concurrent map writes”。

// go version 1.13.8源码// hashWriting 值为 4if h.flags&hashWriting != 0 {throw("concurrent map read and map write")}6. 把稳判断指针类型不为空nil,再操作

package mainimport ("fmt""log""net/http")func main() {resp, err := http.Get("https://www.example.com")if resp.StatusCode != http.StatusOK || err != nil {// 当 resp为nil时 会触发panic// 当 resp.StatusCode != http.StatusOK 时err可能为nil 触发paniclog.Printf("err: %s", err.Error())}}// [Running] go run ".../demo/main.go"// panic: runtime error: invalid memory address or nil pointer dereference

package mainimport ("fmt""log""net/http")func main() {// 仿照要求业务coderesp, err := http.Get("https://www.example.com")fmt.Println(resp, err)if err != nil {// 报错并记录非常日志log.Printf("err: %s", err.Error())return}// 仿照业务code不为成功的codeif 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("Demo"); }}(new ClassDemo())->demoFun();// [Running] php ".../php/demo.php"// string(4) "Demo"

package mainimport ("fmt")//根本构造体type Base struct {}// Base的DemoFunfunc (b Base) DemoFun() {fmt.Println("Base")}func (b Base) PublicFun() {b.DemoFun()}type Demo struct {// 合成复用BaseBase}// Demo的DemoFunfunc (d Demo) DemoFun() {fmt.Println("Demo")}func main() {// 实行(&Demo{}).PublicFun()}// [Running] go run ".../demo/main.go"// Base <------ 把稳此处实行的是被合成复用的构造体的方法进阶利用热加载工具beeGoroutine并发掌握之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="start"2. Goroutine并发掌握之sync.WaitGroup包的利用

浸染:Goroutine可以等待,直到当前Goroutine派生的子Goroutine实行完成。

利用:

package mainimport ("fmt""sync""time")func main() {wg := &sync.WaitGroup{}wg.Add(1)go func(wg sync.WaitGroup) {defer wg.Done()fmt.Println("子a 开始实行")time.Sleep(5 time.Second)fmt.Println("子a 实行完毕")}(wg)wg.Add(1)go func(wg sync.WaitGroup) {defer wg.Done()fmt.Println("子b 开始实行")time.Sleep(5 time.Second)fmt.Println("子b 实行完毕")}(wg)wg.Add(1)go func(wg sync.WaitGroup) {defer wg.Done()fmt.Println("子c 开始实行")time.Sleep(5 time.Second)fmt.Println("子c 实行完毕")}(wg)fmt.Println("主 等待")wg.Wait()fmt.Println("主 退出")}// 第一次实行// [Running] go run ".../demo/main.go"// 子a 开始实行// 子c 开始实行// 子b 开始实行// 主 等待 <------ 把稳这里和下面打印的位置不一样,由于当前代码并发实行是没有保障实行顺序的// 子b 实行完毕// 子a 实行完毕// 子c 实行完毕// 主 退出// 第一次实行// [Running] go run ".../demo/main.go"// 主 等待 <------ 把稳这里和上面打印的位置不一样,由于当前代码并发实行是没有保障实行顺序的// 子a 开始实行// 子c 开始实行// 子b 开始实行// 子b 实行完毕// 子c 实行完毕// 子a 实行完毕// 主 退出 <------ 主Goroutine一贯等待直到子Goroutine都实行完毕3. 子Goroutine超时掌握之context.Context包的利用

浸染:Go措辞第一形参常日都为context.Context类型,1. 通报高下文 2. 掌握子Goroutine超时退出 3. 掌握子Goroutine定时退出

利用:

package mainimport ("context""fmt""time")func main() {ctx, cancel := context.WithTimeout(context.TODO(), 5time.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("超时退出")returncase <-execResult:fmt.Println("处理完成")return}}(ctx)time.Sleep(10 time.Second)}// [Running] go run ".../demo/main.go"// 超时退出4. 并发安全的map之sync.Map包的利用

浸染:并发安全的map,支持并发写。
读多写少场景的性能好。

利用:

package mainimport ("sync""testing")func BenchmarkDemo(b testing.B) {demoMap := &sync.Map{}demoMap.Store("a", "a")demoMap.Store("b", "b")b.RunParallel(func(pb testing.PB) {for pb.Next() {demoMap.Store("a", "aa")}})}// BenchmarkDemo// BenchmarkDemo-4 6334993 203.8 ns/op 16 B/op 1 allocs/op// PASS// 没有panic5. 减少GC压力之sync.Pool包的利用

浸染:复用工具,减少垃圾回收GC压力。

利用:

5.1 不该用sync.Pool代码示例

package mainimport ("sync""testing")type Country struct {ID int `json:"id"`Name string `json:"name"`}type Province struct {ID int `json:"id"`Name string `json:"name"`}type City struct {ID int `json:"id"`Name string `json:"name"`}type County struct {ID int `json:"id"`Name string `json:"name"`}type Street struct {ID int `json:"id"`Name string `json:"name"`}// 仿照数据// 地址信息工具type AddressModule struct {Consignee string `json:"consignee"`Email string `json:"email"`Mobile int64 `json:"mobile"`Country Country `json:"country"`Province Province `json:"province"`City City `json:"city"`County County `json:"county"`Street Street `json:"street"`DetailedAddress string `json:"detailed_address"`PostalCode string `json:"postal_code"`AddressID int64 `json:"address_id"`IsDefault bool `json:"is_default"`Label string `json:"label"`Longitude string `json:"longitude"`Latitude string `json:"latitude"`}// 不该用sync.Poolfunc BenchmarkDemo_NoPool(b testing.B) {b.RunParallel(func(pb testing.PB) {for pb.Next() {// 直接初始化addressModule := &AddressModule{}addressModule.Consignee = ""addressModule.Email = ""addressModule.Mobile = 0addressModule.Country = &Country{ID: 0,Name: "",}addressModule.Province = &Province{ID: 0,Name: "",}addressModule.City = &City{ID: 0,Name: "",}addressModule.County = &County{ID: 0,Name: "",}addressModule.Street = &Street{ID: 0,Name: "",}addressModule.DetailedAddress = ""addressModule.PostalCode = ""addressModule.IsDefault = falseaddressModule.Label = ""addressModule.Longitude = ""addressModule.Latitude = ""// 下面这段代码没意义 只是为了不报语法缺点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 demo21.782s

不该用sync.Pool实行剖析:火焰图&Top函数

可以很明显瞥见GC过程花费了大量的CPU:

5.2 利用sync.Pool代码示例

// 利用sync.Poolfunc BenchmarkDemo_Pool(b testing.B) {// 利用缓存池sync.PooldemoPool := &sync.Pool{// 定义初始化构造体的匿名函数New: func() interface{} {return &AddressModule{Country: &Country{ID: 0,Name: "",},Province: &Province{ID: 0,Name: "",},City: &City{ID: 0,Name: "",},County: &County{ID: 0,Name: "",},Street: &Street{ID: 0,Name: "",},}},}b.RunParallel(func(pb testing.PB) {for pb.Next() {// 从缓存池中获取工具addressModule, _ := (demoPool.Get()).(AddressModule)// 下面这段代码没意义 只是为了不报语法缺点if addressModule == nil {return}// 重置工具 准备归还工具到缓存池addressModule.Consignee = ""addressModule.Email = ""addressModule.Mobile = 0addressModule.Country.ID = 0addressModule.Country.Name = ""addressModule.Province.ID = 0addressModule.Province.Name = ""addressModule.County.ID = 0addressModule.County.Name = ""addressModule.Street.ID = 0addressModule.Street.Name = ""addressModule.DetailedAddress = ""addressModule.PostalCode = ""addressModule.IsDefault = falseaddressModule.Label = ""addressModule.Longitude = ""addressModule.Latitude = ""// 还工具到缓存池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 demo14.215s

利用sync.Pool实行剖析:火焰图&Top函数

runtime.mallocgc 已经在top里面看不见了

关于火焰图和Top函数的利用下面会讲到。
6. 减少缓存穿透利器之singleflight包的利用

浸染:缓存等穿透时减少要求数。

利用:

package mainimport ("io/ioutil""net/http""sync""testing""golang.org/x/sync/singleflight")// 没有利用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("http://example.com")if err != nil {t.Error(err)return}_, err = ioutil.ReadAll(resp.Body)if err != nil {t.Error(err)return}t.Log("log")}()}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()// 利用singleflightres, err, shared := singleGroup.Do("cache_key", func() (interface{}, error) {resp, err := http.Get("http://example.com")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("log", shared, err)}()}wg.Wait()}

抓包域名example.com的要求:tcpdump host example.com

没有利用Singleflight一共发起了3次要求

利用Singleflight只发起了1次要求

7. Channel的利用

浸染:不要通过共享内存来通信,要通过通信来实现共享内存。
相称于管道。

利用:

package mainimport ("fmt""time")// 相应公共构造体type APIBase struct {Code int32 `json:"code"`Message string `json:"message"`}// 仿照接口A的相应构造体type APIDemoA struct {APIBaseData APIDemoAData `json:"data"`}type APIDemoAData struct {Title string `json:"title"`}// 仿照接口B的相应构造体type APIDemoB struct {APIBaseData APIDemoBData `json:"data"`}type APIDemoBData struct {SkuList []int64 `json:"sku_list"`}// 仿照接口逻辑func main() {// 创建接口A传输结果的通道execAResult := make(chan APIDemoA)// 创建接口B传输结果的通道execBResult := make(chan APIDemoB)// 并发调用接口Ago func(execAResult chan<- APIDemoA) {// 仿照接口A远程调用过程time.Sleep(2 time.Second)execAResult <- APIDemoA{}}(execAResult)// 并发调用接口Bgo func(execBResult chan<- APIDemoB) {// 仿照接口B远程调用过程time.Sleep(1 time.Second)execBResult <- APIDemoB{}}(execBResult)var resultA APIDemoAvar resultB APIDemoBi := 0for {if i >= 2 {fmt.Println("退出")break}select {case resultA = <-execAResult: // 等待接口A的相应结果i++fmt.Println("resultA", resultA)case resultB = <-execBResult: // 等待接口B的相应结果i++fmt.Println("resultB", resultB)}}}// [Running] go run ".../demo/main.go"// resultB {{0 } {[]}}// resultA {{0 } {}}// 退出8. 单元测试&基准测试

浸染:开拓阶段调试代码块、接口;对代码块、接口做基准测试,剖析性能问题,包含CPU利用、内存利用等。
可做比拟测试。
ci阶段检测代码质量减少bug。

利用:

8.1 单元测试

一个很大略的单元测试示例:

package mainimport ("io/ioutil""net/http""testing")func TestDemo(t testing.T) {t.Parallel()// 仿照调用接口resp, err := http.Get("http://example.com?user_id=121212")if err != nil {t.Error(err)return}body, err := ioutil.ReadAll(resp.Body)if err != nil {t.Error(err)return}t.Log("body", 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 mainimport ("fmt""io/ioutil""net/http""testing")type Req struct {UserID int64}func TestDemo(t testing.T) {t.Parallel()tests := []struct {TestName stringReq}{{TestName: "测试用例1",Req: &Req{UserID: 12121212,},},{TestName: "测试用例2",Req: &Req{UserID: 829066,},},}for _, v := range tests {t.Run(v.TestName, func(t testing.T) {// 仿照调用接口url := fmt.Sprintf("http://example.com?user_id=%d", 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("body", 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 demo7.984s8.2 基准测试

大略的基准测试:

package mainimport ("sync""testing")// 压力测试sync.Mapfunc BenchmarkSyncMap(b testing.B) {demoMap := &sync.Map{}b.RunParallel(func(pb testing.PB) {for pb.Next() {demoMap.Store("a", "a")for i := 0; i < 1000; i++ {demoMap.Load("a")}}})}// 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 demo13.623s

比拟基准测试:

package mainimport ("sync""testing")// 压力测试sync.Mapfunc BenchmarkSyncMap(b testing.B) {demoMap := &sync.Map{}b.RunParallel(func(pb testing.PB) {for pb.Next() {demoMap.Store("a", "a")for i := 0; i < 1000; i++ {demoMap.Load("a")}}})}// 用读写锁实现一个并发maptype ConcurrentMap struct {value map[string]stringmutex 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]}// 压力测试并发mapfunc BenchmarkConcurrentMap(b testing.B) {demoMap := &ConcurrentMap{}b.RunParallel(func(pb testing.PB) {for pb.Next() {demoMap.Store("a", "a")for i := 0; i < 1000; i++ {demoMap.Load("a")}}})}// 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 demo23.823s9. 性能剖析

浸染:CPU剖析、内存剖析。
通过可视化调用链路、可视化火焰图、TOP函数等快速定位代码问题、提升代码性能。

pproftracedlv

利用:

9.1 pprof的利用9.1.1 基准测试场景首先编写基准测试用例,复用上面sync.Map的用例:

package mainimport ("sync""testing")// 压力测试sync.Mapfunc BenchmarkSyncMap(b testing.B) {demoMap := &sync.Map{}b.RunParallel(func(pb testing.PB) {for pb.Next() {demoMap.Store("a", "a")for i := 0; i < 1000; i++ {demoMap.Load("a")}}})}实行基准测试,天生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做事场景

1. 利用上面全局变量的代码示例,引入net/http/pprof包,并单独注册各端口获取pprof数据。

package mainimport ("net/http"// 引入pprof包// _代表只实行包内的init函数_ "net/http/pprof""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,})})// 再开启一个端口获取pprof数据go func() {http.ListenAndServe(":8888", nil)}()// 启动web做事r.Run()}

2. 访问链接 http://localhost:8888/debug/pprof/,可以瞥见干系profiles。

3. 命令利用pprof工具,获取远程做事profile,命令如下:

go tool pprof -http=:8000 http://localhost:8888/debug/pprof/profile?seconds=5

备注:实行上面命令的时候,可以利用压测工具仿照流量,比如命令:siege -c 50 -t 100 "http://localhost:8080/ping"

同样,我们得到了这个熟习的页面:

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 mainimport ("net/http"// 引入pprof包// _代表只实行包内的init函数_ "net/http/pprof""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,})})// 再开启一个端口获取pprof数据go func() {http.ListenAndServe(":8888", nil)}()// 启动web做事r.Run()}

启动做事后实行如下命令:

1. 天生trace.out文件命令:curl http://localhost:8888/debug/pprof/trace?seconds=20 > trace.out和上面命令同时实行,仿照要求,也可以用ab:siege -c 50 -t 100 "http://localhost:8080/ping"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 mainimport (_ "net/http/pprof""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()}

命令行实行命令:

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 mainimport ("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,})})// 启动web做事r.Run()}找到做事进程ID lsof -i :8080dlv调试进程 dlv attach 36968进入调试模式,调试代码(和上面一样)9.4(扩展) 逃逸剖析

逃逸剖析命令:go build -gcflags "-m -l" .go

package maintype 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"".main STEXT nosplit size=1 args=0x0 locals=0x0 0x0000 00000 (.../demo/main.go:6) TEXT "".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 <------- 天生的文件,浏览器打开此文件

看完文章,是否收成了一定的成果呢?还烦懑去Freemen APP上面试试下水?

本文转载自TIGERB