定时器 Timer

在之前的部分中,已经使用过超时与睡眠(sleep)了。但是更一般的,我们可能想要某段代码在规定的时间执行,或者重复执行。Go内置的timer和ticker可以方便的实现这些功能。
timer包含在time包中,使用如下:

import "time"
...
// 定义一个timer,超时设定2秒
timer1 := time.NewTimer(time.Second *2)
// 在此等待2s
<- timer1.C //注意.C

上面的代码使用timer在某个代码点阻塞等待2秒。但是,如果我们想要实现的知识阻塞等待一段时间,更应该使用time.Sleep()方法。timer的特性在于它是可以取消/stop的。

t2 := time.NewTimer(time.Second * 1)
go func() {
<- t2.C
... // other code
}()
stop2 := t2.Stop()

实际上在上面的代码中,other code部分不会被执行,因为goroutine中的t2在主函数中被stop了, <-t2.C 没有机会到达expire点。

Tickers

timer可以定时执行某段代码,而ticker就像打点器一样循环执行某代码。其作用有点像js中的setTimeout()setInterval()
同时可以把ticker看作一个channel,在初始化时定义一个循环时间,每过一段时间往channel里面塞一个数,然后我们循环去取,这样就实现了循环执行代码的功能,看起来比js的setInterval要麻烦你一点点。

t1 := time.NewTicker(time.Millisecond * 500)
go func() {
for t := range t1.C {
fmt.Println("tick tick @ ", t)
}
}()
time.Sleep(time.Second * 3)
t1.Stop()
fmt.Println("tick tick stop")

上面会打印6次“tick tick @***”,range ticker.C会返回时间戳。
我们看一下ticker的实现:

type Ticker struct {
C <-chan Time // The channel on which the ticks are delivered.
// contains filtered or unexported fields
}

果然起始ticker就是包含一个chan成员的结构体,这样理解起来就更加方便了。

worker pool/worker池

worker这个概念在很多地方都有,印象比较深刻的是在异步javascript编程那篇文章里面讲了很多worker的东西。
这里worker pool主要是借助channel来实现使用多个goroutine处理多个work的编程模型。可以把一个goroutine看作一个worker。给多个worker传入相同的channel,worker从channel中取任务并处理,主线程往channel中添加任务,这样过个goroutine处理一个任务队列。

jobs := make(chan int, 100)
result := make(chan int, 100)
// use five workers
for w:=0; w<5; w++ {
go func(w int, jobs <-chan int, result chan<- int) {
for j:= range jobs {
fmt.Println("worker", w, "processing on", j)
time.Sleep(time.Millisecond * time.Duration(rand.Intn(600))) // sleep a random time
result <- j * 2
}
}(w, jobs, result)
}

// insert jobs
for j:=0; j<10; j++ {
jobs <- j
}

close(jobs)

// can do some others here

// sync here, may wait for works done
for a :=0; a<10; a++ {
<- result
}
fmt.Println("all work done here")

限速/rate-limiting

关于rate-limiting,可以参考 维基百科

Rate limiting is an important mechanism for controlling resource utilization and maintaining quality of service

go使用goroutine,channel, ticker合作实现该特性。

使用rate-limiting的模型,可以应对突然的大量请求进行缓存,有序处理。

rateControlChan := make(chan time.Time, 3) // 用于控制速率的channel
for i:=0; i<3; i++ {
rateControlChan <- time.Now()
}
// send value per 200 millisecond
go func() {
for t := range time.Tick(time.Millisecond * 200) {
rateControlChan <- t
}
}()

reqChannel := make(chan int, 5) // 缓存请求的channel
for i:=0; i< 5; i++ {
reqChannel <- i
}
close(reqChannel)
for r:= range reqChannel {
ht := <- rateControlChan // wait for rate-control condition
// handle request here
fmt.Println("req", r, "handle at", ht)
}

这个模型的关键是,使用两个channel协同,一个用于处理速率的控制,一个用于请求的缓冲。使用一个goroutine不断的定时填充速率控制goroutine,而请求channel则用一个for-range不停行从里面取请求

待续

下一部分主要是原子性,同步与互斥

Goroutines

goroutines就是可以方便的并行执行函数,本身并没有太多东西要注意的,主要的内容在于goroutine和channel结合的并发程序

Channel

channel相当于联系并发的goroutine的管道,我们常使用Linux中的管道pipe做类比:

ls -l | grep .go

上面的命令把当前目录下的文件列出,每个文件占一行,用管道把输出结果送到grep程序的标准输入流,grep把当前目录下的go源程序文件筛选出来。管道吧ls程序和grep程序连接起来,goroutine的作用也差不多,把一个goroutine的输出,可以在另外一个goroutine读出来。
当然channel要比Linux的管道功能更加强大一些,因为是可编程的,它可以传递任意规定的类型的变量,使用也很方便。

msgChannel := make(chan string)

go func() {msgChannel <- "hello"}()
msg := <-msgChannel // receive a msg from channel

上面的代码即声明了一个可传递string类型的channel。第3行代码则使用goroutine执行一个匿名函数,函数的作用是向msgChannel传入一个字符串。

goroutine执行一个匿名还是得语法是很有用的,其格式为go func() { ... }()注意最后的括号表示执行匿名函数。

channel <-通常叫做发送操作,即向channel发送一个值。<- channel通常叫做接受操作,即从channel中取出一个值。
channel的发送和接受是一个同步的过程,只有sender和receiver都准备就绪的时候才能发送/接受,否则会阻塞等待,这在后面的Channel Buffering 中会讲到。

