Skip to content

Latest commit

 

History

History
939 lines (700 loc) · 33.4 KB

File metadata and controls

939 lines (700 loc) · 33.4 KB

四、Go 包、算法和数据结构

本章的主要主题是 Go 包、算法和数据结构。如果您将所有这些结合起来,您将得到一个完整的程序,因为 Go 程序包中包含处理数据结构的算法。这些软件包包括 Go 附带的软件包和您自己创建的用于操作数据的软件包。

因此,在本章中,您将了解以下内容:

  • 大 O 符号
  • 两种排序算法
  • sort.Slice()功能
  • 链表
  • 在 Go 中创建自己的哈希表数据结构
  • Go包
  • Go 中的垃圾收集GC

关于算法

当你需要处理大量数据时,了解算法及其工作方式肯定会对你有所帮助。此外,如果您选择对给定作业使用错误的算法,则可能会减慢整个过程并使软件无法使用。

传统的 Unix 命令行实用程序,如awk(1)sed(1)vi(1)tar(1)cp(1)就是很好的例子,说明了好的算法有多大的帮助,这些实用程序可以处理比机器内存大得多的文件。这在早期的 Unix 时代非常重要,因为当时 Unix 机器上的 RAM 总量约为 64K 甚至更少!

大 O 符号

大 O****符号用于描述算法的复杂度,它直接关系到算法的性能。算法的效率取决于其计算复杂度,而计算复杂度主要与算法需要访问其输入数据以完成其工作的次数有关。通常,您希望了解最坏情况和平均情况。

