Руководство по быстрому переносу PHP to Go

задняя часть Go PHP

Эта статья длинная, рекомендуется нажать на исходный текст в конце статьи и просмотреть после сбора. Или добавьте в закладки подключение к ПК 🔗tigerb.cn/php2go/

Уведомление об авторских правах

  • Распространение данного руководства и существенно измененных версий запрещено без прямого разрешения владельца авторских прав.
  • Распространение этой работы и производных работ в стандартной (бумажной) книжной форме запрещено без предварительного разрешения владельца авторских прав.
  • Ни в коем случае не сотрудничать с какой-либо третьей стороной.

предисловие

Краткое руководство было составлено, чтобы помочь вам эффективно начать работу с языком Go, в основном путем сравнения различий между PHP и Go, чтобы улучшить ваше понимание Содержание в основном разделено на следующие четыре части:

  • различия языковых уровней
  • Основные синтаксические различия
  • Руководство по предотвращению ям
  • Расширенное использование

различия языковых уровней

备注:下文基于PHP主流php-fpm模式。
Контраст PHP Go
строковое представление Одинарная цитата (PSR) Двойные кавычки
объединить строки . +
Совместимость языковых версий фигово обратная совместимость
стиль кода Нет официальных стандартов, стандарты сообщества начинают действовать с опозданием Официально унифицированные стандарты с самого начала и инструменты предоставляются
язык сценариев да нет
строго типизированный язык Нет (PHP7 поддерживает строгий режим) да
Поддерживать ли сборку мусора да да
Объектно-ориентированный язык (ООП) выглядит как Частичная поддержка, ядро ​​синтетическое повторное использование
Поддерживать ли наследование да Нет (с синтетическим мультиплексированием)
Поддерживать ли интерфейс да да
Поддерживать ли try...catch... да нет
Поддерживать ли управление пакетами да да
Поддерживать ли кроссплатформенность да да
Стоимость экологического строительства высокий Низкий
способ исполнения режим командной строки cli, режим php-fpm (①) бинарный
модель процесса мультипрогресс один процесс
Поддерживает ли натив создание служб TCP/UDP Да (плохая поддержка, недоступна в продакшене) да
Поддерживает ли натив создание HTTP-сервисов Да (плохая поддержка, недоступна в продакшене) да
блокировка процесса да нет
Поддерживать ли сопрограммы Нет (②) да
Параллелизм (③) слабый чрезвычайно сильный
Следует ли запускать в резидентной памяти не (④) да
метод импорта файла requireилиincludeсоответствующий файл importимпортный пакет
Поддерживать ли модульное тестирование да да
Поддерживать ли бенчмарки (бенчмарк) нет да
Поддерживать ли анализ производительности Поддержка (xhprof/tideways) Поддержка (pprof/dlv)
Стоимость использования инструментов анализа производительности Высокая (стоимость расширения высока) очень низко
①其他模式还有swoole等
②PHP的swoole协程框架等支持协程
③此处不考虑I/O多路复用,PHP的swoole协程框架等也支持协程并发
④PHP的swoole协程框架是常驻内存,cli命令行模式也可以常驻内存等

В начале процесса перехода с языка PHP на язык Go основное внимание уделяется изменению понимания программирования, особенно следующим моментам:

  • Строгая типизация
  • операция с резидентной памятью
  • Понимание и использование указателей
  • Параллельная безопасность
  • Своевременное освобождение или возврат ресурсов

Основные синтаксические различия

备注:下文基于PHP5.4+版本

Сравнение распространенных базовых типов

Типов PHP относительно немного, и они просты.Обычные типы данных в PHP включают логические, строковые, целочисленные, плавающие, массивы и объекты.

Общие типы данных в PHP сравниваются с соответствующими или похожими типами в языке Go следующим образом:

язык\тип boolean string int float array object
PHP bool string int float array(1,2,3) индексный массив, array('1' => 1, '2' => 2, '3' => 3) ассоциативный массив создать экземпляр класса
Go bool string int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64 поплавок32, поплавок64 [length]type больше похоже на структуру

Кроме того, Go также поддерживает более богатые типы:

тип
slice slice (эквивалентно индексированному массиву PHP)
map (эквивалентно ассоциативному массиву PHP)
канал (канал, делиться через общение, не общаться через обмен)
Указатели (типы значений в языке Go имеют соответствующие типы указателей)
байт (байт, соответствующий псевдониму uint8, может представлять код Ascaii)
руна (соответствует int32, может представлять юникод)
и т.д
пользовательский тип, например.type userDefinedType int32
## Сравнение распространенных методов инициализации базового типа
тип PHP Перейти (определить переменную с помощью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{}
кусочек Как и выше, PHP называется данными индекса. var varSlice []int32 = []int32{}Срезы будут автоматически расширяться относительно данных
map $varMap = array('key' => 'value'); var varMap map[string]int32 = map[string]int32{}
закрытие $varClosure = function() {}; var varClosure func() = func() {}
channel никто var varChannel chan string = make(chan string)нет буферного канала;
var varChannelBuffer chan string = make(chan string, 6)Есть буферизованный канал

Сравнение создания экземпляров классов 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
Сериализация (устаревшая) сериализовать(), десериализовать() СумкаGitHub.com/Ву Лицзюнь/go-…
md5 md5() пакет крипто/md5
вывод терминала эхо, var_dump и т. д. fmt.Println(a ...interface{})
Различные виды обмена интервал () и т. д. пакет strconv

Руководство по предотвращению ям

  1. Используйте глобальные переменные с осторожностью, глобальные переменные не будут уничтожены после запроса, такого как PHP
  2. Формальный параметрslice,mapТип параметра, обратите внимание, что значение может быть изменено глобально
  3. Когда ресурсы израсходованы, не забудьте освободить или переработать ресурсы.
  4. Не полагайтесь на порядок обхода карты
  5. Не пишите карты одновременно
  6. Обратите внимание, что определение того, что тип указателя не является нулевымnil, а затем оперировать
  7. Язык Go не поддерживает наследование, но есть синтетическое повторное использование

1. Используйте глобальные переменные с осторожностью.Глобальные переменные не будут уничтожены после завершения запроса, такого как PHP.

package main

import (
    "sync/atomic"

    "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 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. Не полагайтесь на порядок обхода карты

В прошлом порядок элементов в «Карте» PHP (ассоциативном массиве) был стабильным независимо от того, сколько раз он был пройден, следующим образом:

<?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. Не пишите карты одновременно

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. инструмент для горячей загрузки пчелы
  2. Горутинный контроль параллелизмаsync.WaitGroupИспользование пакетов
  3. Контроль тайм-аута дочерней горутиныcontext.ContextИспользование пакетов
  4. Параллельно-безопасная картаsync.MapИспользование пакетов
  5. уменьшить давление ГХsync.PoolИспользование пакетов
  6. Один из лучших инструментов для уменьшения проникновения в кешsingleflightИспользование пакетов
  7. Использование канала
  8. Юнит-тестирование и бенчмаркинг
  9. анализ производительности

1. Горячая загрузочная пчела

Функция: запуск кода Go в режиме горячей загрузки будет отслеживать изменения кода и повторно запускать код для повышения эффективности разработки.

использовать:

安装
go get github.com/beego/bee/v2

热加载方式启动项目
SOAAGENT=10.40.24.126 bee run -main=main.go -runargs="start"

2. Контроль параллелизма горутины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. Контроль времени ожидания подпрограммыcontext.ContextИспользование пакетов

Функция: первый формальный параметр языка Go обычно имеет тип context.Context, 1. Передача контекста 2. Управление таймаутом и выходом подпрограммы 3. Управление временем выхода подпрограммы

использовать:

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. Одна из карт с защитой от параллелизма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. Уменьшите давление ГХ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

Выполнение анализа без синхронизации. Пул: Функция Flame Graph & Top

Хорошо видно, что процесс GC потребляет много процессорного времени.

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: Flame Graph и Top Function

runtime.mallocgc больше не отображается сверху

关于火焰图和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()
}

Перехватить запрос доменного имени example.com: tcpdump host example.com

Всего было сделано 3 запроса без Singleflight
Только 1 запрос был сделан с использованием Singleflight

7. Использование канала

Роль: Не общайтесь через общую память, а реализуйте общую память через общение. эквивалент трубы.

использовать:

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. Юнит-тестирование и бенчмаркинг

Функция: отладка блоков кода и интерфейсов на этапе разработки; тестирование блоков кода и интерфейсов для анализа проблем с производительностью, включая использование ЦП, использование памяти и т. д. Можно провести сравнительный тест. Этап ci определяет качество кода и уменьшает количество ошибок.

использовать:

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 документ. Команда выглядит следующим образом

go test -benchmem -run=^benchBenchmarkSyncMap -bench ^BenchmarkSyncMap 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
  1. использоватьgo toolВстроенный инструмент pprof анализирует результаты тестирования. Команда выглядит следующим образом:

go tool pprof -http=:8000 cpu.profile

Объяснение общих параметров:

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

Меню опций визуализации
график пламени
схема связи вызова
Верхняя функция

9.1.2 Сценарий веб-сервиса

  1. Используя приведенный выше пример кода для глобальных переменных, импортируйтеnet/http/pprofpackage и зарегистрировать каждый порт отдельно для получения данных 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/, вы можете увидеть соответствующие профили.

  1. Команда использует инструмент pprof для получения профиля удаленной службы.Команда выглядит следующим образом:

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

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

Опять же, мы получаем эту знакомую страницу:

9.2 Использование инструментов трассировки

Функция: Четко просматривать процесс выполнения Goroutine в каждом логическом процессоре, и вы можете интуитивно видеть потребление блокировки Goroutine, включая сетевую блокировку, блокировку синхронизации (блокировку), блокировку системных вызовов, ожидание планирования, время выполнения GC, STW GC ( Stop Мир) занимает много времени.

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 Сценарий веб-сервиса

использовать:

Также импортируйте пакет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 右移

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()
}

Команда выполнения из командной строки:

dlv debug main.go

Введите отладку, общие команды отладки:

  • (список или l: код вывода): list main.go:16
  • (перерыв или b: команда точки останова): выполнитьbreak main.go:16дать линиюGlobalVarDemo++точка останова
  • (продолжить или c: продолжить выполнение): продолжить
  • (напечатать или p: напечатать переменную):print GlobalVarDemo
  • (шаг или s: можно ввести функцию): шаг

Пожалуйста, выполните больше командhelp.

模拟请求:
curl http://localhost:8080/ping

9.3.2 Сценарий веб-сервиса

или это демо

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()
}

  • Найдите идентификатор сервисного процессаlsof -i :8080
  • процесс отладки dlvdlv attach 36968
  • Войдите в режим отладки и отладьте код (так же, как указано выше)

9.4 (Расширенный) Анализ выхода

Команда анализа побега: go build -gcflags "-m -l" *.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
"".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 <------- 生成的文件,浏览器打开此文件

Суммировать

Наконец, давайте подытожим процесс от PHPer до Gopher, Моменты, на которые мы должны обратить внимание, следующие:

  • Соответствие между общими блоками кода в PHP и Go
  • резидентная память
    • использование глобальной переменной
    • ресурс
      • мультиплекс
      • освобожден
      • возвращение
  • указатель
  • параллелизм
    • Параллельная безопасность
    • управление параллелизмом
    • Контроль времени ожидания
  • Юнит-тестирование и бенчмаркинг
  • анализ производительности

Эта статья длинная, рекомендуется нажать на исходный текст в конце статьи и просмотреть после сбора.

Или добавьте в закладки подключение к ПК 🔗tigerb.cn/php2go/