Go提供了go test
命令来进行单元测试和性能测试。测试命令的格式如下,
go test [build/test flags] [packages] [build/test flags & test binary flags]
当我们使用go test的时候,测试文件需要满足一些规则:
_test.go
结尾import testing
下面我们简单介绍几个test flags
,如下表所示,
命令 | 意义 | 示例 |
---|---|---|
-bench regexp | 只执行匹配正则表达式的benchmark | -bench=. // 匹配所有 |
-count n | 执行n次unit test和benchmark,默认为1次 | -count=5 |
-cover | 开启覆盖率分析 | |
-run regexp | 只执行匹配正则表达式的测试 | |
-v | 长格式输出,输出完整的测试信息 | |
-cpuprofile cpu.out | 执行结束前生成cpu profile文件 | |
-timeout d | 执行时间超过d会panic,默认10min | |
-benchmem | 为benchmark开启内存分配分析 |
单元测试中测试函数的格式如下,
func TestXxx(t *testing.T)
另外,单元测试函数需满足以下规则:
Test
开头,TestXxx()
的参数是*testing.T
testing.T
的Error
, Errorf
, FailNow
, Fatal
, Fatalf
方法,说明测试不通过,调用Log
方法用来记录测试的信息。最后直接在测试文件所在包中执行下面命令,即可执行包中所有的测试例,
# $ go test -v #详细显示测试结果
# $ go test file.go file_test.go #测试文件file_test.go
$ go test # 测试包中所有测试例
下面我们从具体例子来进行单元测试,首先创建我们待测试的功能模块–提供除法的功能函数,
package gotest
import (
"errors"
)
func Division(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为0")
}
return a / b, nil
}
再次,我们创建单元测试文件如下,
package gotest
import "testing"
func TestDivision1(t *testing.T) {
if i, e := Division(6, 2); i != 3 || e != nil {
t.Error("Division test fail")
} else {
t.Log("Division test success")
}
}
func TestDivision2(t *testing.T) {
t.Log("Division test success")
}
最后我们执行单元测试命令,
$ go test sample/gotest
ok sample/gotest 0.006s
$ go test sample/gotest -v
=== RUN TestDivision1
--- PASS: TestDivision1 (0.00s)
gotest_test.go:9: Division test success
=== RUN TestDivision2
--- PASS: TestDivision2 (0.00s)
gotest_test.go:14: Division test success
PASS
ok sample/gotest 0.008s
当我们更改单元测试函数如下时,
func TestDivision2(t *testing.T) {
t.Error("Division test fail")
}
执行结果如下,
$ go test sample/gotest
--- FAIL: TestDivision2 (0.00s)
gotest_test.go:14: Division test fail
FAIL
FAIL sample/gotest 0.007s
$ go test sample/gotest -v
=== RUN TestDivision1
--- PASS: TestDivision1 (0.00s)
gotest_test.go:9: Division test success
=== RUN TestDivision2
--- FAIL: TestDivision2 (0.00s)
gotest_test.go:14: Division test fail
FAIL
exit status 1
FAIL sample/gotest 0.007s
性能测试函数的格式如下,
func BenchmarkXxx(b *testing.B)
压力测试函数规则:
Benchmark
开头,BenchmarkXxx()
的参数是*testing.B
testing.B.N
,#go test -run=regexp -bench=regexp -cpuprofile=profile -count n package
go test -run=benchmark_test.go -bench=BenchmarkDivision1 -count=5 -cpuprofile=cpuprofile.file sample/gotest
针对性能测试,如下例所示,
package gotest
import (
"testing"
)
func BenchmarkDivision1(b *testing.B) {
for i := 0; i < b.N; i++ {
Division(4, 5)
}
}
func BenchmarkDivision2(b *testing.B) {
b.StopTimer() //stop time
//do some init
b.StartTimer() //start time
for i := 0; i < b.N; i++ {
Division(4, 5)
}
}
执行结果如下,
$ go test -run=benchmark_test.go -bench=. sample/gotest
goos: darwin
goarch: amd64
pkg: sample/gotest
BenchmarkDivision1-4 2000000000 0.87 ns/op
BenchmarkDivision2-4 2000000000 0.88 ns/op
PASS
ok sample/gotest 3.699s
当我们使用-cpuprofile=xxx
,会生成性能分析文件,针对profile文件的分析请查看下篇pprof文章。
解析JSON字符串,弱类型语言例如PHP来说json_encode()
和json_decode()
就能很好的完成功能,但是对于强类型语言Go来说,解析JSON字符串就需要考虑一些情况了,下面我们对Go对JSON的转换做一些介绍。
在Go中,使用Marshal
函数进行JSON的编码:
func Marshal(v interface{}) ([]byte, error)
如下示例所示,
//Go data structure, Message
type Message struct {
Name string
Body string
Time int64
}
//an instance of Message
m := Message{"Alice", "Hello", 1294706395881547000}
//JSON encode
b, err := json.Marshal(m)
//If all is well, err will be nil and b will be a []byte containing this JSON data
b == []byte(`{"Name":"Alice","Body":"Hello","Time":1294706395881547000}`)
只有能够表示为合法JSON的数据结构才能被编码:
map[string]T
(其中T为json包支持Go的任意类型)json包只能访问结构体的可访问field(大写字母开头的field),因此结构体中只有可访问的field才能表示为JSON的输出。
解析已知类型的数据,我们可以使用Unmarshal
函数:
func Unmarshal(data []byte, v interface{}) error
如下示例所示,
//create a place where the decoded data will be stored
var m Message
//call json.Unmarshal, passing it a []byte of JSON data and a pointer to m
err := json.Unmarshal(b, &m)
//If b contains valid JSON that fits in m,
//after the call err will be nil and the data
//from b will have been stored in the struct m,
//as if by an assignment like
m = Message{
Name: "Alice",
Body: "Hello",
Time: 1294706395881547000,
}
对于JSON字符串中一个已知keyFoo
,Unmarshal将查找目标结构体的field:
Foo
tag的可访问fieldFoo
的可访问fieldFOO
、FoO
或其他与Foo
匹配不区分大小写的可访问field对于JSON字符串不严格匹配定义的数据结构,Unmarshal只解析可以在目标数据结构中能找到的field。因此在下面的例子中,只有Name字段会被解析,而Food字段会被忽略。
b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)
当我们想在一个很大的JSON结构中,只解析少量我们期望的field,这种方式是非常有用的。这也意味着目标结构中任何不能访问的field不会受到Unmarshal的影响。
对于未知的数据类型,json包使用map[string]interface{}
和[]interface{}
来存储未知类型的JSON对象和数组;也可以将任意合法的JSON字符串解析为interface{}
。默认的Go类型为:
如下示例所示,
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
var f interface{}
err := json.Unmarshal(b, &f)
//f would be a map,
//whose keys are strings
//and whose values are themselves stored as empty interface values
/*
f = map[string]interface{}{
"Name": "Wednesday",
"Age": 6,
"Parents": []interface{}{
"Gomez",
"Morticia",
},
}
*/
for k, v := range m {
switch vv := v.(type) {
case string:
fmt.Println(k, "is string", vv)
case float64:
fmt.Println(k, "is float64", vv)
case []interface{}:
fmt.Println(k, "is an array:")
for i, u := range vv {
fmt.Println(i, u)
}
default:
fmt.Println(k, "is of a type I don't know how to handle")
}
}
/*
output:
Name is string Wednesday
Age is float64 6
Parents is an array:
0 Gomez
1 Morticia
*/
对于结构体中的 pointers、slices 和 maps ,Unmarshal将会分配存储结构并解析相应的引用类型。例如,如果JSON对象中存在Bar field,Unmarshal会new Bar结构并解析,否则Bar是nil指针。
type Foo struct {
Bar *Bar
}
json包提供了Decoder和Encoder类型来支持通用的JSON数据的读写流,函数NewDecoder和NewEncoder分别处理 io.Reader 和 io.Writer 接口类型。
func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder
例如,从stdin中读入JSON对象,解析后移除除了Name之外的其他元素,然后输出到stdout中。
package main
import (
"encoding/json"
"log"
"os"
)
func main() {
dec := json.NewDecoder(os.Stdin)
enc := json.NewEncoder(os.Stdout)
for {
var v map[string]interface{}
if err := dec.Decode(&v); err != nil {
log.Println(err)
return
}
for k := range v {
if k != "Name" {
delete(v, k)
}
}
if err := enc.Encode(&v); err != nil {
log.Println(err)
}
}
}
在Go中,由于 Readers 和 Writers 无处不在,Encoder 和 Decoder 有很多的应用场景,例如HTTP链接的读写、WebSockets和file等。
今天推荐一个有意思的git项目gitmoji-cli
, gitmoji是一个在git commit信息中使用mojis的工具🎉。
A gitmoji interactive client for using gitmojis on commit messages.
下面我们看看gitmoji的功能。gitmoji能够在commit信息中添加mojis图案,使得提交信息更容易辨识。
$ npm i -g gitmoji-cli
$ gitmoji --help
A gitmoji client for using emojis on commit messages.
Usage
$ gitmoji
Options
--init, -i Initialize gitmoji as a commit hook
--remove -r Remove a previously initialized commit hook
--config, -g Setup gitmoji-cli preferences.
--commit, -c Interactively commit using the prompts
--list, -l List all the available gitmojis
--search, -s Search gitmojis
--version, -v Print gitmoji-cli installed version
--update, -u Sync emoji list with the repo
Examples
$ gitmoji -l
$ gitmoji bug linter -s
gitmoji主要用来生成commit信息,因此需要项目中已经进行了git add something
。gitmoji主要通过两种方式生成commit信息。
$ gitmoji -c
手动执行上面命令,通过交互生成commit信息。
$ gitmoji -i
将会生成文件 .git/hooks/prepare-commit-msg
然后每次进行git commit
的时候就会自动调用gitmoji,交互生成commit信息。
$ gitmoji -l
//列出所有emoji
$ gitmoji -u
//同步repo中的emoji
在redis中,RIO是对面向流的I/O的简单抽象。rio提供统一的read(从流中读数据)、write(将数据写入流)、tell(获取当前的偏移)等方法。rio实现了以下三种io:Buffer I/O
(内存I/O)、Stdio file pointer
(标准文件)和File descriptors set
(socket)。在选择相应的初始化后,就可以使用统一的方法对I/O进行操作。
一个rio
对象主要包含:
具体结构如下:
struct _rio {
//统一功能函数指针
size_t (*read)(struct _rio *, void *buf, size_t len);
size_t (*write)(struct _rio *, const void *buf, size_t len);
off_t (*tell)(struct _rio *);
int (*flush)(struct _rio *);
//更新校验和函数指针:计算到目前为止所有读写的校验和
void (*update_cksum)(struct _rio *, const void *buf, size_t len);
//记录当前校验和
uint64_t cksum;
//记录读/写的字节数
size_t processed_bytes;
//一次读写块的最大值
size_t max_processing_chunk;
//每种io类型独有的变量
union {
//内存buffer
struct {
sds ptr;
off_t pos;
} buffer;
//标准文件
struct {
FILE *fp;
off_t buffered; //到上次fsync,写入的字节数
off_t autosync; //autosync之后的fsync写入的字节数
} file;
//多个fd集合(写多个socket)
struct {
int *fds; //所有的fd
int *state; //每个fd的状态
int numfds;
off_t pos;
sds buf;
} fdset;
} io;
};
typedef struct _rio rio;
rio提供了rioWrite
、rioRead
、rioTell
和rioFlush
等统一封装的函数,在函数内部调用具体的实现函数。
并且,rio给出每种I/O的初始化函数,用来设置rio对象的信息。当初始化rio后,就可以对该rio对象进行读写等操作。
//将buf写入rio
static inline size_t rioWrite(rio *r, const void *buf, size_t len) {
while (len) {
size_t bytes_to_write = (r->max_processing_chunk && r->max_processing_chunk < len) ? r->max_processing_chunk : len;
if (r->update_cksum) r->update_cksum(r,buf,bytes_to_write);
if (r->write(r,buf,bytes_to_write) == 0)
return 0;
buf = (char*)buf + bytes_to_write;
len -= bytes_to_write;
r->processed_bytes += bytes_to_write;
}
return 1;
}
//从rio中读出到buf
static inline size_t rioRead(rio *r, void *buf, size_t len) {
while (len) {
size_t bytes_to_read = (r->max_processing_chunk && r->max_processing_chunk < len) ? r->max_processing_chunk : len;
if (r->read(r,buf,bytes_to_read) == 0)
return 0;
if (r->update_cksum) r->update_cksum(r,buf,bytes_to_read);
buf = (char*)buf + bytes_to_read;
len -= bytes_to_read;
r->processed_bytes += bytes_to_read;
}
return 1;
}
//获取当前的offset
static inline off_t rioTell(rio *r) {
return r->tell(r);
}
//刷新当前rio
static inline int rioFlush(rio *r) {
return r->flush(r);
}
//初始化为文件
void rioInitWithFile(rio *r, FILE *fp);
//初始化为内存buffer
void rioInitWithBuffer(rio *r, sds s);
//初始化为多个socket fd
void rioInitWithFdset(rio *r, int *fds, int numfds);
Buffer I/O
是维护在内存的sds变量,任何的操作都是是对内存变量的。对buffer io做flush将不做任何操作。
Stdio file pointer
是对磁盘文件的封装,封装了stdio.h
中的相关文件操作,对外提供api。
File descriptors set
是对多个socket fd进行写入操作,不支持读取操作。当执行flush时,清空缓存中的内容。
在redis中很多地方都会用到UNUSED
这个宏,UNUSED
这个宏的主要用途就是抑制c编译器的未使用变量的warning。
具体可以参考“unused parameter” warnings in C
/* Anti-warning macro... */
#define UNUSED(x) (void)(x)
在文件io中,写入文件伪代码如下:
static size_t rioFileWrite(rio *r, const void *buf, size_t len) {
retval = fwrite(buf,len,1,r->io.file.fp);
fflush(r->io.file.fp);
fsync(fileno(r->io.file.fp));
return retval;
}
其中调用了fflush
和fsync
,二者区别如下:
fflush(FILE *); //c标准库函数,从c库缓存到内核缓冲区
fsync(int fd); //系统调用,从内核缓冲区写入到磁盘
Redis的事件包括文件事件和时间事件,在事件处理循环中不断处理文件事件和时间事件。redis将事件做了统一封装在ae.h
中,底层通过select、kqueue、epoll等实现,对外提供统一的api。而RPC请求的socket相关函数则封装在networking.h
中来提供服务。整个事件过程就是针对event loop
做添加、删除以及阻塞等待事件,在相应的事件上调用处理函数,从而完成redis的响应客户端请求、后台程序等过程。
在学习redis时间之前我们先简单回顾一下相关基础知识。我们先简单介绍一下I/O多路复用。
I/O多路复用主要是为了提高io的效率,通过单线程/单进程对fdsets的读写等事件做监控,当事件到来时通过fd的回调函数做相应处理。主要实现有select、poll、kqueue和epoll等,具体参考博文。
wikipedia上对Reactor design pattern的定义如下:
The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.
这句话是说Reactor是处理1个或多个输入同时发送服务请求的事件处理模式。Reactor通过将到来的请求多路复用,并且同步关联到相应的处理程序上。
Redis就是reactor模式的一种简单实现。通过此模式,redis提供高性能的服务响应。
我们有了上述基本概念之后再去理解redis的事件就比较简单了。Redis的核心就是Event Loop
,同步不断的从Event Loop
中获取已触发的事件,调用相应的回调函数进行处理的过程。
下面通过伪代码将redis的流程做一下梳理,首先我们简单介绍一下几个函数的作用:
main
函数是redis server的启动入口,包括创建event loop,添加event等;acceptTcpHandler
、sendReplyToClient
和readQueryFromClient
是对应事件的回调函数;beforeSleep
是每次循环进入阻塞前的处理函数;aeProcessEvents
是时间阻塞函数,获取阻塞fd,调用相应的回调函数。具体伪代码如下:
//入口
int main(){
//1. 初始化server
initServer();
//2. 创建event loop
aeCreateEventLoop(fdNum);
//3. 监听端口设置非阻塞
listenToPort(server.port,server.ipfd,&server.ipfd_count);
//4. 添加时间事件,主要是后台操作
aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL);
//5. 添加文件事件,监听的fd是listen fd可读事件
//并设置触发事件的处理函数acceptTcpHandler
aeCreateFileEvent(server.el, server.ipfd, AE_READABLE, acceptTcpHandler,NULL);
//6. 阻塞前的一些配置,设置sleep前的处理函数beforeSleep
aeSetBeforeSleepProc(server.el,beforeSleep);
//7. 处理事件
while (!eventLoop->stop) {
//执行事件前的工作
eventLoop->beforesleep(eventLoop);
//处理事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
//8. 删除event loop
aeDeleteEventLoop(server.el);
}
//阻塞前的处理函数
void beforeSleep(struct aeEventLoop *eventLoop) {
//1. 其他处理
...
//2. 添加文件事件,监听accept的fd的可写事件
//并设置触发事件的处理函数sendReplyToClient
aeCreateFileEvent(server.el, c->fd, AE_WRITABLE, sendReplyToClient, c);
}
//accept_fd可写的处理函数
void sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask) {
//1. 向客服端send信息
write(fd, ((char*)o->ptr)+c->sentlen,objlen-c->sentlen);
//2. 删除文件事件,删除accept的fd 读&写事件
aeDeleteFileEvent(server.el,c->fd,AE_READABLE);
aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE);
}
//listen_fd可读的处理函数
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
//1. accept connect 请求
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
//2. 添加文件事件,监听已经accept的fd的可读事件
//并设置触发事件的处理函数readQueryFromClient
aeCreateFileEvent(server.el,fd,AE_READABLE,readQueryFromClient, c);
}
//accept_fd可读的处理函数
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
//1. 从fd中读信息
nread = read(fd, c->querybuf+qblen, readlen);
//2. 如果读出错,删除fd的事件
if(error(nread)){
aeDeleteFileEvent(server.el,c->fd,AE_READABLE);
aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE);
}
}
//事件阻塞处理函数
int aeProcessEvents(aeEventLoop *eventLoop, int flags){
//找到第一个要触发的时间事件
shortest = aeSearchNearestTimer(eventLoop);
//获取事件,tvp与shortest相关,调用epoll_wait阻塞等待事件的到来
numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
if (fe->mask & mask & AE_READABLE) {
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
}
if (fe->mask & mask & AE_WRITABLE) {
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
}
}
//处理时间事件
processTimeEvents(eventLoop);
}
对于redis的事件流程,简化为流程图更加直观明了,如图所示: