函数值-学习Go语言

阅读笔记

functions-values.go

package main

import (
    "fmt"
    "math"
)

func compute(fn func(float64, float64) float64) float64 {
    return fn(3, 4)
}

func main() {
    hypot := func(x, y float64) float64 {
        return math.Sqrt(x*x + y*y)
    }
    fmt.Println(hypot(5, 12))

    fmt.Println(compute(hypot))
    fmt.Println(compute(math.Pow))
}

笔记

函数也是值。它们可以像其它值一样传递。
函数值可以用作函数的参数或返回值。
1. 函数可以传值进去
2. 5 12 的平方根 = 13
3. (fn func(float64, float64) float64) 包含起来会好理解些,可以认为是一个申明。

functions-closures.go

package main

import "fmt"

func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    pos, neg := adder(), adder()
    for i := 0; i < 10; i++ {
        fmt.Println(
            pos(i),
            neg(-2*i),
        )
    }
}

笔记

  1. Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被“绑定”在了这些变量上。
  2. 可以想一想JavaScirpt的闭包概念,和Go的闭包是不是有一些类似?

参考链接

  1. 函数值
  2. 函数的闭包

消息队列的思考

什么是消息队列

维基百科的定义:在计算机科学中,消息队列(英语:Message queue)是一种进程间通信或同一进程的不同线程间的通信方式,软件的贮列用来处理一系列的输入,通常是来自用户。消息队列提供了异步的通信协议,每一个贮列中的纪录包含详细说明的数据,包含发生的时间,输入设备的种类,以及特定的输入参数,也就是说:消息的发送者和接收者不需要同时与消息队列交互。消息会保存在队列中,直到接收者取回它。

这定义太复杂了。简单的来讲就是排队系统嘛。这好比,每天到了中午吃饭的点,去麦当劳或者肯德基,都是要排队的嘛,毕竟,员工的数量有限,中午想吃饭的人会很多。大家都会先下单,然后拿着号码去排队取餐。这就是队列啊。

消息队列应用场景

消息队列的应用场景是,当前业务处理量遇到瓶颈,无法及时处理业务,将部分耗时业务分离到消息队列异步处理,避免业务处理失败。
这好比有人在麦当劳点了1个经典套餐,另外一个人点了7个儿童套餐过生日,门店的员工会根据下单情况进行处理,做好经典套餐直接让第一个人来取,肯定不会说,先做完7个儿童套餐再处理第一个任务。

消息队列,可以解耦复杂业务的逻辑,但不可避免,降低了用户的体验。如果每次去麦当劳都需要排对下单,取餐,我想你以后也不大想去他们家吃饭了吧。

优势

队列的好处:
1. 增大的系统的吞吐量,可以处理更多的”请求”。
2. 不管是耗时任务还是即时任务,都可以得到较好的处理。

劣势

队列的劣势:
1. 由于消息队列的存在,任务变成了异步处理,处理完成的时间不可预知。
2. 既然有异步的处理,必然会有数据不一致的情况。

常用的消息队列

RabbitMQ

消息模式

支持消息模式 备注
简单队列 支持单一的消费者
工作队列 支持多个消费者
发布订阅 通过exchange获取队列,消费队列的消息
路由队列 exchange_type='direct' 必须exchange和routing_key匹配才可以消费消费。
主题队列 exchange_type='topic' 可实现队列模糊匹配
RPC 支持RPC调用消费

性能

  1. 根据性能测试第二部分 第一部分 大约在4w个消息每秒,开启ack+持久化后,几乎下降了10倍。该测试在2012年发布的,仅供参考。

实现原理

  1. RabbitMQ是基于AMQP(Advanced Message Queuing Protocol,高级消息队列协议)实现的消息中间件。实现的语言是erlang。
  2. 常用较多的是路由模式。可以实现精确的消息匹配,只需要获取自定义的消息。

PHP支持

  1. 安装Composer包 php-amqplib/php-amqplib
  2. PHP扩展

Kafka

消费模式

暂时只支持一种topics。支持多个消费者,支持分组,可以平均分布消息到不同的分区。

性能

  1. 官方在2014年也做了性能测试。单机达到了80k消息每秒。官方性能测试 文章地址
  2. 可以说是为了性能而存在的消息队列。
  3. Kafka并不保证消息顺序,也不支持数据持久化,部署依赖zookeeper组件实现分布式。

实现原理

Kafka底层是Java实现的。

PHP支持

  1. 优先推荐PHP扩展 rdkafka 主要就基于librdkafka开发的PHP客户端。
  2. 也有相关的Composer包,该包不支持snappy压缩方式的消息。

参考链接

  1. 消息队列及常见消息队列介绍
  2. 消息队列
  3. RabbitMQ和Kafka

MaxWell的学习