因此,O(n)算法,其中 n 是输入的大小,被认为比 O(n2算法更好,后者比 O(n3算法更好。然而,最糟糕的算法是运行时间为 O(n!)的算法,因为这使得它们几乎无法用于 300 个元素以上的输入。请注意,大 O 表示法更多的是关于估计,而不是给出精确的值。因此,它主要用作比较值,而不是绝对值。

此外,大多数内置类型中的 Go 查找操作(如查找映射键的值或访问数组元素)都有一个固定的时间,由 O(1)表示。这意味着内置类型通常比自定义类型快,除非您希望完全控制幕后的情况,否则您通常应该更喜欢它们。此外,并非所有数据结构都是平等创建的。一般来说,数组操作比映射操作快,而映射比数组更通用!

排序算法

最常见的算法类别必须处理排序数据,即按给定顺序排列数据。两种最著名的排序算法如下:

  • 快速排序:这被认为是最快的排序算法之一。快速排序对其数据进行排序所需的平均时间为 O(n logn),但在最坏的情况下,这可能会增长到 O(n2,这主要与数据处理的呈现方式有关。
  • 气泡排序:该算法非常容易实现,平均复杂度为 O(n2)。如果您想开始学习排序,请先从气泡排序开始,然后再研究更难开发的算法。

虽然每种算法都有它的缺点,但是如果你没有太多的数据,只要它能完成任务,算法就不是很重要。

您应该记住的是,Go 在内部实现排序的方式不能由开发人员控制,将来可能会改变;因此,如果您想完全控制排序,您应该编写自己的实现。

sort.Slice()函数

本节将说明 Go 版本 1.8 首次提供的sort.Slice()功能的使用。该功能的使用将在sortSlice.go中进行说明,将分三部分介绍。

第一部分是计划的预期序言和新结构类型的定义,如下所示:

package main 

import ( 
   "fmt" 
   "sort" 
) 

type aStructure struct { 
   person string 
   height int 
   weight int 
} 

正如您所料,您必须导入sort包才能使用其Slice()功能。

第二部分包含切片的定义,它有四个元素:

func main() { 

   mySlice := make([]aStructure, 0) 
   a := aStructure{"Mihalis", 180, 90}

   mySlice = append(mySlice, a) 
   a = aStructure{"Dimitris", 180, 95} 
   mySlice = append(mySlice, a) 
   a = aStructure{"Marietta", 155, 45} 
   mySlice = append(mySlice, a) 
   a = aStructure{"Bill", 134, 40} 
   mySlice = append(mySlice, a)

因此,在第一部分中,您声明了一个结构切片,该切片将在程序的其余部分以两种方式排序,其中包含以下代码:

   fmt.Println("0:", mySlice) 
   sort.Slice(mySlice, func(i, j int) bool { 
         return mySlice[i].weight <mySlice[j].weight 
   }) 
   fmt.Println("<:", mySlice) 
   sort.Slice(mySlice, func(i, j int) bool { 
         return mySlice[i].weight >mySlice[j].weight 
   }) 
   fmt.Println(">:", mySlice) 
} 

这段代码包含了所有的魔力:你只需要定义你想要的sort你的slice的方式,其余的都由 Go 完成。sort.Slice()函数将匿名排序函数作为其参数之一;另一个参数是您想要设置为sortslice变量的名称。请注意,排序后的切片保存在slice变量中。

执行sortSlice.go将生成以下输出:

$ go run sortSlice.go
0: [{Mihalis 180 90} {Dimitris 180 95} {Marietta 155 45} {Bill 134 40}]
<: [{Bill 134 40} {Marietta 155 45} {Mihalis 180 90} {Dimitris 180 95}]
>: [{Dimitris 180 95} {Mihalis 180 90} {Marietta 155 45} {Bill 134 40}]

正如你所看到的,你只需在 Go 代码中更改一个字符,就可以轻松地按升序或降序sort

此外,如果您的 Go 版本不支持sort.Slice(),您将收到类似以下的错误消息:

$ go version
go version go1.3.3 linux/amd64
$ go run sortSlice.go
# command-line-arguments
./sortSlice.go:27: undefined: sort.Slice
./sortSlice.go:31: undefined: sort.Slice

Go中的链表

链表是一种具有有限元素集的结构,其中每个元素使用至少两个内存位置:一个用于存储数据,另一个用于将当前元素链接到构成链表的元素序列中的下一个元素的指针。链表的最大优点是它们易于理解和实现,并且具有足够的通用性,可用于许多不同的情况,并对许多不同类型的数据进行建模。

链表的第一个元素称为,而链表的最后一个元素通常称为。定义链表时,您应该做的第一件事是将链表的头保存在单独的变量中,因为头是访问整个链表所需的唯一对象。

请注意,如果丢失了指向单个链表的第一个节点的指针,则无法再次找到它。

下图显示了链表和双链表的图形表示。双链接列表更灵活,但需要更多的内务管理:

链表和双链表的图形表示

因此,在本节中,我们将介绍一个在linkedList.go中保存的 Go 中的链表的简单实现。

在创建自己的数据结构时,最重要的元素是节点的定义,该定义通常使用结构来实现。

linkedList.go的代码将分为四个部分。

第一部分内容如下:

package main 

import ( 
   "fmt" 
) 

第二部分包含以下 Go 代码:

type Node struct { 
   Value int 
   Next  *Node 
} 

func addNode(t *Node, v int) int { 
   if root == nil { 
         t = &Node{v, nil} 
         root = t 
         return 0 
   } 

   if v == t.Value { 
         fmt.Println("Node already exists:", v) 
         return -1 
   } 

   if t.Next == nil { 
         t.Next = &Node{v, nil} 
         return -2 
   } 

   return addNode(t.Next, v)

} 

在这里,您定义了保存列表中每个元素的结构,以及允许您向列表中添加新节点的函数。为了避免重复条目,您应该检查列表中是否已经存在值。请注意,addNode()是一个递归函数,因为它调用自身,这种方法可能会比迭代慢一点,并且需要更多的内存。

代码的第三部分是traverse()功能:

func traverse(t *Node) { 
   if t == nil { 
         fmt.Println("-> Empty list!") 
         return 
   } 

   for t != nil {

         fmt.Printf("%d -> ", t.Value) 
         t = t.Next 
   } 
   fmt.Println() 
} 

for循环实现了访问链表中所有节点的迭代方法。

最后一部分内容如下:

var root = new(Node)
func main() { 
   fmt.Println(root) 
   root = nil 
   traverse(root) 
   addNode(root, 1) 
   addNode(root, 1) 
   traverse(root) 
   addNode(root, 10) 
   addNode(root, 5) 
   addNode(root, 0) 
   addNode(root, 0) 
   traverse(root) 
   addNode(root, 100) 
   traverse(root) 
}

在本书中,您第一次看到了使用非常量的全局变量。全局变量可以从程序中的任何地方访问和更改,这使得它们的使用既实用又危险。使用名为root的全局变量来保存链表的root是为了显示链表是否为空。这是因为 Go 中的整数值被初始化为0;因此,new(Node)实际上是{0 <nil>},这使得在不向操作链表的每个函数传递额外变量的情况下,无法判断列表头是否为 nil。

执行linkedList.go将生成以下输出:

$ go run linkedList.go
&{0 <nil>}
-> Empty list!
Node already exists: 1
1 ->
Node already exists: 0
1 -> 10 -> 5 -> 0 ->
1 -> 10 -> 5 -> 0 -> 100 ->

Go中的树木

是一组有限且非空的顶点和边。有向图是一个边具有与其相关联的方向的图。有向无环图是一个没有圈的有向图。是一个有向无环图,它满足三个原则:第一,它有一个根节点:树的入口点;其次,除了根之外,每个顶点都有一个且只有一个入口点;第三,有一条路径连接根和每个顶点,属于树。

因此,根是树的第一个节点。根据树类型,每个节点可以连接到一个或多个节点。如果每个节点指向一个且仅指向另一个节点,则该树是一个链表!

最常用的树类型称为二叉树,因为每个节点最多可以有两个子节点。下图显示了二叉树数据结构的图形表示:

二叉树

本文提供的代码将仅向您展示如何创建二叉树,以及如何遍历二叉树以打印其所有元素,从而证明 Go 可用于创建树数据结构。因此,它不会实现二叉树的全部功能,其中还包括删除树节点和平衡树。

tree.go的代码将分三部分介绍。

第一部分是预期的序言以及节点的定义,如下所示:

package main 

import ( 
   "fmt" 
   "math/rand" 
   "time" 
) 
type Tree struct { 
   Left  *Tree 
   Value int 
   Right *Tree 
} 

第二部分包含允许您遍历树以打印其所有元素、使用随机生成的数字创建树以及在其中插入节点的函数:

func traverse(t *Tree) { 
   if t == nil { 
         return 
   } 
   traverse(t.Left) 
   fmt.Print(t.Value, " ") 
   traverse(t.Right) 
} 

func create(n int) *Tree { 
   var t *Tree 
   rand.Seed(time.Now().Unix()) 
   for i := 0; i< 2*n; i++ { 
         temp := rand.Intn(n) 
         t = insert(t, temp) 
   } 
   return t 
} 

func insert(t *Tree, v int) *Tree { 
   if t == nil { 
         return&Tree{nil, v, nil} 
   } 
   if v == t.Value { 
         return t 
   } 
   if v <t.Value { 
         t.Left = insert(t.Left, v) 
         return t 
   } 
   t.Right = insert(t.Right, v) 
   return t 
} 

insert()的第二条if语句检查树中是否已经存在值,以避免再次添加。第三条if语句标识新元素是位于当前节点的左侧还是右侧。

最后一部分是main()功能的实现:

func main() { 
   tree := create(30) 
   traverse(tree) 
   fmt.Println() 
   fmt.Println("The value of the root of the tree is", tree.Value) 
} 

执行tree.go将生成以下输出:

$ go run tree.go
0 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 21 22 23 24 25 26 27 28 29
The value of the root of the tree is 16

请注意,由于树节点的值是随机生成的,因此每次运行程序时,程序的输出都会不同。如果希望始终获得相同的元素,则在create()函数中为种子值使用一个常量。

在 Go 中开发哈希表

严格来说,哈希表是一种数据结构,它存储一个或多个键和值对,并使用键的hashFunction将索引计算到一个桶或槽数组中,从中可以检索到正确的值。理想情况下,hashFunction应该将每个密钥分配给一个唯一的 bucket,前提是您拥有所需数量的 bucket。

一个好的hashFunction必须能够生成散列值的均匀分布,因为使用未使用的桶或桶的基数存在较大差异是低效的。此外,hashFunction应该一致地工作,并为相同的键输出相同的哈希值,因为否则将无法找到您想要的信息!如果你认为哈希表不是有用的,方便的,或者聪明的,你应该考虑如下:当哈希表有一个 T2,n,3,3,3 个关键字,以及 Ty4 T4,k 个 T5,桶,它的搜索速度从 O(n)线性搜索到 O(N/K)!虽然改进可能看起来很小,但您应该意识到,对于只有 20 个插槽的哈希数组,搜索时间将减少 20 倍!这使得哈希表适用于字典或任何其他需要搜索大量数据的类似应用。尽管使用大量存储桶会增加程序的复杂性和内存使用率,但有时还是值得的。

下图显示了包含 10 个存储桶的简单哈希表的图形表示。不难理解,hashFunction是模运算符:

一个简单的哈希表

尽管所展示的哈希表版本使用数字,因为它们更容易实现和理解,但只要找到合适的hashFunction来处理输入,您就可以使用任何想要的数据类型。hash.go的源代码将分三部分介绍。

第一个是:

package main 

import ( 
   "fmt" 
) 

type Node struct { 
   Value int 
   Next  *Node 
} 

type HashTablestruct { 
   Table map[int]*Node

   Size  int 
} 

Node struct定义取自您前面看到的链表的实现。将映射用于Table变量而不是切片的原因是切片的索引只能是自然数,而映射的键可以是任何东西。

第二部分包含以下 Go 代码:

func hashFunction(i, size int) int { 
   return (i % size) 
} 

func insert(hash *HashTable, value int) int { 
   index := hashFunction(value, hash.Size) 
   element := Node{Value: value, Next: hash.Table[index]} 
   hash.Table[index] = &element 
   return index 
} 

func traverse(hash *HashTable) { 
   for k := range hash.Table { 
         if hash.Table[k] != nil { 
               t := hash.Table[k] 
               for t != nil { 
                     fmt.Printf("%d -> ", t.Value) 
                     t = t.Next 
               } 
               fmt.Println() 
         } 
   } 
}

请注意,traverse()函数使用linkedList.go中的 Go 代码来遍历哈希表中每个 bucket 的元素。此外,请注意,insert函数不会检查哈希表中是否已经存在值以节省书本空间,但通常情况并非如此。此外,出于快速和简单的原因,在每个列表的开头插入新元素。

最后一部分是main()功能的实现:

func main() { 
   table := make(map[int]*Node, 10) 
   hash := &HashTable{Table: table, Size: 10} 
   fmt.Println("Number of spaces:", hash.Size) 
   for i := 0; i< 95; i++ { 
         insert(hash, i) 
   } 
   traverse(hash) 
} 

执行hash.go将生成以下输出,证明哈希表按预期工作:

$ go run hash.go
Number of spaces: 10 89 -> 79 -> 69 -> 59 -> 49 -> 39 -> 29 -> 19 -> 9 ->
86 -> 76 -> 66 -> 56 -> 46 -> 36 -> 26 -> 16 -> 6 ->
92 -> 82 -> 72 -> 62 -> 52 -> 42 -> 32 -> 22 -> 12 -> 2 ->
94 -> 84 -> 74 -> 64 -> 54 -> 44 -> 34 -> 24 -> 14 -> 4 ->
85 -> 75 -> 65 -> 55 -> 45 -> 35 -> 25 -> 15 -> 5 ->
87 -> 77 -> 67 -> 57 -> 47 -> 37 -> 27 -> 17 -> 7 ->
88 -> 78 -> 68 -> 58 -> 48 -> 38 -> 28 -> 18 -> 8 ->
90 -> 80 -> 70 -> 60 -> 50 -> 40 -> 30 -> 20 -> 10 -> 0 ->
91 -> 81 -> 71 -> 61 -> 51 -> 41 -> 31 -> 21 -> 11 -> 1 ->
93 -> 83 -> 73 -> 63 -> 53 -> 43 -> 33 -> 23 -> 13 -> 3 ->

如果您多次执行hash.go,您将看到打印行的顺序会有所不同。这是因为无法预测在traverse()函数中找到的range hash.Table的输出,这是因为 Go 有一个未指定的散列返回顺序。

关于Go套餐

软件包用于对相关函数和常量进行分组,以便您可以轻松地传输它们并在自己的 Go 程序中使用它们。因此,除了主程序包之外,程序包不是自治程序。

每个 Go 发行版都附带许多有用的 Go 软件包,包括以下内容:

  • net包:支持便携 TCP 和 UDP 连接
  • http包:这是 net 包的一部分,提供 HTTP 服务器和客户端实现
  • math包:提供数学函数和常数
  • io包:处理原始输入和输出操作
  • os包:这为您提供了操作系统功能的可移植接口
  • time包:这允许您处理时间和日期

有关标准 Go 包的完整列表,请参考https://golang.org/pkg/ 。我强烈建议您在开始开发自己的功能和软件包之前,查看 Go 附带的所有软件包,因为您正在寻找的功能很可能已经在标准 Go 软件包中可用。

使用标准 Go 包

您可能已经知道如何使用标准 Go 包。但是,您可能不知道的是,有些包具有一个结构。因此,例如,net包有几个子目录,分别名为httpmailrpcsmtptextprotourl,应该作为net/httpnet/mailnet/rpcnet/smtpnet/textprotonet/url导入。Go 在合理的情况下对包进行分组,但如果这些包被分组用于分发而不是功能,则这些包也可能是独立的包。

godoc实用程序的帮助下,您可以找到关于 Go 标准包的信息。因此,如果您正在查找有关net包的信息,则应执行godoc net

创建自己的包

软件包使大型软件系统的设计、实现和维护变得更加容易和简单。此外,它们允许多个程序员在同一个项目上工作,而没有任何重叠。所以,如果你发现自己一直在使用相同的功能,你应该认真考虑把它们包括在你自己的 GO 包中。

Go 包的源代码可以包含多个文件,可以在单个目录中找到,该目录以包命名,但主包除外,主包可以有任何名称。

本节将开发的aSimplePackage.go文件的 Go 代码将分为两部分。

第一部分如下:

package aSimplePackage 

import ( 
   "fmt" 
) 

这里没有什么特别的;您只需定义包的名称并包含必要的导入语句,因为包可能依赖于其他包。

第二部分包含以下 Go 代码:

const Pi = "3.14159" 

func Add(x, y int) int { 
   return x + y 
} 

func Println(x int) { 
   fmt.Println(x) 
} 

因此,aSimplePackage包提供了两个函数和一个常量。

aSimplePackage.go代码编写完成后,应执行以下命令,以便在其他 Go 程序或包中使用该包:

$ mkdir ~/go
$ mkdir ~/go/src
$ mkdir ~/go/src/aSimplePackage
$ export GOPATH=~/go
$ vi ~/go/src/aSimplePackage/aSimplePackage.go
$ go install aSimplePackage 

除了前两个mkdir命令外,您应该为创建的每个 Go 包执行所有这些操作,前两个mkdir命令应该只执行一次。

如您所见,每个包都需要在~/go/src中有自己的目录。执行上述命令后,go tool将自动生成您刚刚编译的 Go 包在pkg目录下的ar(1)存档:

$ ls -lR ~/go
total 0
drwxr-xr-x  3 mtsouk  staff  102 Apr  4 22:35 pkg
drwxr-xr-x  3 mtsouk  staff  102 Apr  4 22:35 src

/Users/mtsouk/go/pkg:
total 0
drwxr-xr-x  3 mtsouk  staff  102 Apr  4 22:35 darwin_amd64

/Users/mtsouk/go/pkg/darwin_amd64:
total 8
-rw-r--r--  1 mtsouk  staff  2918 Apr  4 22:35 aSimplePackage.a

/Users/mtsouk/go/src:
total 0
drwxr-xr-x  3 mtsouk  staff  102 Apr  4 22:35 aSimplePackage

/Users/mtsouk/go/src/aSimplePackage:
total 8
-rw-r--r--  1 mtsouk  staff  148 Apr  4 22:30 aSimplePackage.go

尽管您现在已经准备好使用aSimplePackage包,但是如果没有自主程序,您将无法看到该包的功能。

私有变量和函数

私有变量和函数不同于公共变量和函数,因为它们只能在包内部使用和调用。控制which函数和变量是否公开也称为封装。

Go 遵循一条简单的规则,即以大写字母开头的函数、变量、类型等是公共的,而以小写字母开头的函数、变量、类型等是私有的。但是,此规则不影响包名称。

现在您应该了解为什么将fmt.Printf()函数命名为原样,而不是fmt.printf()

为了说明这一点,我们将对aSimplePackage.go模块进行一些更改,并添加一个私有变量和一个私有函数。新单独包的名称将为anotherPackage.go。您可以使用diff(1)命令行实用程序查看对其所做的更改:

$ diff aSimplePackage.go anotherPackage.go
1c1
<packageaSimplePackage
---
>packageanotherPackage
7a8
>const version = "1.1"
15a17,20
>
>func Version() {
>     fmt.Println("The version of the package is", version)
> }

init()函数

每个 Go 包都可以有一个名为init()的函数,该函数在执行开始时自动执行。那么,让我们在anotherPackage.go包的代码中添加以下init()函数:

func init() { 
   fmt.Println("The init function of anotherPackage") 
} 

init()函数的当前实现是幼稚的,没有做任何特殊的事情。但是,在开始使用诸如打开数据库和网络连接之类的软件包之前,有时需要执行重要的初始化:在这些相对罕见的情况下,init()功能是非常宝贵的。

使用您自己的 Go 软件包

本小节将通过介绍两个名为usePackage.goprivateFail.go的小型Go程序,向您展示如何在自己的Go程序中使用aSimplePackageanotherPackage包。

为了使用另一个 Go 程序中位于GOPATH目录下的aSimplePackage包,您需要编写以下 Go 代码:

package main 

import ( 
   "aSimplePackage" 
   "fmt" 
) 

func main() { 
   temp := aSimplePackage.Add(5, 10) 
   fmt.Println(temp)

   fmt.Println(aSimplePackage.Pi) 
} 

首先,如果aSimplePackage尚未编译并位于预期位置,编译过程将失败,并显示类似以下的错误消息:

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

但是,如果aSimplePackage可用,usePackage.go将正常执行:

$ go run usePackage.go
15
3.14159

现在,让我们看看另一个使用anotherPackage的小程序的 Go 代码:

package main 

import ( 
   "anotherPackage" 
   "fmt" 
) 

func main() { 
   anotherPackage.Version() 
   fmt.Println(anotherPackage.version) 
   fmt.Println(anotherPackage.Pi) 
} 

如果您试图从anotherPackage调用私有函数或使用私有变量,您的 Go 程序privateFail.go将无法运行,并显示以下错误消息:

$ go run privateFail.go
# command-line-arguments
./privateFail.go:10: cannot refer to unexported name anotherPackage.version
./privateFail.go:10: undefined: anotherPackage.version

我真的很喜欢显示错误消息,因为大多数书籍都试图隐藏它们,就好像它们不在那里一样。当我学习 Go 时,我花了大约 3 个小时进行调试,直到我发现无法解释错误消息的原因是一个变量的名称!

但是,如果从privateFail.go中删除对私有变量的调用,程序将无错误地执行。此外,您将看到init()函数实际上是自动执行的:

$ go run privateFail.go
The init function of anotherPackage
The version of the package is 1.1
3.14159

使用外部 Go 包

有时软件包在 internet 上可用,您更愿意通过指定其 internet 地址来使用它们。一个这样的例子是 GoMySQL驱动程序,可以在github.com/go-sql-driver/mysql找到。

查看以下 Go 代码,保存为useMySQL.go

package main 

import ( 
   "fmt" 
   _ "github.com/go-sql-driver/mysql"
) 

func main() { 
   fmt.Println("Using the MySQL Go driver!") 
} 

使用_作为包标识符将使编译器忽略包未被使用的事实:绕过编译器的唯一合理原因是,在未使用的包中有一个init函数,您希望执行该函数。另一个合理的理由是说明Go的概念!

如果您尝试执行useMySQL.go,编译过程将失败:

$ go run useMySQL.go
useMySQL.go:5:2: cannot find package "github.com/go-sql-driver/mysql" in any of:
      /usr/local/Cellar/go/1.8/libexec/src/github.com/go-sql-driver/mysql (from $GOROOT)
      /Users/mtsouk/go/src/github.com/go-sql-driver/mysql (from $GOPATH)

要编译useMySQL.go,首先要执行以下步骤:

$ go get github.com/go-sql-driver/mysql
$ go run useMySQL.go
Using the MySQL Go driver!

成功下载所需软件包后,~/go目录的内容验证所需 Go 软件包已下载:

$ ls -lR ~/go
total 0
drwxr-xr-x  3 mtsouk  staff  102 Apr  4 22:35 pkg
drwxr-xr-x  5 mtsouk  staff  170 Apr  6 21:32 src

/Users/mtsouk/go/pkg:
total 0
drwxr-xr-x  5 mtsouk  staff  170 Apr  6 21:32 darwin_amd64

/Users/mtsouk/go/pkg/darwin_amd64:
total 24
-rw-r--r--  1 mtsouk  staff  2918 Apr  4 23:07 aSimplePackage.a
-rw-r--r--  1 mtsouk  staff  6102 Apr  4 22:50 anotherPackage.a
drwxr-xr-x  3 mtsouk  staff   102 Apr  6 21:32 github.com

/Users/mtsouk/go/pkg/darwin_amd64/github.com:
total 0
drwxr-xr-x  3 mtsouk  staff  102 Apr  6 21:32 go-sql-driver

/Users/mtsouk/go/pkg/darwin_amd64/github.com/go-sql-driver:
total 728
-rw-r--r--  1 mtsouk  staff  372694 Apr  6 21:32 mysql.a

/Users/mtsouk/go/src:
total 0
drwxr-xr-x  3 mtsouk  staff  102 Apr  4 22:35 aSimplePackage
drwxr-xr-x  3 mtsouk  staff  102 Apr  4 22:50 anotherPackage
drwxr-xr-x  3 mtsouk  staff  102 Apr  6 21:32 github.com

/Users/mtsouk/go/src/aSimplePackage:
total 8
-rw-r--r--  1 mtsouk  staff  148 Apr  4 22:30 aSimplePackage.go

/Users/mtsouk/go/src/anotherPackage:
total 8
-rw-r--r--@ 1 mtsouk  staff  313 Apr  4 22:50 anotherPackage.go

/Users/mtsouk/go/src/github.com:
total 0
drwxr-xr-x  3 mtsouk  staff  102 Apr  6 21:32 go-sql-driver

/Users/mtsouk/go/src/github.com/go-sql-driver:
total 0
drwxr-xr-x  35 mtsouk  staff  1190 Apr  6 21:32 mysql

/Users/mtsouk/go/src/github.com/go-sql-driver/mysql:
total 584
-rw-r--r--  1 mtsouk  staff   2066 Apr  6 21:32 AUTHORS
-rw-r--r--  1 mtsouk  staff   5581 Apr  6 21:32 CHANGELOG.md
-rw-r--r--  1 mtsouk  staff   1091 Apr  6 21:32 CONTRIBUTING.md
-rw-r--r--  1 mtsouk  staff  16726 Apr  6 21:32 LICENSE
-rw-r--r--  1 mtsouk  staff  18610 Apr  6 21:32 README.md
-rw-r--r--  1 mtsouk  staff    470 Apr  6 21:32 appengine.go
-rw-r--r--  1 mtsouk  staff   4965 Apr  6 21:32 benchmark_test.go
-rw-r--r--  1 mtsouk  staff   3339 Apr  6 21:32 buffer.go
-rw-r--r--  1 mtsouk  staff   8405 Apr  6 21:32 collations.go
-rw-r--r--  1 mtsouk  staff   8525 Apr  6 21:32 connection.go
-rw-r--r--  1 mtsouk  staff   1831 Apr  6 21:32 connection_test.go
-rw-r--r--  1 mtsouk  staff   3111 Apr  6 21:32 const.go
-rw-r--r--  1 mtsouk  staff   5036 Apr  6 21:32 driver.go
-rw-r--r--  1 mtsouk  staff   4246 Apr  6 21:32 driver_go18_test.go
-rw-r--r--  1 mtsouk  staff  47090 Apr  6 21:32 driver_test.go
-rw-r--r--  1 mtsouk  staff  13046 Apr  6 21:32 dsn.go
-rw-r--r--  1 mtsouk  staff   7872 Apr  6 21:32 dsn_test.go
-rw-r--r--  1 mtsouk  staff   3798 Apr  6 21:32 errors.go
-rw-r--r--  1 mtsouk  staff    989 Apr  6 21:32 errors_test.go
-rw-r--r--  1 mtsouk  staff   4571 Apr  6 21:32 infile.go
-rw-r--r--  1 mtsouk  staff  31362 Apr  6 21:32 packets.go
-rw-r--r--  1 mtsouk  staff   6453 Apr  6 21:32 packets_test.go
-rw-r--r--  1 mtsouk  staff    600 Apr  6 21:32 result.go
-rw-r--r--  1 mtsouk  staff   3698 Apr  6 21:32 rows.go
-rw-r--r--  1 mtsouk  staff   3609 Apr  6 21:32 statement.go
-rw-r--r--  1 mtsouk  staff    729 Apr  6 21:32 transaction.go
-rw-r--r--  1 mtsouk  staff  17924 Apr  6 21:32 utils.go
-rw-r--r--  1 mtsouk  staff   5784 Apr  6 21:32 utils_test.go

清除命令

有时,您正在开发一个使用大量非标准 Go 包的大型 Go 程序,并且希望从一开始就开始编译过程。Go 允许您清理包的文件,以便以后重新创建。以下命令在不影响包的代码的情况下清理包:

$ go clean -x -i aSimplePackage
cd /Users/mtsouk/go/src/aSimplePackage
rm -f aSimplePackage.test aSimplePackage.test.exe
rm -f /Users/mtsouk/go/pkg/darwin_amd64/aSimplePackage.a

同样,您也可以清理从 internet 下载的软件包,这也需要使用其完整路径:

$ go clean -x -i github.com/go-sql-driver/mysql
cd /Users/mtsouk/go/src/github.com/go-sql-driver/mysql
rm -f mysql.test mysql.test.exe appengine appengine.exe
rm -f /Users/mtsouk/go/pkg/darwin_amd64/github.com/go-sql-driver/mysql.a

请注意,go clean命令在您希望将项目传输到另一台机器而不包含不必要的文件时也特别有用。

垃圾收集

在本节中,我们将简要介绍 Go 如何处理 GC,它试图有效地释放未使用的内存。garbageCol.go的 Go 代码可分为两部分。

第一部分内容如下:

package main 

import ( 
   "fmt" 
   "runtime" 
   "time" 
) 

func printStats(mem runtime.MemStats) { 
   runtime.ReadMemStats(&mem) 
   fmt.Println("mem.Alloc:", mem.Alloc) 
   fmt.Println("mem.TotalAlloc:", mem.TotalAlloc) 
   fmt.Println("mem.HeapAlloc:", mem.HeapAlloc) 
   fmt.Println("mem.NumGC:", mem.NumGC) 
   fmt.Println("-----") 
} 

每次要读取最新的内存统计信息时,都应该调用runtime.ReadMemStats()函数。

第二部分包含main()功能的实现,其 Go 代码如下:

func main() { 
   var memruntime.MemStats 
   printStats(mem) 

   for i := 0; i< 10; i++ { 
         s := make([]byte, 100000000) 
         if s == nil { 
               fmt.Println("Operation failed!") 
         } 
   } 
   printStats(mem) 

   for i := 0; i< 10; i++ { 
         s := make([]byte, 100000000) 
         if s == nil { 
               fmt.Println("Operation failed!") 
         } 
         time.Sleep(5 * time.Second) 
   } 
   printStats(mem)

} 

这里,您尝试获取大量内存以触发垃圾收集器的使用。

执行garbageCol.go生成以下输出:

$ go run garbageCol.go
mem.Alloc: 53944
mem.TotalAlloc: 53944
mem.HeapAlloc: 53944
mem.NumGC: 0
-----
mem.Alloc: 100071680
mem.TotalAlloc: 1000146400
mem.HeapAlloc: 100071680
mem.NumGC: 10
-----
mem.Alloc: 66152
mem.TotalAlloc: 2000230496
mem.HeapAlloc: 66152
mem.NumGC: 20
-----

因此,输出显示与garbageCol.go程序使用的内存相关的属性信息。如果想得到更详细的输出,可以执行garbageCol.go,如下图:

$ GODEBUG=gctrace=1 go run garbageCol.go

此版本的命令将以以下格式提供信息:

gc 11 @0.101s 0%: 0.003+0.083+0.020 ms clock, 0.030+0.059/0.033/0.006+0.16 mscpu, 95->95->0 MB, 96 MB goal, 8 P

95->95->0 MB部分包含关于各种堆大小的信息,这些信息还显示了垃圾收集器的运行情况。第一个值是 GC 启动时的堆大小,而中间的值显示 GC 结束时的堆大小。第三个值是活动堆的大小。

你的环境

在本节中,我们将展示如何使用runtime软件包了解您的环境:当您必须根据所使用的操作系统和 Go 版本采取某些操作时,这将非常有用。

使用runtime包了解您的环境非常简单,如runTime.go所示:

package main 

import ( 
   "fmt" 
   "runtime" 
) 

func main() { 
   fmt.Print("You are using ", runtime.Compiler, " ") 
   fmt.Println("on a", runtime.GOARCH, "machine") 
   fmt.Println("with Go version", runtime.Version()) 
   fmt.Println("Number of Goroutines:", runtime.NumGoroutine())
} 

只要知道要从运行时包调用什么,就可以获得所需的信息。这里最后一个fmt.Println()命令显示了goroutines的信息:您将在第 9 章、**goroutines-基本特性中了解更多关于 goroutines 的信息。

在 macOS 机器上执行runTime.go会生成以下输出:

$ go run runTime.go
You are using gc on a amd64 machine
with Go version go1.8
Number of Goroutines: 1  

在使用较旧 Go 版本的 Linux 机器上执行runTime.go会产生以下结果:

$ go run runTime.go
You are using gc on a amd64 machine
with Go version go1.3.3
Number of Goroutines: 4

Go 经常更新!

当我写完这一章时,Go有了一些更新。因此,我决定在本书中包含这些信息,以便更好地了解 Go 的更新频率:

$ date
Sat Apr  8 09:16:46 EEST 2017
$ go version
go version go1.8.1 darwin/amd64

练习

  1. 访问运行时包的文档。
  2. 创建您自己的结构,制作一个切片,并使用sort.Slice()对您创建的切片的元素进行排序。
  3. 在 Go 中实现快速排序算法,并对随机生成的数字数据进行排序。
  4. 实现一个双链接列表。
  5. tree.go的实施还远远没有完成!尝试实现一个函数来检查是否可以在树中找到值,以及另一个函数来允许您删除树节点。
  6. 同样,linkedList.go文件的实现也是不完整的。尝试实现一个用于删除节点的函数和另一个用于在链接列表中插入节点的函数。
  7. 再次说明,hash.go的哈希表实现是不完整的,因为它允许重复条目。因此,实现一个函数,在插入密钥之前在哈希表中搜索密钥。

总结

在本章中,您学习了许多与算法和数据结构相关的知识。您还学习了如何使用现有的Go软件包以及如何开发自己的Go软件包。本章还讨论了 Go 中的垃圾收集以及如何查找有关环境的信息。

在下一章中,我们将开始讨论系统编程,并介绍更多的 Go 代码。更准确地说,第 5 章文件和目录将讨论如何使用 Go 中的文件和目录,如何轻松地遍历目录结构,以及如何使用flag包处理命令行参数。但更重要的是,我们将开始开发各种 Unix 命令行实用程序的 Go 版本。