polaris

专注互联网技术

092016
 

本文介绍 Unix I/O 模型中的4个通用系统调用:open()、read()、write()和close() 的 Go 语言封装。

1、Linux 中 open 系统调用的定义

#include <sys/stat.h>
#include <fcntl.h>

int open(const char* pathname, int flags, … /* mode_t mode */);
                   Returns file descriptor on success, or -1 on error

2、Go 中 open 系统调用的封装

一般的,我们使用 os 标准库的 Open/Create 方法来间接调用 open 系统调用,跟踪代码,找到 Go 中 open 系统调用的封装:

func Open(path string, mode int, perm uint32) (fd int, err error) {
    return openat(_AT_FDCWD, path, mode|O_LARGEFILE, perm)
}

2.1 openat 又是什么呢?

从 2.6.16 (Go 支持的 Linux 版本是 2.6.23)开始,Linux 内核提供了一系列新的系统调用(以at结尾),它们在执行与传统系统调用相似任务的同时,还提供了一些附加功能,对某些程序非常有用,这些系统调用使用目录文件描述符来解释相对路径。

#define _XOPEN_SOURCE 700 /* Or define _POSIX_C_SOURCE >= 200809 */
#include <fcntl.h>

int openat(int dirfd, const char* pathname, int flags, … /* mode_t mode */);
                   Returns file descriptor on success, or -1 on error

可见,openat 系统调用和 open 类似,只是添加了一个 dirfd 参数,其作用如下:
1)如果 pathname 中为一相对路径名,那么对其解释则以打开文件描述符 dirfd 所指向的目录为参照点,而非进程的当前工作目录;
2)如果 pathname 中为一相对路径,且 dirfd 中所含为特殊值 AT_FDCWD(其值为-100),那么对 pathname 的解释则相对于进程当前工作目录,这时 openat 和 open 行为一致;
3)如果 pathname 中为绝对路径,那么将忽略 dirfd 参数;

在 Go 中,只要存在相应的 at 系统调用,都会使用它。

2.2 解读 Go 中的 openat

有上可知,Go 中的 Open 并非执行 open 系统调用,而是 openat 系统调用,行为和 open 一致。

func openat(dirfd int, path string, flags int, mode uint32) (fd int, err error) {
    var _p0 *byte
    // 根据要求,path 必须是 C 语言中的字符串,即以 NULL 结尾
    // BytePtrFromString 的作用就是返回一个指向 NULL 结尾的字节数组指针
    _p0, err = BytePtrFromString(path)
    if err != nil {
        return
    }
    // SYS_OPENAT openat 系统调用编号
    r0, _, e1 := Syscall6(SYS_OPENAT, uintptr(dirfd), uintptr(unsafe.Pointer(_p0)), uintptr(flags), uintptr(mode), 0, 0)
    // 空操作,用于保证 _p0 存活
    use(unsafe.Pointer(_p0))
    fd = int(r0)
    if e1 != 0 {
        err = errnoErr(e1)
    }
    return
}

2.3 反过来解读 os.OpenFile 函数

OpenFile 函数有一个点需要注意,创建或打开文件时,自动加上了 O_CLOEXEC 标志,也就是执行 Exec 系统调用时该文件描述符不会被继承;

os.OpenFile 返回 os.File 类型的指针,通过 File.Fd() 可以获取到文件描述符;

3、read、write 和 close 系统调用

通过 open 系统调用的分析可以很容易的自己分析 read、write和close 系统调用。

说明一点:close 系统调用,企图关闭一个未打开的文件描述符或两次关闭同一个文件描述符,会返回错误。一般都不需要关心错误。

4、lseek 系统调用

Go 中的 os 包的 File.Seek 对应的系统调用是 lseek

5、其他文件 I/O 相关的系统调用

os 包中,File 类型中 ReadAt/WriteAt 对应的系统调用是 pread/pwrite

Truncate 对应 truncate 等;

几乎所有 Linux 文件相关系统调用,Go 都有封装;另外,通过上面 Open 的介绍,即使没有封装,自己封装也不是难事。

082016
 

一、系统调用概述

系统调用是受控的内核入口,借助于这一机制,进程可以请求内核以自己的名义去执行某些动作。Linux 内核以 C 语言语法 API 接口形式(头文件),提供有一系列服务供程序访问。可以通过 man 2 syscall 查看系统调用信息。

关于系统调用,需要注意以下几点:
1、系统调用将处理器从用户态切换到核心态,以便 CPU 访问受到保护的内核内存;
2、系统调用的组成是固定的,每个系统调用都由一个唯一的数字来标识;
3、每个系统调用可辅之以一套参数,对用户控件(进程虚拟地址控件)与内核空间之间(相互)传递的信息加以规范;

以C语言为例,执行系统调用时,幕后会历经诸多步骤。以 x86-32 平台为例,按时间发生顺序对这些步骤加以分析:
1、应用程序通过 C 语言函数库中的外壳(wrapper)函数,来发起系统调用;
2、对系统调用中断处理例程来说,外壳函数必须保证所有的系统调用参数可用。通过堆栈,这些参数传入外壳函数,但内核却希望将这些参数置入特定寄存器。因此,外壳函数会将上述参数复制到寄存器;
3、由于所有系统调用进入内核的方式相同,内核需要设法区分每个系统调用。为此,外壳函数会将系统调用编号复制到一个特殊的 CPU 寄存器 (%eax) 中;
4、外壳函数执行一条中断机器指令(int 0×80),引发处理器从用户态切换到核心态,并执行系统中断 0×80(十进制128)的中断矢量所之指向的代码;(2.6内核 和 glibc 2.3.2 以后的版本支持 sysenter 指令,进入内核速度更快);
5、为响应中断 0×80,内核会调用 system_call 例程(内核源码中 arch/i386/entry.S)来处理这次中断;
6、若系统调用服务例程的返回值表明调用有误,外壳函数会使用该值来设置全局变量 errno,然后外壳函数会返回到调用程序,并同时返回一个整数值,以表明系统调用是否成功;

二、Go 语言封装的系统调用

Go 语言调用系统调用,并没有使用系统提供的 C 语言函数形式,而是自己封装了系统调用。以 AMD64 为例,Go 语言提供了如下调用系统调用的方式:

func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)

其中,Syscall 和 RawSyscall 的区别如下:(以6结尾的一样)
从源码可以看出,Syscall 开始和结束,分别调用了 runtime 中的进入系统调用和退出系统调用的函数,这就说明,系统调用被 runtime 运行时(调度器)管理,系统调用可以在任何 goroutine 中执行;而 RawSyscall 并没有,因此它可能会阻塞,导致整个程序阻塞。我们应该总是使用 Syscall,RawSyscall 存在的意义是为那些永远不会阻塞的系统调用准备的,比如 Getpid。我们自己的程序需要时,应该用 Syscall。

Go 中 Syscall 的实现,在汇编文件 syscall/asm_linux_amd64.s 中:

