作为PHP程序员,我是这样学习Go语言的

为什么你应该学习Go语言呢?

作为多年的PHP开发工程师,你是不是总认为自己在堆砌业务代码?

一直以来,都是感觉无法提升自身的技术能力?

想要有所成长,总是控制不住自己,花费大量的时间去刷微博,朋友圈,抖音?

是时候,开始学习一门新的语言了,比如Go语言。

从PHP程序员的角度来说,我觉得有以下几点值得你入手Go语言:
1. Go语言简单,容易上手。你可以很快的上手,开发测试运维Go服务。
2. Go语言有效的提升了并发编程的体验,不再有复杂的并发和控制方式。
3. Go语言的常用库很丰富。基本Web开发,后端编程,网络编程基本上都有。
4. Go语言拥有C语言的灵活,拥抱底层,有着Python的简约,快速开发。

如何快速入门Go语言呢

我认为凡事都应该会有方法论的,除了方法论之外,剩下就是践行了。学好一门静态语言的方法论,大致有一下这些:
1. 基础且必要的知识概念
2. 用自己的熟悉的东西做类比
3. 最后就是亲自实践了

在经过一段时间的学习后,我从以下几个方面对比了PHP和Go的区别:
1. 变量申明
2. 包管理
3. 面向对象
4. 接口
5. 单元测试
6. 协程
7. Http服务

PHP vs Go

变量申明

在PHP的语法里是没有类型的概念,变量是直接申明即可使用的。

<?php
// 数字
$geeks = 11;
// 字符串
$geeks = 'hello world';
// 申明一个数组
$geeks = [];
// 申明一个空对象
$who = new stdClass();

但是Go语言有,你可以使用自动类型推断,也可以申明变量的类型。

package main

import (
    "fmt"
)

func main() {
    // 申明变量geekwho,并自动赋值为0,默认Go会推断为int的类型
    geeks1 := 0
    // 你也可以直接申明为int类型的变量geekwho
    var geeks2 int
    // 申明两个变量i,j为int的类型并初始化
    var i, j int = 1, 2
    // 申明3个变量并初始化
    var go_, php, java = true, false, "no!"
    // 申明了变量,请立即使用
    fmt.Println(geeks1,geeks2,i,j,go_,php,java)
}

包管理

在PHP里,我们使用composer包来管理相关库。通过composer require monolog/monolog 命令安装包,通过include_once 加载包。

<?php
// 在项目引入composer包
$autoload = "vendor/autoload.php";
if(file_exists($autoload)){
    include_once $autoload;
}

在Go语言是直接通过import导入相关的包,下面的例子,引入fmt包以及math包,进行自动格式化和数学相关的函数计算。

package main

import "fmt"
/**
 * 每次导入一个包
 * import "fmt"
 * import "math"
 *
 * 也可以分组导入
 * import (
 *     "fmt"
 *     "math"
 * )
 */

func main() {
    fmt.Println("hello world")
}

面向对象

举例,PHP申明一个类helloWorld,有一个公共方法run计算平方根。

<?php
    class HelloWorld
    {
        public $x;
        public $y;
        public function __construct($x , $y){
            $this->x = $x;
            $this->y = $y;
        }
        public function run()
        {
            echo sqrt($this->x * $this->x + $this->y * $this->y);
        }
    }
    (new HelloWorld(3,4))->run();

Go的世界里没有类的概念,只有结构体,也可以实现类。

package main

import (
    "fmt"
    "math"
)
// 申明一个结构体
type HelloWorld struct {
    X, Y float64
}
// 添加一个Run方法,首字母大写为公用方法,小写为私有方法。
func (v HelloWorld) Run() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    // 初始化类
    v := HelloWorld{3, 4}
    fmt.Println(v.Run())
}

接口

代码申明了一个开关的接口,有两个方法on和off。

<?php
    interface switchs {
        public function on();
        public function off();
    }
    class Light implements switchs
    {
        public $status;
        public function __construct(){
        }
        public function on()
        {
            $this->status = 'light is on';
            echo $this->status . PHP_EOL;
        }
        public function off()
        {
            $this->status = 'light is off';
            echo $this->status . PHP_EOL;
        }
    }
    $light = new Light();
    $light->on();
    $light->off();

那么用Go如何实现呢?

package main

import (
    "fmt"
)
// 申明接口有2个方法
type Switchs interface {
    On() string
    Off() string
}

// 申明一个结构体,类似PHP的Light类
type Light struct {
    Status string
}
//添加一个On方法
func (l Light) On() string {
    l.Status = "light is on"
    return l.Status
}
//添加一个Off方法
func (l Light) Off() string {
    l.Status = "light is off"
    return l.Status
}

