作为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

如何立一个无效的flag?

有效的flag

如果你问我,新年如何立一个有效的flag?我多半是会语塞,因为我自己也没有想到。但在如何立一些无效flag,这方面的经验可以说是非常丰富。每年立一些无效的flag,可能是我这些年来唯一坚持下来的事情。

年终总结

很多年前,我学很多大牛,开始写了年终总结。大体上,是这样写的:
1. 针对过去一年的总结,大部分是工作,学习,生活。
2. 展望新的一年,关于成长,立一些无效的flag。

说真的,那些在文章中的立的flag,在写完文章后,可能我再也没有去看过。直到一年后的同一天想起来,当年还有这么一回事。但这时的脸已经被打肿了。根本不好意思,再写一遍年终总结。这也许是很久都没有写年终总结的原因之一了吧。迄今为止,我的草稿箱已经躺着好几篇年终总结,真的,再也没有勇气发出来了。

早起

在连一个早起的概念都没有整清楚的时候,就尝试去盲目地早起。看完007战友的文章后,热心沸腾,恨不得如果可能的话,每天早上就闻鸡起舞,总是以为改变就是下决定那一瞬间的事,甚至按照笑来老师的套路,在微信公众号上发表文章:
1. 首先写下这件事的重要意义,到底有多重要?是不是一定非要不可?
2. 其他,写下如果没有这件事的意义?你的生活是不是转不动了?
3. 再次,就是社交的学习,找到和你志同道合人一起早起。

但结果呢?好像也并没有什么效果,这是一个我注定无法完成的flag,只是”一瞬间”的想法决定,甚至连可执行的计划都没有的flag。你说,它不是失败,还有谁会失败呢?

卖课程

在听了三九的课程张三九的资本思维法:熊市定投,牛市定抛后,瞬间满血,感觉自己学到很多投资技巧,又可以去市场上大干一场,甚至为了三九同行者计划,建立一个营销计划。我们来看看我是如何建立的计划?
1. 为了这件事,强调了做这件事的意义
2. 设计了营销策略,有很多个方案
3. 甚至预估了实现的时间,营销的数据统计。
4. 推广的返现方案设计
5. 推广的文案设计,优化。
6. 我甚至还做一些反思。

然而,最终的结果呢?我只邀请到一个战友。在推广期间,还数次的刷新朋友圈对我的认知,他怎么也开始变相的推广买课程了?你看,这个flag,我立得非常好,但最终依然被证明是无效的。在最开始的打鸡血时间过后,在不到3个月后,我就把这个计划抛之脑后了。甚至是反思总结了相关问题,但就是没有持续地去做。践行,想起来很容易,行动起来,是那么的难。

写作

我在年初的时候加入了007不出局写作群。你可以看到我在博客写的都是阅读笔记。什么是阅读笔记?就是把别人的文章摘抄一部分出来。这就是我的阅读笔记。这和我们家乡的俗语,鸭背上浇水,有什么区别?只是看文章,看专栏,并没有什么用。在过了一段时间后,自然而然,就忘记了(不要问我为什么)。关键是大家都会去看,但真正按照文章地去做的人有多少呢?去实践了,处理实践中的各种意外,对实践中的各种意外或者结果进行反思总结,实践之后如何改进?这些看不见的思考,还有人在做吗?

这些年立了那么多的无效flag,我总结一套行之有效的方法论。
1. 当你立完flag后,安心去睡觉,不要想着下一步计划。
2. 一般立flag的最佳时机是年底之前到新年之前,这段时间春节的时间,会安慰你之前立flag被打脸的心理伤害。
3. 当你立完一个长期的flag后,践行一段时间后,自然而然,最好休息一段时间,恢复之前立flag消耗的精力。
4. 立flag的事情,最好不要坚持一年,不然你付出的成本相当大。

如果拿践行与王者荣耀的排位赛类比,那么我得到一个表格,如下:

阶段 段位
看到 青铜
知道 白银
想到 黄金
学到 铂金
做到 钻石
持续做到 星耀
反思总结 王者

当前的自己是处在哪一个段位中呢?反观自己,经常在钻石段位徘徊。有时候,人生最痛苦的事情,莫过于,啊,原来我真的不牛啊。只是芸芸众生中,把立无效flag,一直坚持下来的人。所以,关键在于,你想成为什么样的人?

堆排序

具体实现

GitHub版本库的堆排序。

算法实现

实现

该算法按照二叉树的原理。
完全二叉树:深度为k,有n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中,序号为1至n的节点对应时,称之为完全二叉树

 原数组 52,85,6,3,57,27,77,37,96,52
        0   1 2 3 4  5  6  7  8  9
              52
            /   \
           /     \
          85      6
         / \     / \
        /   \   /   \
        3   57 27   77
       / \  /
      6  3 25
 实现堆的排序,在起始数组为0的情况下:
 1. 数组下标i的左子节点在位置  (2*i+1)
 2. 数组下标i的右节点在位置    (2*i+2)
 3. 数组下标i的父节点在位置    floor(i-1)/2

 最大堆
               96
             /    \
            /      \
           85      77
          / \      / \
         /   \    /   \
         57  52  37   27
        / \  /
       25 6  3

 最小堆
               3
             /    \
            /      \
           6       25
          / \      / \
         /   \    /   \
        27   37  52   57
       / \   /
      77 85 96

难点

  1. 数组元素个数为n,则从最后一个父元素(最后一个非叶子节点)开始访问,即$i = floor($n/2)-1
  2. 从根元素开始访问,再访问左节点,再到右结点,称为先序遍历。
  3. 最大的堆,完成排序之后,是从小到大的数组。因为建立最大堆,是最大的在堆顶,但是第二次循环之后,
  4. 最大的被置换到数组最后一个数组。
  5. 最小堆,完成排序的数组是从大到小的数组。

耗时分析

机器配置 ThinkPad T440s CPU i5-4300U Memory 8GB
maxheap num 1000 sort cost time is 0.011452198028564 s
minheap num 1000 sort cost time is 0.0060770511627197 s
maxheap num 10000 sort cost time is 0.092055082321167 s
minheap num 10000 sort cost time is 0.090849161148071 s
maxheap num 100000 sort cost time is 1.2427570819855 s
minheap num 100000 sort cost time is 1.1724309921265 s

算法分析

最坏时间复杂度 О(nlog n)
最优时间复杂度 О(nlog n)
平均时间复杂度 О(nlog n)
空间复杂度   О(n)

参考链接

  1. 堆排序
  2. 堆排序(Heap Sort)算法学习