什么是MaxWell?

MaxWell守护进程是读取MySQL的二进制日志,将更新操作写入到消息队列,例如Kafka,RabbitMQ,Redis的订阅发布的应用。

官网上的示例1,在数据库test的表maxwell插入了一条数据,那么你在名为maxwell的topics会接收到一条消息,含有数据库、表、时间戳、更新类型等字段。

mysql> insert into `test`.`maxwell` set id = 1, daemon = 'Stanislaw Lem';
  maxwell: {
    "database": "test",
    "table": "maxwell",
    "type": "insert",
    "ts": 1449786310,
    "xid": 940752,
    "commit": true,
    "data": { "id":1, "daemon": "Stanislaw Lem" }
  }

示例2,更新语句也是类似的。不同的是,更新的消息会多一个字段old,记录字段尚未被更新之前的数据。

 mysql> update test.maxwell set daemon = 'firebus!  firebus!' where id = 1;
  maxwell: {
    "database": "test",
    "table": "maxwell",
    "type": "update",
    "ts": 1449786341,
    "xid": 940786,
    "commit": true,
    "data": {"id":1, "daemon": "Firebus!  Firebus!"},
    "old":  {"daemon": "Stanislaw Lem"}
  }

生产者

MaxWell支持多种生产者,官网自带配置文档。以Kafka为例,默认会把消息发送到名为maxwell的topics,你也可以自定义配置

踩坑

在MaxWell启动后,Kafka的生产者可以进行高性能的配置。如果添加以下的配置

kafka.acks = 1
kafka.compression.type = snappy
kafka.retries=0

消息会以snappy的方式进行压缩。什么是snappy压缩方式
在采用Composer包nmred/kafka-php消费消息时,遇到MaxWell生产的消息无法读取。
后来定位到是该Composer包不支持snappy压缩的消息,更换为php-rdkafka的扩展即可。

参考链接

  1. Kafka 0.9 Configuration Best Practices
  2. Manually Installing the extension

配置系统的思考

配置系统在整个软件的生命周期里,有着很重要的角色。例如:

在新公司上班,第一天需要配置开发环境。。。

多环境部署,开发、测试、预发布、生产不同环境的配置。。。

项目的数据库,缓存,负载均衡,Nginx各种服务的配置。。。

做这么多年的开发,说来好笑,一直在跟配置系统打交道,但却没有花时间思考配置系统存在的意义。那么如何在目前Linux+Nginx+MySQL+PHP的构架下,尝试合适自己的配置系统呢?

常见的配置系统

既然不知道什么样的配置系统适合自己,那么我们可以看看别人家,是如何使用配置系统的。

Phalcon

Phalcon框架是我使用时间较长的开发框架,先看看它内部的配置如何实现。

<?php

use Phalcon\Config;

$config = new Config(
    [
        'test' => [
            'parent' => [
                'property'  => 1,
                'property2' => 'yeah',
            ],
        ],
    ]
);

echo $config->get('test')->get('parent')->get('property');  // displays 1
echo $config->test->parent->property;                       // displays 1
echo $config->path('test.parent.property');                 // displays 1

#甚至你可以注入配置到di
use Phalcon\Di\FactoryDefault;
use Phalcon\Config;

// Create a DI
$di = new FactoryDefault();

$di->set(
    'config',
    function () {
        $configData = require 'config/config.php';

        return new Config($configData);
    }
);

实现的原理是根据传入的数组转换Phlacon\Config对象的属性。支持不同类型配置器,例如ini、Json、Php、yaml等,你可以理解为Phlacon\Config是一个工厂模式,生产不同的类型的配置实例。

Magento

这是一个流行的开源电子商务框架。在1.x的框架版本都是采用xml去实现的。那时候发现xml做配置,非常“灵活”。注意,我这里用一个非常的形容词,这意味调试开发成本会比较高。那时候我的时间大部分都花费在xml文件里查找对应的配置。幸好,官方终于在2.x版本优化过来了,终于可以解脱了。

The Magento 2 deployment configuration replaces local.xml in Magento 1.x.

config.php 代码实例:

return array (
  'modules' =>
  array (
    'Magento_Core' => 1,
    'Magento_Store' => 1,
    'Magento_Theme' => 1,
    'Magento_Authorization' => 1,
    'Magento_Directory' => 1,
    'Magento_Backend' => 1,
    'Magento_Backup' => 1,
    'Magento_Eav' => 1,
    'Magento_Customer' => 1,
...
  ),
);

json

JSON文件格式是一个伟大的发明。简单的key和value的形式,就解决大部分配置文件,主要应用在不需要复杂逻辑配置,比如Composer、NPM等。