func main() {
    // 初始化类
    l := Light{}
    fmt.Println(l.On())
    fmt.Println(l.Off())
}

单元测试

PHP采用phpunit实现一个单元测试。

<?php
class PHPTest extends \PHPUnit\Framework\TestCase
{
    public function testRun()
    {
        $tests = [
            '\stdClass',
        ];
        foreach ($tests as $test) {
            $this->assertEquals(
                true,
                class_exists($test),
                "$test class not found"
            );
        }
    }
}

Go语言自带单元测试功能,只需要加载两个包即可。

// unittest.go
package main

func Sum(a, b int) int {
    return a + b
}
// unittest_test.go
package main

import (
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestRun(t *testing.T) {
    val := Sum(1, 2)
    assert.Equal(t, 3, val)
}

协程

PHP本身没有协程的概念,可以使用Swoole扩展实现。

<?php
class SwooleCoroutine
{
    public function run()
    {
        extension_loaded('swoole') or die('swoole extension is not installed');
        $this->case1();
        $this->case2();
        $this->case3();
    }

    /**
     * 开启3个协程,无阻塞IO
     */
    public function case1()
    {
        echo "run " . __FUNCTION__ ." ". microtime(true). PHP_EOL;

        go(function () {
            echo "hello go1 " . PHP_EOL;
        });

        echo "hello main " . PHP_EOL;

        go(function () {
            echo "hello go2 " . PHP_EOL;
        });
    }


    /**
     * 开启3个协程,第一个协程阻塞
     */
    public function case2()
    {
        echo "run " . __FUNCTION__ ." " . microtime(true). PHP_EOL;

        go(function () {
            Co::sleep(1); // 只新增了一行代码
            echo "hello go1 " . PHP_EOL;
        });

        echo "hello main " . PHP_EOL;

        go(function () {
            echo "hello go2 " . PHP_EOL;
        });
    }

    /**
     * 开启3个协程,第3个协程阻塞
     */
    public function case3()
    {
        echo "run " . __FUNCTION__ ." ". microtime(true). PHP_EOL;

        go(function () {
            echo "hello go1 " . PHP_EOL;
        });

        echo "hello main " . PHP_EOL;

        go(function () {
            Co::sleep(1); // 只新增了一行代码
            echo "hello go2 " . PHP_EOL;
        });
    }
}
(new SwooleCoroutine())->run();

Go的协程内部是采用复杂的MPG调度器实现。最后的一句time.Sleep(time.Second) 是为了等待协程完成。

package main

import (
    "fmt"
    "time"
)

func say(s string , b bool) {
    if b {
        time.Sleep(100 * time.Millisecond)
    }
    fmt.Println(s,time.Now().UnixNano())
}

func case1() {
    fmt.Println("In case1()-",time.Now().UnixNano())
    go say("case1 hello go1",false)
    say("case1 hello main",false)
    go say("case1 hello go2",false)
}

func case2() {
    fmt.Println("In case2()-",time.Now().UnixNano())
    go say("case2 hello go1" , true)
    say("case2 hello main",false)
    go say("case2 hello go2",false)
}

func case3() {
    fmt.Println("In case3()-",time.Now().UnixNano())
    go say("case3 hello go1",false)
    say("case3 hello main",false)
    go say("case3 hello go2" , true)
}

func main() {
    fmt.Println("In main()---",time.Now().UnixNano())
    case1()
    case2()
    case3()
    time.Sleep(time.Second)
}

实战Http服务

用PHP的Swoole扩展实现一个简单的Http服务。

<?php
// 启动本地的2018端口的Http Server
$server = new Swoole\Http\Server("0.0.0.0", 2018, SWOOLE_BASE);

// 设置worker 数量 和 守护进程化
$server->set([
    'worker_num' => 1,
    'daemonize' => 1,
]);

// 添加请求回调
$server->on('Request', function ($request, $response) {
    // 加载协程类
    include_once dirname(__DIR__) . DIRECTORY_SEPARATOR . 'coroutine/coroutine.php';
    // 从缓冲区获取数据
    ob_start();
    (new SwooleCoroutine())->run();
    $http = ob_get_contents();
    ob_end_clean();
    // 输出结果
    $response->end($http);
});
// 启动服务
$server->start();

Go语言的话,直接采用采用默认的http包就可以了。

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "hello world!")
    })

    fs := http.FileServer(http.Dir("static/"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))

    http.ListenAndServe(":2019", nil)
}

代码

代码放在GitHub版本库。

总结

文章分享了如何快速入门Go语言的方法论,并亲自用熟悉的概念将PHP与Go进行了对比。当然,如果仅仅只是一篇文章,是不足以深入了解语言的真谛,还需要花费时间亲自去践行,实践才检验真理的唯一标准。

参考链接

  1. GO语言、DOCKER 和新技术
  2. Why should you learn Go?
  3. Swoole PHP Coroutine
  4. swoole| swoole 协程初体验
  5. The Go scheduler
  6. The Go Programming Language
  7. Go Web Examples
  8. Package httptest

Tars实践

什么是Tars

Tars这个名字取自于电影”星际穿越”中的机器人,它是基于名字服务使用Tars协议的高性能RPC开发框架,配套一体化的运营管理平台,并通过伸缩调度,实现运维半托管服务。

Tars是腾讯从2008年到今天一直在使用的后台逻辑层的统一应用框架TAF(Total Application Framework),目前支持C++,Java,PHP,Nodejs语言。该框架为用户提供了涉及到开发、运维、以及测试的一整套解决方案,帮助一个产品或者服务快速开发、部署、测试、上线。 它集可扩展协议编解码、高性能RPC通信框架、名字路由与发现、发布监控、日志统计、配置管理等于一体,通过它可以快速用微服务的方式构建自己的稳定可靠的分布式应用,并实现完整有效的服务治理。

目前支持的开发语言如下:

  • C++
  • Java
  • Nodejs
  • PHP
  • Go

协议支持的类型如下:

特点 类型
基本类型 void
bool
byte
short
int
long
float
double
string
unsigned byte
unsigned short
unsigned int
复杂类型 enum
const
struct
vector
map
struct、vector、map的嵌套

其中复杂类型可以嵌套使用,例如:

# map 
map<int,string>
# struct、vector、map的嵌套
vector<string>
vector<map<string,string>>
map<vector<string>,vector<string>>

为什么要用Tars

  1. 性能较好,官方测试 在8 CPU+16GB+50进程+16线程下,TPS可以达41w/s。性能数据
  2. 这套框架比较重,提供涉及到开发、运维、测试等一整套的解决方案。
  3. Tars协议只是实现该方案的高性能RPC协议而已。
  4. 在开源界里,涉及服务治理,多语言,暂时只有Tars一家。

怎么用Tars

架构图

架构拓扑图

核心基础服务

服务名 类型 功能
tarsAdminRegistry 核心 服务注册,接入管理服务
tarsregistry 核心 服务发现,也叫主控,名字服务路由
tarsnode 核心 服务管理,节点管理服务
tarsconfig 核心 配置中心,配置服务
tarspatch 核心 自动发布,发布服务
tarsnotify 普通 服务上报,异常上报统计服务
tarsstat 普通 服务统计,模调数据统计服务
tarsproperty 普通 业务属性统计服务
tarslog 普通 服务日志,日志服务
tarsqueryproperty 普通
tarsquerystat 普通

部署方法

Tars的Docker镜像并未打包MySQL,因此需要自行独立启动MySQL服务,然后再启动Web管理服务。

相关镜像

镜像名称 备注
mysql:5.7 MySQL 5.7官方镜像
tarscloud/tars:php7 开发环境镜像,包含php7.2/swoole/phptars等扩展

拉取相关镜像

docker pull mysql:5.7
docker pull tarscloud/tars:php7

启动MySQL

docker run --name mysql -e MYSQL_ROOT_PASSWORD=password -d -p 3306:3306 -v /tars.data:/var/lib/mysql mysql:5.7 --sql_mode=NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION --innodb_use_native_aio=0

启动Web管理服务

docker run -d -it --name tars --link mysql --env MOUNT_DATA=true --env DBIP=mysql --env DBPort=3306 --env DBUser=root --env DBPassword=password -p 3000:3000 -v /tars.dev:/data tarscloud/tars:php7

进入运行环境,查看问题

docker exec -it tars bash

登录数据库,查看数据

mysql -hmysql -P3306 -uroot -p

访问Web管理后台,浏览器打开

http://0.0.0.0:3000/

遇到的问题

尽量优先查看相关的服务日志,然后阅读并搜索相关的源码,查找并定位问题。

Tars源代码阅读

cd dev
# 拉取Tars源代码
git clone git@github.com:TarsCloud/Tars.git
cd Tars
# 更新子模块代码
git submodule update --init --recursive

日志

