Эта статья длинная, рекомендуется нажать на исходный текст в конце статьи и просмотреть после сбора. Или добавьте в закладки подключение к ПК 🔗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 |
Руководство по предотвращению ям
- Используйте глобальные переменные с осторожностью, глобальные переменные не будут уничтожены после запроса, такого как PHP
- Формальный параметр
slice
,map
Тип параметра, обратите внимание, что значение может быть изменено глобально - Когда ресурсы израсходованы, не забудьте освободить или переработать ресурсы.
- Не полагайтесь на порядок обхода карты
- Не пишите карты одновременно
- Обратите внимание, что определение того, что тип указателя не является нулевым
nil
, а затем оперировать - Язык 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, ¶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] = 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 <------ 注意此处执行的是被合成复用的结构体的方法
Расширенное использование
- инструмент для горячей загрузки пчелы
- Горутинный контроль параллелизма
sync.WaitGroup
Использование пакетов - Контроль тайм-аута дочерней горутины
context.Context
Использование пакетов - Параллельно-безопасная карта
sync.Map
Использование пакетов - уменьшить давление ГХ
sync.Pool
Использование пакетов - Один из лучших инструментов для уменьшения проникновения в кеш
singleflight
Использование пакетов - Использование канала
- Юнит-тестирование и бенчмаркинг
- анализ производительности
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 Сценарий эталонного тестирования
- Сначала напишите контрольный тестовый пример, повторно используйте приведенный выше
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")
}
}
})
}
- Выполнить тесты, сгенерировать
cpu.profile
документы иmem.profile
документ. Команда выглядит следующим образом
go test -benchmem -run=^ 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/
Меню опций визуализации
график пламени
схема связи вызова
Верхняя функция
9.1.2 Сценарий веб-сервиса
- Используя приведенный выше пример кода для глобальных переменных, импортируйте
net/http/pprof
package и зарегистрировать каждый порт отдельно для получения данных 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()
}
- перейти по ссылке
http://localhost:8888/debug/pprof/
, вы можете увидеть соответствующие профили.
- Команда использует инструмент 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
- процесс отладки dlv
dlv 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/