# composer.json
{
    "config": {
        "preferred-install": {
            "my-organization/stable-package": "dist",
            "my-organization/*": "source",
            "partner-organization/*": "auto",
            "*": "dist"
        }
    }
}
# package.josn

{ "name": "ethopia-waza",
  "description": "a delightfully fruity coffee varietal",
  "version": "1.2.3",
  "devDependencies": {
    "coffee-script": "~1.6.3"
  },
  "scripts": {
    "prepare": "coffee -o lib/ -c src/waza.coffee"
  },
  "main": "lib/waza.js"
}

YAML

在GitLab中CI构建配置文件是采用YML。原因是在构建的过程需要存储复杂业务流程,整体逻辑较多,YML适合应用于这类场景。

# see https://gitlab.com/help/ci/yaml/README.md#jobs
# Select image
image: geekwho/code:php
stages:
  - phplint
  - phpfixer
  - build
  - test
  - deploy

# php lint
phplint:
  stage: phplint
  script:
  - bash bin/phplint.sh
  only:
  - master
  - prelease
  - dev

# php code style
phpfixer:
  stage: phpfixer
  script:
  - bash bin/phpfixer.sh
  only:
  - master
  - prelease
  - dev

# unit test
test:
  stage: test
  script:
  - bash bin/test.sh
  only:
  - master
  - prelease
  - dev
  allow_failure: true

PHP数组

最简单的配置系统可以直接采用PHP自带的数组。数组的实现本身也是hash table 。

<?php
// 直接返回数组元素
return [
    'database' => [
      'adapter'     => 'Mysql',
      'host'        => 'localhost',
      'username'    => 'root',
      'password'    => '',
      'dbname'      => '@@name@@',
      'charset'     => 'utf8',
    ],
    'version' => '1.0',

    /**
     * if true, then we print a new line at the end of each execution
     *
     * If we dont print a new line,
     * then the next command prompt will be placed directly on the left of the output
     * and it is less readable.
     *
     * You can disable this behaviour if the output of your application needs to don't have a new line at end
     */
    'printNewLine' => true,
];
// 可以在配置里做一些逻辑判断
<?php

$dsn = 'mysql:host=localhost;dbname=yii2basic';
return [
    'class' => 'yii\db\Connection',
    'dsn' => $dsn,
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
];
// 甚至还可以使用静态变量
<?php

class Config {
    public static $db = 'mysql';
    public static $user = 'root';
    public static $passwd = '';
    public static $host = 'locahost';

}

Bash

甚至有时候在Bash环境也需要添加部分文件,我是这样做的。

# @Author: geekwho
# @Date:   2018-09-18 11:11:11
# @Last Modified by:   geekwho
# @Last Modified time: 2018-09-17 11:11:11
# 配置文件
project="wiki"

domain=".xbc.me"

user="geekwho"

host="your host"

port="your port"

dest=""

# 然后在你需要使用变量的地方进行加载
source ./.config.sh

环境变量

很多时候我们需要判断的环境变量,比如dev、stg、pre、pro等不同环境。

项目目录

这类方式就是直接在当前项目下建一个隐藏文件,类似 .env.dev 代表当前的环境。不好的地方,因为在当前项目下,容易被误修改或删除,导致环境判断异常。

php.ini

直接在php.ini添加自定义的环境变量,例如env=dev。那么在bash环境下,还需要单独去做一次环境变量的解析。

/etc

直接在系统文件下建立一个环境变量的文件。例如/etc/.env.dev。这类操作需要运维配置设置对应环境的变量文件。

两者结合

如何把环境变量和配置系统结合起来呢?

根据环境变量独立配置文件

推荐的配置目录结构如下:

app
    config
        dev
            config.php
        test
            config.php
        pre
            config.php
        prd
            config.php
// 实例
<?php

class Config {
    public static $db = 'mysql';
    public static $user = 'root';
    public static $passwd = '';
    public static $host = 'locahost';

}

All in ONE

全部环境变量全部都写同一个文件里。

#app/config/db.php
<?php

class Config {
    public static $db = [
      'dev' => 'mysql',
      'test' => 'mysql',
      'pre' => 'mysql',
      'prd' => 'mysql',
    ];
    public static $user_dev = [
      'dev' => 'root',
      'test' => 'root',
      'pre' => 'root',
      'prd' => 'root',
    ];
    public static $passwd = [
      'dev' => '',
      'test' => '',
      'pre' => '',
      'prd' => '',
    ]
    public static $hos = [
      'dev' => 'localhost',
      'test' => 'localhost',
      'pre' => 'localhost',
      'prd' => 'localhost',
    ]

}

参考链接

  1. phalcon config
  2. magento config
  3. composer config
  4. npm config
  5. GitLabv CI YAML
  6. PHP Array
  7. LaraDock

堆排序

具体实现

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)算法学习