Channel Buffering

默认的channel是不带缓冲的(unbuffered)如果像上面的方式定义channel的话,只能往里面添加一个元素,并且需要有从channel取数据的receiver,否则再往里面添加元素则会导致无限等待。
所以更常见的是使用带缓冲的channel,这样可以一次性往channel里面添加多个元素。

msgChannel := make(chan string, 2)

msgChanenl <- "hello"
msgChannel <- "world"

fmt.Println(<-msgChannel)
fmt.Println(<-msgChannel)

这样就channel就可以缓存两个变量了。在实际使用中,常会使用更大的缓存channel。

使用channel同步goroutine

我们知道channel的发送和接受操作只有在channel准备就绪时才会进行,否则会阻塞等待,也就是说在向channel发送数据时,需要channel有空余空间,从channel接受数据时,需要channel不能为空,否则也会阻塞等待。如下的代码展示了一个使用不带缓冲的channel实现同步的功能:

func working(flagChan chan bool) {
fmt.Println("working on it")
time.Sleep(2 * time.Second)
fmt.Println("work done")

flagChan <- true
}

func chanSync() {
flagChan := make(chan bool, 1)
go working(flagChan)

// 下面的接受操作会阻塞等待goroutine执行结束
done := <- flagChan
fmt.Println("sync goroutine output", done)
}

指定channel的方向(操作)

我们可以在方法的定义的参数列表处指定channel参数的操作方式为发送或者接受,因为一般发送是一个逻辑,接受是一个逻辑,通常不会把两个操作放在一起,指定某个函数的操作模式可以起到“代码即文档”的作用。比如上面的working函数:

func working (flagChan chan<- bool) { ... }

func recv (flagChan <-chan bool) { ... }

即表明了在函数中channel的数据流向(directions)
网页中的代码示例:

package main
import "fmt"
func pong(pings <-chan string, pongs chan<- string) {
msg := <-pings
pongs <- msg
}
func ping(pings chan<- string, msg string) {
pings <- msg
}
func main() {
pings := make(chan string, 1)
pongs := make(chan string, 1)
ping(pings, "passed message")
pong(pings, pongs)
fmt.Println(<-pongs)
}

多路goroutine: select

select用于从多个channel中轮询状态,从已经准备就绪的channel中接受值并执行相应的代码。select的语法和switch有点相似。请看代码:

import (
"math/rand"
"time"
"strconv"
)

func gr1(c chan string) {
dur := rand.Intn(300)
time.Sleep(time.Millisecond * time.Duration(dur))
c <- "gr1 sleep done " + strconv.Itoa(dur)
}

func gr2(c chan string) {
dur := rand.Intn(400)
time.Sleep(time.Millisecond * time.Duration(dur))
c <- "gr2 sleep done " + strconv.Itoa(dur)
}

func selects() {
c1 := make(chan string)
c2 := make(chan string)
go gr1(c1)
go gr1(c1)
go gr2(c2)
go gr2(c2)

for i:=0; i<4; i++ {
select {
case msg := <-c1:
fmt.Println(msg)
case msg2 := <-c2:
fmt.Println(msg2)
}
}
}

要注意,channel的send与receive操作的个数必须是匹配的,否则会无限阻塞等待。

超时

在访问资源的代码中,我们常需要绑定一个超时时间,如果超过一段时间没有加载到资源,或者某个调用没有返回则需要做出相应的处理。
一个例子就是在channel的接受操作,等待一定时间没有资源可以接受则执行预定代码。可以在select中添加一个超时:

select {
case res <- c1 :
fmt.Println(res)
case <-time.After(time.Seconde * 2)
fmt.Println("timeout")
}

可以理解,time.After()是一个特殊的channel。

非阻塞的channel操作

在上面的例子里面,channel进行send/receive操作时,是阻塞的,有什么办法让channel的操作变得不是阻塞的呢,比如从一个channel中取数据,如果没有数据则直接执执行一个默认的操作,也不要想刚刚讨论的超时那样设置要给time.After().

刚刚我们说了select的语法和switch很相似,那switch中有default可以用来指定当所有case不没有match到的情况,select是不是也有呢?答案是肯定的。

我们可以为select操作添加一个default分支。一般在所有case之后。这样在所有case的channel都没有就绪的时候执行default的分支代码。

select {
case msg := <-c1:
fmt.Println(msg)
case msg2 := <-c2:
fmt.Println(msg2)
default:
// non-blocking channel
fmt.Println("select default")
}

关闭channel

关闭一个channel意味着不再有数据发送到channel中,这在给接收者一个通信完成的信号时是很有用的。
关闭channel的操作很简单,直接调用close(chan)方法即可(在所有数据被发送完后调用close)。
在channel的接受操作时,其实可以指定两个变量:

msg, more := <- msgChannel

如果channel被关闭了,则more被赋值为false,否则more的值为true。 可以用_, more := <- channel 判断一个channel是否被关闭了。

range over channels

可以使用range遍历一个channel中的元素。使用方法也很简单

func rangeChannel() {
c := make(chan string, 2)
c <- "tomorrow is monday"
c <- "fee out"
close(c)
for str := range c {
fmt.Println(str)
}
}

要注意,对于一个close了得channel,range能自动判断是否遍历结束,但是如果channel没有调用过close,在上面的代码中,会在接受第三个值的时候阻塞(异常退出)。

待续

第二部分结束

