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 中获取。

232013
 

在写命令行程序(工具、server)时,对命令参数进行解析是常见的需求。各种语言一般都会提供解析命令行参数的方法或库,以方便程序员使用。如果命令行参数纯粹自己写代码解析,对于比较复杂的,还是挺费劲的。在go标准库中提供了一个包:flag,方便进行命令行解析。

注:区分几个概念
1)命令行参数(或参数):是指运行程序提供的参数
2)已定义命令行参数:是指程序中通过flag.Xxx等这种形式定义了的参数
3)非flag(non-flag)命令行参数(或保留的命令行参数):后文解释

一、flag包概述

flag包实现了命令行参数的解析。

定义flags有两种方式:

1)flag.Xxx(),其中Xxx可以是Int、String等;返回一个相应类型的指针,如:

var ip = flag.Int("flagname", 1234, "help message for flagname")

2)flag.XxxVar(),将flag绑定到一个变量上,如:

var flagvar int
flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname")

另外,还可以创建自定义flag,只要实现flag.Value接口即可(要求receiver是指针),这时候可以通过如下方式定义该flag:

flag.Var(&flagVal, "name", "help message for flagname")

注意到,这种方式是没有提供默认值的,所以默认值就是类型的零值。

在所有的flag定义完成之后,可以通过调用flag.Parse()进行解析。

命令行flag的语法有如下三种形式:
-flag // 只支持bool类型
-flag=x
-flag x // 只支持非bool类型

其中第三种形式只能用于非bool类型的flag,原因是:如果支持,那么对于这样的命令 cmd -x *,如果有一个文件名字是:0或false等,则命令的愿意会改变(之所以这样,是因为bool类型支持-flag这种形式,如果bool类型不支持-flag这种形式,则bool类型可以和其他类型一样处理。也正因为这样,Parse()中,对bool类型进行了特殊处理)。默认的,提供了-flag,则对应的值为true,否则为flag.Bool/BoolVar中指定的默认值;如果希望显示设置为false则使用-flag=false。

int类型可以是十进制、十六进制、八进制甚至是负数;bool类型可以是1, 0, t, f, true, false, TRUE, FALSE, True, False。Duration可以接受任何time.ParseDuration能解析的类型

二、类型和函数

在看类型和函数之前,先看一下变量
ErrHelp:该错误类型用于当命令行指定了-help参数但没有定义时。
Usage:这是一个函数,用户输出所有定义了的命令行参数和帮助信息(usage message)。一般,当命令行参数解析出错时,该函数会被调用。我们可以指定自己的Usage函数,即:flag.Usage = func(){}

1、函数

go标准库中,经常这么做:

定义了一个类型,提供了很多方法;为了方便使用,会实例化一个该类型的实例(通用),这样便可以直接使用该实例调用方法。比如:encoding/base64中提供了StdEncoding和URLEncoding实例,使用时:base64.StdEncoding.Encode()

在flag包中,进行了进一步封装:将FlagSet的方法都重新定义了一遍,也就是提供了一序列函数,而函数中只是简单的调用已经实例化好了的FlagSet实例:commandLine 的方法,这样commandLine实例便不需要export。这样,使用者是这么调用:flag.Parse()而不是flag.commandLine.Parse()

这里不详细介绍各个函数,在类型方法中介绍。

2、类型(数据结构)

1)ErrorHandling

type ErrorHandling int

该类型定义了在参数解析出错时错误处理方式定义了三个该类型的常量:

const (
	ContinueOnError ErrorHandling = iota
	ExitOnError
	PanicOnError
)

三个常量在源码的FlagSet方法parseOne()中使用了。

2)Flag

// A Flag represents the state of a flag.
type Flag struct {
	Name     string // name as it appears on command line
	Usage    string // help message
	Value    Value  // value as set
	DefValue string // default value (as text); for usage message
}

Flag类型代表一个flag的状态。

