史上最细致的Go package和Go module知识

《Mastering Go》翻译第6章Go package和Go module的知识

我的github地址:https://github.com/yunteng9345/Mastering_Go_Second_Edition_Zh_CN

主要内容:

  • go函数开发
  • 匿名函数
  • 多返回值函数
  • 命名函数返回值
  • 函数返回其他函数
  • 可变参数函数
  • 开发Go packages
  • 用Go Modules开发和工作
  • 私有和公有package对象
  • 在packages使用init()函数
  • Go标准包html/template
  • Go标准包text/template
  • Go高级包go/scanner, go/parser, go/token
  • Go标准包syscall,底层包,虽然你可能不直接使用,但是它广泛运用在其他的Go packages

你不知道的Go packages和功能

上一章讲解了如何开发和如何自定的数据结构,如:linked lists、binary trees、hash table。用Go语言生成随机数和密码、执行高性能的矩阵操作等。

这章主要的重点是Go packages,它是用Go的方式组织、交付、使用代码。最重要的通用组件就是Go package中的函数,它使得在Go语言中变得可扩展。紧接着,这章将会讲解Go modules,这是带有版本号的Go packages。在这章的最后,你将了解一些属于Go标准库的packages,为了更好的理解Go packages的不同的创建方式。

本章,你将学到以下主题:

  • go函数开发
  • 匿名函数
  • 多返回值函数
  • 命名函数返回值
  • 函数返回其他函数
  • 可变参数函数
  • 开发Go packages
  • 用Go Modules开发和工作
  • 私有和公有package对象
  • 在packages使用init()函数
  • Go标准包html/template
  • Go标准包text/template
  • Go高级包go/scanner, go/parser, go/token
  • Go标准包syscall,底层包,虽然你可能不直接使用,但是它广泛运用在其他的Go packages

关于Go package

任何内容在Go语言中交付都是以packages的形式。Go package是一个用package关键字开头,后面跟着包名的源文件。一些packages也有结构。例如net包有一系列的子目录,http、mail、rpc、smtp、textproto、url。这些包可以用 net/http, net/mail, net/rpc, net/smtp, net/textproto, net/url 的语法形式单独使用。

为了和Go标准库做分离,存在一些能够使用全路径导入的外部包,在第一次使用之前会被下载。如github.com/matryer/is,是一个Github仓库。

Packages主要使用于将函数、变量、 常量分组。为的是在使用自己的Go程序时迁移这些分组更加便捷。注意,除了主要的main package,其他Go packages 程序不能够被编译到可执行文件。这意味着它们需要被main package直接或者间接的引用。如果你执行一个自己的go package(不是main package)。出现如下错误:

$ go run aPackage.go
go run: cannot run non-main package

关于Go函数

函数在任何编程语言中都是非常重要的一环,因为它允许你可以将大的程序分割成更小、更便于管理的部分。函数必须尽可能独立,必须做好一件工作,而且只做好一件工作。所以如果你发现你写一个函数做很多事,应该考虑用多个函数去替换它。(单一职责原则)

Go语言中最流行的函数就是main(),它是每个独立的Go程序都使用的。你应该已经知道所有的函数定义都以func关键字开头。

匿名函数

匿名函数被定义在内部,不需要函数名,经常用它来实现一段代码量很少的功能。在Go语言中,函数能够返回一个匿名函数,或者将一个匿名函数作为函数的一个参数,此外,匿名函数能够被附加到Go变量。注意匿名函数也被称为闭包,特别是在函数式编程术语中。

对于一个匿名函数来说,需要实现最小的一个重点。如果匿名函数没有重点,你就要考虑将它改造为一个常规的函数。

当一个匿名函数只提供一个功能时,它绝对会使你的工作更加简单便捷。只是如果没有合适的场景时,不要在你的程序中使用太多的匿名函数。稍后你将会看到匿名函数的实际使用。

多返回值函数

如果你已经知道了strconv.Atoi()函数,此函数可以返回多个不同的值,这使您不必为它创建专用的结构
保存函数返回的多个值。你可以定义一个返回4个值的函数,如下

func aFunction() (int, int, float64, string) {
}

接下来解释匿名函数和多个返回值的函数,更详细内容使用functions.go文件来说明,用五部分代码来说明。

第一部分如下:

package main
import (
    "fmt"
    "os"
    "strconv"
    )

第二部分代码如下:

func doubleSquare(x int) (int, int) {
    return x * 2, x * x
}

这里有一个doubleSquare()的函数,它传入一个int类型的参数,返回两个int类型的值,第三部分代码如下:

func main() {
    arguments := os.Args
    if len(arguments) != 2 {
    fmt.Println("The program needs 1 argument!")
        return
    }
    y, err := strconv.Atoi(arguments[1])
    if err != nil {
        fmt.Println(err)
    return
 }

之前的程序是用命令行中附加的参数来处理的。第四部分包含以下内容:

 square := func(s int) int {
 return s * s
 }
 fmt.Println("The square of", y, "is", square(y))
 double := func(s int) int {
 return s + s
 }
 fmt.Println("The double of", y, "is", double(y))

每个square和double指向一个匿名函数的地址。不够好的部分是你可以改变square和double的值,或者其他变量在匿名函数定义之后也可以指向该匿名函数的地址。这意味着这些变量能够改变和计算其他内容。

改变保存匿名函数的变量代码不是好的编程实践,因为这可能是产生bug的根本原因。

最后一部分代码如下:

 fmt.Println(doubleSquare(y))
 d, s := doubleSquare(y)
 fmt.Println(d, s)

所以,你能同时打印函数的返回值,或者将它们分配给其他变量。

执行functions.go后:

$ go run functions.go 1 21
The program needs 1 argument!
$ go run functions.go 10.2
strconv.Atoi: parsing "10.2": invalid syntax
$ go run functions.go 10
The square of 10 is 100
The double of 10 is 20
20 100
20 100

函数的返回值可以被命名

不像C语言,Go允许将函数的返回值命名。当这样的函数有一个不带任何参数的return语句时,该函数会按照函数定义中声明的顺序自动返回每个命名的返回值的当前值。

returnNames.go源码将会说明,已经命名了返回值的函数是怎样工作的。

package main

import (
 "fmt"
 "os"
 "strconv"
)

func namedMinMax(x, y int) (min, max int) {
    if x > y {
        min = y
        max = x
    } else {
        min = x
        max = y
    }
    return
}

在这段代码中,可以看到实现了namedMinMax()的函数,它使用了已经命名的返回参数。然而,这里有一个容易出错的地方:namedMinMax()函数在它的return语句中,不能明确的返回一些变量或值。尽管如此,当函数在命名时指定了返回值,min和max参数将按照它们放入函数定义的顺序自动返回。

第二段代码如下:

func minMax(x, y int) (min, max int) {
    if x > y {
        min = y
        max = x
    } else {
        min = x
        max = y
    }
    return min, max
}

minMax()函数也命名了返回值,但是它的return语句明确定义了返回的顺序和变量。

最后一部分代码如下:

func main() {
    arguments := os.Args
    if len(arguments) < 3 {
    fmt.Println("The program needs at least 2 arguments!")
    return
    }
    a1, _ := strconv.Atoi(arguments[1])
    a2, _ := strconv.Atoi(arguments[2])

    fmt.Println(minMax(a1, a2))
    min, max := minMax(a1, a2)
    fmt.Println(min, max)
    fmt.Println(namedMinMax(a1, a2))
    min, max = namedMinMax(a1, a2)
    fmt.Println(min, max)
}

main()函数的目的是为了验证所有的方法执行的结果是否相同。

最后一段代码执行后输入如下:

$ go run returnNames.go -20 1
-20 1
-20 1
-20 1
-20 1

带有指针参数的函数

只要签名允许,函数可以接受指针形参。ptrFun.go将会讲解如何在函数中使用指针。

第一部分:

package main
import (
 "fmt"
)
func getPtr(v *float64) float64 {
 return *v * *v
}

getPtr()函数接收一个值指向类型为float64的指针变量。第二部分代码展示如下:

func main() {
 x := 12.2
 fmt.Println(getPtr(&x))
 x = 12
 fmt.Println(getPtr(&x))
}

比较复杂的部分是函数参数中需要传入变量的地址,因为它是指针参数所需要的类型。通过在变量前加"&"符号实现。

执行ptrFun.go后将生成如下输出:

$ go run ptrFun.go
148.83999999999997
144

如果想传入一个简单的值到getPtr()函数中调用,类似于getPtr(12.12)。这样程序将会失败,并出现如下错误:

$ go run ptrFun.go
# command-line-arguments
./ptrFun.go:15:21: cannot use 12.12 (type float64) as type *float64 in argument to getPtr

返回指针的函数

从第四章开始,pointerStruct.go文件就用来作为案例代码。对于复合类型的使用,最好的做法是使用单独的函数创建新的结构变量,并从该函数返回指向它们的指针。所以,函数返回指针是很常见的。通常来说,这种函数简化了程序结构,并且允许开发者集中于更多重要的事情上,而不是总是复制一些相同的代码片段。(Ctrl+CCtrl+v,造就了一代又一代的程序员,皮一下)。接下来将通过pointerStruct.go的代码来说明本小节的内容。第一部分代码如下:

package main

import (
 "fmt"
)

func returnPtr(x int) *int {
    y := x * x
    return &y
}

这个函数返回了一个指向int变量的指针。唯一的功能就是使用&y返回y变量的内存地址。第二部分如下:

func main() {
 sq := returnPtr(10)
 fmt.Println("sq value:", *sq)

“*”这个符号可以得到指针变量的值,这意味着它返回了一个在内存地址中实际代表的值。(而不是值的地址),最后一段代码如下所示:

 fmt.Println("sq memory address:", sq)
}

前面的代码将返回sq变量的内存地址。而不是存在其内存中的值。

如果执行returnPtr.go,你将看到以下输出。不同的内存地址,(因为我们在不同的机器执行,所以变量内存不一定分配在哪一片段)

$ go run returnPtr.go
sq value: 100
sq memory address: 0xc00009a000

返回其他函数的函数

在这个章节中,我们一起学习如何用Go语言实现一个返回其他函数的函数。分为三个部分展示,第一部分returnFunction.go如下:

package main
import (
 "fmt"
)
func funReturnFun() func() int {
    i := 0
    return func() int {
        i++
        return i * i
    }
}

我们可以看到funReturnFun()的实现,它的返回值是一个匿名函数function (func() int)

第二部分代码如下:

func main() {
 i := funReturnFun()
 j := funReturnFun()

在这段代码中,调用funReturnFun()两次,并将其返回值(一个函数)赋给两个独立的变量i和j。我们可以看到程序输出中,这两个变量完全不相关。最后一部分代码如下:

 fmt.Println("1:", i())
 fmt.Println("2:", i())
 fmt.Println("j1:", j())
 fmt.Println("j2:", j())
 fmt.Println("3:", i())
}

在这段代码中,使用i变量3次,j变量2次。但是尽管i和j都是通过funReturnFun()函数被创建。但是他们彼此之间相互独立,没有任何相同之处。

执行代码后输出:

$ go run returnFunction.go
1: 1
2: 4
j1: 1
j2: 4
3: 9

从输出内容可以看出,在每一次调用i()或j()之后,i的值保持自增没有变为0,

将函数作为参数的函数

Go函数能够接受其他Go函数作为其参数,它的特征是能够添加其他Go函数实现的其他用途。两个最常用的功能就是元素排序和filepath.Walk()函数。然而,在展示出的例子中,它被命名为funFun.go。我们将实现最简单的处理int值的例子。有关实现将分为三部分实现。

第一部分代码如下:

package main

import "fmt"

func function1(i int) int {
    return i + i
}
func function2(i int) int {
    return i * i
}

我们有两个函数都接收int返回int。这些函数将在一段时间内被作为其他函数的参数来使用。第二部分代码如下图所示:

func funFun(f func(int) int, v int) int {
 return f(v)
}

funFun()函数接收两个参数,一个名为f的函数参数,一个名为v的int参数。f形参应该是一个接受int形参并返回一个int值的函数。最后一部分代码如下:

func main() {
    fmt.Println("function1:", funFun(function1, 123))
    fmt.Println("function2:", funFun(function2, 123))
    fmt.Println("Inline:", funFun(func(i int) int {return i * i * i}, 123))
}

The first fmt.Println() call uses funFun() with function1, without any parentheses,
as its first parameter, whereas the second fmt.Println() call uses funFun() with
function2 as its first parameter.

第一个fmt.Println()调用funFun(),用不带任何圆括号的function1作为第一个形参,而第二个fmt.Println()调用funFun()function2作为形参。

最后一个fmt.Println()语句,有趣的事情发生了,函数的实现放在了方法funFun()第一个参数的位置上,尽管这种方法适用于简单和功能小的函数参数。但是对于包含许多行Go代码的函数来说就不那么好了。

执行代码输出如下:

$ go run funFun.go
function1: 246
function2: 15129
Inline: 1860867

参数可变函数

Go也支持参数可变函数,这些函数能够接收可变的参数个数。最为流行的函数在fmt包中可以找到。接下来通过variadic.go来讲解。将分3部分展示,第一部分如下所示:

package main

import (
 "fmt"
 "os"
)

func varFunc(input ...string) {
    fmt.Println(input)
}

这部分展示了参数可变(变长)函数varFunc()的实现,它接收string类型的可变参数。传入的参数input在函数varFunc()函数内部作为切片使用。而“…”操作叫做包装操作。而拆包操作以“…”结尾,以切片类型开始。可以参数函数的包装操作最多一次。第二部分代码如下所示:

func oneByOne(message string, s ...int) int {
    fmt.Println(message)
    sum := 0
    for i, a := range s {
        fmt.Println(i, a)
        sum = sum + a
    }
    s[0] = -1000
    return sum
}

这里你能看到其他可变参数函数oneByOne(),它接收单个string和一个int类型的可变参数。形参s其实就是一个切片。最后一部分代码如下所示:

func main() {
    arguments := os.Args
    varFunc(arguments...)
    sum := oneByOne("Adding numbers...", 1, 2, 3, 4, 5, -1, 10)
    fmt.Println("Sum:", sum)
    s := []int{1, 2, 3}
    sum = oneByOne("Adding numbers...", s...)
    fmt.Println(s)
}

main函数中使用了2个可变参数函数。对第二个函数oneByOne()使用了切片。对可变参数函数内的切片所做的任何更改在函数退出后仍将保留。

执行代码后输出如下:

$ ./variadic 1 2
[./variadic 1 2]
Adding numbers...
0 1
1 2
2 3
3 4
4 5
5 -1
6 10
Sum: 24
Adding numbers...
0 1
1 2
2 3
[-1000 2 3]

开发自己的Go packages

Go package的源码,可以包含多个文件和文件夹,能够在以包名命名的单个目录内找到。除了显而易见的main包可以放在任何地方。这章的目标,我们将开发一个名为aPackage的Go package。包的源文件为aPackage.go,它的源码分两部分展示。第一部分如下所示:

package aPackage

import (
 "fmt"
)

func A() {
    fmt.Println("This is function A!")
}

在Go package中注意使用首写字母大写。在这里的– aPackage仅仅是作为例子在使用。第二部分代码如下:

func B() {
    fmt.Println("privateConstant:", privateConstant)
}
const MyConstant = 123
const privateConstant = 21

可以看出,开发一个新的Go package非常简单。当前,你不能单独使用这个包,你需要创建一个名为main的包,其中包含main()函数,以便创建一个可执行文件。在这个例子中,将使用aPackage的作为usepackage.go的包名

package main

import (
 "aPackage"
 "fmt"
)

func main() {
    fmt.Println("Using aPackage!")
    aPackage.A()
    aPackage.B()
    fmt.Println(aPackage.MyConstant)
}

如果此时执行,会得到以下错误,这意味着我们的程序还没有完成。

$ go run useAPackage.go
useAPackage.go:4:2: cannot find package "aPackage" in any of:
 /usr/local/Cellar/go/1.9.2/libexec/src/aPackage (from $GOROOT)
 /Users/mtsouk/go/src/aPackage (from $GOPATH)

这里有另外一件事情需要我们处理。假设你已经知道了第一章的内容和操作系统的知识。Go需要在UNIX中执行一些特殊的shell命令来下载所有的外部包,其中也包含了你本地开发的包。因此,在打包之前你要确保当前的UNIX的用户是可用的,并且将之前的包放在合适的路劲下。因此,下载你自己的包涉及到执行以下UNIX的shell命令:

$ mkdir ~/go/src/aPackage
$ cp aPackage.go ~/go/src/aPackage/
$ go install aPackage
$ cd ~/go/pkg/darwin_amd64/
$ ls -l aPackage.a
-rw-r--r-- 1 mtsouk staff 4980 Dec 22 06:12 aPackage.a

如果 ~/go 文件夹不是已经存在的, 你将需要用mkdir命令创建一个。在这个例子中。你将需要和 ~/go/src相似的目录,代码执行后如下图所示:

$ go run useAPackage.go
Using aPackage!
This is function A!
privateConstant: 21
123

编译一个Go package

虽然你不能执行不在main函数内的Go package,但是你仍然可以编译和创建对应的目标文件。如下所示:

$ go tool compile aPackage.go
$ ls -l aPackage.*
-rw-r--r--@ 1 mtsouk staff 201 Jan 10 22:08 aPackage.go
-rw-r--r-- 1 mtsouk staff 16316 Mar 4 20:01 aPackage.o#

私有变量和函数

私有变量和函数与公共变量和函数的区别在于,私有变量和函数可以在包的内部严格使用和调用。控制哪些函数、常量和变量是公共的,也称为封装。Go使用一个简单的规则区分函数或者变量私有或是共有。公有的函数,变量都是以大写字母开头,相反,私有的函数,变量都是以小写字母开头,这就是fmt.Println()中用Println()而不用println()的原因。然而,这个规则不影响包名的大小写。

init() 初始化函数

任何一个Go package能够拥有一个私有函数init(),该函数在其他方法执行之前最开始自动执行。

init()函数被设计为私有函数,这意味着不能从包外部调用它。作为package的用户不能完全控制init()函数,你应该在使用公共包或在init()函数中修改全局状态时,仔细思考。

我们将从多个Go packages中展示一个多个init()函数的例子。案例代码的包名简化为a

package a

import (
 "fmt"
)

func init() {
    fmt.Println("init() a")
}

func FromA() {
    fmt.Println("fromA()")
}

这个a包实现了init()函数和一个名为FromA()的函数。在这之后,我们需要使用如下shell命令让这个包成为当前UNIX用户可用的包。

$ mkdir ~/go/src/a
$ cp a.go ~/go/src/a/
$ go install a

现在,我们看下一个Go语言的包b。

package b

import (
 "a"
 "fmt"
)

func init() {
    fmt.Println("init() b")
}
func FromB() {
    fmt.Println("fromB()")
    a.FromA()
}

这里发生了什么?a包使用了fmt的Go语言标准库。然而,b包需要导入a包才能使用a.FromA()函数。a和b都有一个init()函数。

在这之后,我们需要下载这个包使得它在当前UNIX用户下可用。执行如下shell脚本。

$ mkdir ~/go/src/b
$ cp b.go ~/go/src/b
$ go install b

因此,我们当前有两个包含init()初始话函数的Go packages。现在我们试着猜测下执行manyInit.go后会输出什么,请看以下代码:

package main

import (
 "a"
 "b"
 "fmt"
)

func init() {
    fmt.Println("init() manyInit")
}

func main() {
    a.FromA()
    b.FromB()
}

实际的问题是:在a包执行时init()函数执行了几次?执manyInit.go生成如下输出,并对这个问题给出一些解释:

$ go run manyInit.go
init() a
init() b
init() manyInit
fromA()
fromB()
fromA()

虽然事实是a包被两个不同的包两次导入,但是之前的输出说明了init()函数只执行了一次。作为从manyInit.go导入的第一次执行的代码块,包a和包b的init()函数在manyInit.goinit()函数之前执行,这比较合理。主要原因是manyInit.goinit()函数允许使用a或b中的元素。

当你想要设置一些不需要外部使用的内部变量是,init()函数是非常有用的。举例来说,我们可以在init()函数中找到当前的时区。记住,一个文件中可以有多个init()函数;然而,Go的这个特点很少被使用。

Go modules

Go modules在GO 1.11版本中第一次被提及。在写这些内容的时候最新的Go版本是1.13。但在未来的Go版本中,所介绍的一些细节可能会发生变化。

Go module类似于一个有版本好的Go package。Go对版本控制模块使用语义版本控制。这意味着版本从字母v开头后面加版本号,因此,我们能够有一些类似于v1.0.0, v1.0.5, 和v2.0.2的版本号。

第一位版本号,v1, v2, or v3代表不向后兼容的Go package的大版本。这意味着如果我们的程序工作在v1版本,它不一定能在v2和v3版本的Go环境下正常工作。第二位版本号是特性相关的,通常v1.1.0版本要比v1.0.2或者v1.0.0有更多的新的特性,也比所有老的版本更加完整。最后一位版本号是和bug fix相关的。没有新的饿特性。注意,语义版本控制也用于Go版本。

注意Go modules允许我们在GOPATH路径外写一些东西。

Go module的创建和使用

这部分,我们将创建第一个基础版本的Go module,我们需要一个Github仓库来存储代码。在这部分内容中,代码将放在https://github.com/mactsouk/myModule。我们将创建一个只有README.md的仓库。所以第一次将在shell命令行下执行如下命令:

$ git clone [email protected]:mactsouk/myModule.git
Cloning into 'myModule'...
remote: Enumerating objects: 7, done.
remote: Counting objects: 100% (7/7), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 7 (delta 1), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (7/7), done.
Resolving deltas: 100% (1/1), done.

执行之后,我们将有一个作者的Github仓库,如果你想从头创建你自己的Go模块,你需要创建你自己的空GitHub仓库。

创建v1.0.0版本

我们将需要执行如下命令来创建一个我们自己的v1.0.0 Go module。

$ go mod init
go: creating new go.mod: module github.com/mactsouk/myModule
$ touch myModule.go
$ vi myModule.go
$ git add .
$ git commit -a -m "Initial version 1.0.0"
$ git push
$ git tag v1.0.0
$ git push -q origin v1.0.0
$ go list
github.com/mactsouk/myModule
$ go list -m
github.com/mactsouk/myModule

myModule.go文件内容如下所示:

package myModule

import (
 "fmt"
)

func Version() {
    fmt.Println("Version 1.0.0")
}

go.mod之前就被创建了,如下:

$ cat go.mod
module github.com/mactsouk/myModule
go 1.12

使用v1.0.0版本

在这章中,我们将学习如何使用之前创建的v1.0.0的Go module。为了使用Go modules,我们将创建一个Go程序,useModule.go文件如下所示:

package main

import (
 v1 "github.com/mactsouk/myModule"
)

func main() {
    v1.Version()
}

我们需要包含Go module(github.com/mactsouk/myModule)路径。在这例子这个路径的别名为v1。在Go语言中使用别名是不整洁的。为使阅读方便所以使用了别名。虽然如此,这个特征也不推荐用在生产代码中。

如果我们试着在/tmp路径下执行useModule.go。它将报“找不到github.com/mactsouk/myModule”错误。

$ pwd
/tmp
$ go run useModule.go
useModule.go:4:2: cannot find package "github.com/mactsouk/myModule" in any
of:
 /usr/local/Cellar/go/1.12/libexec/src/github.com/mactsouk/myModule
(from $GOROOT)
 /Users/mtsouk/go/src/github.com/mactsouk/myModule (from $GOPATH)

因此,我们需要执行如下命令获得Go modules,然后成功执行useModule.go

$ export GO111MODULE=on
$ go run useModule.go
go: finding github.com/mactsouk/myModule v1.0.0
go: downloading github.com/mactsouk/myModule v1.0.0
go: extracting github.com/mactsouk/myModule v1.0.0
Version 1.0.0

所以,useModule.go是正确的且能被执行,现在是时候让useModule.go更加规范了。去命名并创建它。

$ go mod init hello
go: creating new go.mod: module hello
$ go build

最后一部分命令是在/tmp下生成一个可执行文件,以及两个额外的名为go.modgo.dum文件。

$ cat go.sum
github.com/mactsouk/myModule v1.0.0
h1:eTCn2Jewnajw0REKONrVhHmeDEJ0Q5TAZ0xsSbh8kFs=
github.com/mactsouk/myModule v1.0.0/go.mod
h1:s3ziarTDDvaXaHWYYOf/ULi97aoBd6JfnvAkM8rSuzg=

go.sum文件的作用是检查所有的module是否已经下载,内容如下所示:

$ cat go.mod
module hello
go 1.12
require github.com/mactsouk/myModule v1.0.0

注意如果go.mod文件在我们的项目中制定了v1.3.0 Go module版本,即使有最新版本的Go module可以使用,项目也不会用到最新的Go module。

创建v1.1.0版本

在这部分,我们将用不同的tag创建一个新版本的myModule文件。然而,这一次不需要执行命令go mod init,像之前那样。我们将需要执行如下命令:

$ vi myModule.go
$ git commit -a -m "v1.1.0"
[master ddd0742] v1.1.0
1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
$ git tag v1.1.0
$ git push -q origin v1.1.0

myModule.go文件内容如下所示:

package myModule

import (
 "fmt"
)

func Version() {
    fmt.Println("Version 1.1.0")
}

使用v1.1.0版本

在这章中,我们将学习如何使用我们创建的Go module v1.1.0。这一次,我们将使用Docker镜像,以便尽可能独立用于开发模块的机器。下面的命令会使我们得到Docker镜像然后进入这个程序的运行环境中。

$ docker run --rm -it golang:latest
root@884c0d188694:/go# cd /tmp
root@58c5688e3ee0:/tmp# go version
go version go1.13 linux/amd64

正如我们所见,Docker镜像使用最新的Go版本,在写本文时就是1.13版本。为了使用一个或多个Go modules。我们将需要创建一个Go程序,命名为useUpdatedModule.go。内容如下:


package main

import (
    v1 "github.com/mactsouk/myModule"
)

func main() {
    v1.Version()
}

useUpdatedModule.gouseModule.go比较相似。非常棒的一件事情就是我们将自动更新到最新的v1版本。在Docker镜像中写完代码后,我们做如下操作。

root@58c5688e3ee0:/tmp# ls -l
total 4
-rw-r--r-- 1 root root 91 Mar 2 19:59 useUpdatedModule.go
root@58c5688e3ee0:/tmp# export GO111MODULE=on
root@58c5688e3ee0:/tmp# go run useUpdatedModule.go
go: finding github.com/mactsouk/myModule v1.1.0
go: downloading github.com/mactsouk/myModule v1.1.0
go: extracting github.com/mactsouk/myModule v1.1.0
Version 1.1.0

这意味着useUpdatedModule.go被自动使用最新的v1版本的Go module。当你开启module支持GO111MODULE=on时是比较危险的。

如果我们尝试执在本地机器上/tmp执行useModule.go时,会得到如下输出:

$ ls -l go.mod go.sum useModule.go
-rw------- 1 mtsouk wheel 67 Mar 2 21:29 go.mod
-rw------- 1 mtsouk wheel 175 Mar 2 21:29 go.sum
-rw-r--r-- 1 mtsouk wheel 92 Mar 2 21:12 useModule.go
$ go run useModule.go
Version 1.0.0

这意味着useModule.go依旧使用老版本的Go module。如果想使用最新的Go module,执行如下命令:

$ rm go.mod go.sum
$ go run useModule.go
go: finding github.com/mactsouk/myModule v1.1.0
go: downloading github.com/mactsouk/myModule v1.1.0
go: extracting github.com/mactsouk/myModule v1.1.0
Version 1.1.0

如果又想使用v1.0.0的Go module时,执行如下命令:

$ go mod init hello
go: creating new go.mod: module hello
$ go build
$ go run useModule.go
Version 1.1.0
$ cat go.mod
module hello
go 1.12
require github.com/mactsouk/myModule v1.1.0
$ vi go.mod
$ cat go.mod
module hello
go 1.12
require github.com/mactsouk/myModule v1.0.0
$ go run useModule.go
Version 1.0.0

下一章节将会创建一个新Go module的大版本,这意味着用不同的tag代替。我们需要使用一个不同的Github分支。

创建v2.0.0版本

在这章中,我们将创建一个第二大的myModule版本。注意,对于主要版本,您需要在import语句中明确表示。

所以github.com/mactsouk/myModule将成为v2版本的github.com/mactsouk/myModule/v2和v3版本的github.com/mactsouk/myModule/v3。第一件事情就是创建一个新的Github分支:

$ git checkout -b v2
Switched to a new branch 'v2'
$ git push --set-upstream origin v2

然后输入如下命令:

$ vi go.mod
$ cat go.mod
module github.com/mactsouk/myModule/v2
go 1.12
$ git commit -a -m "Using 2.0.0"
[v2 5af2269] Using 2.0.0
2 files changed, 2 insertions(+), 2 deletions(-)
$ git tag v2.0.0
$ git push --tags origin v2
Counting objects: 4, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 441 bytes | 441.00 KiB/s, done.
Total 4 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:mactsouk/myModule.git
* [new branch] v2 -> v2
* [new tag] v2.0.0 -> v2.0.0
$ git --no-pager branch -a
 master
* v2
 remotes/origin/HEAD -> origin/master
 remotes/origin/master
 remotes/origin/v2

这个大版本的内容myModule.go如下:

package myModule

import (
    "fmt"
)
func Version() {
    fmt.Println("Version 2.0.0")
}

使用v2.0.0版本

Once again, in order to use our Go modules, we will need to create a Go program, which is called useV2.go and contains the following Go code:

为了使用我们的Go module,我们将需要创建一个名为useV2.go的Go程序:

package main

import (
    v "github.com/mactsouk/myModule/v2"
)

func main() {
    v.Version()
}

我们将使用Docker镜像。这是最便捷的使用Go module的方式,因为我们会从一个新的go环境下安装。

$ docker run --rm -it golang:latest
root@191d84fc5571:/go# cd /tmp
root@191d84fc5571:/tmp# cat > useV2.go
package main

import (
    v "github.com/mactsouk/myModule/v2"
)

func main() {
    v.Version()
}
root@191d84fc5571:/tmp# export GO111MODULE=on
root@191d84fc5571:/tmp# go run useV2.go
go: finding github.com/mactsouk/myModule/v2 v2.0.0
go: downloading github.com/mactsouk/myModule/v2 v2.0.0
go: extracting github.com/mactsouk/myModule/v2 v2.0.0
Version 2.0.0

Docker镜像下使用v2.0.0版本的myModule工作正常。

创建v2.1.0版本

现在我们要创建myModule.go的更新版本。这与使用不同的GitHub标签有关。执行如下命令:

$ vi myModule.go
$ git commit -a -m "v2.1.0"
$ git push
$ git tag v2.1.0
$ git push -q origin v2.1.0

The updated contents of myModule.go will be as follows:

package myModule

import (
    "fmt"
)
func Version() {
    fmt.Println("Version 2.1.0")
}

使用v2.1.0版本

我们目前已经知道,为了使用我们的Go module,我们将需要创建一个名为useUpdatedV2.go的Go程序:

package main

import (
    v "github.com/mactsouk/myModule/v2"
)

func main() {
    v.Version()
}

这里不需要定义我们想要使用的最新的v2版本的Go module,因为这是由Go处理的,这是使用useUpdatedV2.go的主要原因。useV2.go和它都是一样的。

使用Docker镜像的原因就是因为它足够简单。使用vcat(1)命令创建useUpdatedV2.go的原因是因为Doker镜像的独立的,其中并没有vi(1)被下载。

$ docker run --rm -it golang:1.12
root@ccfcd675e333:/go# cd /tmp/
root@ccfcd675e333:/tmp# cat > useUpdatedV2.go
package main

import (
    v "github.com/mactsouk/myModule/v2"
)

func main() {
    v.Version()
}
root@ccfcd675e333:/tmp# ls -l
total 4
-rw-r--r-- 1 root root 92 Mar 2 20:34 useUpdatedV2.go
root@ccfcd675e333:/tmp# go run useUpdatedV2.go
useUpdatedV2.go:4:2: cannot find package "github.com/mactsouk/myModule/v2"
in any of:
 /usr/local/go/src/github.com/mactsouk/myModule/v2 (from $GOROOT)
 /go/src/github.com/mactsouk/myModule/v2 (from $GOPATH)
root@ccfcd675e333:/tmp# export GO111MODULE=on
root@ccfcd675e333:/tmp# go run useUpdatedV2.go
go: finding github.com/mactsouk/myModule/v2 v2.1.0
go: downloading github.com/mactsouk/myModule/v2 v2.1.0
go: extracting github.com/mactsouk/myModule/v2 v2.1.0
Version 2.1.0

我们将在第7章反射和接口中学习更多的关于git(1)和Github的命令。

一个Go module使用不同的版本

在这章中,我们将学习一个Go module使用的两个以上主要版本。如果你想同时使用一个Go模块的两个以上主要版本,也可以使用相同的技术。

Go文件useTwo.go如下所示:

package main

import (
    v1 "github.com/mactsouk/myModule"
    v2 "github.com/mactsouk/myModule/v2"
)

func main() {
    v1.Version()
    v2.Version()
}

所以我们需要明确的导入想使用的不同的版本,并且要给它起别名。

执行useTwo.go输出如下:

$ export GO111MODULE=on
$ go run useTwo.go
go: creating new go.mod: module github.com/PacktPublishing/Mastering-GoSecond-Edition
go: finding github.com/mactsouk/myModule/v2 v2.1.0
go: downloading github.com/mactsouk/myModule/v2 v2.1.0
go: extracting github.com/mactsouk/myModule/v2 v2.1.0
Version 1.1.0
Version 2.1.0

Go代码和Go module的存储

在本节中,我们将了解Go如何存储代码和正在使用的Go模块的信息。使用我们的Go module作为一个例子。在我们使用我们的Go module就会生成一些内容在本地的macOS机器的~/go/pkg/mod/github.com/mactsouk文件夹下面。

$ ls -lR ~/go/pkg/mod/github.com/mactsouk
total 0
drwxr-xr-x 3 mtsouk staff 96B Mar 2 22:38 my!module
dr-x------ 6 mtsouk staff 192B Mar 2 21:18 my![email protected]
dr-x------ 6 mtsouk staff 192B Mar 2 22:07 my![email protected]
/Users/mtsouk/go/pkg/mod/github.com/mactsouk/my!module:
total 0
dr-x------ 6 mtsouk staff 192B Mar 2 22:38 [email protected]
/Users/mtsouk/go/pkg/mod/github.com/mactsouk/my!module/[email protected]:
total 24
-r--r--r-- 1 mtsouk staff 28B Mar 2 22:38 README.md
-r--r--r-- 1 mtsouk staff 48B Mar 2 22:38 go.mod
-r--r--r-- 1 mtsouk staff 86B Mar 2 22:38 myModule.go
/Users/mtsouk/go/pkg/mod/github.com/mactsouk/my![email protected]:
total 24
-r--r--r-- 1 mtsouk staff 28B Mar 2 21:18 README.md
-r--r--r-- 1 mtsouk staff 45B Mar 2 21:18 go.mod
-r--r--r-- 1 mtsouk staff 86B Mar 2 21:18 myModule.go
/Users/mtsouk/go/pkg/mod/github.com/mactsouk/my![email protected]:
total 24
-r--r--r-- 1 mtsouk staff 28B Mar 2 22:07 README.md
-r--r--r-- 1 mtsouk staff 45B Mar 2 22:07 go.mod
-r--r--r-- 1 mtsouk staff 86B Mar 2 22:07 myModule.go

最好的学习Go module的方式就是去实验尝试(多写多用)。你用或不用,Go module就在那里,所以我们开始使用他们吧。

go mod vendor命令

有时,我们需要将所有依赖项存储在同一个地方,并将它们保存在项目文件。在这种情况下go mod vendor命令会帮助我们准确的完成:

$ cd useTwoVersions
$ go mod init useV1V2
go: creating new go.mod: module useV1V2
$ go mod vendor
$ ls -l
total 24
-rw------- 1 mtsouk staff 114B Mar 2 22:43 go.mod
-rw------- 1 mtsouk staff 356B Mar 2 22:43 go.sum
-rw-r--r--@ 1 mtsouk staff 143B Mar 2 19:36 useTwo.go
drwxr-xr-x 4 mtsouk staff 128B Mar 2 22:43 vendor
$ ls -l vendor/github.com/mactsouk/myModule
total 24
-rw-r--r-- 1 mtsouk staff 28B Mar 2 22:43 README.md
-rw-r--r-- 1 mtsouk staff 45B Mar 2 22:43 go.mod
-rw-r--r-- 1 mtsouk staff 86B Mar 2 22:43 myModule.go
drwxr-xr-x 6 mtsouk staff 192B Mar 2 22:43 v2
$ ls -l vendor/github.com/mactsouk/myModule/v2
total 24
-rw-r--r-- 1 mtsouk staff 28B Mar 2 22:43 README.md
-rw-r--r-- 1 mtsouk staff 48B Mar 2 22:43 go.mod
-rw-r--r-- 1 mtsouk staff 86B Mar 2 22:43 myModule.go

关键点在于在执行go mod vendor命令之前去执行go mod init

创建优秀的Go packages

这章将提供一些好的的建议帮助我们开发更好的Go packages。我们屏蔽Go packages将其组织在文件夹中,能够包含一些共有和私有的元素。公有元素可以在包内部或者包外部使用,私有元素只能在包的内部使用。

  • 这里有一些创建go package优秀的规则:第一点非官方的规则就是元素之间必须有一定的关联。因此,你可以创建一个支持car的包,但是创建一个包含有car和bicycles的包就不是那么合理了。简单来说,最好的方式就是将不必要的package放在多个package中,而不是在一个单独的包中添加很多函数。此外,package应该简单和易用,但不能太过于简单和凌乱。(包中功能的粒度不能太小,将同类的功能放一个包中即可)

  • 第二部分实际的规则就是我们应该多次使用自己的package,在使用多次没有问题然后在提供给别人使用。这将会帮助我们发现一些愚蠢的bug,确保package的使用是我们预期的结果。在此之后,在公开package之前,将package交给其他开发人员进行额外的测试。

  • 紧接着,尝试假设一些愉快地使用我们package的用户类型,确保我们的package在使用过程中,会给他们带来超出他们能力范围的问题。

  • 除非我们有很好的理由,否则package不应该输出无穷尽的函数列表。你的package内的很少的函数列表,会使我们很好的理解,使用的方便。在此之后,尝试使用简短的名字命令你的函数名称而不是很长的名字。

  • 接口能够提高函数的可用性,所以当我们认为合适的时候,使用接口代替单一类型的函数参数或者返回类型。

  • 当更新一个包时,除非绝对必要,尽量不要破坏其他package,也不要创建与旧版本产生不兼容的代码。

  • 当开发一个新的Go package时,尝试使用多个文件,为了将相似的任务或者概念分组。

  • 此外,尝试仿照标准库的Go package的规则,阅读标准库的代码将会对我们有益。

  • 不要创建一个已经在之前存在的package。对现有的包进行更改,并创建自己的版本。

  • 没有人想要一个在屏幕上打印日志信息的Go package。在需要用到的时候设置一个日志打开标记来打开日志记录会更专业。

  • 在Go代码里引用的Go package使用要和谐。这意味着,如果你看到一个使用你的包和函数名的程序在代码中以一种糟糕的方式出现,最好更改函数的名称。由于package名称几乎在任何地方都被使用,所以尽量使用简洁而富有表现力的包名。

  • 如果你将新的Go类型定义放在第一次使用它们的地方附近,会更方便,因为包括您在内的任何人都不希望在源文件中搜索新数据类型的定义。

  • 试着在我们的包里创建测试文件,因为有测试文件package比没有的要更加标准,专业。小小的细节将会给使用的人产生很大的便捷,说明你是一个很认真的开发者!请注意,为包编写测试是不是随便的,您应该避免使用不包含测试的包。在11章将学到更多关于测试的内容。

  • 最后,不要写Go包,因为你没有更好的事情做-在这种情况下,找一些更好的事情做,不要浪费你的时间!

永远记住,除了包中的实际Go代码应该是没有bug的这一事实之外,优秀package中最重要的元素就是它的文档以及一些代码示例,澄清其使用和展示的功能包的特性。

syscall包

这章将展示一小部分的syscall标准包中函数的使用。注意syscall提供了很多的和操作系统底层相关的函数和类型。此外,syscall包也被很多的Go package使用,类似于osnettime。这些提供了控制操作系统的一部分接口。这意味着syscall包不是最便捷的Go标准库,这不是它的工作。

虽然UNIX系统有很多相似之处,他们也展示了各个方面的不同,尤其是我们在谈论操作系统内部时,syscall包尽可能聪明的处理他们之间的不兼容问题。这并不是一个秘密,并且有很好的文档,这使得sycall成为一个成功的包。

严格来讲,程序调用系统的方式就是应用程序从操作系统内核请求一些内容。可以推断出,系统调用的责任就是存取并执行UNIX下最底层的元素,如程序,存储设备、打印数据,网络接口,任何种类的文件。简单来说,在UNIX系统下不使用系统调用我们将不能工作。我们可以使用一些类似于strace(1) and dtrace(1)的工具来检查UNIX进程的系统调用。这些工具在第2章《理解Go内部》有提到。

syscall包的使用将在useSyscall.go中分4个部分说明:

你可能不需要直接使用syscall包,除非你的工作的内容偏向于计算机底层。不是所有的Go包对于所有人都是适用的。

第一部分代码useSyscall.go如下所示:

package main
import (
 "fmt"
 "os"
 "syscall"
)

这部分内容导入了程序需要的包,第二部分如下:

func main() {
    pid, _, _ := syscall.Syscall(39, 0, 0, 0)
    fmt.Println("My pid is", pid)
    uid, _, _ := syscall.Syscall(24, 0, 0, 0)
    fmt.Println("User ID:", uid)

在这部分,我们将使用的两个方法syscall.Syscall()找到一些关于进程ID和user Id的信息,它的第一个参数决定请求的信息。

第三部分代码如下:

    message := []byte{'H', 'e', 'l', 'l', 'o', '!', '\n'}
    fd := 1
    syscall.Write(fd, message)

在这部分,我们将使用syscall.Write()函数在屏幕上打印一些信息。第一个参数是我们将要写的文件描述,第二个参数是承载着实际信息的byte切片。syscall.Write()函数十分便捷。

最后一部分代码如下:

    fmt.Println("Using syscall.Exec()")
    command := "/bin/ls"
    env := os.Environ()
    syscall.Exec(command, []string{"ls", "-a", "-x"}, env)
}

最后一部分代码,我们将看到如何使用syscall.Exec()函数执行外部命令。然而,我们不能完全控制输入的命令,它是自动打印在屏幕上的。

在macOS Mojave上执行代码出现如下输出:

$ go run useSyscall.go
My pid is 14602
User ID: 501
Hello!
Using syscall.Exec()
. .. a.go
funFun.go functions.go html.gohtml
htmlT.db htmlT.go manyInit.go
ptrFun.go returnFunction.go returnNames.go
returnPtr.go text.gotext textT.go
useAPackage.go useSyscall.go

在 Debian Linux上执行代码出现如下输出:

$ go run useSyscall.go
My pid is 20853
User ID: 0
Hello!
Using syscall.Exec()
. .. a.go
funFun.go functions.go html.gohtml
htmlT.db htmlT.go manyInit.go
ptrFun.go returnFunction.go returnNames.go
returnPtr.go text.gotext textT.go
useAPackage.go useSyscall.go

所以,虽然大多数的输出和之前的输出相似,但是syscall.Syscall(39, 0, 0, 0)不能工作在Linux上面,因为在Linux上user id不能为0,这意味着命令不是很便捷。

如果想要找到Go标准包的syscall,在UNIX shell下执行如下命令:

$ grep \"syscall\" `find /usr/local/Cellar/go/1.12/libexec/src -name
"*.go"`

请替换/usr/local/Cellar/go/1.12/libexec/src为合适的路径。

fmt.Println()是如何工作的

如果你真的想掌握syscall的使用,通过实现fmt.Println()函数(可以在https://golang.org/src/fmt/print.go)中找到。代码如下:

func Println(a ...interface{}) (n int, err error) {
    return Fprintln(os.Stdout, a...)
}

这意味着fmt.Println()函数调用了fmt.Fprintln()做了一些工作。fmt.Fprintln()的实现,在同一个文件中可以找到,如下所示:

func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
    p := newPrinter()
    p.doPrintln(a)
    n, err = w.Write(p.buf)
    p.free()
    return
}

这意味着在fmt.Fprintln()函数中,实际的去写是通过实现io.Writer接口的Write()函数去写的。在这个例子中。io.Writer的接口是os.Stdout。定义在https://golang.org/src/os/file.go:

var (
    Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
    Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
    Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

现在看NewFile()的实现,在ttps://golang.org/src/os/file_plan9.go中可以找到:

func NewFile(fd uintptr, name string) *File {
    fdi := int(fd)
    if fdi < 0 {
        return nil
    }
    f := &File{&file{fd: fdi, name: name}}
    runtime.SetFinalizer(f.file, (*file).close)
    return f
}

当你看到名为file_plan9.go的源文件时,你应该怀疑它是否包含特殊UNIX变体的命令,这意味着它包含的代码不是很便捷。

我们这里有一个文件结构类型,它嵌入在文件类型中,因为它的名字而被导出。所以,开始在https://golang.org/src/os/file_plan9.go寻找应用在文件结构或者指向文件结构的指针,它允许我们写入数据。作为我们寻找的名为Write()的函数,看Fprintln()的实现,我们将不得不在os包中的所有的源文件中寻找它们。

$ grep "func (f \*File) Write(" *.go
file.go:func (f *File) Write(b []byte) (n int, err error) {

在https://golang.org/src/os/file.go中Write()的实现如下所示:

func (f *File) Write(b []byte) (n int, err error) {
    if err := f.checkValid("write"); err != nil {
        return 0, err
    }
    n, e := f.write(b)
    if n < 0 {
        n = 0
    }
    if n != len(b) {
        err = io.ErrShortWrite
    }
    epipecheck(f, e)
    if e != nil {
        err = f.wrapErr("write", e)
    }
    return n, err
}

这意味着我们现在不得不寻找write()函数。寻找在https://golang.org/src/os/file_plan9.go中的write string:

func (f *File) write(b []byte) (n int, err error) {
    if len(b) == 0 {
         return 0, nil
    }
    return fixCount(syscall.Write(f.fd, b))
}

fmt.Println()函数的调用是使用对syscall.Write()的调用实现的。这说明了syscall包是很重要的。

go/scanner, go/parser, 和go/token

这章将讲解关于go/scanner, go/parser, 和go/token包的内容,和go/ast一样,这是有关Go扫码和解析Go代码的底层知识,可以帮助我们理解Go是如何工作的。然而,如果底层知识太难,你可能想要跳过这一章。

解析语法需要两个阶段。第一个阶段是将输入分解为标记(词法分析),第二个阶段是将所有这些标记提供给解析器,以确保这些标记有意义且顺序正确(语法分析)。仅仅结合英语单词并不总能创造出有效的句子。

go/ast

抽象语法树(AST)是Go源代码的结构化表示的程序。这个树是根据语言中指定的一些规则构造的规范。go/ast在go中一般用在定义数据类型。如果你想要找到更多的有关ast.*类型。go/ast包的源码将是你学习的好地方。

go/scanner

扫描器就是一个在程序中能够读取写入的组件。在案例中用来生成token。go/scanner包用于读取go程序并生成一系列token。go/scanner的使用将在goScanner.go中分三部分解释说明。第一部分如下:

package main

import (
 "fmt"
 "go/scanner"
 "go/token"
 "io/ioutil"
 "os"
)

func main() {
    if len(os.Args) == 1 {
        fmt.Println("Not enough arguments!")
    return
    }

go/token包定义了一些Go程序中的表示字典token的常量。第二部分如下:

    for _, file := range os.Args[1:] {
        fmt.Println("Processing:", file)
        f, err := ioutil.ReadFile(file)
        if err != nil {
        fmt.Println(err)
        return
    }
    One := token.NewFileSet()
    files := one.AddFile(file, one.Base(), len(f))

将被标记化的源文件存储在file变量中而其内容存储在f,最后一部分代码如下:

    var myScanner scanner.Scanner
    myScanner.Init(files, f, nil, scanner.ScanComments)
    for {
        pos, tok, lit := myScanner.Scan()
        if tok == token.EOF {
            break
        }
    fmt.Printf("%s\t%s\t%q\n", one.Position(pos), tok, lit)
    }
  }
}

for循环用来遍历输入的文件。源码中用toker.EOF表示结尾退出for循环。scanner.Scan()函数返回当前文件的下标、token、和文件名。scanner.Init()scanner.ScanComments的使用告诉扫描器返回token的注解。你可以使用1代替scanner.ScanComments,如果你不想看到token的注解输出,用0代替scanner.ScanComments。build后执行代码如下:

$ ./goScanner a.go
Processing: a.go
a.go:1:1 package "package"
a.go:1:9 IDENT "a"
a.go:1:10 ; "\n"
a.go:3:1 import "import"
a.go:3:8 ( ""
a.go:4:2 STRING "\"fmt\""
a.go:4:7 ; "\n"
a.go:5:1 ) ""
a.go:5:2 ; "\n"
a.go:7:1 func "func"
a.go:7:6 IDENT "init"
a.go:7:10 ( ""
a.go:7:11 ) ""
a.go:7:13 { ""
a.go:8:2 IDENT "fmt"
a.go:8:5 . ""
a.go:8:6 IDENT "Println"
a.go:8:13 ( ""
a.go:8:14 STRING "\"init() a\""
a.go:8:24 ) ""
a.go:8:25 ; "\n"
a.go:9:1 } ""
a.go:9:2 ; "\n"
a.go:11:1 func "func"
a.go:11:6 IDENT "FromA"
a.go:11:11 ( ""
a.go:11:12 ) ""
a.go:11:14 { ""
a.go:12:2 IDENT "fmt"
a.go:12:5 . ""
a.go:12:6 IDENT "Println"
a.go:12:13 ( ""
a.go:12:14 STRING "\"fromA()\""
a.go:12:23 ) ""
a.go:12:24 ; "\n"
a.go:13:1 } ""
a.go:13:2 ; "\n"

goScanner.go输出后很简单。注意goScanner.go可以扫描任何类型的文件,即使是二进制文件。然而,如果你扫描二进制文件。你可能得到看不懂的输出。从输出中可以看到,Go扫描器自动加了分隔符。请注意,IDENT通知一个标识符,这是最流行的token类型。

下一章将处理解析过程

go/parser

解析器读取scanner的输出为了生成这些token的结构。解析器使用语法器描述语法,为了确保给的token是有效的语法。这个结构展示出来如同树的结构,它就是AST。

goParser.go说明了处理go/token输出的go/parser包的使用。接下来通过4个部分展示说明:

第一部分代码如下:

package main

import (
 "fmt"
 "go/ast"
 "go/parser"
 "go/token"
 "os"
 "strings"
)

type visitor int

第二部分代码如下:

func (v visitor) Visit(n ast.Node) ast.Visitor {
    if n == nil {
        return nil
    }
    fmt.Printf("%s%T\n", strings.Repeat("\t", int(v)), n)
    return v + 1
}

Visit()函数将被AST的每个Node调用。第三部分代码如下:

func main() {
    if len(os.Args) == 1 {
    fmt.Println("Not enough arguments!")
    return
 }

最后一部分代码如下:

    for _, file := range os.Args[1:] {
    fmt.Println("Processing:", file)
    one := token.NewFileSet()
    var v visitor
    f, err := parser.ParseFile(one, file, nil, parser.AllErrors)
    if err != nil {
        fmt.Println(err)
        return
    }
    ast.Walk(v, f)
    }
}

Walk()函数被递归调用,用深度遍历优先遍历AST,访问所有的节点。

building和执行goParser.go去找到简单又便捷的AST。输出如下:

$ ./goParser a.go
Processing: a.go
*ast.File
 *ast.Ident
 *ast.GenDecl
 *ast.ImportSpec
 *ast.BasicLit
 *ast.FuncDecl
 *ast.Ident
 *ast.FuncType
 *ast.FieldList
 *ast.BlockStmt
 *ast.ExprStmt
 *ast.CallExpr
 *ast.SelectorExpr
 *ast.Ident
 *ast.Ident
 *ast.BasicLit
 *ast.FuncDecl
 *ast.Ident
 *ast.FuncType
 *ast.FieldList
 *ast.BlockStmt
 *ast.ExprStmt
 *ast.CallExpr
 *ast.SelectorExpr
 *ast.Ident
 *ast.Ident
 *ast.BasicLit

goParser.go简单的输出就得到了。然而它和goScanner.go.的输出完全不同。

现在你已经知道了Go扫描器和Go解析器的输出结果,接下来准备看一些更实用的例子。

实用案例

在这章中,我们将写一个在输入文件中计算关键字出现次数的Go程序。在这个例子中,关键字为“var”。功能的名字为varTimes.go,它将分4部分展示。第一部分如下:

package main

import (
 "fmt"
 "go/scanner"
 "go/token"
 "io/ioutil"
 "os"
)

var KEYWORD = "var"
var COUNT = 0

你可以搜索任何你想要的Go关键字——如果你修改了varTimes.go,你甚至可以在运行时设置全局关键字变量的值。

func main() {
    if len(os.Args) == 1 {
        fmt.Println("Not enough arguments!")
        return
    }
    for _, file := range os.Args[1:] {
    fmt.Println("Processing:", file)
    f, err := ioutil.ReadFile(file)
    if err != nil {
    fmt.Println(err)
        sreturn
    }
    one := token.NewFileSet()
    files := one.AddFile(file, one.Base(), len(f))

第三部分代码如下:

    var myScanner scanner.Scanner
    myScanner.Init(files, f, nil, scanner.ScanComments)
    localCount := 0
    for {
        _, tok, lit := myScanner.Scan()
        if tok == token.EOF {
        break
        }

在本例中,发现标记的位置被忽略,因为这不重要。但是,要找出文件的结尾,需要使用tok变量。

最后一部分代码如下:

    if lit == KEYWORD {
    COUNT++
    localCount++
    }
  }
    fmt.Printf("Found _%s_ %d times\n", KEYWORD, localCount)
 }
 fmt.Printf("Found _%s_ %d times in total\n", KEYWORD, COUNT)
}

编译执行后varTimes.go输出如下:

$ go build varTimes.go
$ ./varTimes varTimes.go variadic.go a.go
Processing: varTimes.go
Found _var_ 3 times
Processing: variadic.go
Found _var_ 0 times
Processing: a.go
Found _var_ 0 times
Found _var_ 3 times in total

查找具有给定字符串长度的变量名

这章将展示其他使用案例,将会比之前在varTimes.go的案例更加高级。你将看到如何查找具有给定字符串长度的变量名,你能使用任何你想要的字符串长度。此外,该程序将能够区分全局变量和局部变量。

本地变量定义在函数内部,而全局变量定义在函数外部。全局变量也称为包变量。

The name of the utility is varSize.go and it will be presented in four parts. The first part of varSize.go is as follows:
名为varSize.go的程序将分为4个部分。第一部分如下:

package main

import (
 "fmt"
 "go/ast"
 "go/parser"
 "go/token"
 "os"
 "strconv"
)

var SIZE = 2
var GLOBAL = 0
var LOCAL = 0

type visitor struct {
    Package map[*ast.GenDecl]bool
}

func makeVisitor(f *ast.File) visitor {
    k1 := make(map[*ast.GenDecl]bool)
    for _, aa := range f.Decls {
        v, ok := aa.(*ast.GenDecl)
        if ok {
            k1[v] = true
        }
    }
    return visitor{k1}
}

为了区分本地变量和全局变量之间的关系,我们定义了全局变量GLOBAL和本地变量LOCAL保持他们的计数。visitor结构体的使用将帮助我们区分本地变量和全局变量。因此map字段定义在visitor结构体,makeVisitor()函数的作用是根据参数的值初始化visitor结构体,它是一个代表整个文件的文件节点。第二部分代码的实现如下:

func (v visitor) Visit(n ast.Node) ast.Visitor {
    if n == nil {
        return nil
    }
    switch d := n.(type) {
        case *ast.AssignStmt:
            if d.Tok != token.DEFINE {
                return v
            }
            for _, name := range d.Lhs {
                v.isItLocal(name)
            }
        case *ast.RangeStmt:
            v.isItLocal(d.Key)
            v.isItLocal(d.Value)
        case *ast.FuncDecl:
            if d.Recv != nil {
                v.CheckAll(d.Recv.List)
            }
            v.CheckAll(d.Type.Params.List)
            if d.Type.Results != nil {
                v.CheckAll(d.Type.Results.List)
            }
        case *ast.GenDecl:
            if d.Tok != token.VAR {
                return v
            }
            for _, spec := range d.Specs {
            value, ok := spec.(*ast.ValueSpec)
            if ok {
                for _, name := range value.Names {
                    if name.Name == "_" {
                        continue
                    }
                    if v.Package[d] {
                        if len(name.Name) == SIZE {
                            fmt.Printf("** %s\n", name.Name)
                            GLOBAL++
                        }
                    } else {
                        if len(name.Name) == SIZE {
                            fmt.Printf("* %s\n", name.Name)
                            LOCAL++
                        }
                    }
                }
            }
        }
    }
    return v
}

Visit()主要的工作是决定node的类型,以便采取相应的行动。这需要使用到switch语句的帮助。

ast.AssignStmt节点代表作业或者定义的短变量。ast.RangeStmt节点是一种结构类型,用于表示带有range子句的for语句。这是另一个声明新的局部变量的地方。

ast.FuncDecl是一个代表函数定义的结构体类型,被定义在函数内部的每个变量都是本地变量。最后,ast.GenDecl是一个代表import, constant, type或者变量的结构体类型。
然而我们仅仅需要关注在token.VAR里的tokens。第三部分代码如下:

func (v visitor) isItLocal(n ast.Node) {
    identifier, ok := n.(*ast.Ident)
    if ok == false {
        return
    }
    if identifier.Name == "_" || identifier.Name == "" {
        return
    }
    if identifier.Obj != nil && identifier.Obj.Pos() == identifier.Pos() {
        if len(identifier.Name) == SIZE {
            fmt.Printf("* %s\n", identifier.Name)
            LOCAL++
        }
    }
}

func (v visitor) CheckAll(fs []*ast.Field) {
    for _, f := range fs {
        for _, name := range f.Names {
            v.isItLocal(name)
        }
    }
}

这两个函数是帮助方法。第一个节点决定标识符节点是否是局部变量,第二个节点访问ast.Field节点检查其内容是否包含局部变量。最后一部分代码如下:

func main() {
    if len(os.Args) <= 2 {
        fmt.Println("Not enough arguments!")
        return
    }
    temp, err := strconv.Atoi(os.Args[1])
    if err != nil {
        SIZE = 2
        fmt.Println("Using default SIZE:", SIZE)
    } else {
        SIZE = temp
    }
    var v visitor
    all := token.NewFileSet()
    for _, file := range os.Args[2:] {
        fmt.Println("Processing:", file)
        f, err := parser.ParseFile(all, file, nil, parser.AllErrors)
        if err != nil {
            fmt.Println(err)
            continue
        }
        v = makeVisitor(f)
        ast.Walk(v, f)
    }
    fmt.Printf("Local: %d, Global:%d with a length of %d.\n", LOCAL, GLOBAL, SIZE)
}

这程序生成了它输入的AST,为了选出外部需要的信息。分割了Visit()方法,它是接口的一部分,休息的逻辑在main()函数中的ast.Walk()发生,这个函数自动访问了每个文件的所有的AST节点。执行代码生成如下输出:

$ go build varSize.go
$ ./varSize
Not enough arguments!
$ ./varSize 2 varSize.go variadic.go
Processing: varSize.go
* k1
* aa
* ok
* ok
* ok
* fs
Processing: variadic.go
Local: 6, Global:0 with a length of 2.
$ ./varSize 3 varSize.go variadic.go
Processing: varSize.go
* err
* all
* err
Processing: variadic.go
* sum
* sum
Local: 5, Global:0 with a length of 3.
$ ./varSize 7 varSize.go variadic.go
Processing: varSize.go
Processing: variadic.go
* message
Local: 1, Global:0 with a length of 7.

你可以移除一些fmt.Println()调用,这样就会少一些杂乱的输出。

你现在在Go中做了很聪明的事情,你知道如何解析Go陈序,你甚至可以写出你自己解析器和你自己的语言。如果你真的进入解析,你应该看一眼go/ast这章的内容,它的源码在https://golang.org/pkg/go/ast/和https://github.com/golang/go/tree/master/src/go/ast

Text和HTML模板

这章的主题将会惊艳到你。因为当前展示的包会带给你很大的灵活性,我确信你将会找到创造性的方式去使用它们,模板主要用于分离输出的格式化部分和数据部分。请注意Go模板能够成为文件或者string,一般的想法是对较小的模板使用内联字符串,对较大的模板使用外部文件。

在Go语言中,不能同时导入text/templatehtml/template,因为两个包共享相似的包名(template)。如果绝对需要的话,你可以将其中一个起一个别名。在第四章可以看useStrings.go的使用,复合类型的使用。

Text的输出经常展示在你的屏幕上,因此html的输出需要浏览器的帮助。然而,text的输出要比html的输出更好些。如果你认为你将需要使用其他UNIX的命令行工具执行Go工具的输出,你应该用text/template代替html/template

text/templatehtml/template会告诉你Go包有多么复杂。你很快就会看到,这两个包都支持它们自己的编程语言——好的软件使复杂的东西看起来简单而优雅。

生成text输出

如果需要创建简单的输出,使用text/template包是一个很好的选择。text/templat包将在textT.go文件中分5部分说明。

template经常存储在外部文件中,例子将展示text.gotext模板文件,将分3个部分分析。数据是在text中或者网络中是类型可读的。然而最简单的原因就是text.gotext文件中的数据被程序使用切片转成硬编码。接下来我们将看textT.go的Go代码。第一部分代码如下所示:

package main

import (
 "fmt"
 "os"
 "text/template"
)

第二部分代码如下:

type Entry struct {
 Number int
 Square int
}

你将需要定义一个新的数据类型存储你的数据,除非你处理非常简单的数据。第三部分代码如下:

func main() {
    arguments := os.Args
    if len(arguments) != 2 {
        fmt.Println("Need the template file!")
        return
    }
    tFile := arguments[1]
    DATA := [][]int{{-1, 1}, {-2, 4}, {-3, 9}, {-4, 16}}

DATA变量是一个二维切片,用来初始化你的数据的版本。

第四部分代码如下:

    var Entries []Entry
    for _, i := range DATA {
        if len(i) == 2 {
            temp := Entry{Number: i[0], Square: i[1]}
            Entries = append(Entries, temp)
        }
    }

这个程序从DATA变量中创建了一个切片的数据结构。

最后一部分textT.go的代码如下:

    t := template.Must(template.ParseGlob(tFile))
    t.Execute(os.Stdout, Entries)
}

template.Must()用在了初始化。它返回的数据类型是一个Template,它控制一个解析后的展示内容。template.ParseGlob()函数读取外部template文件。注意对于外部template文件我更喜欢使用gohtml扩展。但是你能使用任何你想要的,目的一致就好了。

最后,template.Execute()函数不能全部工作,它包括执行程序和打印输出到指定的文件,使用os.Stdout

现在是时候看一下template文件的代码,第一部分text template如下:

Calculating the squares of some integers

注意空行在text template文件中也是有效的,它将作为空行在最后的文件中展示出来。

第二部分代码如下:

{{ range . }} The square of {{ printf "%d" .Number}} is {{ printf
 "%d" .Square}}

在这里将会有很多有趣的事情发生。关键字的范围允许你迭代输入的行,作为一个给定的切片结构。简单的text像这样会被打印,因此变量和动态的text必须以“{{”开始以“}}”结束。数据结构的 存取可以用.Number.Square。注意“.”字符在在这个数据类型的前面。最后,格式化打印命令会将最后的输出文件格式化。

第三部分代码如下:

{{ end }}

{{ range }}命令以{{ end }}结尾,意外的将{{ end }}放在错误的地方将会影响你的输出。第一次,在text template模板中时刻注意空行的有效性,因为它将会输出在最后的文件中。

Executing textT.go will generate the following type of output:
执行textT.go输出如下:

$ go run textT.go text.gotext
Calculating the squares of some integers
The square of -1 is 1
The square of -2 is 4
The square of -3 is 9
The square of -4 is 16

展示text.gotext模板文件,将分3个部分分析。数据是在text中或者网络中是类型可读的。然而最简单的原因就是text.gotext文件中的数据被程序使用切片转成硬编码。接下来我们将看textT.go的Go代码。第一部分代码如下所示:

package main

import (
 "fmt"
 "os"
 "text/template"
)

第二部分代码如下:

type Entry struct {
 Number int
 Square int
}

你将需要定义一个新的数据类型存储你的数据,除非你处理非常简单的数据。第三部分代码如下:

func main() {
    arguments := os.Args
    if len(arguments) != 2 {
        fmt.Println("Need the template file!")
        return
    }
    tFile := arguments[1]
    DATA := [][]int{{-1, 1}, {-2, 4}, {-3, 9}, {-4, 16}}

DATA变量是一个二维切片,用来初始化你的数据的版本。

第四部分代码如下:

    var Entries []Entry
    for _, i := range DATA {
        if len(i) == 2 {
            temp := Entry{Number: i[0], Square: i[1]}
            Entries = append(Entries, temp)
        }
    }

这个程序从DATA变量中创建了一个切片的数据结构。

最后一部分textT.go的代码如下:

    t := template.Must(template.ParseGlob(tFile))
    t.Execute(os.Stdout, Entries)
}

template.Must()用在了初始化。它返回的数据类型是一个Template,它控制一个解析后的展示内容。template.ParseGlob()函数读取外部template文件。注意对于外部template文件我更喜欢使用gohtml扩展。但是你能使用任何你想要的,目的一致就好了。

最后,template.Execute()函数不能全部工作,它包括执行程序和打印输出到指定的文件,使用os.Stdout

现在是时候看一下template文件的代码,第一部分text template如下:

Calculating the squares of some integers

注意空行在text template文件中也是有效的,它将作为空行在最后的文件中展示出来。

第二部分代码如下:

{{ range . }} The square of {{ printf "%d" .Number}} is {{ printf
 "%d" .Square}}

在这里将会有很多有趣的事情发生。关键字的范围允许你迭代输入的行,作为一个给定的切片结构。简单的text像这样会被打印,因此变量和动态的text必须以“{{”开始以“}}”结束。数据结构的 存取可以用.Number.Square。注意“.”字符在在这个数据类型的前面。最后,格式化打印命令会将最后的输出文件格式化。

第三部分代码如下:

{{ end }}

{{ range }}命令以{{ end }}结尾,意外的将{{ end }}放在错误的地方将会影响你的输出。第一次,在text template模板中时刻注意空行的有效性,因为它将会输出在最后的文件中。

Executing textT.go will generate the following type of output:
执行textT.go输出如下:

$ go run textT.go text.gotext
Calculating the squares of some integers
The square of -1 is 1
The square of -2 is 4
The square of -3 is 9
The square of -4 is 16

你可能感兴趣的:(go,go,package,go,module,go,编程语言,go语言)