last update: 2015-08-23 10:38:00

Linux的文本处理工具除了awk之外,还有一个利器,那就是sed。sed用于文本的替换,也是以行为单位,使用正则表达式进行匹配。

参考: http://coolshell.cn/articles/9104.html

基础

sed的命令模式是这样的:

sed -i 'sed-command' file-to-process

其中,-i参数是很常用的,如果不带-i参数处理的结果会在终端打印出来,带上-i参数后会将处理结果替换输入文件的内容。

中间引号内的是sed的命令内容,常用单引号,但是要注意在单引号里面,反斜杠()转义将不能起作用。命令常用模式为 s/old-pattern/new-str/g
最后的是要处理的文本的文件名。

由于sed主要依赖正则匹配实现功能,所以先熟悉一下基础的正则规则:

  • ^ 表示一行的开头。如:/^#/ 以#开头的匹配。
  • $ 表示一行的结尾。如:/}$/ 以}结尾的匹配。
  • \< 表示词首。 如 \<abc 表示以 abc 为首的詞。
  • \> 表示词尾。 如abc\> 表示以 abc 結尾的詞。
  • . 表示任何单个字符。
  • * 表示某个字符出现了0次或多次。
  • [ ]字符集合。 如:[abc]表示匹配a或b或c,还有[a-zA-Z]表示匹配所有的26个字符。如果其中有^表示反,如[^a]表示非a的字符

练习:下面的命令可以去掉html文件中的标签,只留下文本:

sed -i "s/<[^>]*>//g" index.html

其中,命令开头的s代表替换,[^>]*表示一个以上的非>字符,替换为空。

上面的命令如果是:

sed -i 's/<.*>//g' index.html

看起来也好像能工作,但是, <.*>会匹配最长的尖括号内容,即从文本的第一个< 到最后一个>,这样达不到我们的效果。

进阶

指定替换

替换指定行的内容
有时候我们仅需要对某些行进行替换,可以在命令中指定行:

# 只对第3行进行替换
sed -i '3s/me/you/g' file-name

#只对第3-6行替换
sed -i '3,6s/me/you/g' file-name

指定每一行替换的个数

# 替换每一行的第一个匹配
sed -i 's/me/you/1' file-name

#替换每一行的第2个匹配
sed -i 's/me/you/2' file-name

#替换第5个以后的所有匹配
sed -i 's/me/you/3g' file-name

多个匹配

如果要对一行进行两个匹配,可以在命令字符串中使用;分割多个匹配项:

sed '1,3s/me/you/g; 3,$s/you/me/g' file-name
# 下面的命令等效
sed -e '1,3s/me/you/g' -e '3,$s/you/me/g' file-name

这样将1-3行的me替换为you,将3到最后一行的you替换为me。

圆括号匹配

类似于正则表达式中的分组,在s中使用的括号,可以在替换串中使用\1,\2指代
比如:

sed 's/This is your \([^,]*\),.*is \(.*\)/\1,\2/g' cats.txt

注意括号部分表示匹配项,括号需要使用斜杠\转义。这与一些编程语言中的规约不一样,一般来说使用$1, $2来指代匹配项。

sed的命令

sed工具有一些工具用来操作文本,比如在指定行插入行:

a/i 命令

// 在第一行后追加一行
sed "1 a This is your mou, you mou's name is miaomiao" cats.txt
// 在第一行插入一行,即原来的第一行成为第二行
sed "1 i this is ****" cats.txt
//在文件结尾追加一行
sed "$ a this is ****" cats.txt
// 在所有匹配行后追加一行
sed "/my/a this is my ***" cats.txt

c命令替换匹配行

//替换第二行
sed "c 2 this is my****" cats.txt
// 替换匹配行
sed /my/c this is your****" cats.txt

d命令删除匹配行

// 删除第二行
sed '2s' cats.txt
// 删除从第3行到结尾
sed '3,$d' cats.txt

p命令打印输出

使用p有点类似grep命令,把匹配行输出

处理目录下所有文件

sed -i '2,$s/\t/,\t/g' *

还可以结合grep命令筛选需要处理的文件,这是替换文件内容包含 pattern 的文件。

sed -i 'command' `grep -rl pattern ./`

grep的l参数表示只输出文件名,r表示recursive

也可以结合 ls 对文件名筛选:

sed -i 'command' `ls | grep .txt`

这样就只对 txt 文件执行替换。

待续

最近要处理一个csv文件,起始是这样的:

id mmid cp
zy006 2200112411 小米商城
zy007 2200123091-3003904564 掌游自有渠道
zy008 0 MM应用商城
zy101 2200131184-2200131784 多酷
zy102 2200017122-2200126498-2200127284-3003898651 3G门户
zy104 3003904473 软吧

第二列是多个元素组合起来的,现在要导入到数据库里面,把第一列和第二列的元素使用一个关联表而不是这样使用数据拼接的方式。

要处理的结果是抽出第一列和第二列,然后第二列使用’-‘分割新建一条记录。

步骤如下:

1 抽出数据到新文件:

awk -F "," 'NR>1{printf("%s,%s\n", $1, $2)}' data.csv> mmid.csv

NR代表当前的记录的数值,表示第几个记录了,这里忽略掉第一行。

2 分割数据并创建新记录

awk -F '[,-]' '{for(i=2;i<NF;i++) {printf("%s,%s\n", $1, $i)}}' mmid.csv > channel_mmid.csv

使用-F 设置两个分割参数,使用一个循环,其中NF是number of fields的简写,表名当前当有多少条记录,对应有NR,number of record

3 完成

使用awk处理excel/csv文件真的非常的方便

4 扩展

删除记录中的某一列

awk -F ',' 'BEGIN{OFS=","; } NR>1{str=""; for (i=1; i<=NF;i++) if (i != 2) str = str "," $i; print str}' channel-manage.csv > new-channel-manage.csv

一直以来对于PHP的扩展编写都比较感兴趣但是却没有什么更深的接触,正好最近要研究一下,做个记录吧。

参考:
http://www.laruence.com/2009/04/28/719.html
http://www.walu.cc/phpbook/5.md

首先我们从头安装一个PHP环境,现在最新的稳定版的PHP是5.6.12。

编译安装php

以下在Linux(centos)下完成

  1. 下载src: wget http://cn2.php.net/get/php-5.6.12.tar.bz2/from/this/mirror wget 下载的文件可能被命名为mirror,需要重命名一下 mv mirror php-5.6.12.tar.bz2
  2. 解压: tar xjvf php-5.6.12.tar.bz2
  3. cd php-5.6.12 编译安装: ./configure --prefix=/home/wuxu/data/php5.6/ --enable-debug --enable-maintainer-zts
  4. make
  5. make install
  6. make clean

这样一个新的php安装在了家目录下(/home/wuxu/data/php5.6)。

扩展的准备工作

首先要理解为PHP内核编写的扩展的两个工作方式,一种是编译为动态共享对象/可装载模块,也就是常见的.so扩展,这种扩展可以在php的配置文件中方便的开启或者关闭;另外一种方式是静态编译到PHP中,使用静态方法编译比较容易上手,鸟哥的文章中也是使用的静态编译方式,所以我们也使用静态编译方式来练习。

PHP在源码中提供了一个扩展骨架构造脚本: ext_skel,脚本放在php-5.6.12/ext目录下。它的使用方式如下:

./ext_skel --extname=mfs --proto=mfs.def

解释一下,–extname明显就是我们要创建的扩展的名称,–proto的proto是prototype的缩写,也就是扩展对外提供的函数原型,可以在这个文件中添加要导出的函数签名,每个函数做一行,这样ext_skel脚本可以自动创建它们的骨架代码。比如鸟哥的例子中的字符串复制函数:

string self_concat(string str, int n)

把这一行保存为mfs.def文件,放在ext文件夹下。

基本骨架

运行上面的ext_skel命令,就会在ext文件夹下创建一个mfs的文件夹,并声称一些代码文件和配置文件。 php扩展在Linux下的配置文件是 ext/mfs/config.m4;m4有自己的语法,不过我们并不需要熟悉它,只需要简单去掉一些注释就可以了。打开配置文件config.m4;大概在16行和18行,找到PHP_ARG_ENABLE(mfs, whether to enable mfs support 相关的内容,这一行是用来重新生成configure文件时起作用的,取消这一行及
它后面的第二行[ --enable-myfunctions Include myfunctions support]),中间有一行不要取消注释。这样就可以重新生成configure文件可以使用enable-mfs来静态编译扩展。

完成上面的工作后,重新生成configure文件并编译安装php。

cd /path/to/php-5.6.12
./buildconf --force
./configure --enable-fms --prefix=/home/wuxu/data/php5.6 --with-config-file-path=/home/wuxu/data/etc/php.ini
make
make install

这样编译的php就带有了fms扩展,并可以使用在def文件中定义的原型函数。但是由于并没有编写那个函数的具体内容,所以这个是str_concat函数并不能起作用,不过ext_skel脚本还导出了一个函数: confirm_mfs_compiled(str); 可以用下面的脚本检测fms扩展是否可用了:

<?php
print confirm_mfs_compiled("myextension");
?>

// output:
// "Congratulations! You have successfully modified ext/mfs
//
// config.m4. Module mfs is now compiled into PHP."

表明脚本已经编译到php了。下面我们就可以开始编写self_concat的具体内容了。

起始代码

先看一下有ext_skel脚本自动生成的self_concat函数代码,在ext/mfs/mfs.c:76

PHP_FUNCTION(self_concat)
{
char *str = NULL;
int argc = ZEND_NUM_ARGS();
int str_len;
long n;
if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)
return;
php_error(E_WARNING, "self_concat: not yet implemented");
}

这些代码是一个到处函数的基本框架了。
使用PHPFUNCTION()宏来生成一个适合于Zend引擎的函数原型。其中比较重要的是 zend_parse_parameters 函数,用来获取函数传递的参数;该函数的原型是:

zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, …);

第一个参数是参数的个数,通常使用ZEND_NUM_ARGS()的返回值,TSRMLS_DC这个照写就可以了,第三个参数比较复杂是一个表示函数期望的参数类型的字符串,后面紧跟参数值的变量列表,这里有一个PHP的松散变量和动态类型推断到C语言类型的转换。

第三个参数根据一个参数类型对照表生成:

类型指定符 对应的C类型 描述
l long 符号整数
d double 浮点数
s char *, int 二进制字符串,长度
b zend_bool 逻辑型(1或0)
r zval * 资源(文件指针,数据库连接等)
a zval * 联合数组
o zval * 任何类型的对象
O zval * 指定类型的对象。需要提供目标对象的类类型
z zval * 无任何操作的zval

考虑上面代码的实例:

if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)
return;

“sl”: 第一个字符s代表二进制字符串,它在后面的参数列表中对应两个值,一个 &str, 一个&strlen;第二个字符’l’(L的小写),表示整数类型参数对应 &n。