// func Syscall(trap int64, a1, a2, a3 int64) (r1, r2, err int64);
// Trap # in AX, args in DI SI DX R10 R8 R9, return in AX DX
// Note that this differs from "standard" ABI convention, which
// would pass 4th arg in CX, not R10.

TEXT    ·Syscall(SB),NOSPLIT,$0-56
    CALL    runtime·entersyscall(SB)
    MOVQ    a1+8(FP), DI
    MOVQ    a2+16(FP), SI
    MOVQ    a3+24(FP), DX
    MOVQ    $0, R10
    MOVQ    $0, R8
    MOVQ    $0, R9
    MOVQ    trap+0(FP), AX    // syscall entry
    SYSCALL     // 进入内核,执行内核处理例程
    // 0xfffffffffffff001 是 linux MAX_ERRNO 取反 转无符号,http://lxr.free-electrons.com/source/include/linux/err.h#L17
    CMPQ    AX, $0xfffffffffffff001        // 发生错误,r1=-1
    JLS    ok
    MOVQ    $-1, r1+32(FP)
    MOVQ    $0, r2+40(FP)
    NEGQ    AX             // 错误码,因为错误码是负数,这里转为正数
    MOVQ    AX, err+48(FP)
    CALL    runtime·exitsyscall(SB)
    RET
ok:
    MOVQ    AX, r1+32(FP)
    MOVQ    DX, r2+40(FP)
    MOVQ    $0, err+48(FP)
    CALL    runtime·exitsyscall(SB)
    RET

相关参考文献
https://groups.google.com/forum/#!searchin/golang-nuts/Syscall$20RawSyscall/golang-nuts/y9lT_1loJj4/g4ZrYB2_80YJ
《The Linux Programming Interface —A Linux and UNIX System Programming Handbook》

142016
 

用过PHP的人都知道,PHP处理JSON数据那是相当方便,json_encode和json_decode两个函数搞定一切。那么在Go中该怎么处理JSON呢?

一、encoding/json标准库

学习 json 库应该先了解 Go 中的 struct tag、reflect等知识。

1、概述

json包实现了json对象的编解码,参见RFC 4627。Json对象和go类型的映射关系请参见Marshal和Unmarshal函数的文档。
参见”JSON and Go”获取本包的一个介绍:http://golang.org/doc/articles/json_and_go.html 这是官方的一篇博文。

2、核心函数和类型

对于函数和类型,我们关注经常使用的。

1)Marshal 和 Unmarshal

这两个是最常使用的函数,也就是 json 对象的编解码。这两个函数的文档很长,详细解释了 Go 类型和 json 对象的映射关系等。映射关系整理如下:

bool, for JSON booleans
float64, for JSON numbers
string, for JSON strings
[]interface{}, for JSON arrays
map[string]interface{}, for JSON objects
nil for JSON null

详细的编码解码规则,文档上解释的很详细,这里说几个关键点:
①默认情况下,按照上面提到的映射进行解析;
②如果对象实现了 json.Marshaler/Unmarshaler 接口且不是 nil 指针,则调用对应的方法进行编解码;如果没有实现该接口,但实现了 encoding.TextMarshaler/TextUnmarshaler 接口,则调用该接口的相应方法进行编解码;
③struct 中通过 “json” tag 来控制相关编解码,后面通过示例说明;
④struct 的匿名字段,默认展开;可以通过指定 tag 来使其不展开;
⑤如果存在匿名字段,如果同级别有相同字段名,不会冲突,具体处理规则文档有说明;
⑥在解码时到struct时,会忽略多余或不存在的字段(包括不导出的),而不会报错;

另外注意,传递给 Unmarshal 的第二个参数必须是指针。

使用示例:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    type Book struct {
        Name  string
        Price float64 // `json:"price,string"`
    }
    var person = struct {
        Name string
        Age  int
        Book
    }{
        Name: "polaris",
        Age:  30,
        Book: Book{
            Price: 3.4,
            Name:  "Go语言",
        },
    }

    buf, _ := json.Marshal(person)
    fmt.Println(string(buf))

    // Output:{"Name":"polaris","Age":30,"Price":3.4}
    // Book 中的 Name 被忽略了
}

如果不希望内嵌类型展开,只需加上 tag:

var person = struct {
        Name string
        Age  int
        Book `json:"Book"`
}

