标签 标准库 下的文章

Golang测试技术

本篇文章内容来源于Golang核心开发组成员Andrew Gerrand在Google I/O 2014的一次主题分享“Testing Techniques”,即介绍使用Golang开发 时会使用到的测试技术(主要针对单元测试),包括基本技术、高级技术(并发测试、mock/fake、竞争条件测试、并发测试、内/外部测 试、vet工具等)等,感觉总结的很全面,这里整理记录下来,希望能给大家带来帮助。原Slide访问需要自己搭梯子。另外这里也要吐槽一 下:Golang官方站的slide都是以一种特有的golang artical的格式放出的(用这个工具http://go-talks.appspot.com/可以在线观看),没法像pdf那样下载,在国内使用和传播极其不便。

一、基础测试技术

1、测试Go代码

Go语言内置测试框架。

内置的测试框架通过testing包以及go test命令来提供测试功能。

下面是一个完整的测试strings.Index函数的完整测试文件:

//strings_test.go (这里样例代码放入strings_test.go文件中)
package strings_test

import (
    "strings"
    "testing"
)

func TestIndex(t *testing.T) {
    const s, sep, want = "chicken", "ken", 4
    got := strings.Index(s, sep)
    if got != want {
        t.Errorf("Index(%q,%q) = %v; want %v", s, sep, got, want)//注意原slide中的got和want写反了
    }
}

$go test -v strings_test.go
=== RUN TestIndex
— PASS: TestIndex (0.00 seconds)
PASS
ok      command-line-arguments    0.007s

go test的-v选项是表示输出详细的执行信息。

将代码中的want常量值修改为3,我们制造一个无法通过的测试:

$go test -v strings_test.go
=== RUN TestIndex
— FAIL: TestIndex (0.00 seconds)
    strings_test.go:12: Index("chicken","ken") = 4; want 3
FAIL
exit status 1
FAIL    command-line-arguments    0.008s

2、表驱动测试

Golang的struct字面值(struct literals)语法让我们可以轻松写出表驱动测试。

package strings_test

import (
        "strings"
        "testing"
)

func TestIndex(t *testing.T) {
        var tests = []struct {
                s   string
                sep string
                out int
        }{
                {"", "", 0},
                {"", "a", -1},
                {"fo", "foo", -1},
                {"foo", "foo", 0},
                {"oofofoofooo", "f", 2},
                // etc
        }
        for _, test := range tests {
                actual := strings.Index(test.s, test.sep)
                if actual != test.out {
                        t.Errorf("Index(%q,%q) = %v; want %v",
                             test.s, test.sep, actual, test.out)
                }
        }
}

$go test -v strings_test.go
=== RUN TestIndex
— PASS: TestIndex (0.00 seconds)
PASS
ok      command-line-arguments    0.007s

3、T结构

*testing.T参数用于错误报告:

t.Errorf("got bar = %v, want %v", got, want)
t.Fatalf("Frobnicate(%v) returned error: %v", arg, err)
t.Logf("iteration %v", i)

也可以用于enable并行测试(parallet test):
t.Parallel()

控制一个测试是否运行:

if runtime.GOARCH == "arm" {
    t.Skip("this doesn't work on ARM")
}

4、运行测试

我们用go test命令来运行特定包的测试。

默认执行当前路径下包的测试代码。

$ go test
PASS

$ go test -v
=== RUN TestIndex
— PASS: TestIndex (0.00 seconds)
PASS

要运行工程下的所有测试,我们执行如下命令:

$ go test github.com/nf/…

标准库的测试:
$ go test std

注:假设strings_test.go的当前目录为testgo,在testgo目录下执行go test都是OK的。但如果我们切换到testgo的上一级目录执行go test,我们会得到什么结果呢?

$go test testgo
can't load package: package testgo: cannot find package "testgo" in any of:
    /usr/local/go/src/pkg/testgo (from $GOROOT)
    /Users/tony/Test/GoToolsProjects/src/testgo (from $GOPATH)

提示找不到testgo这个包,go test后面接着的应该是一个包名,go test会在GOROOT和GOPATH下查找这个包并执行包的测试。

5、测试覆盖率

go tool命令可以报告测试覆盖率统计。

我们在testgo下执行go test -cover,结果如下:

go build _/Users/tony/Test/Go/testgo: no buildable Go source files in /Users/tony/Test/Go/testgo
FAIL    _/Users/tony/Test/Go/testgo [build failed]

显然通过cover参数选项计算测试覆盖率不仅需要测试代码,还要有被测对象(一般是函数)的源码文件。

我们将目录切换到$GOROOT/src/pkg/strings下,执行go test -cover

$go test -v -cover
=== RUN TestReader
— PASS: TestReader (0.00 seconds)
… …
=== RUN: ExampleTrimPrefix
— PASS: ExampleTrimPrefix (1.75us)
PASS
coverage: 96.9% of statements
ok      strings    0.612s

go test可以生成覆盖率的profile文件,这个文件可以被go tool cover工具解析。

在$GOROOT/src/pkg/strings下面执行:

$ go test -coverprofile=cover.out

会再当前目录下生成cover.out文件。

查看cover.out文件,有两种方法:

a) cover -func=cover.out