日志服务 日志路径 备注
服务日志 /usr/local/app/tars/app_log/ /usr/local/app/tars/app_log/应用名/服务名/目录下
Tars管理错误日志 /data/logs/tars-node-web-error.log
Tars管理日志 /data/logs/tars-node-web-out.log
核心服务日志 /data/tars/app_log/tars
./tarsAdminRegistry/tars.tarsAdminRegistry.log tarsAdminRegistry
./tarsregistry/tars.tarsregistry.log tarsregistry
./tarsnode/tars.tarsnode.log tarsnode
./tarsconfig/tars.tarsconfig.log tarsconfig
./tarspatch/tars.tarspatch.log tarspatch
./tarsnotify/tars.tarsnotify.log tarsnotify
./tarsstat/tars.tarsstat.log tarsstat
./tarsproperty/tars.tarsproperty.log tarsproperty
./tarslog/tars.tarslog.log tarslog
./tarsqueryproperty/tars.tarsqueryproperty.log tarsqueryproperty
./tarsquerystat/tars.tarsquerystat.log tarsquerystat
PHPTest /data/tars/app_log/PHPTest/PHPServer PHPTest Server
./log_debug.log debug日志
./PHPTest.PHPServer.log 服务启动日志

服务路径

服务 路径 IP/Port
核心服务路径 /usr/local/app/tars 1000X开头
Web管理服务 /usr/local/tarsweb 0.0.0.0:3000

内部错误的问题

现象:点击服务管理或者运维管理都是出现内部错。

error: 172.17.0.1||TreeController.js:30|[listTree] SequelizeConnectionRefusedError: connect ECONNREFUSED 127.0.0.1:3306

# 编辑Web管理配置文件
vim /usr/local/tarsweb/config/webConf.js
# 修改数据库链接
host: '127.0.0.1' => host: 'mysql'

出现这个错误的原因是MySQL链接失败。如果在docker内部已经link上MySQL,直接修改地址即可。否则,重新初始化安装Web管理服务,在docker 加入正确的MySQL参数。

--env DBIP=mysql --env DBPort=3306 --env DBUser=root --env DBPassword=password

数据库问题

发现数据库tars_stat和tars_property为空。执行模板相关的SQL,发现会提示下面这个问题。

ERROR 1171 (42000): All parts of a PRIMARY KEY must be NOT NULL; if you need NULL in a key, use UNIQUE instead

继续 查找原因,发现部分PRIMARY KEY 不能为空,这个与MySQL5.7的SQL模式有关,开始不兼容这类SQL语句了。

PRIMARY KEY (`source_id`,`f_date`,`f_tflag`,`master_name`,`slave_name`,`interface_name`,`master_ip`,`slave_ip`,`slave_port`,`return_value`,`tars_version`)
# 举例
`source_id` varchar(15) default NULL
# 在MySQL5.7需要修改为下面的方式
`source_id` VARCHAR(15) NOT NULL DEFAULT ''

# 同理
`slave_port` int(10) default NULL
修改为
`slave_port` INT(10) NOT NULL DEFAULT 0

更新模板

  1. 找到运维管理
  2. 进入到模板管理
  3. 找到tars.tarsstat->编辑->修改内容为tars.tarsstat.md
  4. 保存
  5. 重启tarsstat服务,服务tarsproperty也一样处理。

这样数据库建表就成功了。

Connection refused

在相关日志中查看,发现下面的问题

2019-01-18 12:03:05|9262|ERROR|[TARS][CommunicatorEpoll::handleInputImp] connect error tcp -h 172.17.0.5 -p 10003,tars.tarslog.LogObj,_connExcCnt=6,Connection refused

2019-01-18 12:04:00|9262|ERROR|[TARS][CommunicatorEpoll::handleInputImp] connect error tcp -h 172.17.0.5 -p 10005,tars.tarsproperty.PropertyObj,_connExcCnt=1,Connection refused

解决方案是服务管理中重启相关的服务,如tarslog、tarsproperty等。

实战

根据官方文档TARS PHP TCP服务端与客户端开发,我已经把完成版本的代码,放在GitHub上,你可以直接下载到目录执行composer install即可。

PHP TCP服务端开发

由于版本库地址变更,导致一些代码的链接失效。可以直接替换成最新的地址。

将
https://github.com/TarsPHP/TarsPHP/blob
替换为
https://raw.githubusercontent.com/TarsPHP/TarsPHP

为了加速composer安装,将下面的repo加入composer.json

"repositories": [
    {
        "type": "composer",
        "url": "https://packagist.phpcomposer.com"
    },
    {
        "packagist": false
    }
]

代码发布

composer run-script deploy

PHP客户端开发

composer问题