比如:autogo -f abc.txt,代码flag.String(“f”, “a.txt”, “usage”),则该Flag实例(可以通过flag.Lookup(“f”)获得)相应的值为:f, usage, abc.txt, a.txt。

3)FlagSet

// A FlagSet represents a set of defined flags.
type FlagSet struct {
	// Usage is the function called when an error occurs while parsing flags.
	// The field is a function (not a method) that may be changed to point to
	// a custom error handler.
	Usage func()

	name string // FlagSet的名字。commandLine给的是os.Args[0]
	parsed bool // 是否执行过Parse()
	actual map[string]*Flag // 存放实际传递了的参数(即命令行参数)
	formal map[string]*Flag // 存放所有已定义命令行参数
	args []string // arguments after flags // 存放非flag(non-flag)参数
	exitOnError bool // does the program exit if there's an error?
	errorHandling ErrorHandling // 当解析出错时,处理错误的方式
	output io.Writer // nil means stderr; use out() accessor
}

4)Value接口

// Value is the interface to the dynamic value stored in a flag.
// (The default value is represented as a string.)
type Value interface {
	String() string
	Set(string) error
}

所有参数类型需要实现Value接口,flag包中,为int、float、bool等实现了该接口。借助该接口,我们可以自定义flag

三、主要类型的方法(包括类型实例化)

flag包中主要是FlagSet类型。

1、实例化方式

NewFlagSet()用于实例化FlagSet。预定义的FlagSet实例commandLine的定义方式:

// The default set of command-line flags, parsed from os.Args.
var commandLine = NewFlagSet(os.Args[0], ExitOnError)

可见,默认的FlagSet实例在解析出错时会提出程序。

由于FlagSet中的字段没有export,其他方式获得FlagSet实例后,比如:FlagSet{}或new(FlagSet),应该调用Init()方法,初始化name和errorHandling。

2、定义flag参数方法

这一序列的方法都有两种形式,在一开始已经说了两种方式的区别。这些方法用于定义某一类型的flag参数。

3、解析参数(Parse)

func (f *FlagSet) Parse(arguments []string) error

从参数列表中解析定义的flag。参数arguments不包括命令名,即应该是os.Args[1:]。事实上,flag.Parse()函数就是这么做的:

// Parse parses the command-line flags from os.Args[1:].  Must be called
// after all flags are defined and before flags are accessed by the program.
func Parse() {
	// Ignore errors; commandLine is set for ExitOnError.
	commandLine.Parse(os.Args[1:])
}

该方法应该在flag参数定义后而具体参数值被访问前调用。

如果提供了-help参数(命令中给了)但没有定义(代码中),该方法返回ErrHelp错误。默认的commandLine,在Parse出错时会退出(ExitOnError)

为了更深入的理解,我们看一下Parse(arguments []string)的源码:

func (f *FlagSet) Parse(arguments []string) error {
	f.parsed = true
	f.args = arguments
	for {
		seen, err := f.parseOne()
		if seen {
			continue
		}
		if err == nil {
			break
		}
		switch f.errorHandling {
		case ContinueOnError:
			return err
		case ExitOnError:
			os.Exit(2)
		case PanicOnError:
			panic(err)
		}
	}
	return nil
}

真正解析参数的方法是非导出方法parseOne。

结合parseOne方法,我们来解释non-flag以及包文档中的这句话:

Flag parsing stops just before the first non-flag argument (“-” is a non-flag argument) or after the terminator “–”.

我们需要了解解析什么时候停止

根据Parse()中for循环终止的条件(不考虑解析出错),我们知道,当parseOne返回false, nil时,Parse解析终止。正常解析完成我们不考虑。看一下parseOne的源码发现,有两处会返回false, nil。

1)第一个non-flag参数

s := f.args[0]
if len(s) == 0 || s[0] != '-' || len(s) == 1 {
	return false, nil
}

也就是,当遇到单独的一个”-”或不是”-”开始时,会停止解析。比如:

./autogo – -f或./autogo build -f

这两种情况,-f都不会被正确解析。像该例子中的”-”或build(以及之后的参数),我们称之为non-flag参数

2)两个连续的”–”

if s[1] == '-' {
	num_minuses++
	if len(s) == 2 { // "--" terminates the flags
		f.args = f.args[1:]
		return false, nil
	}
}

也就是,当遇到联系的两个”-”时,解析停止。

说明:这里说的”-”和”–”,位置和”-f”这种的一样。也就是说,下面这种情况并不是这里说的:

./autogo -f –

这里的”–”会被当成是f的值

parseOne方法中接下来是处理-flag=x,然后是-flag(bool类型)(这里对bool进行了特殊处理),接着是-flag x这种形式,最后,将解析成功的Flag实例存入FlagSet的actual map中。

另外,在parseOne中有这么一句:

f.args = f.args[1:]

也就是说,没执行成功一次parseOne,f.args会少一个。所以,FlagSet中的args最后留下来的就是所有non-flag参数。

4、Arg(i int)和Args()、NArg()、NFlag()

Arg(i int)和Args()这两个方法就是获取non-flag参数的;NArg()获得non-flag个数;NFlag()获得FlagSet中actual长度(即被设置了的参数个数)。

5、Visit/VisitAll

这两个函数分别用户访问FlatSet的actual和formal中的Flag,而具体的访问方式由调用者决定。

6、PrintDefaults()

打印所有已定义参数的默认值(调用VisitAll),默认输出到标准错误,除非指定了FlagSet的output(通过SetOutput()设置)

7、Set(name, value string)

设置某个flag的值(通过Flag的name)

四、总结

使用建议:虽然上面讲了那么多,一般来说,我们只简单的定义flag,然后parse,就如同开始的例子一样。

如果项目需要复杂或更高级的命令行解析方式,可以试试goptions

如果想要像go工具那样的多命令(子命令)处理方式,可以试试command

十一 292012
 

学习一门语言,熟悉语言语法、规范等之后,应该学习语言的标准库。在Python中,会有一些函数来探究包的内容。在Go中,更多的是通过查看Go标准库文档来学习。

不仅要知其然,更要知其所以然。

实际写代码中,肯定需要用到很多标准库中的包,在学习阶段,可以在需要用某个包时,彻底学习这个包,掌握它。标准库中每个包的文档是学习包最好的资料,一定要仔细看明白。

Go包具体该怎么学了?以下是我自己的学习方法,仅供参考(以time包为例)

1、看文档中的Overview,整体上对该包有一个了解

从这知道,该包用于处理和显示日期和时间。日期都是公历。

2、看Index,关注函数和类型(函数是指不属于某种类型的func),不需要关注类型的方法(Variable和Constants需要大概关注)

1)定义了一些常量,表示时间格式
2)定义了Duration、Location、Month、ParseError、Ticker、Time、Timer和Weekday等类型
从名字可以很容易的知道这些类型所代表的含义
3)有After、Sleep和Tick三个全局函数

其实全局函数还有其他的,但是其他全局函数在文档中放在了某种类型的下面,表示该全局函数会生成一个该类型的实例,这个可以当做类型的方法来研究

这一步,我们可以不关心这些类型该怎么使用,我们关心全局函数怎么用
①func After(d Duration) 在【Index】视图 点击该函数,跳转到该函数的说明处。
函数注释已经说的很清楚:指定的时间过去之后,将当前发送到chanel中返回,和NewTimer(d).C功能相同。另外,还提供了使用示例。
②func Sleep(d Duration)
很明显,暂停当前goroutine一段时间
③func Tick(d Duration) 这是包设计中 设计函数 常用的方式:方便包的使用者,直接使用Tiker类型,而不需要另外实例化再调用其方法。Tick方法适用于那些需要使用Tiker,但是永远不需要停止的场景。

以上三个函数文档都提供了对应的例子

3、关注类型及其方法 (以Duration为例)

1)看类型的定义(结构)

