182017
 

Go 标准库有一个 expvar 包。 该软件包可以通过 JSON 格式的 HTTP API 公开您的应用程序和 Go 运行时的指标。 我认为这个软件包对于每个人 gopher 来说都是有用的。 但是,来自 godoc.org 的数据表明,没有多少人知道这个包。截止目前(2017-6-18),该包被公开的项目 import 2207 次,相比较而言,连 image 包被 import 3491 次之多。这篇文章,我想展示 expvar 包的工作原理,以及它的使用。

包简介

包 expvar 为公共变量提供了一个标准化的接口,如服务器中的操作计数器。它以 JSON 格式通过 /debug/vars 接口以 HTTP 的方式公开这些公共变量。

设置或修改这些公共变量的操作是原子的。

除了为程序增加 HTTP handler,此包还注册以下变量:

    cmdline   os.Args
    memstats  runtime.Memstats

导入该包有时只是为注册其 HTTP handler 和上述变量。 要以这种方式使用,请将此包通过如下形式引入到程序中:

    import _ "expvar"

例子

在浏览此包的详细信息之前,我想演示使用 expvar 包可以做什么。以下代码创建一个在监听 1818端口的 HTTP 服务器。每个请求 hander() 后,在向访问者发送响应消息之前增加计数器。

    package main

    import (
        "expvar"
        "fmt"
        "net/http"
    )

    var visits = expvar.NewInt("visits")

    func handler(w http.ResponseWriter, r *http.Request) {
        visits.Add(1)
        fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
    }

    func main() {
        http.HandleFunc("/", handler)
        http.ListenAndServe(":1818", nil)
    }

导入时,expvar 包将为 http.DefaultServeMux 上的模式 “/debug /vars” 注册一个处理函数。此处理程序返回已在 expvar 包中注册的所有指标。运行代码并访问 http://localhost:1818/debug/vars,您将看到如下所示的内容。输出被截断以增加可读性:

    {
      "cmdline": [
        "/tmp/go-build872151671/command-line-arguments/_obj/exe/main"
      ],
      "memstats": {
        "Alloc": 397576,
        "TotalAlloc": 397576,
        "Sys": 3084288,
        "Lookups": 7,
        "Mallocs": 5119,
        "Frees": 167,
        "HeapAlloc": 397576,
        "HeapSys": 1769472,
        "HeapIdle": 1015808,
        "HeapInuse": 753664,
        "HeapReleased": 0,
        "HeapObjects": 4952,
        "StackInuse": 327680,
        "StackSys": 327680,
        "MSpanInuse": 14240,
        "MSpanSys": 16384,
        "MCacheInuse": 4800,
        "MCacheSys": 16384,
        "BuckHashSys": 2380,
        "GCSys": 131072,
        "OtherSys": 820916,
        "NextGC": 4194304,
        "LastGC": 0,
        "PauseTotalNs": 0,
        "PauseNs": [
          0,
          0,
        ],
        "PauseEnd": [
          0,
          0
        ],
        "GCCPUFraction": 0,
        "EnableGC": true,
        "DebugGC": false,
        "BySize": [
          {
            "Size": 16640,
            "Mallocs": 0,
            "Frees": 0
          },
          {
            "Size": 17664,
            "Mallocs": 0,
            "Frees": 0
          }
        ]
      },
      "visits": 0
    }

信息真不少。这是因为默认情况下该包注册了os.Args 和 runtime.Memstats 两个指标。 我想在这个 JSON 响应结束时关注访问计数器。 因为计数器还没有增加,它的值仍然为0。现在通过访问http:// localhost:1818/golang 来增加计数器,然后返回。计数器不再为0。

expvar.Publish

expvar 包相当小且容易理解。它主要由两个部分组成。第一个是函数 expvar.Publish(name string,v expvar.Var)。该函数可用于在未导出的全局注册表中注册具有特定名称的 v。以下代码段显示了具体实现。接下来的 3 个代码段是从 expvar 包的源代码中截取的。

    // Publish declares a named exported variable. This should be called from a
    // package's init function when it creates its Vars. If the name is already
    // registered then this will log.Panic.
    func Publish(name string, v Var) {
        mutex.Lock()
        defer mutex.Unlock()

        // Check if name has been taken already. If so, panic.
        if _, existing := vars[name]; existing {
            log.Panicln("Reuse of exported var name:", name)
        }

         // vars is the global registry. It is defined somewhere else in the
         // expvar package like this:
         //
         //  vars = make(map[string]Var)
        vars[name] = v
        // 一方面,该包中所有公共变量,放在 vars 中,同时,通过 varKeys 保存了所有变量名,并且按字母序排序,即实现了一个有序的、线程安全的哈希表
        varKeys = append(varKeys, name)
        sort.Strings(varKeys)
    }

expvar.Var

另一个重要的组件是 expvar.Var 接口。 这个接口只有一个方法:

    // Var is an abstract type for all exported variables.
    type Var interface {
            // String returns a valid JSON value for the variable.
            // Types with String methods that do not return valid JSON
            // (such as time.Time) must not be used as a Var.
            String() string
    }

所以你可以在有 String() string 方法的所有类型上调用 Publish() 函数。

expvar.Int

expvar 包附带了其他几个类型,它们实现了 expvar.Var 接口。其中一个是 expvar.Int,我们已经在演示代码中通过 expvar.NewInt(“visits”) 方式使用它了,它会创建一个新的 expvar.Int,并使用 expvar.Publish 注册它,然后返回一个指向新创建的 expvar.Int 的指针。

    func NewInt(name string) *Int {
        v := new(Int)
        Publish(name, v)
        return v
    }

expvar.Int 包装一个 int64,并有两个函数 Add(delta int64) 和 Set(value int64),它们以线程安全的方式修改整数。

Other types

除了expvar.Int,该包还提供了一些实现 expvar.Var 接口的其他类型:

* expvar.Float
* expvar.String
* expvar.Map
* expvar.Func

前两个类型包装了 float64 和 string。后两种类型需要稍微解释下。

expvar.Map 类型可用于使指标出现在某些名称空间下。我经常这样用:

    var stats = expvar.NewMap("tcp")
    var requests, requestsFailed expvar.Int

    func init() {
        stats.Set("requests", &requests)
        stats.Set("requests_failed", &requestsFailed)
    }

这段代码使用名称空间 tcp 注册了两个指标 requests 和 requests_failed。它将显示在 JSON 响应中,如下所示:

    {
        "tcp": {
            "request": 18,
            "requests_failed": 21
        }
    }

当要使用某个函数的执行结果时,您可以使用 expvar.Func。假设您希望计算应用程序的正常运行时间,每次有人访问 http://localhost:1818/debug/vars 时,都必须重新计算此值。

    var start = time.Now()

    func calculateUptime() interface {
        return time.Since(start).String()
    }

    expvar.Publish("uptime", expvar.Func(calculateUptime))

关于 Handler 函数

本文开始时提到,可以简单的导入 expvar 包,然后使用其副作用,导出 /debug/vars 模式。然而,如果我们使用了一些框架,并非使用 http.DefaultServeMux,而是框架自己定义的 Mux,这时直接导入使用副作用可能会不生效。这时我们可以按照使用的框架,定义自己的路径,比如也叫:/debug/vars,然后,这对应的处理程序中,有两种处理方式:

1)将处理直接交给 expvar.Handler,比如:

    handler := expvar.Handler()
    handler.ServeHTTP(w, req)

2)自己遍历 expvar 中的公共变量,构造输出,甚至可以过滤 expvar 默认提供的 cmdline 和 memstats,我们看下 expvarHandler 的源码就明白了:(通过 expvar.Do 函数来遍历)

    func expvarHandler(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json; charset=utf-8")
        fmt.Fprintf(w, "{\n")
        first := true
        Do(func(kv KeyValue) {
            if !first {
                fmt.Fprintf(w, ",\n")
            }
            first = false
            fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
        })
        fmt.Fprintf(w, "\n}\n")
    }

Go 语言中文网 就是用了第1种方式来处理。

总结

通过 expvar 包,使得展示应用程序指标非常容易。我几乎在我写的每个应用程序中使用它来展示一些指示应用程序运行状况的指标。InfluxDB 和 Grafana,加上一个自定义的聚合器,我可以很容易监控我的应用程序。

在英文 expvar in action 的基础上有增减。

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 来优化性能。

282013
 

在golang标准库中,有那么一类包,它们用于处理go项目目录结构、源码、语法、基本操作等。一般程序中可能用不到这些包,但在go工具链源码中用到了,之所以学习这些标准库,是为了更好的看go工具链的源码。首先我们来看收集go包信息的库:go/build

一、build包概述

该包文档中首先介绍了Go Path。如果对该部分还不清楚,可以看下文档的说明;或者官方其他文档;或者看 Go项目的目录结构。

如果你看过go源码,应该见到过类似这样的包注释:+build ignore。这是编译约束条件(Build Constraints),可以理解为条件编译。关于这部分的更多内容,稍后详细介绍。

二、类型和函数

1、ToolDir变量

var ToolDir = filepath.Join(runtime.GOROOT(), "pkg/tool/"+runtime.GOOS+"_"+runtime.GOARCH)

该变量的值是go工具链的路径。6g/6l之类的工具,就在这个路径下

2、ArchChar函数

获得架构的字符表示。在之前的文章中介绍过。比如:x86 32bit用8表示;amd64用6表示等。该函数通过传入goarch,获得对应的架构字符。如:build.ArchChar(runtime.GOARCH)

3、IsLocalImport函数

判断是否为“本地导入“,类似”.”, “..”, “./foo”或者”../foo”。正式项目,一般不建议用本地导入,使用本地导入很多人会说找不到包。

4、Context类型

该类型为构建(build)指定上下文环境。比如:当前操作系统、架构、Go根目录、GOPATH等

该类型除了提供变量成员,还提供了函数成员,如果函数成员是nil,则会使用其他库提供的函数。

5、Package类型

描述Go包

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

1、Context的实例化和方法

var Default Context = defaultContext()

这是默认实现,go build工具使用的就是这个默认实现。Default会使用GOARCH、GOOS、GOROOT和GOPATH环境变量,如果没设置,则使用安装runtime中的值。

1)Import方法

func (ctxt *Context) Import(path string, srcDir string, mode ImportMode) (*Package, error)

导入一个包,返回Package类型指针。path参数跟在代码中import path的path一样。srcDir是源码所在路径;而ImportMode类型,build包中提供了两种:FindOnly和AllowBinary。AllowBinary可以在包源码不存在的时候,编译好的包对象文件直接被引用。

之前,在论坛(关于go的代码组织)中讨论过这样一个问题:go build 的时候,如果依赖的包源码不存在,编译不成功,有一个解决办法是通过go tool 6g这种方式编译。现在,在你知道了AllowBinary参数之后,应该可以通过修改go工具源码来解决这个问题。在src/cmd/go目录中的pkg.go中225有这样的代码:

// TODO: After Go 1, decide when to pass build.AllowBinary here.
// See issue 3268 for mistakes to avoid.
bp, err := buildContext.Import(path, srcDir, 0)

通过查看Import的源码,可以知道包安装的细节,比如安装到哪里。

当目录不包含源码,如果出错,则返回NoGoError错误。

另外,build包提供了Import方法的一个简便方法,即:Import函数,默认调用Default的Import方法

2)ImportDir方法

内部实现:return ctxt.Import(“.”, dir, mode)

3)SrcDirs方法

列出GOROOT和GOPATH中的源码目录。比如,我没有设置GOPATH,执行结果如下:
fmt.Println(build.Default.SrcDirs()) // [ c:\Go\src\pkg]

2、Package的实例化和方法

Context的Import和ImportDir都会返回Package实例(*Package),当然,也可以直接实例化。

Package提供了一个方法,判断一个Package是否是命令,也就是是否是main包

func (p *Package) IsCommand() bool

3、关于Context和Package的字段

由于这两种类型字段很多,包文档中每个字段都有注释,在此不一一解释。

四、构建约束(build constraints)

或者叫条件编译(编译条件)

1、使用说明

在go源码中(src/pkg或src/cmd)搜索+build,发现有不少文件的开头有这样的注释
+build xxx

构建约束是一行以+build开始的注释。在+build之后列出了一些条件,在这个条件成立的时,该文件应该包含在包中(也就是应该被编译进包文件),约束可以出现在任何源文件中,也就是不限于go源文件。不过,这些条件必须在文件最顶部(正是代码的前面,也就是说,+build之前可以有其他注释),在+build注释之后,应该有一个空行(这是为了和package doc区分开)。

语法规则:

1)只允许是字母数字或_

2)多个条件之间,空格表示OR;逗号表示AND;叹号(!)表示NOT

3)一个文件可以有多个+build,它们之间的关系是AND。如:
// +build linux darwin
// +build 386
等价于
// +build (linux OR darwin) AND 386

4)预定义了一些条件:
runtime.GOOS、runtime.GOARCH、compiler(gc或gccgo)、cgo、context.BuildTags中的其他单词

5)如果一个文件名(不含后缀),以 *_GOOS, *_GOARCH, 或 *_GOOS_GOARCH结尾,它们隐式包含了 构建约束

6)当不想编译某个文件时,可以加上// +build ignore。这里的ignore可以是其他单词,只是ignore更能让人知道什么意思

更多详细信息,可以查看go/build/build.go文件中shouldBuild和match方法。

2、应用实例

除了*_GOOS这种预定义的应用,我们看一个实际的应用。

比如,项目中需要在测试环境输出Debug信息,一般通过一个变量(或常量)来控制是测试环境还是生产环境,比如:if DEBUG {},这样在生产环境每次也会进行这样的判断。在golang-nuts邮件列表中有人问过这样的问题,貌似没有讨论出更好的方法(想要跟C中条件编译一样的效果)。下面我们采用Build constraints来实现。

1)文件列表:main.go logger_debug.go logger_product.go
2)在main.go中简单的调用Debug()方法。
3)在logger_product.go中的Debug()是空实现,但是在文件开始加上// + build !debug
4)在logger_debug.go中的Debug()是需要输出的调试信息,同时在文件开始加上// + build debug

这样,在测试环境编译的时传递-tags参数:go build/install -tags “debug” logger。生产环境:go build/install logger就行了。

对于生产环境,不传递-tags时,为什么会编译logger_product.go呢?因为在go/build/build.go中的match方法中有这么一句:

if strings.HasPrefix(name, "!") { // negation
	return len(name) > 1 && !ctxt.match(name[1:])
}

也就是说,只要有!(不能只是!),tag不在BuildTags中时,总是会编译。

完整源码在github

五、使用实例

对go/build的使用实例,后续文章在介绍go build的时候会介绍。

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(&amp;flagvar, "flagname", 1234, "help message for flagname")

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

flag.Var(&amp;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