有时候,比如之前是 PHP(弱类型语言) 写的,Age 的值很可能是 “Age”:”30” 这种形式,现在改为用 Go 实现,为了兼容;或者返回给客户端 Price 这样的浮点值,可能会涉及精度问题,客户端只是单纯的展示,返回浮点值的字符串即可。针对这样的情况,只需要加上这样的 tag:`json:”,string” 即可。这里逗号后面的“string”是 tag option。

如果想忽略某个字段,加上`json:”-”`;如果在值为空时忽略,加上
omitempty option,如:`json:”,omitempty”`

在解码时,优先匹配 struct 导出字段的 tag,之后是 Field,最后是 Field 的各种大小写不明感的形式,如 Name,能匹配 NAME/NAme等等。

2)MarshalIndent 函数

该函数的功能和 Marshal一致,只是格式化 json,方便人工阅读。如上面例子使用该函数,MarshalIndent(person, “”, “\t”) 输出如下:

{
    "Name": "polaris",
    "Age": 30,
    "Price": 3.4
}

3)Encoder 和 Decoder

有时候,我们可能从 Request 之类的输入流中直接读取 json 进行解析或将编码的 json 直接输出,为了方便,标准库为我们提供了 Decoder 和 Encoder 类型。它们分别通过一个 io.Reader 和 io.Writer 实例化,并从中读取数据或写数据。

通过阅读源码可以发现,Encoder.Encode/Decoder.Decode 和 Marshal/Unmarshal 实现大体是一样;有一些不同点:Decoder 有一个方法 UseNumber,它的作用:默认情况下,json 的 number 会映射为 Go 中的 float64,有时候,这会有些问题,比如:

b := []byte(`{"Name":"polaris","Age":30,"Money":20.3}`)

var person = make(map[string]interface{})
err := json.Unmarshal(b, &person)
if err != nil {
    log.Fatalln("json unmarshal error:", err)
}

age := person["Age"]
log.Println(age.(int))

我们希望 age 是 int,结果 panic 了:interface conversion: interface is float64, not int.

我们改为 Decoder.Decode(用上 UseNumber) 试试:

b := []byte(`{"Name":"polaris","Age":30,"Money":20.3}`)

var person = make(map[string]interface{})

decoder := json.NewDecoder(bytes.NewReader(b))
decoder.UseNumber()
err := decoder.Decode(&person)
if err != nil {
    log.Fatalln("json unmarshal error:", err)
}

age := person["Age"]
log.Println(age.(json.Number).Int64())

我们使用了 json.Number 类型。

4)RawMessage 类型

该类型的定义是 type RawMessage []byte,可见保存的是原始的 json 对象,它实现了 Marshaler 和 Unmarshaler 接口,能够延迟对 json 进行解码。使用示例可以参考 http://docs.studygolang.com/pkg/encoding/json/#RawMessage上的例子。

5)其他函数或类型不常用(如错误类型等),在此不赘述,可以直接查阅官方文档。

二、实际应用中的问题

当客户端和服务器通讯使用 json 这种数据格式时,我们一方面会解码客户端的 json 数据,另一方面,需要对数据进行 json 编码,发送给客户端。

一般的,服务器发送给客户端的 json 数据,是通过 struct、[]struct 或 map[string]interface{} 等编码得到。这里除了上文说到的,可能需要对数值类型使用 string tag options 之外,对于 time.Time 类型(实现了Marshaler 接口),默认编码得到的时间格式是:RFC3339即2006-01-02T15:04:05Z07:00,很多时候客户端可能不希望得到这样的时间,他们更多时候只是需要一个可读的时间字符串,如 2006-01-02 15:04:05。对此,我们可以定义自己的类型 type OftenTime time:

func (self OftenTime) MarshalJSON() ([]byte, error) {
    t := time.Time(self)
    if y := t.Year(); y < 0 || y >= 10000 {
        return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]")
    }
    return []byte(t.Format(`"2006-01-02 15:04:05"`)), nil
}

func (this *OftenTime) UnmarshalJSON(data []byte) (err error) {
    t := time.Time(*this)
    return t.UnmarshalJSON(data)
}

另外,有一个坑,json 对象的 key 必须是字符串,所以 map[int]interface{} 在编码时会报错,错误是 json.UnsupportedTypeError.

对于接收客户端数据,进行 json 解码,遇到的问题可能比较多,特别是同时接收多种语言的数据,比如 PHP、Java 等。比如 b := []byte(`{“Name”:”polaris”,”Age”:30,”Money”:20.3}`),PHP 传递过来的可能是:b := []byte(`{“Name”:”polaris”,”Age”:”30″,”Money”:”20.3″}`),在使用 struct 接收数据时,对于 Age,如果是 int,我们可以直接定义为 int 类型,但如果是string,可以通过 string tag options 接收;但如果Age有时是 int, 有时是 string,就会出问题。最理想的情况,当然是不希望出现这种情况,但有一点,程序要保证出现这种情况时,不能 panic。

针对这种情况,有一种办法,是定义为 json.Number 类型。不过这样有两个问题:1)使用时不方便,需要调用 json.Number 的方法,而且它的方法返回值还得判断错误;2)编码 json.Number 时,是数值,如果客户端需要字符串,就不行了。

在实际应用中,我就遇到了上面的问题,于是,自己写了一个 json 解析,能支持自动类型转换。代码开源在 github:https://github.com/polaris1119/jsonutils

三、性能问题

很明显,json 的编解码,使用了 Go 的反射功能,所以,性能自然不是太好,正因为如此,有了 ffjson、easyjson 之类的开源库(在 github 上),它们的原理是通过 go generate 根据 struct 生成相应的代码,避免反射。如果你对性能要求比较高,但又不想使用msgpack/pb/thrift 之类的,那么可以考虑使用 ffjson/easyjson 来优化性能。

132015
 

微信支付获取 prepay id 偶尔失败问题总结。

微信支付会要求先从微信服务器获取 prepay id (https://api.mch.weixin.qq.com/pay/unifiedorder)。我们开发完成后(语言是PHP,使用微信的支付SDK,请求时使用curl),在测试环境的机器上,基本没有发现请求失败的情况,上线后,却发现经常出现错误,概率1/5甚至更高。开始没有深究原因,采用重试的方式,不过发现,只要失败了,重试也会失败。

记录下 curl 的错误是:errno:35, error: SSL Connect Error。

网上查相关资料,没有找到解决方案。联系微信技术支持,他们没有任何建议,觉得是我们的问题,让我们自己查。

在我们服务器上通过 tcpdump 抓包:tcpdump -i eth1 ip host 140.207.69.102 -w wxpay.cap,对比成功和失败的包(使用wireshark分析):

成功的数据包:

失败的数据包:

可以看到,失败的时候,TCP 3次握手后马上进行4次挥手操作 没有任何内容交互,而且 close 是客户端(我们这边)发起的。

推论:与remote服务端应该无关,还没开始https内容部分的交换,应该是本地在进行某些工作的时候发现异常,close了连接(但程序如果让我写会考虑先本地工作结束后再connect,不清楚为什么先connect然后再获取本地资源)。

可以通过 strace curl -X POST https://api.mch.weixin.qq.com/pay/unifiedorder 查看整个系统调用过程,是先 connect,然后从 系统本地etc目录下加载一些CA相关文件。

也可以通过 curl -v -X POST https://api.mch.weixin.qq.com/pay/unifiedorder 查看相关信息。对于 PHP 中,可以设置:curl_setopt($ch, CURLOPT_VERBOSE, true)

失败时是类似这样的输出:

* About to connect() to api.mch.weixin.qq.com port 443 (#0)
* Trying 140.207.69.102… connected
* Connected to api.mch.weixin.qq.com (140.207.69.102) port 443 (#0)
* Initializing NSS with certpath: sql:/etc/pki/nssdb
* CAfile: /etc/pki/tls/certs/ca-bundle.crt
CApath: none
* NSS error -5990
* Closing connection #0
* SSL connect error
curl: (35) SSL connect error

成功时输出如下(省略了response):

* About to connect() to api.mch.weixin.qq.com port 443 (#0)
* Trying 140.207.69.102… connected
* Connected to api.mch.weixin.qq.com (140.207.69.102) port 443 (#0)
* Initializing NSS with certpath: sql:/etc/pki/nssdb
* CAfile: /etc/pki/tls/certs/ca-bundle.crt
CApath: none
* NSS: client certificate not found (nickname not specified)
* SSL connection using TLS_DHE_RSA_WITH_AES_256_CBC_SHA
* Server certificate:
* subject: CN=payapp.weixin.qq.com,OU=R&D,O=Tencent Technology (Shenzhen) Company Limited,L=shenzhen,ST=guangdong,C=CN
* start date: 4月 28 00:00:00 2015 GMT
* expire date: 4月 27 23:59:59 2016 GMT
* common name: payapp.weixin.qq.com
* issuer: CN=GeoTrust SSL CA – G2,O=GeoTrust Inc.,C=US
> POST /pay/unifiedorder HTTP/1.1
> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.16.2.3 Basic ECC zlib/1.2.3 libidn/1.18 libssh2/1.4.2
> Host: api.mch.weixin.qq.com
> Accept: */*

在网上找到了一个帖子: http://serverfault.com/questions/606135/curl-35-ssl-connect-error, 问题和我们遇到的一样,虽然操作系统版本和NSS版本不一样。按上面的提示,升级 nss(Mozilla Network Security Services 网络安全服务):yum update nss, 然后重启 php,问题解决。

总结:
1、从现象:有些机器有问题,有些没有问题;有些概率大,有些概率小;可大体上推断,跟机器有关系,可能机器环境不一样导致的;
2、抓包分析原因,进一步确认不是服务端(微信支付)的问题;
3、根据错误描述在网络上寻求帮助;
4、学习 curl 相关选项的使用;strace 的使用;

062015
 

做过 Web 开发的,应该都用过或听过 jQuery,它提供了方便的操作 DOM 的 API。使用 Go 语言做服务器端开发,有时候需要解析 HTML 文件,比如抓取网站内容、写一个爬虫等。这时候如果有一个类似 jQuery 的库可以使用,操作 DOM 会很方便,而且,上手也会很快。github.com/PuerkitoBio/goquery 这个库就实现了类似 jQuery 的功能,让你能方便的使用 Go 语言操作 HTML 文档。

1 概述

Go 实现了类似 jQuery 的功能,包括链式操作语法、操作和查询 HTML 文档。它基于 Go net/html 包和 CSS 选择器库 cascadia。由于 net/html 解析器返回的是 DOM 节点,而不是完整的 DOM 树,因此,jQuery 的状态操作函数没有实现(像 height(),css(),detach())。

由于 net/html 解析器要求文档必须是 UTF-8 编码,因此 goquery 库也有此要求。如果文档不是 UTF-8 编码,使用者需要自己转换。进行编码转换,可以使用如下库:
iconv 的 Go 封装,如:github.com/djimenez/iconv-go
官方提供的 text 子仓库,text/encoding,用于其他编码和 UTF-8 之间进行转换

除了实现和 jQuery 类似的功能外,在函数名方面,也尽量和 jQuery 保持一致,也支持链式语法。

2 goquery 提供的主要类型和方法

2.1 Document

Document 代表一个将要被操作的 HTML 文档,不过,和 jQuery 不同,它装载的是 DOM 文档的一部分。

type Document struct {
	*Selection
	Url *url.URL
	rootNode *html.Node // 文档的根节点
}

因为 Document 中内嵌了一个 Selection 类型,因此,Document 可以直接使用 Selection 类型的方法。

有五种方法获取一个 Document 实例,分别是从一个 URL 创建、从一个 *html.Node 创建、从一个 io.Reader 创建、从一个 *http.Response 创建和从一个已有的 Document Clone 一个。

2.2 Selection

Selection 代表符合特定条件的节点集合。

type Selection struct {
	Nodes []*html.Node
	document *Document
	prevSel *Selection
}

一般地,得到了 Document 实例后,通过 Dcoument.Find 方法获取一个 Selection 实例,然后像 jQuery 一样使用链式语法和方法操作它。

Selection 类型提供的方法可以分为如下几大类(注意,3个点(…)表示有重载的方法):

1)类似函数的位置操作

- Eq()
- First()
- Get()
- Index…()
- Last()
- Slice()

2)扩大 Selection 集合(增加选择的节点)

- Add…()
- AndSelf()
- Union(), which is an alias for AddSelection()

3)过滤方法,减少节点集合

- End()
- Filter…()
- Has…()
- Intersection(), which is an alias of FilterSelection()
- Not…()

4)循环遍历选择的节点

- Each()
- EachWithBreak()
- Map()

5)修改文档

- After…()
- Append…()
- Before…()
- Clone()
- Empty()
- Prepend…()
- Remove…()
- ReplaceWith…()
- Unwrap()
- Wrap…()
- WrapAll…()
- WrapInner…()

6)检测或获取节点属性值

- Attr(), RemoveAttr(), SetAttr()
- AddClass(), HasClass(), RemoveClass(), ToggleClass()
- Html()
- Length()
- Size(), which is an alias for Length()
- Text()

7)查询或显示一个节点的身份

- Contains()
- Is…()

8)在文档树之间来回跳转(常用的查找节点方法)

- Children…()
- Contents()
- Find…()
- Next…()
- Parent[s]…()
- Prev…()
- Siblings…()

2.3 Matcher 接口

该接口定义了一些方法,用于匹配 HTML 节点和编译过的选择器字符串。Cascadia’s Selector 实现了该接口。

type Matcher interface {
	Match(*html.Node) bool
	MatchAll(*html.Node) []*html.Node
	Filter([]*html.Node) []*html.Node
}

3 实战演练

该库提供的类型很少,但方法却很多,我们不可能一个个方法讲解。这里通过模拟几个使用场景来讲解该库的使用。

3.1 抓取 Go语言中文网 社区主题 — http://studygolang.com/topics

查看页面 HTML 结构后,就跟使用 jQuery 操作页面一样。这个例子,我们获取社区主题的标题列表。

主要代码如下(为了节省篇幅,包导入等语句省略,完整代码,参见文章最后说明):

func main() {
	doc, err := goquery.NewDocument("http://studygolang.com/topics")
	if err != nil {
		log.Fatal(err)
	}

	doc.Find(".topics .topic").Each(func(i int, contentSelection *goquery.Selection) {
		title := contentSelection.Find(".title a").Text()
		log.Println("第", i+1, "个帖子的标题:", title)
	})
}

编译、运行输出如下(你看到的内容和当时社区的主题列表一致):

2015/04/06 22:15:24 第 1 个帖子的标题: 问个加载包的问题
2015/04/06 22:15:24 第 2 个帖子的标题: Tango v0.4版本发布,带来统一高性能的新路由
2015/04/06 22:15:24 第 3 个帖子的标题: 创业团队缺后端开发
2015/04/06 22:15:24 第 4 个帖子的标题: 自由是创造力和灵感的催化剂,需要golang后端开发人员
2015/04/06 22:15:24 第 5 个帖子的标题: cgo编译出来的文件怎么用?
2015/04/06 22:15:24 第 6 个帖子的标题: cgo编译问题,Undefined symbols
2015/04/06 22:15:24 第 7 个帖子的标题: 北京 GO 研发程序员,全职,20K+
2015/04/06 22:15:24 第 8 个帖子的标题: Go 1.5 计划启动,使用 Go 来编译 Go
2015/04/06 22:15:24 第 9 个帖子的标题: beego.Error 原理
2015/04/06 22:15:24 第 10 个帖子的标题: 插入数据库操作测试 1分钟一百万条数据 这数据怎么样?
2015/04/06 22:15:24 第 11 个帖子的标题: Go最新资料汇总(十一)
2015/04/06 22:15:24 第 12 个帖子的标题: Azul3D_Go开发的3D游戏引擎简介
2015/04/06 22:15:24 第 13 个帖子的标题: Golang中goroutine线程何时终止问题
2015/04/06 22:15:24 第 14 个帖子的标题: 标准库中文版的testing是不是打不开了?
2015/04/06 22:15:24 第 15 个帖子的标题: 如何使用cgo编译出来的文件

是不是很简单?

这里我们使用了 Each 这个方法。在 jQuery 中,each 迭代时,如果返回 false,可以终止迭代。比如,我们希望遇到标题中包含 cgo 的主题时,停止迭代,可以使用 EachWithBreak(之所以没有使用 Each,是因为迭代终止的功能是后来加入的,为了不改变 Each 的行为,保持兼容性,引入了该方法):

doc.Find(".topics .topic").EachWithBreak(func(i int, contentSelection *goquery.Selection) bool {
	title := contentSelection.Find(".title a").Text()
	log.Println("第", i+1, "个帖子的标题:", title)
	if strings.Contains(title, "cgo") {
		return false
	}
	return true
})

从上面的输出可以看到,Each 遍历是按照页面节点的顺序的。如果我们希望反着处理,也就是先处理页面最底下的节点。查看文档,发现没有直接提供这样的方法。那么该怎么实现呢?

topicsSelection := doc.Find(".topics .topic")

for i := topicsSelection.Length() - 1; i >= 0; i-- {
	// 返回的是 *html.Node
	topicNode := topicsSelection.Get(i)
	title := goquery.NewDocumentFromNode(topicNode).Find(".title a").Text()
	log.Println("第", i+1, "个帖子的标题:", title)
}

这里用到了 NewDocumentFromNode,把其中某一块 HTML 当做文档,对其进行操作。

输出如下:

2015/04/06 22:50:28 第 15 个帖子的标题: 如何使用cgo编译出来的文件
2015/04/06 22:50:28 第 14 个帖子的标题: 标准库中文版的testing是不是打不开了?
2015/04/06 22:50:28 第 13 个帖子的标题: Golang中goroutine线程何时终止问题
2015/04/06 22:50:28 第 12 个帖子的标题: Azul3D_Go开发的3D游戏引擎简介
2015/04/06 22:50:28 第 11 个帖子的标题: Go最新资料汇总(十一)
2015/04/06 22:50:28 第 10 个帖子的标题: 插入数据库操作测试 1分钟一百万条数据 这数据怎么样?
2015/04/06 22:50:28 第 9 个帖子的标题: beego.Error 原理
2015/04/06 22:50:28 第 8 个帖子的标题: Go 1.5 计划启动,使用 Go 来编译 Go
2015/04/06 22:50:28 第 7 个帖子的标题: 北京 GO 研发程序员,全职,20K+
2015/04/06 22:50:28 第 6 个帖子的标题: cgo编译问题,Undefined symbols
2015/04/06 22:50:28 第 5 个帖子的标题: cgo编译出来的文件怎么用?
2015/04/06 22:50:28 第 4 个帖子的标题: 自由是创造力和灵感的催化剂,需要golang后端开发人员
2015/04/06 22:50:28 第 3 个帖子的标题: 创业团队缺后端开发
2015/04/06 22:50:28 第 2 个帖子的标题: Tango v0.4版本发布,带来统一高性能的新路由
2015/04/06 22:50:28 第 1 个帖子的标题: 问个加载包的问题

除了获取节点的文本内容,还可以获取节点的属性值、判断是否有某个 class 等,gopher 们可以自己试验。

【未完待续。。。】

032014
 

工作中碰到这样一个问题:

有一个文本文件,有上亿行数据,每行数据是 unsigned int。现在需要将其中可能重复的数只保留一个,同时和另外一个或多个这样的文件进行排重(即和它们做差集)。要求尽可能快的筛选出来。

开始实现比较简单粗暴,将数据直接通过 LOAD DATA INFILE 导入 MySQL 表中,然后多表之间做 LEFT JOIN。数据不是特别大,比如几千万,且就要排重的文件不多时,比如一个,速度还可以接受。然而,当数据上亿,且有多个文件需要排重时,性能急剧下降,必须进行优化。而这,正是 Bitmap 的应用场景。

1、Bitmap 概念

Bitmap 是一个十分有用的数据结构。所谓的 Bit-map 就是用一个 bit 位来标记某个元素对应的 Value,而 Key 即是该元素。由于采用了 Bit 为单位来存储数据,因此在内存占用方面,可以大大节省。(《编程珠玑》第一章引入的问题,提到了 Bitmap)

2、Bitmap 的实现原理

以一个简单的数组排序来说明 Bitmap 的实现原理:array[4,6,3,1,7]

Bitmap 采用的是以空间换时间的思想,数组中最大元素值为7,所以在内存中开辟8位的存储空间,存储空间大小的确定方法是(元素最大值进位到8的倍数/8),之所以除以8,是因为开辟空间的时候以byte为单位,1byte=8bit。

开辟8位的空间后,每位初始化为0,如下表:

0号位 1号位 2号位 3号位 4号位 5号位 6号位 7号位
0 0 0 0 0 0 0 0

开始遍历 array 数组,array[0]=4 时,则将 4号位 置1,变为下表:

0号位 1号位 2号位 3号位 4号位 5号位 6号位 7号位
0 0 0 0 1 0 0 0

array[1]=6 时,则将 6号位 置1,变为下表:

0号位 1号位 2号位 3号位 4号位 5号位 6号位 7号位
0 0 0 0 1 0 1 0

直至遍历完 array 数组,空间各位如下表:

0号位 1号位 2号位 3号位 4号位 5号位 6号位 7号位
0 1 0 1 1 0 1 1

最后,从头开始遍历空间中各位,为1的输出其 位号,得:1,3,4,6,7,其效率为O(n)=8

3、Bitmap 编码实现

一般的,静态语言比较容易实现 Bitmap。在下面的实现中,Bitmap 数据结构可以直接定义为 byte 数组,然而,出于使用的方面原因,这里 Bitmap 的实现额外保存了一些其他信息,因此 Go 和 C 中,使用 struct 定义 Bitmap。

从上面的排序例子知道,实现的关键是 置位 和 清位。

3.1 Go 语言实现

Bitmap 数据结构定义如下:

type Bitmap struct {
    // 保存实际的 bit 数据
    data []byte
    // 指示该 Bitmap 的 bit 容量
    bitsize uint64
    // 该 Bitmap 被设置为 1 的最大位置(方便遍历)
    maxpos uint64
}

置位 和 清位 方法:

// SetBit 将 offset 位置的 bit 置为 value (0/1)
func (this *Bitmap) SetBit(offset uint64, value uint8) bool {
     index, pos := offset/8, offset%8

     if this.bitsize < offset {
          return false
     }

     if value == 0 {
          // &^ 清位
          this.data[index] &^= 0x01 << pos
     } else {
          this.data[index] |= 0x01 << pos

          // 记录曾经设置为 1 的最大位置
          if this.maxpos < offset {
               this.maxpos = offset
          }
     }

     return true
}

完整实现代码:Github Bitmap Golang

3.2 C 语言实现

Bitmap 数据结构定义如下:

typedef struct {
     uint64_t bitsize;
     uint64_t maxpos;

     /* 不定长,必须是结构的最后一个成员 */
     uint8_t data[];
} Bitmap;

置位 和 清位 方法:

bool set_bit(Bitmap* bitmap, uint64_t offset, uint8_t value)
{
     uint64_t index, pos;

     index = offset / 8;
     pos = offset % 8;

     if (bitmap->bitsize < offset) {
          return false;
     }

     if (value) {
          bitmap->data[index] |= 1 << pos;

          if (bitmap->maxpos < offset) {
               bitmap->maxpos = offset;
          }
     } else {
          bitmap->data[index] &= BITMAP_MASK ^ (1 << pos);
     }

     return true;
}

相比和 Go 语言版的不同点是,C 没有直接的 清位 操作符

完整实现代码:Github Bitmap C

3.3 Java 语言实现

Bitmap 采用类实现,定义如下成员变量

private byte[] data;

private long bitsize;
private long maxpos;

置位 和 清位 方法:

public boolean setBit(long offset, int value) {
     if (this.bitsize < offset) {
          return false;
     }

     int index = (int) offset / 8;
     int pos = (int) offset % 8;

     if (value == 1) {
          this.data[index] |= 1 << pos;
          if (this.maxpos < offset) {
               this.maxpos = offset;
          }
     } else {
          this.data[index] &= Bitmap.BITMAP_MASK ^ (1 << pos);
     }

     return true;
}

跟 C 语言一样, Java 也没有直接提供 清位 操作符。

完整实现代码:Github Bitmap Java

3.4 三种语言实现的不同点

通过对比三种语言的实现,可以看出一些不同点:

1)数据类型的支持:Go 和 C 可以实现 uint8/uint64,而 Java 不区分是否有符号(Java 8 提供了无符号数);
2)清位操作:Go 提供了清位操作符;而 C 和 Java 需要自己实现,关键点是 0×01 和 MASK 做异或操作;

4、Bitmap 具体实现的关键点

从上面三种语言的具体实现可以看出,Bitmap 实现的关键有如下几点:

1)使用 byte 数组保存数据,这样可以极大的节省内存空间;
2)某个元素(也就是某个位置)要置为0或1,通过对 8 (1byte=8bit) 做除法和取余来实现;
3)具体的置位或清位,使用位操作实现:没有直接提供操作符的,可以通过多种操作符组合实现;

5、Bitmap 更多应用

一般地涉及到无重复、int 类型的问题,可以考虑 Bitmap 是否能够实现。

1)文章开头提到的排重实现(代码见 github 的taskdiff)

2)《编程珠玑》第一章的问题,可以参考《编程珠玑–位图法排序》

3)已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数

8位最多99 999 999,大概需要99m个bit,大概10几m字节的内存即可。 (可以理解为从0-99 999 999的数字,每个数字对应一个Bit位,所以只需要99M个Bit==12.5MBytes,这样,就用了小小的12M左右的内存表示了所有的8位数的 电话)

4)2.5亿个整数中找出不重复的整数的个数(内存空间不足以容纳这2.5亿个整数)

将bit-map扩展一下,用2bit表示一个数即可,0表示未出现,1表示出现一次,2表示出现2次及以上,在遍历这些数的时候,如果对应位置的 值是0,则将其置为1;如果是1,将其置为2;如果是2,则保持不变。或者我们不用2bit来进行表示,我们用两个bit-map即可模拟实现这个 2bit-map,都是一样的道理。

6、题外话

用三种语言实现的Bitmap进行排重处理,C 和 Java 版本速度差不多,1.5 亿数据60秒内处理完;而 Go 版本需要 1分50秒 左右(主要 bufio 包性能不太理想);内存占用方面,C 最少,Go 次之,Java 最多。

132014
 

热爱Go语言,一直使用着、关注着。那么如何获取Go最新动态,使用它最新的特性能?

1、获取最新动态

获取Go语言的最新动态有以下几种方法。

1.1 最直接最原始的方式 —— 官方源码库(hg clone https://code.google.com/p/go/),即 tip。关注Go开发者们提交代码的注释、代码diff。

1.2 关注 golang-dev 讨论组。Go开发者会在这上面讨论Go语言的开发

1.3 关注 issues,以及代码review

1.4 通过 Go dashboard 了解 Go 某个版本的 issues 解决情况(链接最后修改为相应版本即可)

1.5 Go官方网站对应的 tip 版

1.6 关注国内 Go 社区的一些信息

2、使用最新特性

在新版本还未发布时,可能会有些新特性提前公布出来,如果想试验下,就需要安装 tip 版 Go了。

clone Go tip 代码,Windows 下建议使用 TortoiseHg,管理、查看都很方便。

2.1、编译 tip 版本

使用 tip 版本,只能自己编译。在 Unix 下,编译很方便,而在 Windows 下相对就麻烦些,需要安装 MinGW 这样的工具。MinGW 比 Cygwin 轻,下载地址:去下载

安装好 MinGW后(保证命令行能使用 gcc),可以跟 Unix 下一样编译 Go 了。多版本并存问题,请参考《Go语言:安装多版本》。

然后,编译的时候可能会遇到如下问题:

*** failed to import extension codereview from c:\go\lib\codereview\
codereview.py: No module named HTMLParser

咋一看,以为没有安装 Python的原因。但是安装 Python 后,问题依旧

2.2、寻找答案的途径

一般的,我们遇到问题会上谷歌、百度之类的搜索引擎查。对于 Go 语言,目前还比较小众,有些问题可能搜索引擎找不到答案。因此,我们可以考虑其他途径。

个人建议遇到 Go 方面的问题,可以考虑先到 golang-nuts 讨论组去搜索,一般都会找到答案,如果找不到,可以在上面描述你的问题,很快就会有人解答的。

当然,如果你的英文不太好,或苦于翻墙费劲,可以在国内的社区提问,比如:Go语言学习园地, 会尽快得到答复的。

针对这个问题,以 No module named HTMLParser 为关键词,在 golang-nuts 上搜索,能较快找到答案:codereview extension under Windows/Mingw Mercurial

1. install mercurial binary package
2. inastall python2.7 binary package
3. copy following modules into the root folder of “library.zip” from python2.7/lib/
markupbase.py
htmlentitydefs.py
HTMLParser.py

其中,安装了 TortoiseHg 后(不需要再安装 mercurial),在其目录中就会有 library.zip

照着做了后,再编译,一切都 OK 了。

2.3、使用新特性

安装了 tip 版,就可以使用 Go 的最新特性了,尽情享受 Go 带给你的快了吧!

注:以上不少网址可能都被墙了,程序员应该学会翻墙!

update

1. 发现了一个 go 源码的 github 只读镜像,代码几乎和官方同步,不用翻墙可以看Go最新变化了。 https://github.com/jnwhiteh/golang

132014
 

在开发或者自己学习Go的过程中,可能会对比不同版本Go语言的特性、性能等,特别是可能想提前用上tip版本的一些特性,这个时候,系统中可能需要多个Go版本。那么该如何处理这个问题呢?

一般地,我觉得有两种处理方法。

1、目录改名法

比如 Go 版本(go1.3)安装在 /usr/local/go 目录中,并配置了 GOROOT(GOROOT=/usr/local/go)和 PATH (PATH=$PATH:$GOROOT/bin),这时,我们想安装一个Go1.2版本,比如放在了 /usr/local/go1.2 中。

通常,我们都在使用 Go1.3,但某个时候,我们想使用 Go1.2,这个时候,可以将 /usr/local/go 改名为 /usr/local/go1.3,同时将 /usr/local/go1.2 改名为 /usr/local/go,这样,Go版本就变成了 1.2 了。有其他版本时,一样处理。

2、不设置GOROOT的方法

最早时,Go需要设置不少环境变量,比如:GOARCH/GOOS/GOBIN/GOROOT 等,特别是GOBIN,网上有不少旧文章讲安装 Go 时,都提到设置这个环境变量,而实际上,如果你设置了GOBIN,使用 go install 时会出现可执行文件被安装到了 GOBIN 目录中,而不是 GOPATH/bin 目录中。

一般地,Go1.0 之后,只需要设置 GOROOT 和 GOPATH 就可以了,我们通过 go env 命令能够看到Go相关的环境变量:

GOARCH=”amd64″
GOBIN=”"
GOCHAR=”6″
GOEXE=”"
GOHOSTARCH=”amd64″
GOHOSTOS=”linux”
GOOS=”linux”
GOPATH=”/root/golang/myproject/”
GORACE=”"
GOROOT=”/usr/local/go”
GOTOOLDIR=”/usr/local/go/pkg/tool/linux_amd64″
CC=”gcc”
GOGCCFLAGS=”-fPIC -m64 -pthread -fmessage-length=0″
CXX=”g++”
CGO_ENABLED=”1″

也就是说,虽然,我们只设置了GOROOT和GOPATH,但实际上,很多其他环境变量也是有值的。实际上,在 《分析源码安装Go的过程》 一文中提到,make.bash 脚本会 export GOROOT,也就是说在安装时,并不需要自己设置 GOROOT,那么安装完成之后呢?可以试试在安装完成之后,不设置GOROOT,看看 go env 的输出,你会发现,GOROOT已经正确设置了。

可见,即使没有主动设置GOROOT,实际上 Go 也能知道 GOROOT 指向哪里,也就是说,主动设置 GOROOT 会覆盖默认的 GOROOT 设置。既然不设置 GOROOT,Go也能正确的找到 Go 安装目录,那么我们为啥还要设置呢?(可能设置的另一个理由是,设置 PATH 时,可以通过 $GOROOT 这种方式引入,改变 GOROOT 就可以起到改变 PATH 的目的)

这样一来,我们安装多个版本的Go,各个版本的GOROOT总是能够指向自己的目录,基于这一点,我们可以做到同时使用多版本。

当多版本时,我们可以将主版本的 bin 目录加入 PATH,以方便直接使用 go 命令。对于其他版本,我们可以通过建立软连接的方式来使用,比如:ln -s /usr/local/go1.2/bin/go /usr/bin/go1.2 (windows 下可以考虑直接改名)(注:Go1.4 将支持 Windows 下创建符号链接,这样可以用 Go 实现在Windows下建立符号链接,而不是改名,详见:《【Go1.4】主要改动》

综上,选择哪种方式根据你的喜好。个人觉得方法2方便些。配置了一次后,直接就可以使用了,而方法1总是得修改目录。

3、我的多版本配置

命令行 go/go1.0/go1.1/go1.2/go1.3/go_tip 分别使用对应的Go版本。(其中,go 使用了 最新稳定版)

Linux下:
多版本
多版本

Windows下:
多版本
多版本

注:quick_tools 下的文件是 通过自己写的 Windows 下创建符号链接工具创建的。

4、参考文献

5、代码下载

Windows 下的 ln 源码下载

二进制程序下载

6、更新说明

1)2014-08-13 初始版本
2)2014-08-15 增加 我的多版本配置 部分

222014
 

1、cron 表达式的基本格式

用过 linux 的应该对 cron 有所了解。linux 中可以通过 crontab -e 来配置定时任务。不过,linux 中的 cron 只能精确到分钟。而我们这里要讨论的 Go 实现的 cron 可以精确到秒,除了这点比较大的区别外,cron 表达式的基本语法是类似的。(如果使用过 Java 中的 Quartz,对 cron 表达式应该比较了解,而且它和这里我们将要讨论的 Go 版 cron 很像,也都精确到秒)

cron(计划任务),顾名思义,按照约定的时间,定时的执行特定的任务(job)。cron 表达式 表达了这种约定。

cron 表达式代表了一个时间集合,使用 6 个空格分隔的字段表示。

字段名 是否必须 允许的值 允许的特定字符
秒(Seconds) 0-59
* / , -
分(Minutes) 0-59
* / , -
时(Hours) 0-23
* / , -
日(Day of month) 1-31
* / , – ?
月(Month) 1-12 or JAN-DEC
* / , -
星期(Day of week) 0-6 or SUM-SAT
* / , – ?

注:
1)月(Month)和星期(Day of week)字段的值不区分大小写,如:SUN、Sun 和 sun 是一样的。
2)星期
(Day of week)字段如果没提供,相当于是 *

2、特殊字符说明

1)星号(*)

表示 cron 表达式能匹配该字段的所有值。如在第5个字段使用星号(month),表示每个月

2)斜线(/)

表示增长间隔,如第1个字段(minutes) 值是 3-59/15,表示每小时的第3分钟开始执行一次,之后每隔 15 分钟执行一次(即 3、18、33、48 这些时间点执行),这里也可以表示为:3/15

3)逗号(,)

用于枚举值,如第6个字段值是 MON,WED,FRI,表示 星期一、三、五 执行

4)连字号(-)

表示一个范围,如第3个字段的值为 9-17 表示 9am 到 5pm 直接每个小时(包括9和17)

5)问号(?)

只用于 日(Day of month) 和 星期(Day of week),表示不指定值,可以用于代替 *

3、主要类型或接口说明

1)Cron:包含一系列要执行的实体;支持暂停【stop】;添加实体等

type Cron struct {
    entries  []*Entry
    stop     chan struct{}   // 控制 Cron 实例暂停
    add      chan *Entry     // 当 Cron 已经运行了,增加新的 Entity 是通过 add 这个 channel 实现的
    snapshot chan []*Entry   // 获取当前所有 entity 的快照
    running  bool            // 当已经运行时为true;否则为false
}

注意,Cron 结构没有导出任何成员。

注意:有一个成员 stop,类型是 struct{},即空结构体。

2)Entry:调度实体

type Entry struct {
    // The schedule on which this job should be run.
    // 负责调度当前 Entity 中的 Job 执行
    Schedule Schedule

    // The next time the job will run. This is the zero time if Cron has not been
    // started or this entry's schedule is unsatisfiable
    // Job 下一次执行的时间
    Next time.Time

    // The last time this job was run. This is the zero time if the job has never
    // been run.
    // 上一次执行时间
    Prev time.Time

    // The Job to run.
    // 要执行的 Job
    Job Job
}

3)Job:每一个实体包含一个需要运行的Job

这是一个接口,只有一个方法:run

type Job interface {
    Run()
}

由于 Entity 中需要 Job 类型,因此,我们希望定期运行的任务,就需要实现 Job 接口。同时,由于 Job 接口只有一个无参数无返回值的方法,为了使用方便,作者提供了一个类型:

type FuncJob func()

它通过简单的实现 Run() 方法来实现 Job 接口:

func (f FuncJob) Run() { f() }

这样,任何无参数无返回值的函数,通过强制类型转换为 FuncJob,就可以当作 Job 来使用了,AddFunc 方法 就是这么做的。

4)Schedule:每个实体包含一个调度器(Schedule)

负责调度 Job 的执行。它也是一个接口。

type Schedule interface {
    // Return the next activation time, later than the given time.
    // Next is invoked initially, and then each time the job is run.
    // 返回同一 Entity 中的 Job 下一次执行的时间
    Next(time.Time) time.Time
}

Schedule 的具体实现通过解析 Cron 表达式得到。

库中提供了 Schedule 的两个具体实现,分别是 SpecSchedule 和 ConstantDelaySchedule。

① SpecSchedule

type SpecSchedule struct {
    Second, Minute, Hour, Dom, Month, Dow uint64
}

从开始介绍的 Cron 表达式可以容易得知各个字段的意思,同时,对各种表达式的解析也会最终得到一个 SpecSchedule 的实例。库中的 Parse 返回的其实就是 SpecSchedule 的实例(当然也就实现了 Schedule 接口)。

② ConstantDelaySchedule

type ConstantDelaySchedule struct {
    Delay time.Duration // 循环的时间间隔
}

这是一个简单的循环调度器,如:每 5 分钟。注意,最小单位是秒,不能比秒还小,比如 毫秒。

通过 Every 函数可以获取该类型的实例,如:

constDelaySchedule := Every(5e9)

得到的是一个每 5 秒执行一次的调度器。

4、主要实例化方法

1) 函数

① 实例化 Cron

func New() *Cron {
    return &Cron{
        entries:  nil,
        add:      make(chan *Entry),
        stop:     make(chan struct{}),
        snapshot: make(chan []*Entry),
        running:  false,
    }
}

可见实例化时,成员使用的基本是默认值;

② 解析 Cron 表达式

func Parse(spec string) (_ Schedule, err error)

spec 可以是:

  • Full crontab specs, e.g. “* * * * * ?”
  • Descriptors, e.g. “@midnight”, “@every 1h30m”

② 成员方法

// 将 job 加入 Cron 中
// 如上所述,该方法只是简单的通过 FuncJob 类型强制转换 cmd,然后调用 AddJob 方法
func (c *Cron) AddFunc(spec string, cmd func()) error

// 将 job 加入 Cron 中
// 通过 Parse 函数解析 cron 表达式 spec 的到调度器实例(Schedule),之后调用 c.Schedule 方法
func (c *Cron) AddJob(spec string, cmd Job) error

// 获取当前 Cron 总所有 Entities 的快照
func (c *Cron) Entries() []*Entry

// 通过两个参数实例化一个 Entity,然后加入当前 Cron 中
// 注意:如果当前 Cron 未运行,则直接将该 entity 加入 Cron 中;
// 否则,通过 add 这个成员 channel 将 entity 加入正在运行的 Cron 中
func (c *Cron) Schedule(schedule Schedule, cmd Job)

// 新启动一个 goroutine 运行当前 Cron
func (c *Cron) Start()

// 通过给 stop 成员发送一个 struct{}{} 来停止当前 Cron,同时将 running 置为 false
// 从这里知道,stop 只是通知 Cron 停止,因此往 channel 发一个值即可,而不关心值是多少
// 所以,成员 stop 定义为空 struct
func (c *Cron) Stop()

5、使用示例

package main

import (
    "github.com/robfig/cron"
    "log"
)

func main() {
    i := 0
    c := cron.New()
    spec := "*/5 * * * * ?"
    c.AddFunc(spec, func() {
        i++
        log.Println("cron running:", i)
    })
    c.Start()

    select{}
}

这是一个简单示例。

输出类似这样:

2014/02/22 21:23:40 cron running: 1
2014/02/22 21:23:45 cron running: 2
2014/02/22 21:23:50 cron running: 3
2014/02/22 21:23:55 cron running: 4
2014/02/22 21:24:00 cron running: 5

可见 cron 的使用还是挺简单的。

6、Go 版 cron 源码地址

查看源码

082014
 

在公司内部,经常会有域名是需要绑定host才能访问的,如果是通过浏览器访问,我们会修改本机的hosts文件;然而,如果是要通过程序访问这样的域名,我们是否依然必须绑定host呢?答案当然是否定的,而且,依赖本地绑定的host,程序到其他机器部署,也必须在那台机器绑定host,如果机器很多呢?

本文示例:
IP:192.168.1.102,也就是说需要访问这台机器上的资源
域名:www.studygolang.com,nginx 配置的虚拟主机
url path:/testhost.txt,内容是:Welcome to studygolang.com

需求:需要请求服务器上的 testhost.txt 资源

1、Linux Shell的解决方案

Linux下的curl程序可以绑定host,因此,在 shell 中可以很简单的实现,如: curl -H “Host:www.studygolang.com” http://192.168.1.102/testhost.txt

2、PHP 的解决方案

1)通过 curl 扩展实现

     $ch = curl_init();
     curl_setopt($ch, CURLOPT_HTTPHEADER, array('Host:www.studygolang.com'));
     curl_setopt($ch, CURLOPT_URL, 'http://192.168.1.102/testhost.txt');
     curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
     $ret = curl_exec($ch);
     var_dump($ret);
    

2)不依赖 curl 扩展的方式

     // Create a stream
     $opts = array(
         'http'=>array(
             'method'=>"GET",
             'header'=>"Host:www.studygolang.com"
         )
     );

     $context = stream_context_create($opts);

     // Open the file using the HTTP headers set above
     $ret = file_get_contents('http://192.168.1.102/testhost.txt', false, $context);
     var_dump($ret);
 

3、Golang 的解决方案

由于 Go 标准库实现了 http 协议,在 net/http 包中寻找解决方案。

一般的,请求一个 url,我们通过以下代码实现:

http.Get(url)

然而,针对本文说到的这种情况,无论 url = “http://192.168.1.102/testhost.txt” 还是 url = “http://www.studygolang.com/testhost.txt”,都无法请求到资源(没有绑定 host 的情况)。

在 http 包中的 Request 结构中,有一个字段:Host,我们可以参考上面两种解决方案,设置 Host 的值。方法如下:

     package main

     import (
         "net/http"
         "io/ioutil"
         "fmt"
     )

     func main() {
         req, err := http.NewRequest("GET", "http://192.168.1.102/testhost.txt", nil)
         if err != nil {
             panic(err)
         }
         req.Host = "www.studygolang.com"
         resp, err := http.DefaultClient.Do(req)
         if err != nil {
             panic(err)
         }
         defer resp.Body.Close()
         body, err := ioutil.ReadAll(resp.Body)
         if err != nil {
             panic(err)
         }
         fmt.Println(string(body))
     }

4、总结

不管是什么方式、什么语言,归根结底,需要告知服务器请求的是哪个 Host,这个是 HTTP 协议的 Host 头。如果不手动设置 Host 头,则会从请求的 url 中获取。