type Duration int64
类型注释说,Duration表示两个时间的间隔,单位是纳秒(nanosecond)
该类型预定义了一些常量,这样方便类型转换、计算以及阅读。另外还提供了一些示例。

2)实例化方式

由于Duration是int64的别名,整型字面值常量可以直接赋值给Duration。对于整型类型的变量,则需要类型转换
比如,Sleep函数接收Duration类型的参数。如果想Sleep 2秒,这样写:Sleep(2 * time.Second)或者Sleep(2e9)。或者 i := 2; Sleep(time.Duration(i) * time.Second)

另外,ParseDuration可以将字符串转为Duration;Since()也可以获取Duration的实例

3)提供的方法

分别转为时、分、纳秒、秒。除了纳秒,其他都是float64。
这里说一下String()这个方法:
△如果某种类型定义了String()方法,那么,fmt包格式化时,直接传入类型,内部会调用类型的String()方法进行格式化,类似于Java中toString方法

4、自己写几个例子练练手

比如,想要格式化输出当前时间,格式为:2012-11-27 15:10:00
time.Now().Format(“2006-01-02 15:04:05″)

注意,2006这样的必须固定,不能为2007什么的。(不知道为什么这么设计)

设计了一个包实现类似PHP中date()和strtotime()的功能

https://github.com/polaris1119/times

5、学习第三方包(几种生成文档的方式)

1)将包放入$GOROOT/src/pkg/ 目录下,godoc 时会生成该包的文档(当做标准库包了)
2)设定GOPATH,将第三方包放在GOPATH中
3)使用godoc命令时,通过path指定第三方包的路径
4)直接通过godoc命令在命令行看起文档(不直观)
5)http://go.pkgdoc.org/在这个网站搜索包,有的话,会提供文档(这个网站会搜索github上的资源)

十一 292012
 

这里以第三方包goptions为例说明:(只说核心数据结构)

1、定义数据结构(struct等),如FlagSet、Flag,以及数据结构对应的方法。(这里一般会提供实例化数据结构的方法,比如:NewFlagSet())

一般地,依赖这些就可以供外部使用(一般会就该包的功能提供一个外部可用的入口)
比如,外部可以这么使用goptions这个包:
options := struct{}{}
flagSet := goptions.NewFlagSet(filepath.Base(os.Args[0]), &options)
flagSet.Parse(os.Args[1:])

相当于自己实现了goptions包中的Parse()或ParseAndFail()

这样,每个调用者都得这么实现一次。

2、定义函数,提供给外部方便调用的接口;

即:将上面的封装成函数供使用者直接使用。
goptions提供了两种封装:
1)普通封装->Parse(),错误需要自己处理;
2)完全封装->ParseAndFail(),错误都处理好,比显示出友好的错误提示
当然,这里之所以处理错误,正如文档中说的,出错应该是编译期错误,而不是在运行时报出来

3、另外,为了区分不同的错误提示不一样,包中经常会自定义自己的错误类型,比如:

var ErrHelpRequest = errors.New(“Request for Help”)

☆包的其他方面

1、测试用例

以_test.go为后缀,测试方法包含唯一参数:t *testing.T

2、跨平台

以 _$GOOS.go为后缀。如
windows平台以 _windows.go为后缀
Linux平台以 _linux.go为后缀
Mac平台以_darwin.go

3、为包提供例子

以example_test.go为文件名,包名为:要包名加上_test,函数名为Example加上实际的函数。如:
为time包中的After()函数提供例子
文件名:example_test.go,包名:time_test,函数:ExampleAfter()

上面是建议的命名规则,实际上,两处是必须的,其他的地方无所谓,但建议标准库中的方式写。必须的两处是:
①文件名必须以_test.go为后缀(这和测试用例一样)
②为哪个函数提供例子,必须以Example开头,后接函数名
另外,如果函数名最后加上_xxx,则文档中,xxx会变为(xxx)
在 Example 函数尾部用”// Output: ” 来标注正确输出结果