$sudo go tool cover -func=cover.out
strings/reader.go:24:    Len                66.7%
strings/reader.go:31:    Read                100.0%
strings/reader.go:44:    ReadAt                100.0%
strings/reader.go:59:    ReadByte            100.0%
strings/reader.go:69:    UnreadByte            100.0%
… …
strings/strings.go:638:    Replace                100.0%
strings/strings.go:674:    EqualFold            100.0%
total:            (statements)            96.9%

b) 可视化查看

执行go tool cover -html=cover.out命令,会在/tmp目录下生成目录coverxxxxxxx,比如/tmp/cover404256298。目录下有一个 coverage.html文件。用浏览器打开coverage.html,即可以可视化的查看代码的测试覆盖情况。

 

关于go tool的cover命令,我的go version go1.3 darwin/amd64默认并不自带,需要通过go get下载。

$sudo GOPATH=/Users/tony/Test/GoToolsProjects go get code.google.com/p/go.tools/cmd/cover

下载后,cover安装在$GOROOT/pkg/tool/darwin_amd64下面。

二、高级测试技术

1、一个例子程序

outyet是一个web服务,用于宣告某个特定Go版本是否已经打标签发布了。其获取方法:

go get github.com/golang/example/outyet

注:
go get执行后,cd $GOPATH/src/github.com/golang/example/outyet下,执行go run main.go。然后用浏览器打开http://localhost:8080即可访问该Web服务了。

2、测试Http客户端和服务端

net/http/httptest包提供了许多帮助函数,用于测试那些发送或处理Http请求的代码。

3、httptest.Server

httptest.Server在本地回环网口的一个系统选择的端口上listen。它常用于端到端的HTTP测试。

type Server struct {
    URL      string // base URL of form http://ipaddr:port with no trailing slash
    Listener net.Listener

    // TLS is the optional TLS configuration, populated with a new config
    // after TLS is started. If set on an unstarted server before StartTLS
    // is called, existing fields are copied into the new config.
    TLS *tls.Config

    // Config may be changed after calling NewUnstartedServer and
    // before Start or StartTLS.
    Config *http.Server
}

func NewServer(handler http.Handler) *Server

func (*Server) Close() error

4、httptest.Server实战

下面代码创建了一个临时Http Server,返回简单的Hello应答:

    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello, client")
    }))
    defer ts.Close()

    res, err := http.Get(ts.URL)
    if err != nil {
        log.Fatal(err)
    }

    greeting, err := ioutil.ReadAll(res.Body)
    res.Body.Close()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%s", greeting)

5、httptest.ResponseRecorder

httptest.ResponseRecorder是http.ResponseWriter的一个实现,用来记录变化,用在测试的后续检视中。

type ResponseRecorder struct {
    Code      int           // the HTTP response code from WriteHeader
    HeaderMap http.Header   // the HTTP response headers
    Body      *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
    Flushed   bool
}

6、httptest.ResponseRecorder实战

向一个HTTP handler中传入一个ResponseRecorder,通过它我们可以来检视生成的应答。

    handler := func(w http.ResponseWriter, r *http.Request) {
        http.Error(w, "something failed", http.StatusInternalServerError)
    }

    req, err := http.NewRequest("GET", "http://example.com/foo", nil)
    if err != nil {
        log.Fatal(err)
    }

    w := httptest.NewRecorder()
    handler(w, req)

    fmt.Printf("%d – %s", w.Code, w.Body.String())

7、竞争检测(race detection)

当两个goroutine并发访问同一个变量,且至少一个goroutine对变量进行写操作时,就会发生数据竞争(data race)。

为了协助诊断这种bug,Go提供了一个内置的数据竞争检测工具。

通过传入-race选项,go tool就可以启动竞争检测。