在composer install时出现错误。

Your requirements could not be resolved to an installable set of packages.
Problem 1
    - The requested package phptars/tars-client 0.1.1 exists as phptars/tars-client[0.1.5, 0.1.6, 0.2.0, 0.2.1, 0.2.2, dev-master] but these are rejected by your constraint.

修改composer.json中的phptars/tars-client版本为0.1.5或者0.2.0。

"phptars/tars-client" : "0.2.0"

日志错误

PHP Notice:  Undefined index: iRequestId in /data/tarsnode_data/PHPTest.PHPServer/bin/src/vendor/phptars/tars-server/src/protocol/TARSProtocol.php on line 103

PHP Notice:  Undefined index: protocolName in /data/tarsnode_data/PHPTest.PHPServer/bin/src/vendor/phptars/tars-server/src/core/Server.php on line 68

# 对代码做下兼容处理
$iRequestId = isset[$unpackResult['iRequestId']]?$unpackResult['iRequestId']:0;

$this->protocolName = isset($this->tarsServerConfig['protocolName'])?$this->tarsServerConfig['protocolName']:'';

请求异常

如果你在请求的时候遇到下面的错误:

PHP Fatal error:  Uncaught Exception: Rout fail in /data/web/client/vendor/phptars/tars-client/src/Communicator.php:82
Stack trace:

首先尝试telnet下

yum install telnet
telnet 172.17.0.5 2019 #可以成功连接上,说明服务器程序已经运行了。
# 找到你的docker内网地址
ip addr

客户端需要针对服务请求做一些调整,服务发现采用指定服务地址,需要修改index.php。

# host 为docker的内网地址 port 为你的服务端口号
$host = "172.17.0.5";
$port = "2019";
$config = new \Tars\client\CommunicatorConfig();
$config->setLocator(sprintf('tars.tarsregistry.QueryObj@tcp -h %s -p %s',$host,$port));

修改为下面
$host = "172.17.0.5";
$port = "2019";
$routeInfo[] = [
'sIp' => $host,
'iPort' => $port,
];
$config = new \Tars\client\CommunicatorConfig();
$config->setRouteInfo($routeInfo);

执行测试

cd client
php index.php

你会发现了久违的Hello World,在相关的日志中也有了。

[2019-01-18 10:07:10] tars_logger.INFO: sayHelloWorld name:ted [] []

疑问

类似核心服务tarsAdminRegistry如何实现多接点部署呢?

如果加入到监控体系,那么核心服务类似于tarsAdminRegistry挂了,监控体系可以自动拉取服务?我能想到的方案是必须哟一个监控脚本实时监控服务吧。

那为什么没有把对核心服务tarsAdminRegistry加入到这套服务治理的体系里面来呢?这有点像鸡生蛋,蛋生鸡的问题了。

总结

  1. 安装时,多查看相关的服务日志。
  2. 根据日志搜索相关源代码,定位问题。
  3. 多看说明文档。

参考链接

  1. Tars 基本介绍
  2. Tars 详细介绍
  3. Tencent Tars 的Docker镜像脚本与使用
  4. TARS PHP TCP服务端与客户端开发
  5. Tencent Tars 的Docker镜像脚本与使用 GitHub 源代码
  6. TARS Kubernetes 部署
  7. 腾讯 Tars 基础框架手动搭建——填掉官方 Guide 的坑
  8. TARS 快速入门
  9. 解读 | TARS 开源项目发布 Go 语言版本
  10. Tars PHP Demo
  11. 源代码 Tars TarsPHP

练习 切片

问题

实现 Pic。它应当返回一个长度为 dy 的切片,其中每个元素是一个长度为 dx,元素类型为 uint8 的切片。当你运行此程序时,它会将每个整数解释为灰度值(好吧,其实是蓝度值)并显示它所对应的图像。

图像的选择由你来定。几个有趣的函数包括 (x+y)/2, xy, x^y, xlog(y) 和 x%(y+1)。

(提示:需要使用循环来分配 [][]uint8 中的每个 []uint8;请使用 uint8(intValue) 在类型之间转换;你可能会用到 math 包中的函数。)

背景知识

  1. 学习Go语言的数组用法,多维数组的定义。
  2. 了解数组的内部实现,数组的长度和容量。
  3. 注意键值对的数组遍历。

实现思路

  1. 定义Pic函数,输入int参数,返回数组元素。
  2. 定义一维数组,将元素按照指定参数,添加到一维数组。
  3. 定义二维数组,将上面的一维数组循环添加到二维数组。

实现代码

GitHub

参考链接

  1. 练习:切片