扩展中的字符串都是二进制字符串,即并不以\0作为字符串结束,而是使用一个str_len表示字符串长度,具体看_zval结构体。

下面的工作就是修改这个函数了。

完成第一个导出函数

将自动生成的函数更新为:

PHP_FUNCTION(self_concat)
{
char *str = NULL;
int argc = ZEND_NUM_ARGS();
int str_len;
long n;
char *result; /* Points to resulting string */
char *ptr; /* Points at the next location we want to copy to */
int result_length; /* Length of resulting string */

if (zend_parse_parameters(argc TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE) {
return;
}

result_length = (str_len * n);
result = (char *) emalloc(result_length + 1);
ptr = result;
while (n--) {
memcpy(ptr, str, str_len);
ptr += str_len;
}

*ptr = '\0';

RETURN_STRINGL(result, result_length, 0);
}

按照上面的方法,重新编译php, 就可以在php文件中直接使用slef_concat()函数拼接字符串了。

<?php
print self_concat("pop_", 10);

保存为confirm.php;运行:

php confirm.php

会输出拼接的字符串了。

待续

PHP 5.3中引入了一个后期静态绑定的功能,对于静态绑定都比较熟悉了,就是static修饰的方法或者字段,那这个后期静态绑定又是干什么的呢?

如果在一个类中定义一个静态方法或者静态域成员,如下:

class Person {
public static $type = 'man';
public static function run() {
echo 'run';
}
}

以对于静态成员的理解,这些static修饰的成员在所有对象中共享,只有一个实例,可以使用Person::run() 这样的方式直接调用而不需要实例化类。

self调用

如果在类的内部直接调用这些静态方法,我们一般使用self来指代当前类:

class Person {
...

public staic function eat() {
self::run(); // run before eat...
// do some eating here
}
}

这里使用self来指代当前的类,并用于调用其静态方法。这里,self::__CLASS__ 是一样的,都是对当前类的静态引用

后期静态绑定

常用示例代码:

<?php
class A {
public static function who() {
echo __CLASS__;
}
public static function test() {
self::who();
}
}

class B extends A {
public static function who() {
echo __CLASS__;
}
}

B::test();
?>

后期静态绑定本想通过引入一个新的关键字表示运行时最初调用的类来绕过限制。简单地说,这个关键字能够让你在上述例子中调用 test() 时引用的类是 B 而不是 A。最终决定不引入新的关键字,而是使用已经预留的 static 关键字。

这里简单的理解就是,B是A的子类,有时候在使用B调用A中实现的的静态方法时,需要使用子类B的一些静态域实现,而不是A实现的静态域。使用self:: 调用静态成员只会调用A类的静态成员。这是后应该使用后期静态绑定关键字 static来代替self

<?php
class A {
public static function who() {
echo __CLASS__;
}
public static function test() {
static::who(); // 后期静态绑定从这里开始
}
}

class B extends A {
public static function who() {
echo __CLASS__;
}
}

B::test(); // return B
?>

好像还是不是很好理解,下面来看这个后静态绑定在MVC框架中的用途,这样更容易理解一些.

使用后期静态绑定实现MVC框架中的model基类

在一个MVC框架中,通常使用Model来对数据库抽象,把一张数据库表映射到一个Model的子类。比如,一张person表可以对应一个下面的类:

namespace model

class Person extends Model {
public $name;
public $age;

public function say($words) {
...
}
}

那么有一些通用的数据库操作,我们希望可以放到Model类中实现了,这样在各个Model子类中都可以直接调用,比如 findById() 用来根据id返回一个Person的实例。这个时候有一个问题就是,Model基类中如何知道各个Model子类的表名呢,我们需要子类把表名告知Model基类,这样才能方便些sql语句。

这时候就可以使用后期静态绑定了,因为后期静态绑定可以在子类调用父类的静态方法时将一些静态域使用自己(子类)的静态域。

class Model {
public static function findById($id) {
$tableName = static::tableName();
$sql = "select * from {$tableName} where id={$id} ";
// 下面的数据库查询只是一个示例,使用了一个数据库封装
$row = DBConnector::createSql($sql)->queryOne();
return new static($row);
}
}

// 改进子类
class Person extends Model {

public static functin tableName() {
return 'person';
}

}

这样可以直接使用静态方法获得一个person实例: Person::findById(1); 就可以了。
这样其他的model子类也是要在类中实现了静态域 public static function tableName() 就能直接使用Model类中findById方法了。

扩展

可以在Model中添加更多的方法来使得数据库操作变得简单,比如 deleteByid(), findByArray() 等等,这非常有用。

  • 在非静态环境下,所调用的类即为该对象实例所属的类。由于 $this-> 会在同一作用范围内尝试调用私有方法,而 static:: 则可能给出不同结果。另一个区别是 static:: 只能用于静态属性
  • 后期静态绑定的解析会一直到取得一个完全解析了的静态调用为止。另一方面,如果静态调用使用 parent:: 或者 self:: 将转发调用信息
<?php
class A {
public static function foo() {
static::who();
}

public static function who() {
echo __CLASS__."\n";
}
}

class B extends A {
public static function test() {
A::foo();
parent::foo();
self::foo();
}

public static function who() {
echo __CLASS__."\n";
}
}
class C extends B {
public static function who() {
echo __CLASS__."\n";
}
}

C::test();
// 输出:A C C
?>

参考: http://php.net/manual/zh/language.oop5.late-static-bindings.php

在Linux下我们经常需要对一些文本文档做一些处理,尤其像从日志里提取一些数据,这是我们一般会用awk工具和sed工具去实现需求,这里对awk的入门使用简单记录。

awk可以看作一种文本处理工具,一种专注数据操作的编程语言,一个数据处理引擎。其名字来源于三个发明者的姓名首字母。一般在Linux下使用的awk是gawk(gnu awk)。

入门

awk把文本文档看作是数据库,每一行看作一条数据库中的记录,可以指定数据列的分隔符,默认的分隔符是”\t”,即Tab。
awk工作流程是这样的:读入有’\n’换行符分割的一条记录,然后将记录按指定的域分隔符划分域,填充域,$0则表示所有域,$1表示第一个域,$n表示第n个域。默认域分隔符是”空白键” 或 “[tab]键”
awk的执行模式是: awk '{pattern + action}' {filenames}

awk的执行方式:

1.命令行方式

awk [-F  field-separator]  'commands'  input-file(s)

其中,commands 是真正awk命令,[-F域分隔符]是可选的。 input-file(s) 是待处理的文件。
在awk中,文件的每一行中,由域分隔符分开的每一项称为一个域。通常,在不指名-F域分隔符的情况下,默认的域分隔符是空格。

2.shell脚本方式
将所有的awk命令插入一个文件,并使awk程序可执行,然后awk命令解释器作为脚本的首行,一遍通过键入脚本名称来调用。
相当于shell脚本首行的:#!/bin/sh
可以换成:#!/bin/awk

3.将所有的awk命令插入一个单独文件,然后调用:

awk -f awk-script-file input-file(s)

其中,-f选项加载awk-script-file中的awk脚本,input-file(s)跟上面的是一样的。

一般是哟你哦个命令行模式就能满足需求了。

这样下面的一行文本:

abc def 123
dfg jik 234

在awk看来就是一个包含三个字段的记录,可以类比到mysql的一行记录,只不过awk没有一个mysql那么强的scheme。
这样比如我们要抽出中间的那一行数据,假设文本保存为文件 data.txt

awk '{print $2}'

很简单,这样就可以打印出中间的字符def 和jik 了。

下面来一个点点复杂的:

Beth	4.00	0
Dan 3.75 0
kathy 4.00 10
Mark 5.00 20
Mary 5.50 22
Susie 4.25 18

对于这样的数据
使用 awk '$3>0 { print $, $2 * $3 }' data.txt 这样会输出

Kathy 40
Mark 100
Mary 121
Susie 76.5

理解就是可以在{}前面添加一个判断的语句,只有符合条件的行才会执行后面的语句。

进阶

相对于print输出,可以使用printf进行格式化输出:

awk  -F ':'  '{printf("filename:%10s,linenumber:%s,columns:%s,linecontent:%s\n",FILENAME,NR,NF,$0)}' /etc/passwd

print函数的参数可以是变量、数值或者字符串。字符串必须用双引号引用,参数用逗号分隔。如果没有逗号,参数就串联在一起而无法区分。这里,逗号的作用与输出文件的分隔符的作用是一样的,只是后者是空格而已。

printf函数,其用法和c语言中printf基本相似,可以格式化字符串,输出复杂时,printf更加好用,代码更易懂。

PHP自动加载是一个很有用的技巧,我们应该在项目中尽量使用autoload来减少维护类加载的工作。

使用__autoload

在使用PHP的项目中,如何实现自动加载对于新人总是一个很疑惑的问题,一般写过的一些PHP,或者用过一些框架的同学都知道通过实现__autoload()函数来实现简单的自动加载:

function __autoload($classname) {
$filename = "./". $classname .".php";
include_once($filename);
}

不过这样的方式缺点也能明显就是只能定义一种加载策略,或者把这个函数实现的很复杂来实现多样的自动加载策略。

使用spl_autoload_register

现在推荐使用的是spl_autoload_register()来注册自动加载器,它可以用来注册多个自动加载策略。它的函数签名是:

bool spl_autoload_register ([ callable $autoload_function [, bool $throw = true [, bool $prepend = false ]]] )

一般来说,我们只关注第一个参数,他是一个callable类型的function,一般简单的传入一个函数名称的字符串,也可以传入类名和方法名的数组,也可以传入一个匿名函数(PHP 5.3之后)。

function my_autoloader($class) {
include 'classes/' . $class . '.class.php';
}

spl_autoload_register('my_autoloader');

// Or, using an anonymous function as of PHP 5.3.0
spl_autoload_register(function ($class) {
include 'classes/' . $class . '.class.php';
});

在PHP 5.3 引入命名空间后,我们还可以使用一种更方便的自动加载策略

spl_autoload_extensions(".php"); // comma-separated list
spl_autoload_register();

// 等价于
function my_autoload ($pClassName) {
include(__DIR__ . "/" . $pClassName . ".php");
}
spl_autoload_register("my_autoload");

据说第一种可以效率更高。

使用SplClassLoader

SplClassLoader是Jonathan H. Wage,Roman S. Borschel等人开发的一个用于PHP 5.3根据命名空间和类名实现自动加载的类。使用比较方便,这里推荐使用。其实现并不复杂使用一个类,源码如下(去掉了注释):

class SplClassLoader
{
private $_fileExtension = '.php';
private $_namespace;
private $_includePath;
private $_namespaceSeparator = '\\';

public function __construct($ns = null, $includePath = null) {
$this->_namespace = $ns;
$this->_includePath = $includePath;
}
public function setNamespaceSeparator($sep) {
$this->_namespaceSeparator = $sep;
}
public function getNamespaceSeparator() {
return $this->_namespaceSeparator;
}
public function setIncludePath($includePath) {
$this->_includePath = $includePath;
}
public function getIncludePath() {
return $this->_includePath;
}
public function setFileExtension($fileExtension) {
$this->_fileExtension = $fileExtension;
}
public function getFileExtension() {
return $this->_fileExtension;
}
public function register() {
spl_autoload_register(array($this, 'loadClass'));
}
public function unregister() {
spl_autoload_unregister(array($this, 'loadClass'));
}
public function loadClass($className) {
if (null === $this->_namespace || $this->_namespace.$this->_namespaceSeparator === substr($className, 0, strlen($this->_namespace.$this->_namespaceSeparator))) {
$fileName = '';
$namespace = '';
if (false !== ($lastNsPos = strripos($className, $this->_namespaceSeparator))) {
$namespace = substr($className, 0, $lastNsPos);
$className = substr($className, $lastNsPos + 1);
$fileName = str_replace($this->_namespaceSeparator, DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
}
$fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . $this->_fileExtension;

// @add wuxu 验证文件是否存在,要不可能会抛出异常
$fileName = ($this->_includePath !== null ? $this->_includePath . DIRECTORY_SEPARATOR : '') . $fileName;

if (file_exists($fileName)) require $fileName;
}
}
}

完整的代码见 GitHub Gist
(还有一些在此基础上的修改版本,比如:https://gist.github.com/jwage/221634)

它的使用方式如下:

// Example which loads classes for the Doctrine Common package in the
// Doctrine\Common namespace.
$classLoader = new SplClassLoader('Doctrine\Common', '/path/to/doctrine');
$classLoader->register();

这是一个实现使用命名空间的PHP项目的自动加载的方式(PSR-0),使用上面的注册代码,可以注册一个Doctrine\Common的namespace,它对应的根目录是/path/to/doctrine。

总结

这样对于实现一个使用命名空间的项目非常重要。
接下来会写一篇如何在一个项目中使用namespace(PSR-0)来使项目的结构更加科学。

PHP本身是一种弱类型的语言,可以在程序中改变变量存储的值的类型。那么这些变量在PHP的底层是如何实现的呢,理解内核变中变量的实现机制将有利于我们理解PHP的变量系统。

变量的类型

对PHP有一定理解的同学应该都已经知道了PHP在内核中是使用zval这个结构体来存储变量的,也就是说不同的变量在底层都是一个zval结构体,它定义在Zend/zend.h中:

struct _zval_struct {
zvalue_value value; /* 变量的值 */
zend_uint refcount__gc;
zend_uchar type; /* 变量当前的数据类型 */
zend_uchar is_ref__gc;
};
typedef struct _zval_struct zval;

//在Zend/zend_types.h里定义的:
typedef unsigned int zend_uint;
typedef unsigned char zend_uchar;

其中保存变量的值的value则是zvalue_value类型,它是一个union:

typedef union _zvalue_value {
long lval; /* long value */
double dval; /* double value */
struct {
char *val;
int len;
} str;
HashTable *ht; /* hash table value */
zend_object_value obj;
} zvalue_value;

PHP使用zvalue_value实现8种数据类型,这些类型在内核中分别对应特定的常量。通过这个union可以理解常用的类型判断函数: is_null, is_bool, is_long, is_double, is_string, is_array, is_object, is_resource 他们的效率应该是很高的。同时也可以猜想gettype函数的实现大概使用了Z_TYPE_P宏,这个宏大概和zval结构的type有关
在Zend/zend_operators.h中定义的宏:

#define Z_TYPE(zval)        (zval).type
#define Z_TYPE_P(zval_p) Z_TYPE(*zval_p)
#define Z_TYPE_PP(zval_pp) Z_TYPE(**zval_pp)

注意,虽然在32位系统中,signed long的存储范围是-2147483648~2147483647 的整数,但是要注意,如果整数变量超出了这个范围并不会直接溢出,而是会转换程double类型继续计算。

PHP内核还同时在zval结构里保存着字符串的实际长度, 这个设计使PHP可以在字符串中嵌入‘\0’字符,也使PHP的字符串是二进制安全的, 可以安全的存储二进制数据

变量的值

string型变量比较特殊,因为内核在保存String型变量时,不仅保存了字符串的值,还保存了它的长度, 所以它有对应的两种宏组合STRVAL和STRLEN,即:Z_STRVAL、Z_STRVAL_P、Z_STRVAL_PP与Z_STRLEN、Z_STRLEN_P、Z_STRLEN_PP。 前一种宏返回的是char *型,即字符串的地址;后一种返回的是int型,即字符串的长度。

Array型变量的值其实是存储在C语言实现的HashTable中的, 我们可以用ARRVAL组合宏(Z_ARRVAL, Z_ARRVAL_P, Z_ARRVAL_PP)这三个宏来访问数组的值。

对象是一个复杂的结构体(zend_object_value结构体),不仅存储属性的定义、属性的值,还存储着访问权限、方法等信息。 内核中定义了以下组合宏让我们方便的操作对象: OBJ_HANDLE:返回handle标识符, OBJ_HT:handle表, OBJCE:类定义, OBJPROP:HashTable的属性, OBJ_HANDLER:在OBJ_HT中操作一个特殊的handler方法。

资源型变量的值其实就是一个整数,可以用RESVAL组合宏来访问它,我们把它的值传给zend_fetch_resource函数,便可以得到这个资源的操作句柄,如mysql的链接句柄等。

创建PHP变量

在内核中是如何创建zval的呢,PHP内核中提供了一个MAKE_STD_ZVAL(pzv)宏,它使用内核的方式类申请一块内存,并将其地址赋给pzv。这个宏能自动处理内存不足的问题。
获取空间后,就可以给这个zval赋值了。旧的做法是先确定zval的类型:Z_TYPE_P(pzv)=ISNULL 来设置其为null类型,再通过Z_SOMEVAL_P的宏类赋值:

Z_TYPE_P(pzv)=IS_BOOL;
Z_BVAL(PZV)=1;

不过现在PHP内核提供了更多的宏可以更方便的操作zval的值。

新宏 其它宏的实现方法

  • ZVAL_NULL(pvz); (注意这个Z和VAL之间没有下划线!) Z_TYPE_P(pzv) = IS_NULL;(IS_NULL型不用赋值,因为这个类型只有一个值就是null)
  • ZVAL_BOOL(pzv, b); (将pzv所指的zval设置为IS_BOOL类型,值是b) Z_TYPE_P(pzv) = IS_BOOL; Z_BVAL_P(pzv) = b ? 1 : 0;
  • ZVAL_TRUE(pzv); (将pzv所指的zval设置为IS_BOOL类型,值是true) ZVAL_BOOL(pzv, 1);
  • ZVAL_FALSE(pzv); (将pzv所指的zval设置为IS_BOOL类型,值是false) ZVAL_BOOL(pzv, 0);
  • ZVAL_LONG(pzv, l); (将pzv所指的zval设置为IS_LONG类型,值是l) Z_TYPE_P(pzv) = IS_LONG;Z_LVAL_P(pzv) = l;
  • ZVAL_DOUBLE(pzv, d); (将pzv所指的zval设置为IS_DOUBLE类型,值是d) Z_TYPE_P(pzv) = IS_DOUBLE; Z_DVAL_P(pzv) = d;
  • ZVAL_STRINGL(pzv,str,len,dup);(下面单独解释) Z_TYPE_P(pzv) = IS_STRING;Z_STRLEN_P(pzv) = len;if (dup) {Z_STRVAL_P(pzv) =estrndup(str, len + 1);} else {Z_STRVAL_P(pzv) = str;}
  • ZVAL_STRING(pzv, str, dup); ZVAL_STRINGL(pzv, str,strlen(str), dup);
  • ZVAL_RESOURCE(pzv, res); Z_TYPE_P(pzv) = IS_RESOURCE;
  • Z_RESVAL_P(pzv) = res;

变量的存储方式

用户在PHP中定义的变量可以在一个HashTable中找到,当PHP中定义了一个变量,内核会自动把它的信息存储到一个用HashTable实现的符号表里。
全局作用域的符号表在调用扩展的RINIT方法前创建,并且在RSHUTDOWN方法执行之后自动销毁。

一个例子

<?php
$foo = 'bar';
?>

上面是一段PHP语言的例子,我们创建了一个变量,并把它的值设置为’bar’,在以后的代码中我们便可以使用$foo变量。相同的功能我们怎样在内核中实现呢?我们可以先构思一下步骤:

  • 创建一个zval结构,并设置其类型。
  • 设置值为’bar’。
  • 将其加入当前作用域的符号表,只有这样用户才能在PHP里使用这个变量。

具体的代码为:

{
zval *fooval;

MAKE_STD_ZVAL(fooval);
ZVAL_STRING(fooval, "bar", 1);
ZEND_SET_SYMBOL( EG(active_symbol_table) , "foo" , fooval);
}

首先,我们声明一个zval指针,并申请一块内存。然后通过ZVAL_STRING宏将值设置为‘bar’,最后一行的作用就是将这个zval加入到当前的符号表里去,并将其label定义成foo,这样用户就可以在代码里通过$foo来使用它了。

变量的检索

在PHP中定义的变量,在内核中通过zend_hash_find()函数来找到当前作用域下用户定义好的变量。zend_hash_find是内核提供的操作hashTable的API之一。
http://www.walu.cc/phpbook/2.5.md

类型转换

我们可以通过符号表获取用户在PHP中定义的变量了,想想一下在PHP中的自动类型转换,它在底层C中是怎么实现的呢。
内核中提供了很多函数专门来帮助实现类型转换,这类函数统一的形式为: convert_to_*()

//将任意类型的zval转换成字符串
void change_zval_to_string(zval *value)
{
convert_to_string(value);
}

//其它基本的类型转换函数
ZEND_API void convert_to_long(zval *op);
ZEND_API void convert_to_double(zval *op);
ZEND_API void convert_to_null(zval *op);
ZEND_API void convert_to_boolean(zval *op);
ZEND_API void convert_to_array(zval *op);
ZEND_API void convert_to_object(zval *op);

ZEND_API void _convert_to_string(zval *op ZEND_FILE_LINE_DC);
#define convert_to_string(op) if ((op)->type != IS_STRING) { _convert_to_string((op) ZEND_FILE_LINE_CC); }

其中,convert_to_string其实是一个宏函数,调用的另外一个函数;另外没有convert_to_resource()的转换函数,因为资源的值在用户层面上,根本就没有意义,内核不会对它的值(不是指那个数字)进行转换。

参考: PHP变量在内核中的实现