最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • PHP到Go速转手册

    正文概述 转载于:掘金(TIGERB)   2021-07-19   318

    PHP到Go速转手册

    版权声明

    • 未经版权所有者明确授权,禁止发行本手册及其被实质上修改的版本。
    • 未经版权所有者事先授权,禁止将此作品及其衍生作品以标准(纸质)书籍形式发行。
    • 未与任何第三方以任何形式合作。

    前言

    整理了一份简要的手册,帮助大家高效的上手Go语言,主要是通过对比php和Go的不同点来强化理解,内容主要分为以下四部分:

    • 语言层面差异
    • 基础语法差异
    • 避坑指南
    • 进阶使用

    语言层面差异

    备注:下文基于PHP主流php-fpm模式。
    
    对比项PHPGo
    字符串表示单引号(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语言对应或者类似的类型做个对比,如下:

    语言\类型booleanstringintfloatarrayobject
    PHPboolstringintfloatarray(1,2,3)索引数组、array('1' => 1, '2' => 2, '3' => 3)关联数组实例化类classGoboolstringint、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64float32、float64[length]type比较像struct

    除此之外Go还支持更丰富的类型:

    类型
    slice切片(相当于PHP的索引数组)map(相当于PHP的关联数组)channel(管道,通过通信共享,不要通过共享来通信)指针(Go语言的值类型都有对应的指针类型)byte(字节,对应uint8别名,可以表示Ascaii码)rune(对应int32,可以表示unicode)等等自定义类型,例如type userDefinedType int32## 常用基本类型初始化方式对比
    类型PHPGo(定义变量带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 = 0int64同上var varInt64 int64 = 0float32$varNum = 0.01;var varFloat32 float32 = 0float64同上var varFloat64 float64 = 0array$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() {}channelvar 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{}
    

    常用函数对比

    常用函数描述PHPGo
    数组长度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) stringjson序列化json_encode()json.Marshal(v interface{}) ([]byte, error)json反序列化json_decode()json.Unmarshal(data []byte, v interface{}) error序列化(不再建议使用)serialize()、unserialize()包github.com/wulijun/go-…md5md5()包crypto/md5终端输出echo、var_dump等fmt.Println(a ...interface{})各种类型互转intval()等包strconv

    避坑指南

    1. 谨慎使用全局变量,全局变量不会像PHP一样,在完成一次请求之后被销毁
    2. 形参是slicemap类型的参数,注意值可被全局修改
    3. 资源使用完毕,记得释放资源或回收资源
    4. 不要依赖map遍历的顺序
    5. 不要并发写map
    6. 注意判断指针类型不为空nil,再操作
    7. 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"
    // {"message":2} <------- 值在递增
    // curl "127.0.0.1:8080/ping"
    // {"message":3} <------- 值在递增
    

    2.形参是slicemap类型的参数,注意值可被全局修改

    // 切片
    package main
    
    import "fmt"
    
    func main() {
    	paramDemo := []int32{1}
    	fmt.Printf("main.paramDemo 1 %v, pointer: %p \n", paramDemo, &paramDemo)
    	// 浅拷贝
    	demo(paramDemo)
    	fmt.Printf("main.paramDemo 2 %v, pointer: %p \n", paramDemo, &paramDemo)
    }
    
    func demo(paramDemo []int32) ([]int32, error) {
    	fmt.Printf("main.demo.paramDemo pointer: %p \n", &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 "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] = 2
    	return paramDemo, nil
    }
    
    // [Running] go run ".../demo/main.go"
    // main.paramDemo 1 [1]
    // main.paramDemo 2 [1] <------- 值未被修改
    
    //===========Map同样有这个问题===========
    
    package main
    
    import "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 main
    
    import "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] = 2
    	return paramDemo, nil
    }
    
    // [Running] go run ".../demo/main.go"
    // main.paramDemo 1 [1]
    // main.paramDemo 2 [1]
    
    

    3.资源使用完毕,记得释放资源或回收资源

    package main
    
    import (
    	"github.com/gomodule/redigo/redis"
    )
    
    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(
        '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 main
    
    import "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 main
    
    import (
    	"testing"
    )
    
    func BenchmarkDemo(b *testing.B) {
    	var demoMap map[string]string = map[string]string{
    		"a": "a",
    		"b": "b",
    	}
    	// 模拟并发写map
    	b.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 值为 4
    if h.flags&hashWriting != 0 {
    	throw("concurrent map read and map write")
    }
    

    6.注意判断指针类型不为空nil,再操作

    package main
    
    import (
    	"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 触发panic
    		log.Printf("err: %s", err.Error())
    	}
    }
    
    
    // [Running] go run ".../demo/main.go"
    // panic: runtime error: invalid memory address or nil pointer dereference
    
    
    package main
    
    import (
    	"fmt"
    	"log"
    	"net/http"
    )
    
    func main() {
    	// 模拟请求业务code
    	resp, err := http.Get("https://www.example.com")
    	fmt.Println(resp, err)
    	if err != nil {
    		// 报错并记录异常日志
    		log.Printf("err: %s", 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("Demo");
        }
    }
    
    (new ClassDemo())->demoFun();
    
    // [Running] php ".../php/demo.php"
    // string(4) "Demo"
    
    package main
    
    import (
    	"fmt"
    )
    
    //基础结构体
    type Base struct {
    }
    
    // Base的DemoFun
    func (b *Base) DemoFun() {
    	fmt.Println("Base")
    }
    
    func (b *Base) PublicFun() {
    	b.DemoFun()
    }
    
    type Demo struct {
    	// 合成复用Base
    	Base
    }
    
    // Demo的DemoFun
    func (d *Demo) DemoFun() {
    	fmt.Println("Demo")
    }
    
    func main() {
    	// 执行
    	(&Demo{}).PublicFun()
    }
    
    // [Running] go run ".../demo/main.go"
    // Base <------ 注意此处执行的是被合成复用的结构体的方法
    

    进阶使用

    1. 热加载工具bee
    2. Goroutine并发控制之sync.WaitGroup包的使用
    3. 子Goroutine超时控制之context.Context包的使用
    4. 并发安全的map之sync.Map包的使用
    5. 减少GC压力之sync.Pool包的使用
    6. 减少缓存穿透利器之singleflight包的使用
    7. Channel的使用
    8. 单元测试&基准测试
    9. 性能分析

    1.热加载工具bee

    使用:

    安装
    go get github.com/beego/bee/v2
    
    热加载方式启动项目
    SOAAGENT=10.40.24.126 bee run -main=main.go -runargs="start"
    

    2.Goroutine并发控制之sync.WaitGroup包的使用

    使用:

    package main
    
    import (
    	"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包的使用

    使用:

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    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("超时退出")
    			return
    		case <-execResult:
    			fmt.Println("处理完成")
    			return
    		}
    	}(ctx)
    
    	time.Sleep(10 * time.Second)
    }
    
    // [Running] go run ".../demo/main.go"
    // 超时退出
    

    4.并发安全的map之sync.Map包的使用

    使用:

    package main
    
    import (
    	"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
    // 没有panic
    

    5.减少GC压力之sync.Pool包的使用

    使用:

    5.1 不使用sync.Pool代码示例

    package main
    
    import (
    	"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.Pool
    func BenchmarkDemo_NoPool(b *testing.B) {
    	b.RunParallel(func(pb *testing.PB) {
    		for pb.Next() {
    			// 直接初始化
    			addressModule := &AddressModule{}
    			addressModule.Consignee = ""
    			addressModule.Email = ""
    			addressModule.Mobile = 0
    			addressModule.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 = false
    			addressModule.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  	demo	21.782s
    
    

    不使用sync.Pool执行分析:火焰图&Top函数

    可以很明显看见GC过程消耗了大量的CPU。

    PHP到Go速转手册 PHP到Go速转手册

    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: "",
    				},
    				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 = 0
    			addressModule.Country.ID = 0
    			addressModule.Country.Name = ""
    			addressModule.Province.ID = 0
    			addressModule.Province.Name = ""
    			addressModule.County.ID = 0
    			addressModule.County.Name = ""
    			addressModule.Street.ID = 0
    			addressModule.Street.Name = ""
    			addressModule.DetailedAddress = ""
    			addressModule.PostalCode = ""
    			addressModule.IsDefault = false
    			addressModule.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  	demo	14.215s
    
    

    使用sync.Pool执行分析:火焰图&Top函数

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

    PHP到Go速转手册 PHP到Go速转手册
    关于火焰图和Top函数的使用下面会讲到。
    

    6.减少缓存穿透利器之singleflight包的使用

    使用:

    package main
    
    import (
    	"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()
    			// 使用singleflight
    			res, 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()
    }
    
    

    没有使用Singleflight一共发起了3次请求
    PHP到Go速转手册
    使用Singleflight只发起了1次请求
    PHP到Go速转手册

    7. Channel的使用

    使用:

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    // 响应公共结构体
    type APIBase struct {
    	Code    int32  `json:"code"`
    	Message string `json:"message"`
    }
    
    // 模拟接口A的响应结构体
    type APIDemoA struct {
    	APIBase
    	Data APIDemoAData `json:"data"`
    }
    
    type APIDemoAData struct {
    	Title string `json:"title"`
    }
    
    // 模拟接口B的响应结构体
    type APIDemoB struct {
    	APIBase
    	Data APIDemoBData `json:"data"`
    }
    
    type APIDemoBData struct {
    	SkuList []int64 `json:"sku_list"`
    }
    
    // 模拟接口逻辑
    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("退出")
    			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. 单元测试&基准测试

    使用:

    8.1 单元测试

    一个很简单的单元测试示例:

    package main
    
    import (
    	"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 main
    
    import (
    	"fmt"
    	"io/ioutil"
    	"net/http"
    	"testing"
    )
    
    type Req struct {
    	UserID int64
    }
    
    func TestDemo(t *testing.T) {
    	t.Parallel()
    	tests := []struct {
    		TestName string
    		*Req
    	}{
    		{
    			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  	demo	7.984s
    
    

    8.2 基准测试

    简单的基准测试:

    package main
    
    import (
    	"sync"
    	"testing"
    )
    
    // 压力测试sync.Map
    func 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  	demo	13.623s
    

    对比基准测试:

    package main
    
    import (
    	"sync"
    	"testing"
    )
    
    // 压力测试sync.Map
    func 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")
    			}
    		}
    	})
    }
    
    // 用读写锁实现一个并发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("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  	demo	23.823s
    
    

    9.性能分析

    • pprof
    • trace
    • dlv

    使用:

    9.1 pprof的使用

    9.1.1 基准测试场景

    1. 首先编写基准测试用例,复用上面sync.Map的用例:
    package main
    
    import (
    	"sync"
    	"testing"
    )
    
    // 压力测试sync.Map
    func 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")
    			}
    		}
    	})
    }
    
    1. 执行基准测试,生成cpu.profile文件和mem.profile 文件。命令如下

    常用参数解释:

    -benchmem: 输出内存指标
    -run: 正则,指定需要test的方法
    -bench: 正则,指定需要benchmark的方法
    -v: 即使成功也输出打印结果和日志
    -count: 执行次数
    -cpuprofile: 输出cpu的profile文件
    -memprofile: 输出内存的profile文件
    -benchtime: 执行时间
    
    更多参数请查看:
    go help testflag
    
    1. 使用go tool自带的pprof工具分析测试结果。命令如下:

    常用参数解释:

    -http: 指定ip:port,启动web服务可视化查看分析,浏览器会自动打开页面 http://localhost:8000/ui/
    

    可视化选项菜单
    PHP到Go速转手册
    火焰图
    PHP到Go速转手册
    调用链路图
    PHP到Go速转手册
    Top函数
    PHP到Go速转手册

    9.1.2 Web服务场景

    1. 使用上面全局变量的代码示例,引入net/http/pprof包,并单独注册各端口获取pprof数据。
    package main
    
    import (
    	"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. 访问链接 http://localhost:8888/debug/pprof/,可以看见相关profiles。

    PHP到Go速转手册

    1. 命令使用pprof工具,获取远程服务profile,命令如下:
    备注:
    执行上面命令的时候,可以使用压测工具模拟流量,比如命令:siege -c 50 -t 100 "http://localhost:8080/ping"
    

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

    PHP到Go速转手册

    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
    PHP到Go速转手册 PHP到Go速转手册 PHP到Go速转手册

    使用sync.Pool
    PHP到Go速转手册

    9.2.2 Web服务场景

    使用:

    同样引入包net/http/pprof

    package main
    
    import (
    	"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 右移
    

    PHP到Go速转手册 PHP到Go速转手册 PHP到Go速转手册 PHP到Go速转手册 PHP到Go速转手册

    9.3 dlv工具的使用

    9.3.1 基准测试场景

    作用:断点调试等。

    安装:

    go install github.com/go-delve/delve/cmd/dlv@latest
    

    使用:

    package main
    
    import (
    	_ "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()
    }
    
    

    命令行执行命令:

    进入调试,常用调试命令:

    • (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
    

    PHP到Go速转手册

    PHP到Go速转手册

    9.3.2 Web服务场景

    还是这个demo

    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,
    		})
    	})
    	// 启动web服务
    	r.Run()
    }
    
    
    • 找到服务进程ID lsof -i :8080
    • dlv调试进程 dlv attach 36968
    • 进入调试模式,调试代码(和上面一样)

    9.4(扩展) 逃逸分析

    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(扩展) 汇编代码

    # 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       
    		略......                              
    
    dumped SSA to ./ssa.html <------- 生成的文件,浏览器打开此文件
    

    PHP到Go速转手册

    总结

    最后我们再总结下,从PHPer到Gopher的过程,我们重点需要关注的几点如下:

    • PHP和Go常用代码块的对应关系
    • 常驻内存
      • 全局变量使用
      • 资源
        • 复用
        • 释放
        • 返还
    • 指针
    • 并发
      • 并发安全
      • 并发控制
      • 超时控制
    • 单元测试&基准测试
    • 性能分析

    起源地下载网 » PHP到Go速转手册

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元