$ go test -race mypkg    // to test the package
$ go run -race mysrc.go  // to run the source file
$ go build -race mycmd   // to build the command
$ go install -race mypkg // to install the package

注:一个数据竞争检测的例子

例子代码:

//testrace.go

package main

import "fmt"
import "time"

func main() {
        var i int = 0
        go func() {
                for {
                        i++
                        fmt.Println("subroutine: i = ", i)
                        time.Sleep(1 * time.Second)
                }
        }()

        for {
                i++
                fmt.Println("mainroutine: i = ", i)
                time.Sleep(1 * time.Second)
        }
}

$go run -race testrace.go
mainroutine: i =  1
==================
WARNING: DATA RACE
Read by goroutine 5:
  main.func·001()
      /Users/tony/Test/Go/testrace.go:10 +0×49

Previous write by main goroutine:
  main.main()
      /Users/tony/Test/Go/testrace.go:17 +0xd5

Goroutine 5 (running) created at:
  main.main()
      /Users/tony/Test/Go/testrace.go:14 +0xaf
==================
subroutine: i =  2
mainroutine: i =  3
subroutine: i =  4
mainroutine: i =  5
subroutine: i =  6
mainroutine: i =  7
subroutine: i =  8

8、测试并发(testing with concurrency)

当测试并发代码时,总会有一种使用sleep的冲动。大多时间里,使用sleep既简单又有效。

但大多数时间不是”总是“。

我们可以使用Go的并发原语让那些奇怪不靠谱的sleep驱动的测试更加值得信赖。

9、使用静态分析工具vet查找错误

vet工具用于检测代码中程序员犯的常见错误:
    – 错误的printf格式
    – 错误的构建tag
    – 在闭包中使用错误的range循环变量
    – 无用的赋值操作
    – 无法到达的代码
    – 错误使用mutex
    等等。

使用方法:
    go vet [package]

10、从内部测试

golang中大多数测试代码都是被测试包的源码的一部分。这意味着测试代码可以访问包种未导出的符号以及内部逻辑。就像我们之前看到的那样。

注:比如$GOROOT/src/pkg/path/path_test.go与path.go都在path这个包下。

11、从外部测试

有些时候,你需要从被测包的外部对被测包进行测试,比如测试代码在package foo_test下,而不是在package foo下。

这样可以打破依赖循环,比如:

    – testing包使用fmt
    – fmt包的测试代码还必须导入testing包
    – 于是,fmt包的测试代码放在fmt_test包下,这样既可以导入testing包,也可以同时导入fmt包。

12、Mocks和fakes

通过在代码中使用interface,Go可以避免使用mock和fake测试机制。

例如,如果你正在编写一个文件格式解析器,不要这样设计函数:

func Parser(f *os.File) error

作为替代,你可以编写一个接受interface类型的函数:

func Parser(r io.Reader) error

bytes.Buffer、strings.Reader一样,*os.File也实现了io.Reader接口。

13、子进程测试

有些时候,你需要测试的是一个进程的行为,而不仅仅是一个函数。例如:

func Crasher() {
    fmt.Println("Going down in flames!")
    os.Exit(1)
}

为了测试上面的代码,我们将测试程序本身作为一个子进程进行测试:

func TestCrasher(t *testing.T) {
    if os.Getenv("BE_CRASHER") == "1" {
        Crasher()
        return
    }
    cmd := exec.Command(os.Args[0], "-test.run=TestCrasher")
    cmd.Env = append(os.Environ(), "BE_CRASHER=1")
    err := cmd.Run()
    if e, ok := err.(*exec.ExitError); ok && !e.Success() {
        return
    }
    t.Fatalf("process ran with err %v, want exit status 1", err)
}

Go语言标准库概览

本文翻译自Dr.Dobb's的"A Brief Tour of the Go Standard Library"一文。

Go语言五周系列教程的最后一部分中,我们将带领大家一起来浏览一下Go语言丰富的标准库。

Go标准库包含了大量包,提供了丰富广泛的功能特性。这里提供了概览仅仅是有选择性的且非常简单。本文发表后,标准库的内容还可能继续增加,因此 建议大家最好是通过在线查阅库API或使用godoc(包含在Go发布包中)来获取最新信息以及全面了解每个包所具备的功能。

exp包(试验性的)是那些未来可能被加入标准库的包起步的地方,因此除非你想参加这些包的开发(通过测试、讨论、提交补丁),否则不应该使用其 下面的包。exp包通常只存在于从Google Go源码树上签出的源码包中,但一般不会包含在预构建好的包中。其他包可以放心使用,虽然在写下本文的这一刻,很多包依旧不够完整。

Archive(归档)和Compression(压缩)包

Go支持读写tarball和.zip文件。与此相关的包为archive/tar和archive/zip;以及用于压缩tarball的 compress/gzip和compress/bzip2。

Go同样也支持其他压缩格式;例如用于TIFF图像和PDF文件的Lempel-Ziv-Welch (compress/lzw)格式。

Bytes(字节)和String(字符串)相关包

bytes和strings包中有很多相同的函数,但前者操作的是[]byte类型的值,而后者操作的是string类型的值。strings包 提供了所有最有用的功能函数,诸如查找子字符串、替换子字符串、拆分字符串、剔除字符串以及大小写变换等。strconv包提供了数字和布尔类型 与string类型相互转换的功能。

fmt包提供了大量有用的print和scan函数,它们在本系列教程的第一和第二部分已有相关介绍。

unicode包提供一些用于确定字符属性的函数,诸如判断一个字符是否是可打印的,或是否是一个数字。unicode/utf8与 unicode/utf16这两个包提供了rune(即,Unicode码点/字符)的编码和解码功能。

text/template和html/template包可以被用于创建模板,这些模板可基于填入的数据生成文本形式的输出(例如HTML)。 这里是一个小且简单的有关text/template包使用的例子。

type GiniIndex struct {
    Country string
    Index float64
}
gini := []GiniIndex{{"Japan", 54.7}, {"China", 55.0}, {"U.S.A.", 80.1}}
giniTable := template.New("giniTable")
giniTable.Parse(
'<TABLE>' +
    '{{range .}}' +
    '{{printf "<TR><TD>%s</TD><TD>%.1f%%</TD></TR>"' +
    '.Country .Index}}'+
    '{{end}}' +
    '</TABLE>')
err := giniTable.Execute(os.Stdout, gini)

输出:

<TABLE>
<TR><TD>Japan</TD><TD>54.7%</TD></TR>
<TR><TD>China</TD><TD>55.0%</TD></TR>
<TR><TD>U.S.A.</TD><TD>80.1%</TD></TR>
</TABLE>

template.New()函数用给定的名字创建了一个新的template.Template。模板名字用于识别模板,尤其是嵌入在其他模板 中时。template.Template.Parse()函数用于解析一个模板(通常从一个.html文件中),解析后模板即可用。 template.Template.Execute()函数执行这个模板,将结果输出到给定的io.Writer,并且从其第二个参数那里读取 用于生成模板的数据。在这个例子中,我将结果输出到os.Stdout,并将GiniIndex类型的gini切片作为数据传入(我将输出拆分为 多行以便让结果更加清晰)。

在模板内部,行为(action)包含在双大括号中({{和}})。{{range}} … {{end}}可用于迭代访问一个切片中的每个元素。这里我将切片中的每个GiniIndex设置为点(.);即是当前的元素。我们可以通过在名字访问导 出字段,当然名字前面需要用.来指明当前元素。{{printf}}的行为与fmt.Printf()函数类似,但用空格替换括号以及用于分隔参 数的逗号。

text/template和html/template包自身支持一种复杂的模板语言,包括许多action,迭代和条件分支,支持变量和方法 调用,以及其它一些。除此之外,html/templage包还对代码注入免疫。

集合包

切片是Go语言提供了最高效的集合类型,但有些时候使用一个更为特定的集合类型更有用或有必要。在多数情况下,内置的map类型已经足够了,但Go标准库还是提供了container包,其中包含了各种不同的集合包。

container/heap包提供了操作heap(堆)的函数,这里heap必须是一个自定义类型的值,该类型必须满足定义在heap包中 heap.Interface。一个heap(严格地说是一个min-heap)按特定顺序维护其中的值 – 即第一个元素总是heap中最小的(对于max-heap,应该是最大的)- 这就是熟知的heap属性。heap.Interface中嵌入了sort.Interface以及Push()和Pop方法。

我们可以很容易地创建一个满足heap.Interface的自定义heap类型。下面是一个正在使用的heap的例子:

ints := &IntHeap{5, 1, 6, 7, 9, 8, 2, 4}
heap.Init(ints) // Heapify
ints.Push(9) // IntHeap.Push() doesn't preserve the heap property
ints.Push(7)
ints.Push(3)
heap.Init(ints) // Must reheapify after heap-breaking changes
for ints.Len() > 0 {
    fmt.Printf("%v ", heap.Pop(ints))
}
fmt.Println() // prints: 1 2 3 4 5 6 7 7 8 9 9

下面是完整的自定义heap实现。

type IntHeap []int

func (ints *IntHeap) Less(i, j int) bool {
    return (*ints)[i] < (*ints)[j]
}

func (ints *IntHeap) Swap(i, j int) {
    (*ints)[i], (*ints)[j] = (*ints)[j], (*ints)[i]
}

func (ints *IntHeap) Len() int {
    return len(*ints)
}

func (ints *IntHeap) Pop() interface{} {
    x := (*ints)[ints.Len()-1]
    *ints = (*ints)[:ints.Len()-1]
    return x
}

func (ints *IntHeap) Push(x interface{}) {
    *ints = append(*ints, x.(int))
}

对于多数情况这个实现都足以应付了。我们可以将IntHeap类型换为type IntHeap struct { ints []int},这样代码更漂亮,我们可以在方法内部使用ints.ints,而不再是*ints了。

container/list包提供了双向链表。元素以interface{}类型的值加入链表。从链表中获取的元素的类型为list.Element,其原始值可通过list.Element.Value访问到。

items := list.New()
for _, x := range strings.Split("ABCDEFGH", "") {
    items.PushFront(x)
}
items.PushBack(9)
for element := items.Front(); element != nil;
    element = element.Next() {
    switch value := element.Value.(type) {
        case string:
            fmt.Printf("%s ", value)
        case int:
            fmt.Printf("%d ", value)
    }
}
fmt.Println() // prints: H G F E D B A 9

在这里例子中,我们将8个单字母字符串推入一个新链表的前端,将一个整型数推入尾端。接下来,我们迭代访问链表中的元素并打印元素的值。我们不是真的需要 type switch,因为我们可以使用fmt.Printf(%v ", element.Value)打印元素值,但如果我们要做的不仅仅是打印的话,如果列表中包含的元素类型不同,我们将需要type switch。当然,如果所有的元素都具有同一类型,我们可以使用type assertion,例如对string类型元素,我们使用element.Value.(string)。

除了上述提到的方法之外,list.List类型还提供了其他许多方法,包括Back(),Init()(用于清理链 表),InsertAfter(),InsertBefore(),Len(),MoveToBack(),MoveToFront(),PushBackList() (用于将一个链表推入另外一个链表的尾端),以及Remove()。

标准库还提供了container/ring包,这个包实现了一个环形链表。

然而所有的集合类型都将数据存储在内存中,Go还提供了database/sql包,该包提供了一个通用的SQL数据库接口。当与真实数据库交互时,特定的数据库驱动包必须被单独安装。这些包,以及其他许多集合包都放在了Go Bashboard上。

文件,操作系统以及相关包

标准库提供了许多支持文件和目录操作以及与操作系统交互的包。在许多情况下,这些包提供了操作系统无关的抽象使得创建跨平台Go应用更为简单。

os(操作系统)包提供了与操作系统交互相关的函数,诸如改变当前工作目录,修改文件模式和所有权,获取和设置环境变量,创建和删除文件和目录等。

此外,该包还提供了创建和打开文件(os.Create()和os.Open())、获取文件属性(例如,通过os.FileInfo类型),以及在之前系列文章中我们所见过的函数。

一旦文件被打开,尤其是对于那些文本文件,通过一个buffer来访问该文件是非常常见的情况(将读取的行存入字符串而不是byte切片)。我们需要的这 个功能由bufio包提供。除了用bufio.Reader和bufio.Writer进行读写字符串外,我们还可以读(不读)rune,读(不读)单字 节,读多字节以及写rune和写单字节以及多字节。

io(input/output)包提供了大量的函数用于与io.Reader和io.Writer一起工作(这两个接口都可以被os.File类型值满 足)。例如,我们曾用过io.Copy()函数将数据从一个reader拷贝到一个writer中。这个包还包含了用于创建同步的内存管道(pipe)的函数。

io/iotuil包提供了一些非常易用的函数。其中,这个包提供的ioutil.ReadAll()函数用于读取一个io.Reader的所有数据,并 将数据放入一个[]byte中返回;ioutil.ReadFile()函数所做的事情类似,只是参数由一个io.Reader换成了一个字符串(文件 名);ioutil.TempFile()函数返回一个临时文件(一个os.File);ioutil.WriteFile()函数向一个给定名字的文件 中写入由[]byte承载的数据。

path包提供的函数用于操作Unix样式路径,例如Linux和Mac OS X路径,用于处理URL路径,git“引用”,FTP文件等。path/filepath包提供提供了与path相同的函数- 许多其他的 – 函数被设计用于提供平台中立的路径处理。这个包还提供了filepath.Walk()函数用于递归地对给定路径下的所有文件和目录进行迭代访问。

runtime包包含了许多函数和类型用于访问Go的运行时系统。这里面的大多数都是高级功能,在日常创建标准Go程序时不应该使用到这些功能。但是,一 些包中的常量可能十分有用 – 例如,字符串runtime.GOOS(其值例如,"darwin," "freebsd," "linux," 或 "windows")和字符串runtime.GOARCH(其值例如386," "amd64,"或 "arm")。runtime.GOROOT()函数返回GOROOT环境变量的值(或者如果该环境变量没有设置,返回Go构建根目 录),runtime.Version()返回Go版本(以一个字符串形式)。runtime.GOMAXPROCS()和 runtime.NumCPU()函数保证Go使用机器的所有处理器,在Go的文档中有详尽解释。

文件格式相关包

Go提供出色的文件处理功能,既可用于文本文件(使用7-bit ASCII编码或UTF-8和UTF-16 Unicode编码),也可用于二进制文件。Go提供了专门的包,用于处理JSON和XML文件以及它自己专有的快速、简洁以及方便的Go二进制格式。此 外,Go提供了csv包用于读取CSV(逗号分隔的值)文件。这个包将这些文件视为记录(每行算作一个记录),么个记录由多个(逗号分隔的)字段组成。这 个包用途非常广泛,例如,可以用它修改分隔符(从逗号改为tab或其他字符),以及其他诸如如何读写记录和字段的方面。

encoding包包含许多子包,其中的encoding/binary包我们曾用于读写二进制数据。其他包提供了针对各种格式的编解码功能 – 例如,encoding/base64包可以用于编码和解码我们日常常用的URL。

图像相关包

Go的image包提供了一些高层次的函数和类型,用于创建和持有图像数据。它还提供了一些包,可用于不同种类标准图像文件格式的编解码,例如image/jpeg和image/png。

image/draw包提供了一些基本的绘图函数。第三方的freetype包加入了更多绘图函数。freetype自身可以使用任意指定TrueType字体绘制文本,freetype/raster包可以绘制线条以及立方和二次曲线。

数学包

math/big包提供了无限大(实际受限于内存)整型数(big.Int)以及有理数(big.Rat)。math包提供了所有标准数学函数(基于float64)以及一些标准常量。math/cmplx包提供一些用于复数计算的标准函数(基于complex128)。

其他杂项包

除了这些可以被粗略分组的包外,标准库还包含了许多相对独立的包。

crypto包提供了使用MD5, SHA-1, SHA-224, SHA-256, SHA-384以及SHA-512算法的Hash(每个算法由一个包提供,例如crypto/sha512)。此外,crypto还提供了用于加密和解密 的子包,这些包使用了不同算法,诸如AES、DES等等。每个包都对应相应的名字(例如,crypto/aes和crypto/des)。

exec包用于运行外部程序。我们也可以使用os.StartProcess来完成这件事,但exec.Cmd类型用起来更加简单。

flag包提供了一个命令行解析器。它接受X11风格的命令行选项(例如,-width,非GNU风格的-w以及–width)。这个包产生一个非常基 本的usage消息并且没有提供除值类型之外的任何校验(因此,这个包可以用于指定一个int型选项,而不是用于检查接受哪些值)。还有一些候选包可以在 Go Bashboard中找到。

log包提供了一些函数,用于记录日志信息(默认输出到os.Stdout)、结束程序或抛出异常(panick)并携带一条日志信息。log包输出目标 可以使用log.SetOutput()函数变更为任何io.Writer。日志信息以一个时间戳加后续消息的格式输出;时间戳的分割符可以在调用第一个 log函数之前通过log.SetFlags(0)设置。通过log.New()函数我们还可以创建自定义的logger。

math/rand包提供许多有用的伪随机数生成函数,包括返回一个随机整型数的rand.Int()以及rand.Intn(n),后者返回[0,n]范围内的一个随机整数。crypto/rand包中有一个函数,可用于产生加密的强伪随机数字。regexp包提供快速且强大的正则式引擎,并支持RE2引擎的语法。

sort包提供了许多方便易用的函数,用于对ints、float64以及string类型的切片进行排序,并且提供基于有序切片的高效(二分查找)的查找。它还提供了用于自定义数据的通用sort.Sort()和sort.Search函数。

time包提供了用于测量时间、解析和格式化日期,日期/时间以及时间值的函数。time.After()函数可用于在特定纳秒后,向通道 (channel)发送当前时间。time.Tick()和time.NewTicker()函数可用于提供一个通道,它会返回在特定时间间隔后将 'tick'发送到该通道上。time.Time结构具有一些方法,可提供当前时间,将data/time格式化为一个字符串以及解析data /time。

网络包

Go标准库中有许多包用于支持网络以及相关方面的编程。net包提供的函数和类型可用于使用Unix域以及网络socket通信、TCP/IP和UDP编程。

这个包还提供了用于域名解析的函数。net/http包充分利用了net包,并提供了解析HTTP请求和应答的功能,并提供了一个基本的HTTP客户端。net/http包也包含一个易于扩展的HTTP server。net/url包提供了URL解析和查询转义。

标准库中还包含其他一些其他高层次的网络包。一个是net/rpc(远程过程调用)包,它允许一个服务端提供导出可被客户端调用的方法的对象。另外一个是net/smtp(简单邮件传输协议)包,可用于发送email。

Reflect包

reflect包提供了运行时反射(或称为自省);即,在运行时访问和与任意类型的值交互的能力。

这个包还提供了一些有用的工具函数,诸如reflect.DeepEqual()用于比较任意两个值 – 例如,切片,我们无法用==和!=操作符对其进行比较。

Go中的每个值都有两个属性:它的实际值与类型。reflect.TypeOf()函数可以告诉我们任意值的类型。

x := 8.6
y := float32(2.5)
fmt.Printf("var x %v = %v\n", reflect.TypeOf(x), x)
fmt.Printf("var y %v = %v\n", reflect.TypeOf(y), y)

输出

var x float64 = 8.6
var y float32 = 2.5

这里我们使用reflection输出两个浮点变量和它们的类型,类似Go的变量声明。

当将reflect.ValueOf函数用于一个值时,该函数返回一个reflect.Value,它持有值但它本身却不是那个值。如果我们想访问那个被持有的值,我们必须使用reflect.Value的一个方法。

word := "Chameleon"
value := reflect.ValueOf(word)
text := value.String()
fmt.Println(text)

输出:

Chameleon

reflect.Value类型拥有很多可以提取底层类型的方法,包括 reflect.Value.Bool(), reflect.Value.Complex(), reflect.Value.Float(), reflect.Value.Int(),以及reflect.Value.String()。

reflect包也可以与集合类型一起使用,比如切片和map,也可以与struct一起使用;它甚至可以访问结构体tag的文本(这种能力被用到了JSON和XML的编码和解码中)。

type Contact struct {
    Name string "check:len(3,40)"
    Id int "check:range(1,999999)"
}
person := Contact{"Bjork", 0xDEEDED}
personType := reflect.TypeOf(person)
if nameField, ok := personType.FieldByName("Name"); ok {
    fmt.Printf("%q %q %q\n", nameField.Type, nameField.Name, nameField.Tag)
}

输出:

"string" "Name" "check:len(3,40)"

reflect.Value持有的真实值如果是"可设置的",那么它可以被改变。是否具备可设置能力可以通过reflect.Value.CanSet()来获知,该函数返回一个布尔值。

presidents := []string{"Obama", "Bushy", "Clinton"}
sliceValue := reflect.ValueOf(presidents)
value = sliceValue.Index(1)
value.SetString("Bush")
fmt.Println(presidents)

输出:
[Obama Bush Clinton]

虽然Go的字符串是不可改变的,但给定[]string中的任意一个元素都可以被另外一个字符串所替代,这就是我们在这里所做的。(顺利成章地,在这个特定的例子中,最容易的修改方法应该是presidents[1] = "Bush",而且完全没有用到自省特性)。

你无法改变不可改变的值本身,但如果我们得到原值的地址,我们可以将原不可改变的值替换为另一个新值。

count := 1
if value = reflect.ValueOf(count); value.CanSet() {
    value.SetInt(2) // 将抛出异常,我们不能设置int
}
fmt.Print(count, " ")
value = reflect.ValueOf(&count)
// 不能在值上调用SetInt(),因为值是一个*int,而不是一个int
pointee := value.Elem()
pointee.SetInt(3) // OK. 我们可以通过值指针替换
fmt.Println(count)

输出:

1 3

这小段代码的输出表明如果条件表达式求值结果为false,其分支语句将不会被执行。虽然我们无法重新设置那些不可改变的值,诸如ints、 float64或字符串,但我们可以使用reflect.Value.Elem()方法来获取一个reflectValue,通过它我们可以重新设置该地 址上的值,这就是我们在这段代码结尾处所做的。

我们还可以使用反射来调用任意函数和方法。这里是一个例子,例子用两次调用了自定义函数TitleCase,一次是用传统的方式,一次则是用反射。

caption := "greg egan's dark integers"
title := TitleCase(caption)
fmt.Println(title)
titleFuncValue := reflect.ValueOf(TitleCase)
values := titleFuncValue.Call(
[]reflect.Value{reflect.ValueOf(caption)})
title = values[0].String()
fmt.Println(title)

输出:

Greg Egan's Dark Integers
Greg Egan's Dark Integers

reflect.Value.Call()方法接受以及返回一个类型[]reflect.Value的切片。在这个例子中,我们传入一个单一值(作为一个长度为1的切片),并获取到一个单一的结果值。

我们可以用同样的方法调用方法 – 事实上,我们甚至可以查询一个方法是否存在,并且在它确实存在的情况下再调用它。

a := list.New() // a.Len() == 0
b := list.New()
b.PushFront(1) // b.Len() == 1
c := stack.Stack{}
c.Push(0.5)
c.Push(1.5) // c.Len() == 2
d := map[string]int{"A": 1, "B": 2, "C": 3} // len(d) == 3
e := "Four" // len(e) == 4
f := []int{5, 0, 4, 1, 3} // len(f) == 5
fmt.Println(Len(a), Len(b), Len(c), Len(d), Len(e), Len(f))

输出:

0 1 2 3 4 5

这里我们创建了两个链表(使用container/list包),我们给其中一个加入一个元素。我们还创建了一个stack,并向其中加入两个元素。我们 接下来创建了一个map,一个字符串以及一个int类型切片,它们长度各不相同。我们使用Len()函数获取了它们的长度。

func Len(x interface{}) int {
    value := reflect.ValueOf(x)
    switch reflect.TypeOf(x).Kind() {
        case reflect.Array, reflect.Chan, reflect.Map,
                reflect.Slice, reflect.String:
            return value.Len()
        default:
            if method := value.MethodByName("Len"); method.IsValid() {
                values := method.Call(nil)
                return int(values[0].Int())
            }
        }
    panic(fmt.Sprintf("'%v' does not have a length", x))
}

这个函数返回传入值的长度或当值类型不支持长度概念时引发异常。

我们开始获得reflect.Value类型值,因为我们后续需要这个值。接下来我们根据reflect.Kind做switch判断。如果value的 kind是某支持内建len()函数的内建类型的话,我们可以在该值上直接调用reflect.Value.Len()函数。否则,我们要么得到一个不支 持长度概念的类型,要么是一个拥有Len()方法的类型。我们使用reflect.Value.MethodByName()方法来获取这个方法-或者获 取一个无效的reflect.Value。如果这个方法有效,我们就调用它。

这个例子用没有任何参数传入,因为传统Len()方法不接收任何参数。当我们使用reflect.Value.MethodByName()方法获取一个 方法时,返回的reflect.Value既持有方法,又持有这个value。因此当我们调用reflect.Value.Call()时,这个 value将传入并作为接收者。

reflect.Value.Int()方法返回一个int64类型值;我们这里已将其转换成一个普通的int以匹配通用的Len()函数的返回值类型。

如果一个传入的值不支持内建的len()函数并且没有Len()方法,通用的Len()将引发异常。我们本可以采用其他方式处理这个错误情况 – 例如,返回-1一表明"不支持长度",或返回一个整型值和一个错误码。

Go的reflect包十分灵活,允许我们在运行时根据程序的动态状态做一些事情。但是,这里引用Rob Pike的观点,反射是“一个强大的工具,需谨慎并尽量避免使用,除非非常必要。(Rob Pick撰写了一篇非常有趣和实用的有关Go反射的博客文章)。

结论

这篇文章给Go语言五周系列教程做了一个收尾。此时此刻,你应该对这门语言,其工具以及它的标准库有了一个很好的感性认识了。- 甚至是如何在Google App Engine上编写Go程序以运行一个Web应用。我希望你能认可这一点:Go是一门非常有趣的语言,它提供了一种编写可移植的、本地代码的愉快的方式。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats