├── .editorconfig ├── .github └── ISSUE_TEMPLATE │ └── ----.md ├── .gitignore ├── README.md ├── SUMMARY.md ├── cover.jpg ├── eBook ├── chapter1 │ ├── 01.0.md │ ├── 01.1.md │ ├── 01.10.md │ ├── 01.11.1.md │ ├── 01.11.2.md │ ├── 01.11.3.md │ ├── 01.11.md │ ├── 01.12.md │ ├── 01.13.1.md │ ├── 01.13.2.md │ ├── 01.13.3.md │ ├── 01.13.4.md │ ├── 01.13.5.md │ ├── 01.13.6.md │ ├── 01.13.7.md │ ├── 01.13.8.md │ ├── 01.13.md │ ├── 01.14.1.md │ ├── 01.14.2.md │ ├── 01.14.md │ ├── 01.15.md │ ├── 01.16.md │ ├── 01.17.md │ ├── 01.2.md │ ├── 01.3.1.md │ ├── 01.3.2.md │ ├── 01.3.3.md │ ├── 01.3.md │ ├── 01.4.md │ ├── 01.5.md │ ├── 01.6.1.md │ ├── 01.6.2.md │ ├── 01.6.md │ ├── 01.7.md │ ├── 01.8.md │ └── 01.9.md ├── chapter2 │ ├── 02.0.md │ ├── 02.1.md │ ├── 02.10.md │ ├── 02.11.md │ ├── 02.12.md │ ├── 02.13.1.md │ ├── 02.13.2.md │ ├── 02.13.3.md │ ├── 02.13.4.md │ ├── 02.13.5.md │ ├── 02.13.md │ ├── 02.14.md │ ├── 02.15.md │ ├── 02.16.md │ ├── 02.2.1.md │ ├── 02.2.2.md │ ├── 02.2.3.md │ ├── 02.2.4.md │ ├── 02.2.5.md │ ├── 02.2.6.md │ ├── 02.2.md │ ├── 02.3.1.md │ ├── 02.3.2.md │ ├── 02.3.md │ ├── 02.4.1.md │ ├── 02.4.2.md │ ├── 02.4.md │ ├── 02.5.1.md │ ├── 02.5.md │ ├── 02.6.1.md │ ├── 02.6.md │ ├── 02.7.1.md │ ├── 02.7.2.md │ ├── 02.7.md │ ├── 02.8.md │ └── 02.9.md ├── chapter3 │ ├── 03.0.md │ ├── 03.1.1.md │ ├── 03.1.2.md │ ├── 03.1.3.md │ ├── 03.1.4.md │ ├── 03.1.md │ ├── 03.10.md │ ├── 03.2.1.md │ ├── 03.2.2.md │ ├── 03.2.3.md │ ├── 03.2.4.md │ ├── 03.2.md │ ├── 03.3.1.md │ ├── 03.3.2.md │ ├── 03.3.md │ ├── 03.4.1.md │ ├── 03.4.2.md │ ├── 03.4.3.md │ ├── 03.4.4.md │ ├── 03.4.5.md │ ├── 03.4.6.md │ ├── 03.4.7.md │ ├── 03.4.8.md │ ├── 03.4.md │ ├── 03.5.1.md │ ├── 03.5.2.md │ ├── 03.5.md │ ├── 03.6.1.md │ ├── 03.6.md │ ├── 03.7.1.md │ ├── 03.7.md │ ├── 03.8.1.md │ ├── 03.8.2.md │ ├── 03.8.3.md │ ├── 03.8.4.md │ ├── 03.8.5.md │ ├── 03.8.6.md │ ├── 03.8.7.md │ ├── 03.8.md │ └── 03.9.md ├── chapter4 │ ├── 04.0.md │ ├── 04.1.md │ ├── 04.2.1.md │ ├── 04.2.2.md │ ├── 04.2.md │ ├── 04.3.md │ ├── 04.4.1.md │ ├── 04.4.2.md │ ├── 04.4.3.md │ ├── 04.4.4.md │ ├── 04.4.md │ ├── 04.5.1.md │ ├── 04.5.2.md │ ├── 04.5.3.md │ ├── 04.5.md │ ├── 04.6.md │ ├── 04.7.md │ ├── 04.8.md │ ├── 04.9.1.md │ ├── 04.9.2.md │ ├── 04.9.3.md │ ├── 04.9.md │ ├── 4.10.1.md │ ├── 4.10.md │ ├── 4.11.md │ ├── 4.12.md │ ├── 4.13.md │ └── 4.14.md ├── chapter5 │ ├── 05.0.md │ ├── 05.01.md │ ├── 05.02.md │ ├── 05.03.01.md │ ├── 05.03.02.md │ ├── 05.03.md │ ├── 05.04.01.md │ ├── 05.04.02.md │ ├── 05.04.03.md │ ├── 05.04.md │ ├── 05.05.01.md │ ├── 05.05.02.md │ ├── 05.05.md │ ├── examples │ │ ├── binTree.go │ │ ├── hashTable.go │ │ ├── hashTableLookup.go │ │ └── linkedList.go │ └── images │ │ ├── image050301.png │ │ ├── image050401.png │ │ ├── image050501.png │ │ └── image050502.png ├── chapter6 │ ├── 06.0.md │ ├── 06.1.md │ ├── 06.10.md │ ├── 06.11.md │ ├── 06.2.1.md │ ├── 06.2.10.md │ ├── 06.2.3.md │ ├── 06.2.4.md │ ├── 06.2.5.md │ ├── 06.2.6.md │ ├── 06.2.7.md │ ├── 06.2.8.md │ ├── 06.2.9.md │ ├── 06.3.0.md │ ├── 06.3.1.md │ ├── 06.3.2.md │ ├── 06.3.3.md │ ├── 06.4.0.md │ ├── 06.4.1.0.md │ ├── 06.4.1.1.md │ ├── 06.4.1.2.md │ ├── 06.4.1.3.md │ ├── 06.4.1.4.md │ ├── 06.4.1.5.md │ ├── 06.4.1.6.md │ ├── 06.4.1.7.md │ ├── 06.4.1.8.md │ ├── 06.4.2.md │ ├── 06.4.3.md │ ├── 06.4.4.md │ ├── 06.5.md │ ├── 06.6.0.md │ ├── 06.6.1.md │ ├── 06.7.0.md │ ├── 06.7.1.md │ ├── 06.7.2.md │ ├── 06.7.3.md │ ├── 06.7.4.md │ ├── 06.7.5.md │ ├── 06.8.0.md │ ├── 06.8.1.md │ ├── 06.8.2.md │ └── 06.9.md ├── chapter8 │ ├── 08.0.md │ ├── 08.1.md │ ├── 08.2.md │ ├── 08.3.1.md │ ├── 08.3.2.md │ ├── 08.3.3.md │ ├── 08.3.4.md │ └── 08.3.md └── chapter9 │ ├── 09.0.md │ ├── 09.1.1.md │ ├── 09.1.2.md │ ├── 09.1.md │ ├── 09.10.md │ ├── 09.11.md │ ├── 09.2.1.md │ ├── 09.2.2.md │ ├── 09.2.md │ ├── 09.3.1.md │ ├── 09.3.md │ ├── 09.4.1.md │ ├── 09.4.2.md │ ├── 09.4.3.md │ ├── 09.4.4.md │ ├── 09.4.md │ ├── 09.5.md │ ├── 09.6.md │ ├── 09.7.md │ ├── 09.8.md │ └── 09.9.md └── test.go /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # all files 5 | [*] 6 | indent_style = tab 7 | indent_size = 4 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/----.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 章节领取 3 | about: '标明你要领取的翻译章节 example: 1.1-1.5' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### 领取的章节 11 | 12 | 13 | ### 翻译的大致的DDL 14 | 15 | ### 讨论的关键点 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS ### 2 | # General 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Icon must end with two \r 8 | Icon 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear in the root of a volume 14 | .DocumentRevisions-V100 15 | .fseventsd 16 | .Spotlight-V100 17 | .TemporaryItems 18 | .Trashes 19 | .VolumeIcon.icns 20 | .com.apple.timemachine.donotpresent 21 | 22 | # Directories potentially created on remote AFP share 23 | .AppleDB 24 | .AppleDesktop 25 | Network Trash Folder 26 | Temporary Items 27 | .apdisk 28 | 29 | ### VisualStudioCode ### 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | ### VisualStudioCode Patch ### 37 | # Ignore all local history of files 38 | .history 39 | 40 | ### Windows ### 41 | # Windows thumbnail cache files 42 | Thumbs.db 43 | Thumbs.db:encryptable 44 | ehthumbs.db 45 | ehthumbs_vista.db 46 | 47 | # Dump file 48 | *.stackdump 49 | 50 | # Folder config file 51 | [Dd]esktop.ini 52 | 53 | # Recycle Bin used on file shares 54 | $RECYCLE.BIN/ 55 | 56 | # Windows Installer files 57 | *.cab 58 | *.msi 59 | *.msix 60 | *.msm 61 | *.msp 62 | 63 | # Windows shortcuts 64 | *.lnk -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantmac/Mastering_Go_Second_Edition_Zh_CN/1338952db08c951b1de87a35045d377f84054cff/cover.jpg -------------------------------------------------------------------------------- /eBook/chapter1/01.0.md: -------------------------------------------------------------------------------- 1 | # Go和操作系统 2 | 3 | 本章介绍Go语言的各种主题,这些主题初学者会发现很有用。有经验的Go开发者也可以把这章当做基础知识的复习。因为这些都是实践性的主题,所以最好的理解方法就是去实验他们。在这里,实验就是自己写代码,自己发现错误,并且从错误中学习。不要被错误信息和bug吓倒。 4 | 5 | 本章中,你将学到以下这些: 6 | 7 | * Go语言的历史和未来 8 | * Go的优势 9 | * 编译Go代码 10 | * 执行Go代码 11 | * 下载和使用Go语言的外部包 12 | * UNIX的标准输入,输出和错误 13 | * 在屏幕上打印数据 14 | * 获取用户输入 15 | * 打印数据到标准错误 16 | * 使用日志文件 17 | * 使用Docker来编译和执行Go源码文件 18 | * Go语言的错误处理 -------------------------------------------------------------------------------- /eBook/chapter1/01.1.md: -------------------------------------------------------------------------------- 1 | ## Go的历史 2 | 3 | Go语言是一种现代的,多用途的开源编程语言,发布于2009年末。Go一开始的时候是Google的内部实验项目,受到很其他语言的启发,包括 C, Pascal, Alef和Oberon。Go语言的精神教父是专业的程序员*Robert Griesemer*, *Ken Thomson*, and *Rob Pike*. 4 | 5 | 他们为那些希望构建可靠,健壮,高效的软件系统的专业程序员设计了Go语言。除了语法和标准函数之外,Go还提供了相当丰富的标准库的实现。 6 | 7 | 编写此书的时候,Go的稳定版本是1.13。不过,即使你的Go版本比这个要高,本书的内容依然是不会过时的。 8 | 9 | 如果你是第一次安装Go语言,你可以从访问[https://golang.org/dl/](https://golang.org/dl/)开始。但是,有很大的可能你的unix系统已经提供了一个Go语言的安装包,所以可以用你喜欢包管理器来安装它。 10 | 11 | -------------------------------------------------------------------------------- /eBook/chapter1/01.10.md: -------------------------------------------------------------------------------- 1 | ## 使用标准输出 2 | 3 | 标准输出差不多就是在屏幕打印的意思。然而,使用标准输出需要使用一些`fmt`包之外的函数,这也是我们为什么单独一节来展示这些内容。 4 | 5 | 相关的技术细节会在`stdOUT.go`文件中展示,我们分三部分分析这个文件。第一部分是这样的: 6 | 7 | ```go 8 | package main 9 | import ( 10 | "io" 11 | "os" 12 | ) 13 | ``` 14 | 15 | 由这部分可以看出,我们将要是**io**包而不是**fmt**包。**os**用来读取命令行参数,访问标准输出`os.Stdout`。 16 | 17 | `stdOUT.go`文件的第二部分包含以下这些代码: 18 | 19 | ```go 20 | func main() { 21 | myString := "" 22 | arguments := os.Args 23 | if len(arguments) == 1 { 24 | myString = "Please give me one argument!" 25 | } else { 26 | myString = arguments[1] 27 | } 28 | ``` 29 | 30 | 变量`myString`包含将要被打印到屏幕的数据,或者是第一个命令行参数,或者当程序没有命令行参数的时候,是一段硬编码的消息。 31 | 32 | 程序的第三部分是这样的: 33 | 34 | ```go 35 | io.WriteString(os.Stdout, myString) 36 | io.WriteString(os.Stdout, "\n") 37 | } 38 | ``` 39 | 40 | 在这里,函数`io.WriteString()`函数和函数`fmt.Print()`的工作方式是一样的。区别是前者只接收两个参数,第一个参数是你要写入的文件,在这里,就是`os.Stdout`,第二个参数就是一个字符串变量。 41 | 42 | 执行`stdOUT.go`程序,会得到以下输出内容: 43 | 44 | ```shell 45 | $ go run stdOUT.go 46 | Please give me one argument! 47 | $ go run stdOUT.go 123 12 48 | 123 49 | ``` 50 | 51 | 上面的输出验证了函数`io.WriteString()`的作用就是当第一个参数是`os.Stdout`,发送第二个参数的内容到屏幕 -------------------------------------------------------------------------------- /eBook/chapter1/01.11.1.md: -------------------------------------------------------------------------------- 1 | ### 关于`:=`和`=` 2 | 3 | 在我们继续学习之前,来说明一下`:=`和`=`之间的差别是非常有意义的。`:=`的官方名字是短变量声明。短变量声明用来声明不明确的变量,明确的变量用var。 4 | 5 | `:=`操作符下下面这样工作: 6 | 7 | ```go 8 | m := 123 9 | ``` 10 | 11 | 上面语句的结果就是一个叫`m`新的整型变量,这个变量的值是123。 12 | 13 | 不过,如果你在一个已经声明的变量前使用`:=`,那么编译会失败,并给出以下的错误信息,这个错误信息给出明确的解析: 14 | 15 | ```shell 16 | $ go run test.go 17 | # command-line-arguments 18 | ./test.go:5:4: no new variables on left side of := 19 | ``` 20 | 21 | 现在你也许会问,当一个函数返回两个或者更多的值,并且你希望把其中一个赋值给一个已经存在变量,应该怎么做?用`=`还是`:=`?答案非常简单,应该使用`:=`,就像下面这样: 22 | 23 | ```go 24 | i, k := 3, 4 25 | j, k := 1, 2 26 | ``` 27 | 28 | 注意在第二行中,变量`j`是第一次使用,而k已经在第一行中被定义过了。 29 | 30 | 虽然讨论这样细小的知识点看起来很没有意思,但是从长远看,知道这些会帮助你从各种各样的错误中解脱出来。 31 | 32 | ### -------------------------------------------------------------------------------- /eBook/chapter1/01.11.2.md: -------------------------------------------------------------------------------- 1 | ### 从标准输入读取数据 2 | 3 | 我们会在`stdIN.go`程序中展示如何从标准输入读取数据,这个程序分为两个部分。第一部分如下所示: 4 | 5 | ```go 6 | package main 7 | import ( 8 | "bufio" 9 | "fmt" 10 | "os" 11 | ) 12 | ``` 13 | 14 | 从上面的代码中,你可以看到我们在这本书中第一次使用`buffio`这个包。 15 | 16 | 虽然大部分时间`bufio`用来处理文件的输入和输出,你还是会一直看到`os`包,因为`os`包中包含许多有用的函数。它最常用的功能就是提供了一种访问go程序的命令行参数的方法。 17 | 18 | `os`包的官方声明指出它提供了执行系统操作的函数。这些函数不仅包括创建,删除,重命名文件和目录,还包括读取文件和目录的权限和其他属性。`os`包的最大好处就是,它是系统独立的。简单说,这些函数在UNIX和Microsoft Windows机器上都可以工作。 19 | 20 | `stdIN.go`程序的第二部分包含以下内容: 21 | 22 | ```go 23 | func main() { 24 | var f *os.File 25 | f = os.Stdin 26 | defer f.Close() 27 | scanner := bufio.NewScanner(f) 28 | for scanner.Scan() { 29 | fmt.Println(">", scanner.Text()) 30 | } 31 | } 32 | ``` 33 | 34 | 首先,这里调用函数`bufio.NewScanner()`,并用标准输入作为参数。这个调用返回一个`bufio.Scanner`类型的变量,这变量用`Scan()`来从`os.Stdin`一行一行的读取数据。在获取新一行前,读取的每一行都被打印到屏幕。注意,由程序打印到屏幕的每一行都是`>`开始的。 35 | 36 | 执行`stdIN.go`会获得以下结果: 37 | 38 | ```shell 39 | $ go run stdIN.go 40 | This is number 21 41 | > This is number 21 42 | This is Mihalis 43 | > This is Mihalis 44 | Hello Go! 45 | > Hello Go! 46 | Press Control + D on a new line to end this program! 47 | > Press Control + D on a new line to end this program! 48 | ``` 49 | 50 | 按照UNIX的工作方法,你可以通过在命令行输入*Ctrl* + *D*来停止读取数据。 51 | 52 | ## -------------------------------------------------------------------------------- /eBook/chapter1/01.11.3.md: -------------------------------------------------------------------------------- 1 | ## 操作命令行参数 2 | 3 | 这一节的技术会在`cla.go`程序中看到,我们分三个部分来展示。这个程序会找到最大和最小的命令行参数。 4 | 5 | 第一部分是这样的: 6 | 7 | ```go 8 | package main 9 | import ( 10 | "fmt" 11 | "os" 12 | "strconv" 13 | ) 14 | ``` 15 | 16 | 这里的一个要点是我们要认识到获取命令行参数需要用到`os`包。另外还有需要一个叫`strcov`的包,用来转换命令行参数,命令行参数都是string类型的,转换成数值类型的。 17 | 18 | 第二部分像这样: 19 | 20 | ```go 21 | func main() { 22 | if len(os.Args) == 1 { 23 | fmt.Println("Please give one or more floats.") 24 | os.Exit(1) 25 | } 26 | arguments := os.Args 27 | min, _ := strconv.ParseFloat(arguments[1], 64) 28 | max, _ := strconv.ParseFloat(arguments[1], 64) 29 | ``` 30 | 31 | 这里,程序通过检查`os.Args`的长度来确定是否有命令行参数。这是因为程序需要至少一个参数来操作。请注意,`os.Args`是一个string类型的slice。slice中的第一个元素是正在执行的程序的名字。因此,为了初始化变量`min`和`max`,你需要使用`os.Args`第二个参数,而第二个参数的索引值是1。 32 | 33 | 这里有一个很重要的点就是,你期望获得一个或者多个浮点型的数,但是用户不一定会给你有效的值,可能是有意的或者无意的。但是,因为目前我们还没有讨论go语言中的错误处理,我们就假设所有的命令行参数都是格式正确,可以接受的。所以程序就忽略了函数`strconv.ParseFloat()`的错误值,就像下面这样: 34 | 35 | ```go 36 | n, _ := strconv.ParseFloat(arguments[i], 64) 37 | ``` 38 | 39 | 上面的语句告诉go语言,你只想获得函数`strconv.ParseFloat()`返回的第一个值,对第二个错误值不感兴趣。那个下划线,官方名称叫空描述符,是go语言忽略值得一种方式。如果一个go程序返回多个值,空描述符可以使用多次。 40 | 41 | 代码的第三部分是这样的: 42 | 43 | ```go 44 | for i := 2; i < len(arguments); i++ { 45 | n, _ := strconv.ParseFloat(arguments[i], 64) 46 | if n < min { 47 | min = n 48 | } 49 | if n > max { 50 | max = n 51 | } 52 | } 53 | fmt.Println("Min:", min) 54 | fmt.Println("Max:", max) 55 | } 56 | ``` 57 | 58 | 这里,你可以使用一个for循环语句来访问`os.Args`这个slice中的所有元素,这个slice已经赋值给`arguments`这个变量。 59 | 60 | 执行`cla.go`程序会得到以下输出: 61 | 62 | ```shell 63 | $ go run cla.go -10 0 1 64 | Min: -10 65 | Max: 1 66 | $ go run cla.go -10 67 | Min: -10 68 | Max: -10 69 | ``` 70 | 71 | 或许你希望看到当程序没有收到正确的参数输入,程序会出现异常。最坏的事情就是,程序处理命令参数的时候不会产生任何警告来通知用户这里有一个或者多个错误。 72 | 73 | ```shell 74 | $ go run cla.go a b c 10 75 | Min: 0 76 | Max: 10 77 | ``` 78 | 79 | ### -------------------------------------------------------------------------------- /eBook/chapter1/01.11.md: -------------------------------------------------------------------------------- 1 | ## 获取用户输入 2 | 3 | 我们主要有三种方式来获取用户输入。第一,通过读取程序的命令行参数;第二种,让用户输入;第三,读取外部文件。这一节展示前两种。如果你想学习如何读取外部文件,你可以访问第八章《告诉UNIX系统要做啥》。 4 | -------------------------------------------------------------------------------- /eBook/chapter1/01.12.md: -------------------------------------------------------------------------------- 1 | ### 关于错误输出 2 | 3 | 这一节会展示向UNIX标准错误发送数据的技术,这是UNIX区分实际的数据和错误的方式。 4 | 5 | 程序`stdERR.go `会向我们展示标准错误的使用,我们分两部分来看。由于向标准错误写数据会用到标准错误相关的文件描述符,所以`stdERR.go `程序会基于`stdOUT.go`。 6 | 7 | 第一部分是这样的: 8 | 9 | ```go 10 | package main 11 | import ( 12 | "io" 13 | "os" 14 | ) 15 | func main() { 16 | myString := "" arguments := os.Args 17 | if len(arguments) == 1 { 18 | myString = "Please give me one argument!" 19 | } else { 20 | myString = arguments[1] 21 | } 22 | ``` 23 | 24 | 到目前为止,程序`stdERR.go`几乎和`stdOUT.go`是一样的。 25 | 26 | 然后我们看`stdERR.go`的第二部分: 27 | 28 | ```go 29 | io.WriteString(os.Stdout, "This is Standard output\n") 30 | io.WriteString(os.Stderr, myString) 31 | io.WriteString(os.Stderr, "\n") 32 | } 33 | ``` 34 | 35 | 这里你调用`io.WriteString()`两次向`os.Stderr`写入,调用一次向`os.Stdout`写数据。 36 | 37 | 执行`stdERR.go`会得到以下输出: 38 | 39 | ```shell 40 | $ go run stdERR.go 41 | This is Standard output 42 | Please give me one argument! 43 | ``` 44 | 45 | 上面的输出不会帮助你区分哪些数据是写到标准输出的,哪些数据写到标准错误的,而区分这些有时候是非常有用的。当然,如果你使用的是`bash`shell,这里有一个技巧可以帮助你区分这些数据是写到哪里的。几乎所有的shell用他们自己的方式都提供了这项功能。 46 | 47 | 当你使用`bash`的时候,你可以像下面这样把标准错误重定向到一个文件: 48 | 49 | ```shell 50 | $ go run stdERR.go 2>/tmp/stdError 51 | This is Standard output 52 | $ cat /tmp/stdError 53 | Please give me one argument! 54 | ``` 55 | 56 | 相似的,你也可以把错误输入重定向到`/dev/null`这个设备来忽略他们,这样就告诉系统完全无视这些错误: 57 | 58 | ```shell 59 | $ go run stdERR.go 2>/dev/null 60 | This is Standard output 61 | ``` 62 | 63 | 在前面的两个例子中,我们把标准错误的文件描述符分别重定向到一个文件和`/dev/null`。如果你希望同时保存标准错误和标准输出到同一个文件,你可以把标准错误的文件描述符(2)重定向到标准输出的文件描述符(1)。下面的命令展示了这项技术,这在unix系统中是非常常见的。 64 | 65 | ```shell 66 | $ go run stdERR.go >/tmp/output 2>&1 67 | $ cat /tmp/output 68 | This is Standard output 69 | Please give me one argument! 70 | ``` 71 | 72 | 最后,你也可以把标准错误和标准输出到发送到`/dev/null`: 73 | 74 | ```shell 75 | $ go run stdERR.go >/dev/null 2>&1 76 | ``` 77 | 78 | -------------------------------------------------------------------------------- /eBook/chapter1/01.13.1.md: -------------------------------------------------------------------------------- 1 | ### 日志级别 2 | 3 | 日志级别是一个值,用来指定某条日志的严重程度。这里有各种各样的日志级别,包括`debug`,`info`,`notice`,`warning`,`err`,`crit`,`alert`和`emerg`,越后面的级别,严重程度就越高。 4 | 5 | ### -------------------------------------------------------------------------------- /eBook/chapter1/01.13.2.md: -------------------------------------------------------------------------------- 1 | ### 日志工具 2 | 3 | 日志工具就是用来记录信息的。日志工具的值可以是*auth*, *authpriv*, *cron*, *daemon*, *kern*, *lpr*, *mail*, *mark*, *news*, *syslog*, *user*, *UUCP*, *local0*, *local1*, *local2*, *local3*, *local4*, *local5*, *local6*, or *local7*中的一个,在`/etc/syslog.conf`,`/etc/rsyslog.conf`或者其他合适文件中被定义,这取决于用来处理系统日志的进程。 4 | 5 | 这就意味着,如果一个日志工具没有被定义或者处理,那么你发送的日志消息可能会被忽略,然后丢失。 -------------------------------------------------------------------------------- /eBook/chapter1/01.13.3.md: -------------------------------------------------------------------------------- 1 | ### 日志服务器 2 | 3 | 所有的UNIX机器都有一个单独的服务器进程来处理接受日志输入、写入文件这写事情。UNIX机器上有各种各样的日志服务器。但是,只有其中两个**syslogd(8)**和**rsyslogd(8)**被使用在大多数UNIX变种上。 4 | 5 | 在MacOS机器上,处理日志的进场叫**syslogd(8)**。而在大部分Linux机器上,用的是**rsyslogd(8)**,这个是**syslogd(8)**的一个改进版本,更值得信赖。**syslogd(8)**是原先UNIX机器上的消息日志工具。 6 | 7 | **rsyslogd(8)**的配置文件一般叫`rsyslog.conf`,被放在`/etc`目录下。这个文件里的内容,去除注释和以`$`开头的行,看起来是这样的: 8 | 9 | ```shell 10 | $ grep -v '^#' /etc/rsyslog.conf | grep -v '^$' | grep -v '^\$' 11 | auth,authpriv.* /var/log/auth.log 12 | *.*;auth,authpriv.none -/var/log/syslog 13 | daemon.* -/var/log/daemon.log 14 | kern.* -/var/log/kern.log 15 | lpr.* -/var/log/lpr.log 16 | mail.* -/var/log/mail.log 17 | user.* -/var/log/user.log 18 | mail.info -/var/log/mail.info 19 | mail.warn -/var/log/mail.warn 20 | mail.err /var/log/mail.err 21 | news.crit /var/log/news/news.crit 22 | news.err n /var/log/news/news.err 23 | ews.notice -/var/log/news/news.notice 24 | *.=debug;\ 25 | auth,authpriv.none;\ 26 | news.none;mail.none -/var/log/debug 27 | *.=info;*.=notice;*.=warn;\ 28 | auth,authpriv.none;\ 29 | cron,daemon.none;\ 30 | mail,news.none -/var/log/messages 31 | *.emerg :omusrmsg:* 32 | daemon.*;mail.*;\ 33 | news.err;\ 34 | *.=debug;*.=info;\ 35 | *.=notice;*.=warn |/dev/xconsole 36 | local7.* /var/log/cisco.log 37 | ``` 38 | 39 | 所以,为了发送日志信息到`/var/log/cisco.log`这个文件,你需要用**local7**这个日志工具。日志工具后面的星号告诉日志服务器要捕获所有级别的日志,把这些日志写到`/var/log/cisco.log`文件中。 40 | 41 | **syslogd(8)**服务器有一个十分相似的配置文件,一般是是`/etc/syslog.conf`。在macOS High Sierras上,这个几乎是空的,它被`/etc/asl.conf`替代了。尽管如此,这些配置文件背后的逻辑都是一样的。 -------------------------------------------------------------------------------- /eBook/chapter1/01.13.5.md: -------------------------------------------------------------------------------- 1 | ### 关于log.Fatal() 2 | 3 | 本节中,你会看到函数`log.Fatal()`的用法。这个函数用在当你的程序发生很严重的错误,并且你只想在报告错误后尽快退出程序。 4 | 5 | 程序`logFatal.go`会展示`log.Fatal()`的用法,包含以下代码: 6 | 7 | ```go 8 | package main 9 | import ( 10 | "fmt" 11 | "log" 12 | "log/syslog" 13 | ) 14 | func main() { 15 | sysLog, err := syslog.New(syslog.LOG_ALERT|syslog.LOG_MAIL, "Some program!") 16 | if err != nil { 17 | log.Fatal(err) } else { 18 | log.SetOutput(sysLog) 19 | } 20 | log.Fatal(sysLog) 21 | fmt.Println("Will you see this?") 22 | } 23 | ``` 24 | 25 | 执行`logFatal.go`这个程序会生成以下输出: 26 | 27 | ```shell 28 | $ go run logFatal.go 29 | exit status 1 30 | ``` 31 | 32 | 你可以看到,正因为`log.Fatal(sysLog)`语句的调用中断了程序的运行,你才看不到` fmt.Println("Will you see this?")`这条语句产生的输出。 33 | 34 | 当然,因为`syslog.New()`这句的执行,程序向日志文件`/var/log/mail.log`发送了一条日志。 35 | 36 | ```shell 37 | $ grep "Some program" /var/log/mail.log 38 | Jan 10 21:29:34 iMac Some program![7123]: 2019/01/10 21:29:34 &{17 Some program! iMac.local {0 0} 0xc00000c220} 39 | ``` 40 | 41 | ### -------------------------------------------------------------------------------- /eBook/chapter1/01.13.6.md: -------------------------------------------------------------------------------- 1 | ### 关于log.panic() 2 | 3 | 有的时候程序一直崩溃,你想获得尽可能多的错误信息。 4 | 5 | 在这种困难的情况下,你可以考虑使用`log.Panic()`,这就是本节中的程序`logPanic.go`要展示的。 6 | 7 | 这个程序是这样的: 8 | 9 | ```go 10 | package main 11 | import ( 12 | "fmt" 13 | "log" 14 | "log/syslog" 15 | ) 16 | func main() { 17 | sysLog, err := syslog.New(syslog.LOG_ALERT|syslog.LOG_MAIL, "Some program!") 18 | if err != nil { 19 | log.Fatal(err) 20 | } else { 21 | log.SetOutput(sysLog) 22 | } 23 | log.Panic(sysLog) 24 | fmt.Println("Will you see this?") 25 | } 26 | ``` 27 | 28 | 在macOS Mojave上执行这个程序,会生成以下的输出: 29 | 30 | ```shell 31 | $ go run logPanic.go 32 | panic: &{17 Some program! iMac.local {0 0} 0xc0000b21e0} 33 | goroutine 1 [running]: 34 | log.Panic(0xc00004ef68, 0x1, 0x1) 35 | /usr/local/Cellar/go/1.11.4/libexec/src/log/log.go:326 +0xc0 36 | main.main() 37 | /Users/mtsouk/Desktop/mGo2nd/Mastering-Go-Second- Edition/ch01/logPanic.go:17 +0xd6 38 | exit status 2 39 | ``` 40 | 41 | 在Debian系统下,Go版本1.3.3,执行同样的代码会生成以下输出: 42 | 43 | ```shell 44 | $ go run logPanic.go 45 | panic: &{17 Some program! mail {0 0} 0xc2080400e0} 46 | goroutine 16 [running]: 47 | runtime.panic(0x4ec360, 0xc208000320) 48 | /usr/lib/go/src/pkg/runtime/panic.c:279 +0xf5 49 | log.Panic(0xc208055f20, 0x1, 0x1) 50 | /usr/lib/go/src/pkg/log/log.go:307 +0xb6 51 | main.main() 52 | /home/mtsouk/Desktop/masterGo/ch/ch1/code/logPanic.go:17 +0x169 53 | goroutine 17 [runnable]: 54 | runtime.MHeap_Scavenger() 55 | /usr/lib/go/src/pkg/runtime/mheap.c:507 56 | runtime.goexit() 57 | /usr/lib/go/src/pkg/runtime/proc.c:1445 58 | goroutine 18 [runnable]: 59 | bgsweep() 60 | /usr/lib/go/src/pkg/runtime/mgc0.c:1976 61 | runtime.goexit() 62 | /usr/lib/go/src/pkg/runtime/proc.c:1445 63 | goroutine 19 [runnable]: 64 | runfinq() 65 | /usr/lib/go/src/pkg/runtime/mgc0.c:2606 66 | runtime.goexit() 67 | /usr/lib/go/src/pkg/runtime/proc.c:1445 68 | exit status 2 69 | ``` 70 | 71 | 因此,函数`log.Panic()`会生成底层的错误信息,这些信息会帮助你解决程序中的难题。 72 | 73 | 和函数`log.Fatal()`一样,`log.Panic()`也会发送一条日志到对应的日志文件,并且终止程序的执行。 74 | 75 | ### -------------------------------------------------------------------------------- /eBook/chapter1/01.13.7.md: -------------------------------------------------------------------------------- 1 | ### 写入指定的日志文件 2 | 3 | 有时候,你希望把日志写入指定的文件。这样做可能有很多原因,可能是因为调试信息太多,不想污染系统的日志文件,或者希望把程序产生的日志和系统分割开来存储到数据库,或者向以不同的格式来存储日志。这一小节将教会你如何把日志写入到指定的文件。 4 | 5 | 我们把这个程序命名为`customLog.go`,日志文件设置为`/tmp/mGo.log`。 6 | 7 | 我们分三个部分来展示这个文件,第一部分是这样的: 8 | 9 | ```go 10 | package main 11 | import ( 12 | "fmt" 13 | "log" 14 | "os" 15 | ) 16 | var LOGFILE = "/tmp/mGo.log" 17 | ``` 18 | 19 | 我们用一个叫`LOGFILE`的全局变量以硬编码的方式指定日志文件的路径。出于演示的目的,我们把日志文件放在`/tmp`目录下,一般我们不会在这个地方存储数据,因为每次系统重启后,这个目录都会被清空。当然,在这里,可以使我们在没有**root**的权限的情况下执行`customLog.go`程序,避免给系统增加无用的文件。如果你打算在实际的应用中使用这段程序,你需要把这个目录改到一个更加合适的位置。 20 | 21 | 程序的第二部分是这样的: 22 | 23 | ```go 24 | func main() { 25 | f, err := os.OpenFile(LOGFILE, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 26 | if err != nil { 27 | fmt.Println(err) 28 | return 29 | } 30 | defer f.Close() 31 | ``` 32 | 33 | 在这里,你用函数`os.OpenFile()`创建了一个新的日志文件,并给这个文件(0644)的权限。 34 | 35 | 程序的最后一部分: 36 | 37 | ```go 38 | iLog := log.New(f, "customLogLineNumber ", log.LstdFlags) 39 | iLog.SetFlags(log.LstdFlags) 40 | iLog.Println("Hello there!") 41 | iLog.Println("Another log entry!") 42 | } 43 | ``` 44 | 45 | 如果你查阅`log`包的文档,文档可以在`https://golang.org/pkg/log/`或者别的地方找到,你会发现函数`SetFlags`允许你设置当前日志的选项。选项`LstFlags`的默认值是`Ldate`和`Ltime`,这就是意味着你每条写入文件的日志都会包含当前的日期和时间。 46 | 47 | 执行`customLog.go`并不会有什么可见的输出。当然,执行这个程序两次,文件`/tmp/mGo.log`的内容会是这样的: 48 | 49 | ```shell 50 | $ go run customLog.go 51 | $ cat /tmp/mGo.log 52 | customLog 2019/01/10 18:16:09 Hello there! 53 | customLog 2019/01/10 18:16:09 Another log entry! 54 | $ go run customLog.go 55 | $ cat /tmp/mGo.log 56 | customLog 2019/01/10 18:16:09 Hello there! 57 | customLog 2019/01/10 18:16:09 Another log entry! 58 | customLog 2019/01/10 18:16:17 Hello there! 59 | customLog 2019/01/10 18:16:17 Another log entry! 60 | ``` 61 | 62 | ### -------------------------------------------------------------------------------- /eBook/chapter1/01.13.8.md: -------------------------------------------------------------------------------- 1 | ### 在日志中打印行号 2 | 3 | 在这一节中,你将通过程序`customLogLineNumber.go`来学习如何在日志文件中打印输出日志的代码的行号。这个程序会分两部分展示。第一部分是这样的: 4 | 5 | ```go 6 | package main 7 | import ( 8 | "fmt" 9 | "log" 10 | "os" 11 | ) 12 | var LOGFILE = "/tmp/mGo.log" 13 | func main() { 14 | f, err := os.OpenFile(LOGFILE, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 15 | if err != nil { 16 | fmt.Println(err) 17 | return 18 | } 19 | defer f.Close() 20 | ``` 21 | 22 | 到目前为止,这个程序和前面学习的`customLog.go`没有什么差别。 23 | 24 | 剩下的代码如下所示: 25 | 26 | ```go 27 | iLog := log.New(f, "customLogLineNumber ", log.LstdFlags) 28 | iLog.SetFlags(log.LstdFlags | log.Lshortfile) 29 | iLog.Println("Hello there!") 30 | iLog.Println("Another log entry!") 31 | } 32 | ``` 33 | 34 | 所有的神奇之处都在`iLog.SetFlags(log.LstdFlags | log.Lshortfile)`这一行。在这行中,除了`log.LstdFlags`这个选项,还多列一个选项`log.Lshortfile`。后一个选项在日志中增加了全部的文件名和打印日志的语句的行号。 35 | 36 | 执行` customLogLineNumber.go`不会生成什么可见的输出。当然,执行两次这个程序后,日志文件`/tmp/mGo.log的`内容就变得像下面这样: 37 | 38 | ```shell 39 | $ go run customLogLineNumber.go 40 | $ cat /tmp/mGo.log 41 | customLogLineNumber 2019/01/10 18:25:14 customLogLineNumber.go:26: Hello there! 42 | customLogLineNumber 2019/01/10 18:25:14 customLogLineNumber.go:27: Another log entry! 43 | $ go run customLogLineNumber.go 44 | $ cat /tmp/mGo.log 45 | customLogLineNumber 2019/01/10 18:25:14 customLogLineNumber.go:26: Hello there! 46 | customLogLineNumber 2019/01/10 18:25:14 customLogLineNumber.go:27: Another log entry! 47 | customLogLineNumber 2019/01/10 18:25:23 customLogLineNumber.go:26: Hello there! 48 | customLogLineNumber 2019/01/10 18:25:23 customLogLineNumber.go:27: Another log entry! 49 | ``` 50 | 51 | 你可以看到,在命令行使用长名字会是日志不易阅读。 -------------------------------------------------------------------------------- /eBook/chapter1/01.13.md: -------------------------------------------------------------------------------- 1 | ## 写入日志文件 2 | 3 | 标准库包`log`允许你发送日志信息到UNIX系统日志服务,在`syslog`包中,它是`log`包的一部分,允许你定义要日志级别和要使用的日志工具。 4 | 5 | 通常情况下,UNIX系统的大部分日志都可以在`/var/log`目录下找到。当然,许多流行的服务软件,比如Apache和Nginx,他们的日志文件可能在其他别的地方,取决于这些软件的配置文件。 6 | 7 | 一般而言,和直接输出日志在屏幕相比,把日志信息写到文件被认为是一种比较好的实践,有以下两个原因:一来,日志被保存到一个文件,没有丢失,二来,这些日志文件可以被诸如`grep`、`awk`、`sed`这些工具来搜索和处理。而单纯输出到屏幕就做不到这样。 8 | 9 | `log`包提供了很多函数来写入到UNIX的系统日志服务,这些函数包括:`log.Printf()`,` log.Print()`, `log.Println()`, `log.Fatalf()`, `log.Fatalln()`, `log.Panic()`, `log.Panicln()`, and `log.Panicf()`。 -------------------------------------------------------------------------------- /eBook/chapter1/01.14.1.md: -------------------------------------------------------------------------------- 1 | ### 错误类型 2 | 3 | 当你开发自己的Go应用的时候,有很多场景都会使你最终走向创建自己的错误。**error**数据类型就是帮助你来创建自己的错误的。 4 | 5 | 这一节会教你如何创建自己的错误变量。你会看到,为了创建一些新的错误变量,你需要使用Go标准库中`errors`包的`New()`函数。 6 | 7 | 展示这个过程的实例代码在`newError.go`文件中,会分两部分来看。第一部分是这样的: 8 | 9 | ```go 10 | package main 11 | import ( 12 | "errors" 13 | "fmt" 14 | ) 15 | func returnError(a, b int) error { 16 | if a == b { 17 | err := errors.New("Error in returnError() function!") 18 | return err 19 | } else { 20 | return nil 21 | } 22 | } 23 | ``` 24 | 25 | 这里有很多有趣的事情。首先,你可以看到本书中第一次定义一个不是**main**函数。这个新的函数叫`returnError()`。其次,你可以看到`errrs.New()`函数的使用了,这个函数接受一个`string`值作为参数。最后,如果一个函数应该返回一个`error`变量但是没有返回,它就返回一个`nil`。 26 | 27 | 程序`newError.go`的第二部分是这样的: 28 | 29 | ```go 30 | func main() { 31 | err := returnError(1, 2) if err == nil { 32 | fmt.Println("returnError() ended normally!") 33 | } else { 34 | fmt.Println(err) 35 | } 36 | err = returnError(10, 10) 37 | if err == nil { 38 | fmt.Println("returnError() ended normally!") 39 | } else { 40 | fmt.Println(err) 41 | } 42 | if err.Error() == "Error in returnError() function!" { 43 | fmt.Println("!!") 44 | } 45 | } 46 | ``` 47 | 48 | 从上面的代码看出,大部分时候,你需要检查`error`变量是否等于`nil`,然后采取相应的处理。这里还展示了`errors.Error()`函数的使用,这个函数允许你把一个`error`变量转化成字符串变量。这个函数可以使一个`error`变量和`string`变量做比较。 49 | 50 | 执行` newError.go`会生成以下输出: 51 | 52 | ```shell 53 | $ go run newError.go 54 | returnError() ended normally! Error in returnError() function! 55 | !! 56 | ``` 57 | 58 | 如果你试图比较一个`string`变量和一个`error`变量,却没有先转换`error`变量为`string`变量,那么Go编译器会发出下面的错误信息: 59 | 60 | ```shell 61 | # command-line-arguments 62 | ./newError.go:33:9: invalid operation: err == "Error in returnError() function!" (mismatched types error and string) 63 | ``` 64 | 65 | ### -------------------------------------------------------------------------------- /eBook/chapter1/01.14.md: -------------------------------------------------------------------------------- 1 | ## Go语言的错误处理 2 | 3 | 错误类型和错误处理是Go语言中非常重要的主题。Go语言喜欢错误信息以致于为错误提供了一个单独的数据类型,就叫error。这也意味着如果你认为Go语言的提供的错误信息不够用的话,你也可以方便创建自己的错误信息。 4 | 5 | 当你开发自己的Go包的时候,你可能会非常喜欢创建和处理自己的错误类型。 6 | 7 | 请注意,程序中遇到错误是一件事情,而如何处理错误则完全是另一件事情。简单说,并不是所有的错误处理都是样的,也就是,一些错误情况需要立即终止程序的执行,而另外一些则只需要打印一个警告信息,不需要终止程序。这取决于开发者来如何处理可能遇到的错误。 -------------------------------------------------------------------------------- /eBook/chapter1/01.16.md: -------------------------------------------------------------------------------- 1 | ## 练习和链接 2 | 3 | * 访问Go语言官网: https://golang.org/. 4 | 5 | * 访问Docker官网: https://www.docker.com/. 6 | 7 | * 访问Docker Hub: https://hub.docker.com/. 8 | 9 | * Go 2 草案: https://blog.golang.org/go2draft. 10 | 11 | * 浏览Go的官方文档: https://golang.org/doc/. 12 | 13 | * 访问标准库包log的文档: https://golang.org/pkg/log/. 14 | 15 | * 访问标准库包log/syslog的文档 : https://golang.org/pkg/log/syslog/. 16 | 17 | * 访问标准库包os的文档 https://golang.org/pkg/os/. 18 | 19 | * Have a look at https://golang.org/cmd/gofmt/, which is the documentation page of the gofmt tool that is used for formatting Go code. 20 | 21 | * 写一个Go程序计算所有命令行参数中数字的和. 22 | 23 | * 写一个Go程序计算命令行参数中浮点数的平均值. 24 | 25 | * Write a Go program that keeps reading integers until it gets the word END as input. 26 | 27 | * Can you modify customLog.go in order to write its log data into two log files at the same time? You might need to read Chapter 8, *Telling a UNIX System What to Do,* for help. 28 | 29 | * If you are working on a Mac machine, check the **TextMate** editor at http://macromates.com/, as well as **BBEdit** at https://www.barebones.com/products/bbedit/. 30 | 31 | * Visit the documentation page of the fmt package at https://golang.org/pkg/fmt/ to learn more about verbs and the available functions. 32 | 33 | * Please visit https:/​/​blog.​golang.​org/​why-​generics to learn more about Go and Generics. 34 | 35 | ## -------------------------------------------------------------------------------- /eBook/chapter1/01.17.md: -------------------------------------------------------------------------------- 1 | ## 总结 2 | 3 | 本章中讨论了Go语言中很多有趣的主题,包括编译Go代码,使用标准输入,输出,错误,处理命令行参数,在屏幕上打印,使用UNIX系统的日志服务,错误处理等等。你应该认为这些是Go语言的基础。 4 | 5 | 下一章是关于Go的内部实现,包括垃圾回收,Go编译器,在Go中调用C代码,defer关键字,Go汇编,和WebAssembly,panic和recover。 -------------------------------------------------------------------------------- /eBook/chapter1/01.2.md: -------------------------------------------------------------------------------- 1 | ## Go的未来 2 | 3 | Go社区已经在讨论Go语言的下一个主版本,这个主版本将被称为**Go 2** ,但是目前还没有什么定论。 4 | 5 | **Go 1**团队希望**Go 2**更加的社区化。一般意义上来说,这是一个好主意,但是这也是很危险的,因为很多人试图在Go语言重大变化上发表意见,而这个项目一开始是由几个伟大的程序员设计和开发的内部项目。 6 | -------------------------------------------------------------------------------- /eBook/chapter1/01.3.1.md: -------------------------------------------------------------------------------- 1 | ## Go是完美的么? 2 | 3 | 没有完美的编程语言,Go也不例外。然而,某些编程语言在某些编程领域比其他要好,或者我们更喜欢他们。就我个人而言,我不喜欢Java(me too),而且我以前喜欢C++,现在不喜欢了。作为一个编程语言,C++变得太复杂了,而Java的代码太难看了。 4 | 5 | Go的一些不足之处: 6 | 7 | * Go没有直接支持面向对象编程,这对那些以前以面向对象的方式来写代码的程序员可能是个问题。当然,你可以用组合来模拟继承。 8 | * 对于一些人而言,Go永远不能取代C 9 | * 对系统编程而言,C依然比其他语言都快,主要是因为unix是C写的 10 | 11 | 尽管如此,Go依然是一个十分优雅的编程语言,如果你花时间学习和使用它,它不会让你失望的。 -------------------------------------------------------------------------------- /eBook/chapter1/01.3.2.md: -------------------------------------------------------------------------------- 1 | ## 什么是预处理器 2 | 3 | 我前面说过Go语言没有预处理器,而且这样是一件好事。预处理器是一个程序,它处理你输入的数据,生成输出给另一个程序用。这编程语言这个层面,预处理器的输入就是我们写的源码,这些源码在被编译器编译之前被预处理器处理。 4 | 5 | 预处理器最大的问题在于,它对语言,或者语法一无所知。这就意味着当使用了预处理器,你就不能和确定,你代码的最后版本就是你想要的, 因为预处理器可能会改变你原始代码的逻辑和语义。 6 | 7 | 包含预处理器的语言有 C, C++, Ada, and PL/SQL.臭名昭著的C处理器处理以**#**开头的行,称为**directives**或者**pragmas**。就像前面提到的, directives和pragmas 并不是C语言的一部分。 8 | -------------------------------------------------------------------------------- /eBook/chapter1/01.3.3.md: -------------------------------------------------------------------------------- 1 | ## godoc工具 2 | 3 | Go语言自带很多可以使你的程序员生活更加容易的工具。其中一个就是**godoc**,它可以帮助你在没有网络连接的情况下查看现存包和函数的文档。 4 | 5 | **godoc**工具即可以像一般命令行命令那样直接在终端显示结果,也可以在命令行下启动一个web服务器。启动web服务器的,你需要一个浏览器来查看Go的文档。 6 | 7 | 第一种方式就像用man命令一样,不过我们查询的是Go的函数和包。因此,为了获取 fmt包中 Printf() 函数的信息,你可以执行下面的命令: 8 | 9 |  10 | 11 | ```shell 12 | $ go doc fmt.Printf 13 | ``` 14 | 15 | 同样的,你用下面这个命令可以找到**fmt**包的信息: 16 | 17 | ```shell 18 | $ go doc fmt 19 | ```` 20 | 21 | 第二种需要执行**godoc**命令,并且带有**-http**参数: 22 | 23 | ```shell 24 | $ godoc -http=:8001 25 | ``` 26 | 27 | 上面命令中的数值,就是那个**8001**,是HTTP服务器监听的端口号。你可以使用任何你有权限,并且可用的值。然而,注意0到1023是被限制的,只有root用户可用,所以最好是避免使用这之间的一个,选个别的,并且这个端口没有被其他进程占用。 28 | 29 | 你也可以没有上面程序中那个**=**,用一个空格代替。下面这个命令和上面的命令是完全一样的: 30 | 31 | ```shell 32 | $ godoc -http :8001 33 | ``` 34 | 35 | 然后,你就可以打开浏览器,访问[http://localhost:8001/pkg/](http://localhost:8001/pkg/),就可以浏览Go的文档了。 -------------------------------------------------------------------------------- /eBook/chapter1/01.3.md: -------------------------------------------------------------------------------- 1 | ## Go的特性 2 | 3 | Go有很多特性,有一些是Go独有的,有一些其他语言也具备。 4 | 5 | Go语言最主要的特性有以下这些: 6 | 7 | * Go是一个现代编程语言,易读,易写,并且由有经验的程序员创建 8 | * Go希望使程序员快乐,因为快乐的程序员可以写出更好的代码。 9 | * Go编译器打印出有实际意义的警告和错误信息,这些可以帮助你解决实际的问题。简单说,Go编译器是为了帮助你,而不是打印出毫无意义的内容使你感受生活的艰难。 10 | * Go代码是可移植的,尤其是在unix机器之间。 11 | * Go已经支持过程式,并发和分布式编程。 12 | * Go支持垃圾回收,因此你不用操心内存分配和内存回收。 13 | * Go没有预处理器的同时还可以高速的编译。因此,Go也可以被用作脚本语言 14 | * Go可以构建web应用,并且提供了一个简单的web服务器来进行测试 15 | * Go标准库提供了很多包,这些包可以简化开发者的工作。另外,标准库中的函数已经进过Go语言开发者的测试并修改错误,也就是,大部分时候,这些函数都是没有bug的。 16 | * Go默认是用静态链接,即生成的二进制文件可以容易的在相同的os之间传输而不用担心库,依赖,和不同的库版本。 17 | * 你不在需要图形界面来开发、调试和测试Go应用,因为命令行就可以,我想这也是很unix用户更喜欢的 18 | * Go支持Unicode,即你不需要更多的代码就可以来打印多种文字。 19 | * Go保持概念的正交,因为几个正交的特色比重叠的工作的更好。 -------------------------------------------------------------------------------- /eBook/chapter1/01.4.md: -------------------------------------------------------------------------------- 1 | ## 编译Go代码 2 | 3 | 在这一节中你将会学习如何编译Go代码。好消息是你可以在命令行编译你的Go代码,不需要图形界面。更好的是,Go不关心代码文件的名字,只要你的包名是**main**,并且包中有且只有一个函数叫**main**。这是因为**main()**函数是程序执行开始的地方。因此,在一个项目中不能有多个函数的名字是**main()**。 4 | 5 | 我们首先编译一个叫**aSourceFile.go**的文件,这个文件中包含以下内容: 6 | 7 | ```go 8 | package main 9 | import ( 10 | "fmt" 11 | ) 12 | func main() { 13 | fmt.Println("This is a sample Go program!") 14 | } 15 | ``` 16 | 17 | 注意,Go社区倾向源码文件的名字是**source_file.go**,而不是**aSourceFile.go**。不管你使用哪种,请保持一致。 18 | 19 | 为了编译**aSourceFile.go**这个文件,并且生成一个静态链接的可执行文件,你需要执行下面的命令: 20 | 21 | ```shell 22 | $ go build aSourceFile.go 23 | ``` 24 | 25 | 然后,你就有了一个叫做**aSourceFile**的可执行文件: 26 | 27 | ```shell 28 | $ file aSourceFile 29 | aSourceFile: Mach-O 64-bit executable x86_64 30 | $ ls -l aSourceFile 31 | -rwxr-xr-x 1 mtsouk staff 2007576 Jan 10 21:10 aSourceFile $ ./aSourceFile 32 | This is a sample Go program! 33 | ``` 34 | 35 | 这个文件体积这么大的主要原因是它是静态链接的,运行的时候不需要外部库。 -------------------------------------------------------------------------------- /eBook/chapter1/01.5.md: -------------------------------------------------------------------------------- 1 | ## 执行Go代码 2 | 3 | 这里还有一种方法可以执行你的Go代码,同时不生成可执行文件,它只是生成一些中间文件,然后自动的删除这些中间文件。 4 | 5 | 因此,为了执行**aSourceFile.go**这个文件的同时不生成可执行文件,,你需要执行下面的命令: 6 | 7 | ```shell 8 | $ go run aSourceFile.go 9 | This is a sample Go program! 10 | ``` 11 | 12 | 你可以看到,输出的结果和上一个命令完全一样。 13 | 14 | 本书主要使用**go run**来执行示例代码,主要是因为比运行**go build**命令然后执行可执行文件要简单。另外,**go run**命令在代码完成了执行之后不会再磁盘上留下任何文件。 -------------------------------------------------------------------------------- /eBook/chapter1/01.6.1.md: -------------------------------------------------------------------------------- 1 | ### package 的导入规则 2 | 3 | 对于包的使用的,Go有着严格的规范,因此,你不能引入一个你认为将来会用的包,然后你没有使用它。 4 | 5 | 看下面这个幼稚的程序,它被保存为**packageNotUsed.go**: 6 | 7 | ```go 8 | package main 9 | import ( 10 | "fmt" 11 | "os" 12 | ) 13 | func main() { 14 | fmt.Println("Hello there!") 15 | } 16 | ``` 17 | 18 | 如果你执行这个文件,你会获得下面的错误信息,并且程序并不会被执行: 19 | 20 | ```shell 21 | $ go run packageNotUsed.go 22 | # command-line-arguments 23 | ./packageNotUsed.go:5:2: imported and not used: "os" 24 | ``` 25 | 26 | 如果你从**import**列表中删除了**os**包的话,这个程序会正确的执行。你可以自己试一下。 27 | 28 | 虽然现在并不是讨论如何打破Go语言的规范的最佳时间,但是确实可以绕开这个限制。下面这个示例中的代码保存在叫**packageNotUsedUnderscore.go**中: 29 | 30 | ```go 31 | package main 32 | import ( 33 | "fmt" 34 | _ "os" 35 | ) 36 | func main() { 37 | fmt.Println("Hello there!") 38 | } 39 | ``` 40 | 41 | 就这样,在一个包名字前面加一个下划线,在编译过程中并不会有错误信息,即使没有使用这个包: 42 | 43 | ```shell 44 | $ go run packageNotUsedUnderscore.go 45 | Hello there! 46 | ``` 47 | 48 | ### 49 | -------------------------------------------------------------------------------- /eBook/chapter1/01.6.2.md: -------------------------------------------------------------------------------- 1 | ### 大括号唯一位置 2 | 3 | 我们来看下面这个叫**curly.go**的Go程序: 4 | 5 | ```go 6 | package main 7 | import ( 8 | "fmt" 9 | ) 10 | func main() 11 | { 12 | fmt.Println("Go has strict rules for curly braces!") 13 | } 14 | ``` 15 | 16 | 虽然这个程序看起来很好,但是如果你执行它,你会非常失望。因为你会获得这个语法错误的信息,程序不会被编译和执行: 17 | 18 | ```shell 19 | $ go run curly.go 20 | # command-line-arguments 21 | ./curly.go:7:6: missing function body for "main" 22 | ./curly.go:8:1: syntax error: unexpected semicolon or newline before { 23 | ``` 24 | 25 | 关于这个错误的官方解释是:在许多上下文中Go把分号作为语句的结尾,编译器会在他认为需要插入分号的地方插入分号。因此,把前大括号作为单独一行会使编译器在前面**func main()**这一行插入一个分号,这是产生错误的原因。 26 | -------------------------------------------------------------------------------- /eBook/chapter1/01.6.md: -------------------------------------------------------------------------------- 1 | ## Go的两条规范 2 | 3 | Go语言有严格的编码规范,这些规范可以帮助你避免代码中出现一些愚蠢的错误和bug,同时也使你的代码更容易被别人阅读。这一节会介绍你需要知道的两个规范。 4 | 5 | 和前面提到的一样,请记住Go编译器是帮助你而不是使你的生活痛苦的。Go编译器的主要目标就是编译代码并且提高你的代码质量。 -------------------------------------------------------------------------------- /eBook/chapter1/01.7.md: -------------------------------------------------------------------------------- 1 | ## Go pakcage 的下载 2 | 3 | 虽然Go提供了丰富的标准库,但是有时候为了使用某些外部包的功能,你仍然需要下载这些外部的包。本节将教你如何下载外部包,以及这些包在你的机器上存在哪里。 4 | 5 | 看下面这个被保存为**getPackage.go**的Go程序: 6 | 7 | ```go 8 | package main 9 | import ( 10 | "fmt" 11 | "github.com/mactsouk/go/simpleGitHub" 12 | ) 13 | func main() { 14 | fmt.Println(simpleGitHub.AddTwo(5, 6)) 15 | } 16 | ``` 17 | 18 | 这个程序使用了一个外部包,因为`import`的第二行使用了一个网络地址。这个外部包叫`simpleGitHub`,位于`github.com/mactsouk/go/simpleGitHub`。 19 | 20 | 如果你试图立即执行这个文件,你会失望的: 21 | 22 | ```shell 23 | $ go run getPackage.go 24 | getPackage.go:5:2: cannot find package "github.com/mactsouk/go/simpleGitHub" in any of: 25 | /usr/local/Cellar/go/1.9.1/libexec/src/github.com/mactsouk/go/ simpleGitHub (from $GOROOT) 26 | /Users/mtsouk/go/src/github.com/mactsouk/go/simpleGitHub (from $GOPATH) 27 | ``` 28 | 29 | 所以,你需要获取在你的电脑上获取这个包。为了下载它,你需要执行以下命令: 30 | 31 | ```shell 32 | $ go get -v github.com/mactsouk/go/simpleGitHub 33 | github.com/mactsouk/go (download) 34 | github.com/mactsouk/go/simpleGitHub 35 | ``` 36 | 37 | 然后,你会在下面这个目录中找到下载的文件: 38 | 39 | ```shell 40 | $ ls -l ~/go/src/github.com/mactsouk/go/simpleGitHub/ 41 | total 8 42 | -rw-r--r-- 1 mtsouk staff 66 Oct 17 21:47 simpleGitHub.go 43 | ``` 44 | 45 | 而且,`go get`命令还编译了这个包,相关的文件可以在这里找到: 46 | 47 | ```shell 48 | $ ls -l ~/go/pkg/darwin_amd64/github.com/mactsouk/go/simpleGitHub.a 49 | -rw-r--r-- 1 mtsouk staff 1050 Oct 17 21:47 /Users/mtsouk/go/pkg/darwin_amd64/github.com/mactsouk/go/simpleGitHub.a 50 | ``` 51 | 52 | 现在你可以没有任何问题地运行`getPackage.go `: 53 | 54 | ```shell 55 | $ go run getPackage.go 56 | 11 57 | ``` 58 | 59 | 你也可以像下面这样删除下载的外部包生成的中间文件: 60 | 61 | ```shell 62 | $ go clean -i -v -x github.com/mactsouk/go/simpleGitHub 63 | cd /Users/mtsouk/go/src/github.com/mactsouk/go/simpleGitHub 64 | rm -f simpleGitHub.test simpleGitHub.test.exe 65 | rm -f /Users/mtsouk/go/pkg/darwin_amd64/github.com/mactsouk/go/ simpleGitHub.a 66 | ``` 67 | 68 | 同样的,你也可以在执行`go clean`后使用`rm`命令来删除你下载的整个包。 69 | 70 | ```shell 71 | $ go clean -i -v -x github.com/mactsouk/go/simpleGitHub 72 | $ rm -rf ~/go/src/github.com/mactsouk/go/simpleGitHub 73 | ``` 74 | 75 | 执行了上面的命令后,如果要执行`getPackage.go `,你需要再次下载这个包, 76 | -------------------------------------------------------------------------------- /eBook/chapter1/01.8.md: -------------------------------------------------------------------------------- 1 | ## UNIX标准输入,标准输出,标准错误 2 | 3 | 每个UNIX系统都为它的进程们一直打开了三个文件。记住UNIX认为一切皆文件,即使是打印机或者鼠标。 4 | 5 | UNIX使用文件描述符作为访问所有打开文件的内部表示,这些描述符是正整数,比长长的路径要美观许多。 6 | 7 | 因此,所有的UNIX系统默认的支持三个特殊且标准的文件名:`/dev/stdin`, `/dev/stdout`和 `/dev/stderr`,相应的这些文件可以用文件描述符0,1,2来直接访问。这三个文件描述符也叫标准输入,标准输出,标准错误。另外,在macOS系统上文件描述符0可以在`/dev/fd/0`访问到,在debain系统中`/dev/fd/0 `和`/dev/pts/0`都可以。 8 | 9 | Go语言用`os.Stdin`来访问标准输入,`os.Stdout`来访问标准输出,`os.Stderr`来访问标准错误。虽然你可以继续使用`/dev/stdin`,`/dev/stdout`,`/dev/stderr`或者相关的文件描述符值来访问这些设备,但是使用Go提供的` os.Stdin`, `os.Stdout`,` os.Stderr`更好,更安全,可移植性更高 -------------------------------------------------------------------------------- /eBook/chapter1/01.9.md: -------------------------------------------------------------------------------- 1 | ## 关于打印输出 2 | 3 | 和UNIX中的C一样,Go有很多方式打印输出到屏幕。本节中所有的打印函数都在一个叫`fmt`的标准包中,我们将会在一个叫` printing.go`的程序中展示这些函数。 4 | 5 | 在Go语言中,最简单的打印方法就是是使用`fmt.Println()`和`fmt.Printf()`这两个函数。`fmt.Printf()`和C语言中的`printf`有很多相似的地方。你可以用` fmt.Print()`而不是`fmt.Println()`。这两个函数最主要的区别是后者在每次调用结束后会自动添加一个换行符。 6 | 7 | 另一方面,`fmt.Println()`和`fmt.Printf()`最大的区别是后者对于你要打印的东西都需要一个格式化指示符,和C中的`printf`函数一样的,这意味你可以更好的控制输出的内容,当然你需要写更多的代码。Go把这些格式化指示符称为`verbs`。你可以在 [https://golang.org/pkg/fmt/](https://golang.org/pkg/fmt/)这里找到更多关于verbs的信息。 8 | 9 | 如果打印的时候需要执行格式化的操作,或者有多个参数需要处理,用`fmt.Printf()`或许是一个更好的选择。当然,如果只有一个参数,你或许会用`fmt.Print()`或者 `fmt.Println(),`这取决于你是否需要一个换行符。 10 | 11 | **printing.go**文件的第一部分包含以下代码: 12 | 13 | ```go 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | ) 19 | func main() { 20 | v1 := "123" 21 | v2 := 123 22 | v3 := "Have a nice day\n" 23 | v4 := "abc" 24 | ``` 25 | 26 | 在这部分中,你可以看到导入了了`fmt`包,定义了四个变量。其中变量`v3`末尾的`\n`是换行符。当然,如果你只是想在输出的末尾插入一个换行符,你可以不带参数地调用`fmt.Println()`,而不是用类似`fmt.Print("\n")`这种方式。 27 | 28 | 第二部分是这样的: 29 | 30 | ```go 31 | fmt.Print(v1, v2, v3, v4) 32 | fmt.Println() 33 | fmt.Println(v1, v2, v3, v4) 34 | fmt.Print(v1, " ", v2, " ", v3, " ", v4, "\n") 35 | fmt.Printf("%s%d %s %s\n", v1, v2, v3, v4) 36 | } 37 | ``` 38 | 39 | 在这部分中,为了更好的理解这几个函数有什么不同,分别使用`fmt.Println()`,` fmt.Print()`, 和fmt.Printf()`打印了这四个变量. 40 | 41 | 如果运行` printing.go` 程序,你会得到以下输出内容: 42 | 43 | ```shell 44 | $ go run printing.go 123123Have a nice day abc 45 | 123 123 Have a nice day 46 | abc 47 | 123 123 Have a nice day 48 | abc 49 | 123123 Have a nice day 50 | abc 51 | ``` 52 | 53 | 从前面的输出可以看到,函数`fmt.Println()`在参数之间加入了空格,而`fmt.Print()`则没有。 54 | 55 | 结果就是语句`fmt.Println(v1, v2)`和`fmt.Print(v1, " ", v2, "\n")`的效果是一样的。 56 | 57 | 除了`fmt.Println()`,`fmt.Print()`和`fmt.Printf()`这些最简单的打印输出到屏幕的方式之外,还有**S**家族的函数,包括有`fmt.Sprintln()`,` fmt.Sprint()`和`fmt.Sprintf()`,这些函数用来基于某种给定的格式生成字符串。 58 | 59 | 最后,还有一个**F**家族的函数,包括`fmt.Fprintln()`, `fmt.Fprint()`, 和`fmt.Fprintf()`。这些函数是通过一个`io.Writer.`写入到文件的。 60 | 61 | 下一节将会是教你如何打印数据到标准输出,这在UNIX世界是非常常见的。 -------------------------------------------------------------------------------- /eBook/chapter2/02.0.md: -------------------------------------------------------------------------------- 1 | # 理解 Go 的内部构造 2 | 3 | 你在上一章中学到的所有 Go 功能都非常方便,你将一直使用它们。但是,更有价值的是看到和理解这背后发生的事情以及 Go 在幕后的操作方式。 4 | 5 | 在本章中,你将学习 Go 垃圾收集器及其工作方式。此外,你还将了解如何从 Go 程序中调用 C 代码,这在某些情况下可能是必不可少的。当然,因为 Go 是一种非常强大的编程语言,你可能不需要经常使用此功能。 6 | 7 | 此外,你还将学习如何从 C 代码中调用 Go 函数,以及如何使用`panic()`和`restore()`函数和`defer`关键字。 8 | 9 | 本章将会涵盖: 10 | 11 | - Go 编译器 12 | - Go 的垃圾回收是如何工作的 13 | - 如何检查垃圾收集器的操作 14 | - 从 Go 代码调用 C 代码 15 | - 从 C 代码调用 Go 代码 16 | - `panic()` 和 `recover()` 函数 17 | - `unsafe` 包 18 | - 方便但棘手的`defer`关键字 19 | - `strace(1)` Linux 工具 20 | - `dtrace(1)` 实用程序,可以在 FreeBSD 系统(包括 macOS Mojave)中找到 21 | - 查找有关你的 Go 环境的信息 22 | - Go 创建的节点树 23 | - 从 Go 创建 WebAssembly 代码 24 | - Go 汇编器 25 | -------------------------------------------------------------------------------- /eBook/chapter2/02.1.md: -------------------------------------------------------------------------------- 1 | ## Go 编译器 2 | 3 | Go 编译器在 go 工具的帮助下执行,该工具不仅生成可执行文件,还执行更多的操作。 4 | 5 | > Tip: 本节中使用的`unsafe.go`文件不包含任何内容特殊代码–所提供的命令将在每个有效的 Go 源文件上运行。 6 | 7 | 你可以通过`go tool compile` 命令编译 Go 源代码。你将得到一个**目标文件**,这将是一个以`.o`扩展结尾的文件。在 macOS Mojave 环境下执行的下一个命令的输出中对此进行了说明: 8 | 9 | ```shell 10 | $ go tool compile unsafe.go 11 | $ ls -l unsafe.o 12 | -rw-r--r-- 1 mtsouk staff 6926 Jan 22 21:39 unsafe.o 13 | $ file unsafe.o 14 | unsafe.o: current ar archive 15 | ``` 16 | 17 | 目标文件是包含目标代码的文件,该目标代码是可重定位格式的机器代码,在大多数情况下,这些代码不能直接执行。可重定位格式的最大优点是在链接阶段它只需要较少的内存。 18 | 19 | 如果你在运行`go tool compile`的时候加上了`-pack`标志,你将会得到**压缩文件**而非目标文件: 20 | 21 | ```shell 22 | $ go tool compile -pack unsafe.go 23 | $ ls -l unsafe.a 24 | -rw-r--r-- 1 mtsouk staff 6926 Jan 22 21:40 unsafe.a 25 | $ file unsafe.a 26 | unsafe.a: current ar archive 27 | ``` 28 | 29 | 压缩文件是包含一个或多个文件的二进制文件,主要是 30 | 用于将多个文件分组为一个文件。这些格式之一是 `ar`,它是 Go 使用的格式。 31 | 32 | 你可以像这样打印出一个 `.a` 压缩文件的内容: 33 | 34 | ```shell 35 | $ ar t unsafe.a 36 | __.PKGDEF 37 | _go_.o 38 | ``` 39 | 40 | `go tool compile` 另一个真正有价值的命令行标志是`-race`,它使你能够检测**竞争条件**。你将在第10章*Go的并发性-高级主题*中了解有关竞争条件以及为什么要避免竞争条件的更多信息。 41 | 42 | 本章的最后讨论汇编语言和节点树的时候,你还会学到一个更多有用的`go tool compile`命令。不过目前你可以尝试一下下面的命令: 43 | ``` 44 | $ go tool compile -S unsafe.go 45 | ``` 46 | 47 | 你可能会发现,以上命令的输出会让你难以理解,这正说明了Go可以很好地帮你隐藏不必要的复杂性,除非你要求Go展示出来。 48 | -------------------------------------------------------------------------------- /eBook/chapter2/02.10.md: -------------------------------------------------------------------------------- 1 | ## Go 汇编器 2 | 3 | 本节将简要讨论汇编语言和 Go 汇编器,这是一个 Go 工具,可让你查看 Go 编译器使用的汇编语言。 4 | 5 | 例如,通过执行以下,你可以看到在本章上一节中看到的`goEnv.go`程序的汇编语言: 6 | 7 | ```shell 8 | $ GOOS=darwin GOARCH=amd64 go tool compile -S goEnv.go 9 | ``` 10 | 11 | `GOOS`变量的值定义目标操作系统的名称,而`GOARCH`变量的值定义编译体系结构。以上的命令在 macOS Mojave 机器上执行,因此将`darwin`值赋予`GOOS`变量。 12 | 13 | 即使对于像`goEnv.go`这样的简单程序,以上命令的输出也非常复杂: 14 | 15 | ```shell 16 | "".main STEXT size=859 args=0x0 locals=0x118 17 | 0x0000 00000 (goEnv.go:8) TEXT "".main(SB), $280-0 18 | 0x00be 00190 (goEnv.go:9) PCDATA $0, $1 19 | 0x0308 00776 (goEnv.go:13) PCDATA $0, $5 20 | 0x0308 00776 (goEnv.go:13) CALL runtime.convT2E64(SB) 21 | "".init STEXT size=96 args=0x0 locals=0x8 22 | 0x0000 00000 (:1) TEXT "".init(SB), $8-0 23 | 0x0000 00000 (:1) MOVQ (TLS), CX 24 | 0x001d 00029 (:1) FUNCDATA $0, 25 | gclocals d4dc2f11db048877dbc0f60a22b4adb3(SB) 26 | 0x001d 00029 (:1) FUNCDATA $1, 27 | gclocals 33cdeccccebe80329f1fdbee7f5874cb(SB) 28 | ``` 29 | 30 | 包含`FUNCDATA`和`PCDATA`指令的行由 Go 垃圾收集器读取和使用,并由 Go 编译器自动生成。 31 | 32 | 这条命令和以上命令是等效的: 33 | 34 | ```shell 35 | $ GOOS=darwin GOARCH=amd64 go build -gcflags -S goEnv.go 36 | ``` 37 | 38 | `GOOS`变量可能的取值包括`android, darwin, dragonfly, freebsd, linux, nacl, netbsd, openbsd, plan9, solaris, windows, 和 zos`。`GOARCH`可能的取值包括`386, amd64, amd64p32, arm, armbe, arm64, arm64be, ppc64, ppc64le, mips, mipsle, mips64, mips64le, mips64p32, mips64p32le, ppc, s390, s390x, sparc, 和sparc64`。 39 | 40 | > Tip: 如果你真的对Go汇编器感兴趣,并且想要更多信息,请访问https://golang.org/doc/asm。 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /eBook/chapter2/02.13.1.md: -------------------------------------------------------------------------------- 1 | ### 对 Webassembly 的简单介绍 2 | 3 | WebAssembly(Wasm)是针对虚拟机的机器模型和可执行格式。它旨在提高速度减小文件大小。这意味着你可以在任何平台上使用WebAssembly二进制文件,而无需进行任何更改。 4 | WebAssembly有两种格式:纯文本格式和二进制格式。纯文本格式的WebAssembly文件扩展名为.wat,而二进制文件的扩展名为.wasm。请注意,一旦有了WebAssembly二进制文件,就必须使用JavaScript API加载和使用它。 5 | 除了Go之外,还可以从其他支持静态类型的编程语言(包括Rust,C和C ++)中生成WebAssembly。 6 | -------------------------------------------------------------------------------- /eBook/chapter2/02.13.2.md: -------------------------------------------------------------------------------- 1 | ### 为什么 WebAssembly 很重要 2 | 3 | 这里有几个主要的原因: 4 | 5 | - WebAssembly 代码的运行速度非常接近 native,这意味着 WebAssembly 速度很快。 6 | - 你可以使用多种编程语言来创建 WebAssembly 代码,这些语言可能包括你已经知道的编程语言。 7 | - 大多数现代 Web 浏览器本身都支持 WebAssembly,而无需插件或安装其他任何软件。 8 | - WebAssembly 的运行效率比**Javascript**快得多。 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /eBook/chapter2/02.13.3.md: -------------------------------------------------------------------------------- 1 | ### Go 与 WebAssembly 2 | 3 | 对于 Go,WebAssembly 只是另一种体系结构。因此,你可以使用 Go 的交叉编译功能来创建 WebAssembly 代码。 4 | 5 | 你将在第 11 章,“代码测试,优化和分析”中学习有关 Go 的交叉编译功能的更多信息。现在,请注意将 Go 代码编译到 WebAssembly 时正在使用的`GOOS`和`GOARCH`环境变量的值。 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /eBook/chapter2/02.13.4.md: -------------------------------------------------------------------------------- 1 | ## 示例 2 | 3 | 在本节中,我们将了解如何将 Go 程序编译为 WebAssembly 代码。 `toWasm.go`的 Go 代码如下: 4 | 5 | ```Go 6 | package main 7 | import ( 8 | "fmt" 9 | ) 10 | func main() { 11 | fmt.Println("Creating WebAssembly code from Go!") 12 | } 13 | ``` 14 | 15 | 这里要注意的是此代码并没有用到 WebAssembly,并且 toWasm.go 可以单独编译和执行,这意味着它没有与 WebAssembly 相关的外部依赖关系。 16 | 17 | 创建 WebAssembly 代码所需执行的最后一步是执行以下命令: 18 | 19 | ```shell 20 | $ GOOS=js GOARCH=wasm go build -o main.wasm toWasm.go 21 | $ ls -l 22 | total 4760 23 | -rwxr-xr-x 1 mtsouk staff 2430633 Jan 19 21:00 main.wasm 24 | -rw-r--r--@ 1 mtsouk staff 100 Jan 19 20:53 toWasm.go 25 | $ file main.wasm 26 | main.wasm: , created: Thu Oct 25 20:41:08 2007, modified: Fri May 28 13:51:43 2032 27 | ``` 28 | 29 | 因此,在第一个命令中通过`GOOS`和`GOARCH`的值告诉 Go 创建 WebAssembly 代码。如果未输入正确的`GOOS`和`GOARCH`值,则编译将不会生成 WebAssembly 代码。 30 | -------------------------------------------------------------------------------- /eBook/chapter2/02.13.5.md: -------------------------------------------------------------------------------- 1 | ### 使用创建好的 WebAssembly 代码 2 | 3 | 到目前为止,我们只生成了一个 WebAssembly 二进制文件。所以,你仍然需要采取一些步骤,才能使用该 WebAssembly 二进制文件并在 Web 浏览器的窗口中查看其结果。 4 | 5 | 如果你使用**Google Chrome 浏览器**,则有一个标志可让你启用 Liftoff,Liftoff 是 WebAssembly 的编译器,从理论上讲,它可以提高 WebAssembly 代码的运行效率。尝试以下没什么副作用,你可以访问 chrome:// flags/#enable-webassembly-baseline 来开启它。 6 | 7 | 第一步是将`main.wasm`复制到 Web 服务器的目录中。接下来,你将需要执行以下命令: 8 | 9 | ```shell 10 | $ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" . 11 | ``` 12 | 13 | 这会将 Go 安装的`wasm_exec.js`复制到当前目录中。你应该将该文件放在与`main.wasm`相同的 Web 服务器目录中。 14 | 15 | 这里用到的`index.html`的代码: 16 | 17 | ```html 18 | 19 | 20 | 21 | Go and WebAssembly 22 | 23 | 24 | 25 | 50 | 51 | 52 | 53 | ``` 54 | 55 | 请注意,由 HTML 代码创建的`Run`按钮只有在 WebAssembly 代码加载完成的情况下点击有效。 56 | 57 | 下图显示了 WebAssembly 代码的输出,如 Google Chrome Web 浏览器的 JavaScript 控制台中所示。其他 Web 浏览器将显示类似的输出。 58 | 59 | ![02.13.5-1](http://ww1.sinaimg.cn/large/c0802412gy1gdip8z89nuj21hc0jagoc.jpg) 60 | 61 | > Tip: 在第 12 章“Go 中的网络编程基础”中,你将学习如何在 Go 中开发自己的 Web 服务器。 62 | 63 | 但其实有一种测试 WebAssembly 应用程序的简便得多的方法,那就是使用**Node.js**。不需要 Web 服务器,因为 Node.js 是基于 Chrome V8 的 JavaScript 引擎构建的**JavaScript**运行时。 64 | 65 | 如果你已经在本地计算机上安装了Node.js,则可以执行以下命令: 66 | 67 | ```shell 68 | $ export PATH="$PATH:$(go env GOROOT)/misc/wasm" 69 | $ GOOS=js GOARCH=wasm go run . 70 | Creating WebAssembly code from Go! 71 | ``` 72 | 73 | 第二个命令的输出将验证WebAssembly代码是否正确并生成所需的消息。请注意,第一个命令并不是必须的,因为它只是更改PATH环境变量的当前值,以便包括当前Go安装程序存储与WebAssembly相关的文件的目录。 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /eBook/chapter2/02.13.md: -------------------------------------------------------------------------------- 1 | ## 创建 WebAssembly 代码 2 | 3 | Go可以让你借助go工具创建WebAssembly代码。在说明过程之前,我先分享有关WebAssembly的更多信息。 4 | 5 | -------------------------------------------------------------------------------- /eBook/chapter2/02.14.md: -------------------------------------------------------------------------------- 1 | ## Go 编码风格建议 2 | 3 | 以下是编码风格建议,可帮助你编写更好的 Go 代码: 4 | 5 | - 如果你在 Go 函数中有错误,请记录或返回它;除非你有充分的理由这样做,否则请不要两者都做。 6 | - Go 接口定义的是行为,不是数据和数据结构。 7 | - 尽可能使用`io.Reader`和`io.Writer`接口,因为它们使你的代码更具可扩展性。 8 | - 确保仅在需要时才将指向变量的指针传递给函数。其余时间,只需传递变量的值即可。 9 | - 表示程序错误的变量不是`string`变量,是`error`变量! 10 | - 除非有充分的理由,否则请不要在生产环境上测试 Go 代码。 11 | - 如果你真的不了解 Go 功能,请在首次使用它之前对其进行测试,尤其是当你正在开发将被大量用户使用的应用程序或实用程序时。 12 | - 如果你害怕犯错误,那么你很可能最终会无所事事。尽可能多地尝试! 13 | -------------------------------------------------------------------------------- /eBook/chapter2/02.15.md: -------------------------------------------------------------------------------- 1 | ## 练习和相关链接 2 | 3 | - 访问 https://golang.org/pkg/unsafe/ 上的文档页面,以了解有关`unsafe`package标准Go软件包的更多信息。 4 | - 访问 DTrace 网站 http://dtrace.org/。 5 | - 在 Linux 机器上使用 `strace(1)`检查某些标准 UNIX 实用程序(例如 `cp(1)`和 `ls(1)`)的操作,你发现了了什么? 6 | - 如果使用的是 macOS 计算机,请使用`dtruss(1)`查看 `sync(8)`实用程序的工作方式。 7 | - 编写你自己的示例,在其中使用 Go 程序调用 C 代码。编写 Go 函数并在 C 程序中使用它。 8 | - 你可以通过以下方式找到有`runtime` package 的更多信息:https://golang.org/pkg/runtime/。 9 | - 阅读研究论文可能很困难,但很有收获。请下载“On-the-Fly Garbage Collection: An Exercise in Cooperation”论文并阅读。可以在许多地方找到该论文,包括 https://dl.acm.org/citation.cfm?id=359655。 10 | - 访问 https://github.com/gasche/gc-latency-experiment 以查找各种编程语言的垃圾收集器的基准测试代码。 11 | - 可以通过 https://nodejs.org/en/ 访问 Node.js 网站。 12 | - 你可以在 https://webassembly.org/ 上了解有关 WebAssembly 的更多信息。 13 | - 如果你想了解有关垃圾收集的更多信息,请务必访问http://gchandbook.org/。 14 | - 访问 cgo 的文档页面,网址为https://golang.org/cmd/cgo/。 15 | -------------------------------------------------------------------------------- /eBook/chapter2/02.16.md: -------------------------------------------------------------------------------- 1 | ## 本章小结 2 | 3 | 本章讨论了许多有趣的 Go 相关主题,包括有关 Go 垃圾收集器的理论和实践信息。如何从 Go 程序中调用 C 代码;方便但不那么好掌握的`defer`关键字; `panic()`和`recovery()`函数; `strace(1)`,`dtrace(1)和 dtruss(1)`UNIX 工具;使用`unsafe package`;如何从 Go 生成 WebAssembly 代码;和 Go 生成的汇编代码。使用`runtime` package 查看了有关 Go 环境的信息,并展示了如何输出和解释 Go 程序的节点树。最后,给了你一些不错的 Go 编码建议。 4 | 5 | 在本章中,你应该记住的是,`unsafe` package 和从 Go 调用 C 代码的功能通常在以下三种情况下使用:首先,当你想要最佳性能时,又想为此牺牲一些 Go 安全性;其次,当你想与另一种编程语言交流时;第三,当你想要实现 Go 中无法实现的功能时。 6 | 7 | 在下一章中,我们将开始学习 Go 附带的基本数据类型,包括**arrays, slices, maps**。尽管它们很简单,但是这些数据类型几乎是每个 Go 应用程序的构建基础,因为它们是更复杂的数据结构的基础,它使你可以在 Go 项目中存储数据和移动信息。 8 | 9 | 此外,你还将学习**pointers, Go loop**,这和在其他编程语言中类似。以及 Go 与日期和时间配合使用的特有方式。 10 | -------------------------------------------------------------------------------- /eBook/chapter2/02.2.2.md: -------------------------------------------------------------------------------- 1 | ### 有关 Go 垃圾收集器操作的更多信息 2 | 3 | 这一小节会深入探索 go 垃圾回收器。 4 | 5 | go 垃圾回收器的主要关注点是低延迟,也就是说为了进行实时操作它会有短暂的暂停。另一方面,创建新对象然后使用指针操作存活对象是程序始终在做的事情,这个过程可能最终会创建出不会再被访问到的对象,因为没有指向那些对象的指针。这种对象即为垃圾对象,它们等待被垃圾回收器清理然后释放它们的空间。之后它们释放的空间可以再次被使用。 6 | 7 | 垃圾回收中使用的最简单的算法就是经典的**标记清除算法(mark-and-sweep)**:算法为了遍历和标记堆中所有可触达对象,会把程序停下来(**stop the world**)。之后,它会去清扫(sweeps)不可触达的对象。在算法的标记(mark)阶段,每个对象被标记为白色、灰色或黑色。灰色的子对象标记为灰色,而原始的对象此时会标记为黑色。没有更多灰色对象去检查的话就会开始清扫阶段。这个技术适用是因为没有从黑色指向白色的指针,这是算法的基本不变要素。 8 | 9 | 尽管标记清除算法很简单,但是它会暂停程序的运行,这意味着实际过程中它会增加延迟。go 会通过把垃圾回收器作为一个并发的处理过程,同时使用前一节讲的三色算法,来降低这种延迟。但是,在垃圾回收器并发运行时候,其它的过程可能会移动指针或者创建对象,这会让垃圾回收器处理非常困难。所以,让三色算法并发运行的关键点就是,维持标记清除算法的不变要素即没有黑色的对象能够指向白色集合对象。 10 | 11 | 因此,新对象必须进入到灰色集合,因为这种方式下标记清除的不变要素不会被改变。另外,当程序的一个指针移动,要把指针所指的对象标记为灰色。你可以说灰色集合是白色集合和黑色集合中间的“屏障”。最后,每次一个指针移动,会自动执行一些代码,也就是我们之前提到的**写屏障**,它会去进行重新标色。 12 | 13 | 为了能够并发运行垃圾回收器,写屏障代码产生的延迟是必要的代价。 14 | 15 | 注意**Java**程序语言有许多垃圾回收器,它们在各种参数下能够进行高度配置。其中一种垃圾回收器叫 G1,推荐在低延迟应用的程序使用它。 16 | 17 | > Tip: 一定要记住,Go 垃圾回收器是一个实时的垃圾回收器 ,它是和其他 goroutines 一起并发运行的 ,并且只针对低延迟进行优化。 18 | 19 | 在第 11 章*代码测试,优化以及分析*,你会学习到如何能够用图表的方式呈现程序的性能。这一章节也会包括一些关于 Go 垃圾回收器操作的一些信息。 20 | -------------------------------------------------------------------------------- /eBook/chapter2/02.2.4.md: -------------------------------------------------------------------------------- 1 | ### Unsafe code 2 | 3 | **Unsafe code**是一种绕过 go 类型安全和内存安全检查的 Go 代码。大多数情况,unsafe code 是和指针相关的。但是要记住使用 unsafe code 有可能会损害你的程序,所以,如果你不完全确定是否需要用到 unsafe code 就不要使用它! 4 | 5 | Unsafe code 的使用将在 unsafe.go 程序中进行说明,该程序分为三个部分。 6 | 7 | `unsafe.go`第一部分 8 | 9 | ```Go 10 | package main 11 | import ( 12 | "fmt" 13 | "unsafe" 14 | ) 15 | ``` 16 | 17 | 你会注意到,为了使用 unsafe code,你将需要导入不安全的标准 Go package。 18 | 19 | 第二部分 Go 代码: 20 | 21 | ```GO 22 | func main() { 23 | var value int64 = 5 24 | var p1 = &value 25 | var p2 = (*int32)(unsafe.Pointer(p1)) 26 | ``` 27 | 28 | 请注意此处使用`unsafe.Pointer()`函数,该函数使我们自己承担创建一个名为`p2`的`int32`指针的风险,该指针指向一个名为`value`的`int64`变量,可以使用`p1`指针对其进行访问。任何 Go 指针都可以转换为`unsafe.Pointer`。 29 | 30 | > Tip: 类型为`unsafe.Pointer`的指针可以覆盖 Go 的类型系统。这无疑是很快的,但是如果使用不正确或不小心,也会带来危险。此外,它使开发人员可以更好地控制数据。 31 | 32 | 最后一部分 Go 代码: 33 | 34 | ```GO 35 | fmt.Println("*p1: ", *p1) 36 | fmt.Println("*p2: ", *p2) 37 | *p1 = 5434123412312431212 38 | fmt.Println(value) 39 | fmt.Println("*p2: ", *p2) 40 | *p1 = 54341234 41 | fmt.Println(value) 42 | fmt.Println("*p2: ", *p2) 43 | } 44 | ``` 45 | 46 | > 你可以**取消引用指针**,并使用星号(\*)获取,使用或设置其值。 47 | 48 | 如果你执行`unsafe.go`,你会得到以下的输出: 49 | 50 | ```shell 51 | $ go run unsafe.go 52 | *p1: 5 53 | *p2: 5 54 | 5434123412312431212 55 | *p2: -930866580 56 | 54341234 57 | *p2: 54341234 58 | ``` 59 | 60 | 此输出告诉我们什么?它告诉我们 32 位指针不能存储 64 位整数。 61 | 正如你将在下一节中看到的那样,`unsafe`包的功能可以用内存做更多有趣的事情。 62 | -------------------------------------------------------------------------------- /eBook/chapter2/02.2.5.md: -------------------------------------------------------------------------------- 1 | ### 有关 unsafe 包 2 | 3 | 现在你已经看到了`unsafe`包在起作用,现在该讨论更多有关使其成为特殊程序包的方法了。首先,如果你查看`unsafe`包的源代码,你可能会有些惊讶。在使用 Homebrew(https://brew.sh/)安装的 Go 版本为 1.11.4 的 macOS Mojave 系统上,`unsafe`包的源代码位于`/usr/local/Cellar/go/1.11.4/libexec/src/unsafe/unsafe.go`,不包含注释,它的内容如下: 4 | 5 | ```shell 6 | $ cd /usr/local/Cellar/go/1.11.4/libexec/src/unsafe/ 7 | $ grep -v '^//' unsafe.go | grep -v '^$' 8 | package unsafe 9 | type ArbitraryType int 10 | type Pointer *ArbitraryType 11 | func Sizeof(x ArbitraryType) uintptr 12 | func Offsetof(x ArbitraryType) uintptr 13 | func Alignof(x ArbitraryType) uintptr 14 | ``` 15 | 16 | 那么,`unsafe`包的其余 Go 代码在哪里?答案很简单:当你 import 到你程序里的时候,Go 编译器实现了这个 unsafe 库。 17 | 18 | > Tip: 许多系统库,例如`runtime,syscall`和`os`会经常使用到`unsafe`库 19 | -------------------------------------------------------------------------------- /eBook/chapter2/02.2.6.md: -------------------------------------------------------------------------------- 1 | ### 另一个 usafe 包的例子 2 | 3 | 在这一节,你会了解到更多关于 unsafe 库的东西,以及通过一个 moreUnsafe.go 的小程序来了解 unsafe 库的兼容性。moreUnsafe.go 做的事情就是使用指针来访问数组里的所有元素。 4 | 5 | ```Go 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "unsafe" 11 | ) 12 | 13 | func main() { 14 | array := [...]int{0, 1, -2, 3, 4} 15 | pointer := &array[0] 16 | fmt.Print(*pointer, " ") 17 | memoryAddress := uintptr(unsafe.Pointer(pointer)) + unsafe.Sizeof(array[0]) 18 | for i := 0; i < len(array)-1; i++ { 19 | pointer = (*int)(unsafe.Pointer(memoryAddress)) 20 | fmt.Print(*pointer, " ") 21 | memoryAddress = uintptr(unsafe.Pointer(pointer)) + unsafe.Sizeof(array[0]) 22 | } 23 | ``` 24 | 25 | 首先,`pointer`变量指向`array[0]`的地址,`array[0]`是整型数组的第一个元素。接下来指向整数值的`pointer`变量会传入`unsafe.Pointer()`方法,然后传入`uintptr`。最后结果存到了`memoryAddress`里。 26 | 27 | `unsafe.Sizeof(array[0])`的值使你可以进入数组的下一个元素,因为这是每个数组元素占用的内存。因此,该值将在 for 循环的每次迭代中添加到`memoryAddress`变量中,从而使你可以获取下一个数组元素的内存地址。 `*pointer`表示法取消引用指针并返回存储的整数值。 28 | 29 | 第三部分代码: 30 | 31 | ```Go 32 | fmt.Println() 33 | pointer = (*int)(unsafe.Pointer(memoryAddress)) 34 | fmt.Print("One more: ", *pointer, " ") 35 | memoryAddress = uintptr(unsafe.Pointer(pointer)) + unsafe.Sizeof(array[0]) 36 | fmt.Println() 37 | ``` 38 | 39 | 在最后一部分中,我们尝试使用指针和内存地址访问数组中不存在的元素。由于使用了`unsafe`包,Go 编译器无法捕获此类逻辑错误,因此将返回不正确的内容。 40 | 41 | 执行`moreUnsafe.go`将会输出: 42 | 43 | ```shell 44 | $ go run moreUnsafe.go 45 | 0 1 -2 3 4 46 | One more: 824634208008 47 | ``` 48 | 49 | 你刚刚使用指针访问了 Go 数组的所有元素。但是,这里的真正问题是,当你尝试访问无效的数组元素时,程序没有抛出错误,而是返回了一个随机数。 50 | -------------------------------------------------------------------------------- /eBook/chapter2/02.3.1.md: -------------------------------------------------------------------------------- 1 | ### 在同一文件用 Go 调用 C 代码 2 | 3 | 从 Go 程序调用 C 代码的最简单方法是将 C 代码包含在 Go 源文件中。这需要特殊处理,但是速度很快,而且没有那么困难。 4 | 5 | 包含 C 和 Go 代码的 Go 源文件的名称为`cGo.go`,将分为三部分。 6 | 7 | 第一部分的代码: 8 | 9 | ```Go 10 | package main 11 | //#include 12 | //void callC() { 13 | // printf("Calling C code!\n"); 14 | //} 15 | import "C" 16 | ``` 17 | 18 | > Tip: 如你所看到的,C 代码包含在 Go 程序的注释中。但是,由于使用了`c` Go package,`go`知道如何处理此类注释。 19 | 20 | 第二部分代码: 21 | 22 | ```Go 23 | import "fmt" 24 | 25 | func main() { 26 | ``` 27 | 28 | 所有其他的包都必须被单独导入。 29 | 30 | 最后一部分代码: 31 | 32 | ```Go 33 | fmt.Println("A Go statement") 34 | C.callC() 35 | fmt.Println("Another Go Statement!") 36 | } 37 | ``` 38 | 39 | 为了执行`callC()` c 函数,你需要像这样`C.callC()`调用. 40 | 41 | 执行`cGo.go`你会得到这样的输出: 42 | 43 | ```shell 44 | $ go run cGo.go 45 | A Go statement! 46 | Calling C code! 47 | Another Go statement! 48 | ``` 49 | -------------------------------------------------------------------------------- /eBook/chapter2/02.3.md: -------------------------------------------------------------------------------- 1 | ## 从 Go 调用 C 代码 2 | 3 | 与 C 相比,尽管 Go 的编程体验更好,但是 C 仍然是一种非常有用的编程语言。这意味着在某些情况下(例如,使用数据库或使用 C 编写的设备驱动程序)仍需要使用 C,这意味着你将需要在 Go 项目中使用 C 代码。 4 | 5 | > Tip: 如果发现你在同一项目中多次使用此功能,则可能需要重新考虑你的方法或选择的编程语言是不是有问题。 6 | -------------------------------------------------------------------------------- /eBook/chapter2/02.4.1.md: -------------------------------------------------------------------------------- 1 | ### Go 包 2 | 3 | 本小节将向你提供将在 C 程序中使用的 Go package 的代码。 Go package 的名称必须是`main`,但其文件名可以是你想要的任何名称;在这我们将文件命名为为`useByC.go`,并分为三部分。 4 | 5 | > Tip: 你将在*第 6 章-你可能不了解 Go package 和 Go 函数*中了解有关 Go package 的更多信息。 6 | 7 | 第一部分代码: 8 | 9 | ```Go 10 | package main 11 | import "C" 12 | import ( 13 | "fmt" 14 | ) 15 | ``` 16 | 17 | 正如我之前所说,必须将 Go 包命名为`main`。你还需要在 Go 代码中导入 C 包。 18 | 19 | 第二部分代码: 20 | 21 | ```Go 22 | //export PrintMessage 23 | func PrintMessage() { 24 | fmt.Println("A Go function!") 25 | } 26 | ``` 27 | 28 | 必须将 C 代码准备调用的 Go 函数导出。这意味着你应在导出之前放置一个以`// export`开头的注释行。在`// export`的后面你将需要输入函数的名称,因为这是 C 代码将使用的名称。 29 | 30 | 最后一部分代码: 31 | 32 | ```Go 33 | //export Multiply 34 | func Multiply(a, b int) int { 35 | return a * b 36 | } 37 | 38 | func main() { 39 | } 40 | ``` 41 | 42 | `usedByC.go`的`main()`函数不需要代码,因为它不会被导出,因此不会被 C 程序使用。此外,由于还希望导出`Multiply()`函数,因此需要写上`// export Multiply`。 43 | 44 | 之后,你需要通过执行以下命令从 Go 代码生成 C 共享库: 45 | 46 | ```shell 47 | $ go build -o usedByC.o -buildmode=c-shared usedByC.go 48 | ``` 49 | 50 | 前面的命令将生成两个名为`usedByC.h`和`usedByC.o`的文件: 51 | 52 | ```shell 53 | $ ls -l usedByC.* 54 | -rw-r--r--@ 1 mtsouk staff 204 Jan 10 09:17 usedByC.go 55 | -rw-r--r-- 1 mtsouk staff 1365 Jan 22 22:14 usedByC.h 56 | -rw-r--r-- 1 mtsouk staff 2329472 Jan 22 22:14 usedByC.o 57 | $ file usedByC.o 58 | usedByC.o: Mach-O 64-bit dynamically linked shared library x86_64 59 | ``` 60 | 61 | 你不应对 usedByC.h 进行任何更改。 62 | -------------------------------------------------------------------------------- /eBook/chapter2/02.4.2.md: -------------------------------------------------------------------------------- 1 | ### C 代码 2 | 3 | 可以在`willUseGo.c`源文件中找到相关的 C 代码,该文件分为两部分。接下来是`willUseGo.c`的第一部分: 4 | 5 | ```c 6 | #include 7 | #include "usedByC.h" 8 | int main(int argc, char **argv) { 9 | GoInt x = 12; 10 | GoInt y = 23; 11 | 12 | printf("About to call a Go function!\n"); 13 | PrintMessage(); 14 | ``` 15 | 16 | 如果你已经知道 C,那么你将理解为什么需要包含 usedByC.h;这就是 C 代码了解库的可用功能的方式。 17 | 18 | 第二部分的代码: 19 | 20 | ```C 21 | GoInt p = Multiply(x,y); 22 | printf("Product: %d\n",(int)p); 23 | printf("It worked!\n"); 24 | return 0; 25 | } 26 | ``` 27 | 28 | `GoInt p`表示使用`(int)p`转换为 C 整数变量,是从 Go 函数获取整数值的必要方式。 29 | 30 | 在 macOS Mojave 机器上编译并执行`willUseGo.c`将输出: 31 | 32 | ```shell 33 | $ gcc -o willUseGo willUseGo.c ./usedByC.o 34 | $ ./willUseGo 35 | About to call a Go function! 36 | A Go function! 37 | Product: 276 38 | It worked! 39 | ``` 40 | -------------------------------------------------------------------------------- /eBook/chapter2/02.4.md: -------------------------------------------------------------------------------- 1 | ## 从 C 调用 Go 代码 2 | 3 | 也可以从C代码中调用Go函数。因此,本节将为你提供一个小示例,它将从C程序中调用两个Go函数。 Go包将转换为**C共享库**,该库将在C程序中使用。 4 | 5 | -------------------------------------------------------------------------------- /eBook/chapter2/02.5.1.md: -------------------------------------------------------------------------------- 1 | ### 用 defer 打印日志 2 | 3 | 本节将介绍与日志记录相关的`defer`应用。该技术的目的是帮助你更好地组织功能的日志记录信息。该试例程序的名称是`logDefer.go`,它将分为三个部分。 4 | 5 | 第一部分代码: 6 | 7 | ```Go 8 | package main 9 | import ( 10 | "fmt" 11 | "log" 12 | "os" 13 | ) 14 | 15 | var LOGFILE = "/tmp/mGo.log" 16 | 17 | func one(aLog *log.Logger) { 18 | aLog.Println("-- FUNCTION one ------") 19 | defer aLog.Println("-- FUNCTION one ------") 20 | 21 | for i := 0; i < 10; i++ { 22 | aLog.Println(i) 23 | } 24 | } 25 | ``` 26 | 27 | 名为`one()`的函数正在使用`defer`来确保第二个`aLog.Println()`调用将在该函数即将返回之前执行。因此,该函数的所有日志消息都将被嵌入在打开`aLog.Println()`和关闭`aLog.Println()`之间。这样,在日志文件中查看该功能的日志消息就会容易得多。 28 | 29 | 第二部分代码: 30 | 31 | ```Go 32 | func two(aLog *log.Logger) { 33 | aLog.Println("---- FUNCTION two") 34 | defer aLog.Println("FUNCTION two ------") 35 | 36 | for i := 10; i > 0; i-- { 37 | aLog.Println(i) 38 | } 39 | } 40 | ``` 41 | 42 | 名为`two()`的函数仍然使用`defer`方便地对其日志消息进行分组。但是,这次`two()`使用的消息格式与函数`one()`略有不同,不过这完全取决于你。 43 | 44 | 最后一部分代码: 45 | 46 | ```Go 47 | func main() { 48 | f, err := os.OpenFile(LOGFILE,os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 49 | if err != nil { 50 | fmt.Println(err) 51 | return 52 | } 53 | defer f.Close() 54 | 55 | iLog := log.New(f, "logDefer ", log.LstdFlags) 56 | iLog.Println("Hello there!") 57 | iLog.Println("Another log entry!") 58 | 59 | one(iLog) 60 | two(iLog) 61 | } 62 | ``` 63 | 64 | 执行`logDefer.go`将不会在控制台产生输出。但是,查看程序正在使用的日志文件`/tmp/mGo.log`的内容,你就知道`defer`到底有多方便了: 65 | 66 | ```shell 67 | $ cat /tmp/mGo.log 68 | logDefer 2019/01/19 21:15:11 Hello there! 69 | logDefer 2019/01/19 21:15:11 Another log entry! 70 | logDefer 2019/01/19 21:15:11 -- FUNCTION one ------ 71 | logDefer 2019/01/19 21:15:11 0 72 | logDefer 2019/01/19 21:15:11 1 73 | logDefer 2019/01/19 21:15:11 2 74 | logDefer 2019/01/19 21:15:11 3 75 | logDefer 2019/01/19 21:15:11 4 76 | logDefer 2019/01/19 21:15:11 5 77 | logDefer 2019/01/19 21:15:11 6 78 | logDefer 2019/01/19 21:15:11 7 79 | logDefer 2019/01/19 21:15:11 8 80 | logDefer 2019/01/19 21:15:11 9 81 | logDefer 2019/01/19 21:15:11 -- FUNCTION one ------ 82 | logDefer 2019/01/19 21:15:11 ---- FUNCTION two 83 | logDefer 2019/01/19 21:15:11 10 84 | logDefer 2019/01/19 21:15:11 9 85 | logDefer 2019/01/19 21:15:11 8 86 | logDefer 2019/01/19 21:15:11 7 87 | logDefer 2019/01/19 21:15:11 6 88 | logDefer 2019/01/19 21:15:11 5 89 | logDefer 2019/01/19 21:15:11 4 90 | logDefer 2019/01/19 21:15:11 3 91 | logDefer 2019/01/19 21:15:11 2 92 | logDefer 2019/01/19 21:15:11 1 93 | logDefer 2019/01/19 21:15:11 FUNCTION two ------ 94 | ``` 95 | -------------------------------------------------------------------------------- /eBook/chapter2/02.5.md: -------------------------------------------------------------------------------- 1 | ## defer 关键字 2 | 3 | `defer`关键字将函数的执行推迟到周围的函数返回之前,这在文件输入和输出操作中被广泛使用,因为它使你不必记住何时关闭打开的文件。使用`defer`关键字,可以将关闭已打开文件的函数调用放在打开该文件的函数调用附近。在第 8 章“告诉 UNIX 系统该做什么”中你将学习`defer`在与文件相关的操作,在本节将介绍`defer`的两种不同用法。你还将在讨论`panic()`和`recover()`内置 Go 函数的部分以及与日志记录相关的部分中看到`defer`。 4 | 5 | 重要的是要记住,延迟函数在返回周围函数后以后进先出(LIFO)的顺序执行。简而言之,这意味着如果在同一个周围函数中先延迟函数`f1()`,然后再延迟函数`f2()`,然后再延迟函数`f3()`,则当周围函数即将返回时,将执行函数`f3()`,然后`f2()`,最后才是`f3()`。 6 | 7 | 由于对`defer`的定义尚不清楚,我认为你可以通过查看 Go 代码和`defer.go`程序的输出来更好地理解`defer`的用法,该代码将分为三部分。 8 | 9 | 第一部分的代码: 10 | 11 | ```Go 12 | package main 13 | import ( 14 | "fmt" 15 | ) 16 | func d1() { 17 | for i := 3; i > 0; i-- { 18 | defer fmt.Print(i, " ") 19 | } 20 | } 21 | ``` 22 | 23 | 除了 import 块,前面的 Go 代码还实现了一个名为`d1()`的函数。包含有`for`循环和`defer`语句,该语句将执行 3 次。 24 | 25 | 第二部分的代码: 26 | 27 | ```Go 28 | func d2() { 29 | for i := 3; i > 0; i-- { 30 | defer func() { 31 | fmt.Print(i, " ") 32 | }() 33 | } 34 | fmt.Println() 35 | } 36 | ``` 37 | 38 | 在这部分代码中,你可以看到另一个函数的实现,该函数名为`d2()`。`d2()`函数还包含一个`for`循环和一个`defer`语句,它们还将执行 3 次。但是,这一次,将 defer 关键字应用于匿名函数,而不是单个`fmt.Print()`语句。此外,匿名函数不带任何参数。 39 | 40 | 最后一部分的代码: 41 | 42 | ```Go 43 | func d3() { 44 | for i := 3; i > 0; i-- { 45 | defer func(n int) { 46 | fmt.Print(n, " ") 47 | }(i) 48 | } 49 | } 50 | 51 | func main() { 52 | d1() 53 | d2() 54 | fmt.Println() 55 | d3() 56 | fmt.Println() 57 | } 58 | ``` 59 | 60 | 除了调用`d1(),d2()和d3()`函数的`main()`函数之外,你还可以看到`d3()`函数的实现,该函数具有一个`for`循环,该循环使用匿名的`defer`关键字功能。但是,这一次,匿名函数需要一个名为`n`的整数参数。Go 代码告诉我们,`n`参数从`for`循环中使用的`i`变量中获取其值。 61 | 62 | 执行`defer.go`将会得的输出: 63 | 64 | ```shell 65 | $ go run defer.go 66 | 1 2 3 67 | 0 0 0 68 | 1 2 3 69 | ``` 70 | 71 | 你很可能会发现生成的输出非常复杂且难以理解,这证明如果代码不清晰,则操作和使用`defer`的结果可能会很棘手。 72 | 73 | 我们将从`d1()`函数生成的第一行输出`(1 2 3)`开始。`d1()`中`i`的值依次为`3、2和1`。 `d1()`中延迟的函数是 fmt.Print()语句;结果,当`d1()`函数将要返回时,你将以相反的顺序获得 for 循环的 i 变量的三个值,因为延迟的函数以 LIFO 顺序执行。 74 | 75 | 现在,让我解释一下`d2()`函数产生的第二行输出。我们得到三个零而不是`1 2 3`真是奇怪。但是,原因很简单。 76 | 77 | 在`for`循环结束之后,`i`的值为 0,因为正是`i`的值使`for`循环终止。但是,这里的棘手之处在于,延迟的匿名函数在 for 循环结束后进行计算, 因为该匿名函数没有参数。这意味着对于`i`值为 0 进行了三次计算,因此生成了输出。这种令人困惑的代码可能会导致在你的项目中创建烦人的错误,因此请尽量避免写这种代码。 78 | 79 | 最后,我们将讨论输出的第三行,它是由`d3()`函数生成的。由于匿名函数有参数,每次推迟匿名函数时,它都会获取并因此使用`i`的当前值。结果,匿名函数的每次执行都需要处理不同的值,因此产生的输出也不同。 80 | 81 | 所以你应该清楚,使用`defer`的最佳方法是第三种方法,它出现在`d3()`函数中,这种方法以易于理解的方式在匿名函数中传递了所需的变量。 82 | -------------------------------------------------------------------------------- /eBook/chapter2/02.6.1.md: -------------------------------------------------------------------------------- 1 | ### 单独使用 Panic 函数 2 | 3 | 你也可以单独使用`panic()`函数,而无需和**recover**配合,该小节将使用`justPanic.go`的 Go 代码显示其结果,该代码分为两部分。 4 | 5 | 第一部分代码: 6 | 7 | ```Go 8 | package main 9 | import ( 10 | "fmt" 11 | "os" 12 | ) 13 | ``` 14 | 15 | 使用`panic()`不要求引入任何的 Go package。 16 | 17 | 第二部分代码: 18 | 19 | ```Go 20 | func main() { 21 | if len(os.Args) == 1 { 22 | panic("Not enough arguments!") 23 | } 24 | fmt.Println("Thanks for the argument(s)!") 25 | } 26 | ``` 27 | 28 | 如果你的 Go 程序没有接收一个命令行参数,它将调用`panic()`函数。`panic()`函数需要一个参数,这是你要在屏幕上打印的错误消息。 29 | 30 | 在 macOS Mojave 机器上执行`justPanic.go`将输出: 31 | 32 | ```shell 33 | $ go run justPanic.go 34 | panic: Not enough arguments! 35 | goroutine 1 [running]: 36 | main.main() 37 | /Users/mtsouk/ch2/code/justPanic.go:10 +0x91 38 | exit status 2 39 | ``` 40 | 41 | 因此,单独使用`panic()`函数将终止 Go 程序,而不会给你处理异常机会。因此,组合使用`panic()`和`recovery()`比仅使用`panic()`更实用且显得专业。 42 | 43 | > Tip: `panic()`函数的输出类似于日志包中`Panic()`函数的输出。但是,`panic()`函数不会将任何内容发送到 UNIX 计算机的日志记录服务。 44 | -------------------------------------------------------------------------------- /eBook/chapter2/02.6.md: -------------------------------------------------------------------------------- 1 | ## Panic 和 Recover 2 | 3 | 本节将向你介绍上一章中首先提到的技巧。该技术涉及`panic()`和`recover()`函数的使用,将在`panicRecover.go`中进行介绍,同样分为三部分。 4 | 5 | 第一段代码: 6 | 7 | ```Go 8 | package main 9 | import ( 10 | "fmt" 11 | ) 12 | 13 | func a() { 14 | fmt.Println("Inside a()") 15 | defer func() { 16 | if c := recover(); c != nil { 17 | fmt.Println("Recover inside a()!") 18 | } 19 | }() 20 | fmt.Println("About to call b()") 21 | b() 22 | fmt.Println("b() exited!") 23 | fmt.Println("Exiting a()") 24 | } 25 | ``` 26 | 27 | 除了`import`,此部分还包括`a()`函数的实现。函数`a()`的最重要部分是延迟代码块,该代码块实现了一个匿名函数,当调用`panic()`时将调用该匿名函数。 28 | 29 | 第二段代码: 30 | 31 | ```Go 32 | func b() { 33 | fmt.Println("Inside b()") 34 | panic("Panic in b()!") 35 | fmt.Println("Exiting b()") 36 | } 37 | ``` 38 | 39 | 最后一段代码: 40 | 41 | ```Go 42 | func main() { 43 | a() 44 | fmt.Println("main() ended!") 45 | } 46 | ``` 47 | 48 | 执行`panicRecover.go`产生如下输出: 49 | 50 | ```shell 51 | $ go run panicRecover.go 52 | Inside a() 53 | About to call b() 54 | Inside b() 55 | Recover inside a()! 56 | main() ended! 57 | ``` 58 | 59 | 这个输出结果让人有点惊讶。因为,从输出中可以看到,`a()`函数没有正常结束,它的最后两个语句没有得到执行: 60 | 61 | ```Go 62 | fmt.Println("b() exited!") 63 | fmt.Println("Exiting a()") 64 | ``` 65 | 66 | 不过,好在`panicRecover.go`程序会按我们的意愿结束而不会发生崩溃,因为在`defer`中使用的匿名函数控制了异常情况。还要注意,函数`b()`对函数`a()`一无所知;但是,函数 67 | `a()`包含处理函数`b()`异常情况的 Go 代码。 68 | -------------------------------------------------------------------------------- /eBook/chapter2/02.7.1.md: -------------------------------------------------------------------------------- 1 | ### strace 2 | 3 | `strace(1)`命令行实用程序允许你跟踪系统调用和信号。由于`strace(1)`仅适用于 Linux 计算机,因此本节将使用 Debian Linux 计算机展示`strace(1)`。 4 | 5 | `strace(1)`生成的输出如下所示: 6 | 7 | ```shell 8 | $ strace ls 9 | execve("/bin/ls", ["ls"], [/* 15 vars */]) = 0 10 | brk(0) = 0x186c000 11 | fstat(3, {st_mode=S_IFREG|0644, st_size=35288, ...}) = 0 12 | ``` 13 | 14 | `strace(1)`输出显示每个系统调用及其参数和返回值。请注意,在 UNIX 世界中,返回值为 0 是一件好事。 15 | 16 | 为了处理二进制文件,你需要将`strace(1)`命令放在要处理的可执行文件的前面。但是,你将需要自己解释输出,以便从中得出有用的结论。好消息是,像`grep(1)`这样的工具可以为你提供你真正想要的输出: 17 | 18 | ```shell 19 | $ strace find /usr 2>&1 | grep ioctl 20 | ioctl(0, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or 21 | TCGETS, 0x7ffe3bc59c50) = -1 ENOTTY (Inappropriate ioctl for device) 22 | ioctl(1, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or 23 | TCGETS, 0x7ffe3bc59be0) = -1 ENOTTY (Inappropriate ioctl for device) 24 | ``` 25 | 26 | 与`-c`命令行选项一起使用时,`strace(1)`工具可以为每个系统调用打印计数时间,调用信息和错误信息: 27 | 28 | ```shell 29 | $ strace -c find /usr 1>/dev/null 30 | % time seconds usecs/call calls errors syscall 31 | ------ ----------- ----------- --------- --------- ------------- 32 | 82.88 0.063223 2 39228 getdents 33 | 16.60 0.012664 1 19587 getdents 34 | 0.16 0.000119 0 19618 13 open 35 | ``` 36 | 37 | 由于普通程序输出以标准输出打印,而`strace(1)`的输出以标准错误打印,因此上一条命令将丢弃所检查命令的输出,并显示`strace(1)`的输出。从输出的最后一行可以看到,`open(2)`系统调用被调用了 19,618 次,产生了 13 个错误,并且花费了整个命令执行时间的 0.16%大约 0.000119 秒。 38 | -------------------------------------------------------------------------------- /eBook/chapter2/02.7.2.md: -------------------------------------------------------------------------------- 1 | ## dtrace 2 | 3 | 尽管诸如`strace(1)`和`truss(1)之类的调试实用程序可以跟踪进程产生的系统调用,但是它们可能很慢,因此不适合解决繁忙的 UNIX 系统上的性能问题。另一个名为**DTrace**的工具可让你在系统范围内查看幕后发生的情况,而无需修改或重新编译任何内容。它还使你可以在生产环境上工作,并动态监视正在运行的程序或服务器进程,同时又不会造成很大的开销。 4 | 5 | > Tip: 尽管有`dtrace(1)`有 Linux 版本,但`dtrace(1)`工具在 macOS 和其他 FreeBSD 变体上效果最佳。 6 | 7 | 本小节将使用 macOS 自带的`dtruss(1)`命令行实用程序,它只是一个`dtrace(1)`脚本,该脚本显示了进程的系统调用,使我们不必编写`dtrace(1)`代码。请注意,`dtrace(1)和`dtruss(1)`都需要 root 权限才能运行。 8 | 9 | ```shell 10 | $ sudo dtruss godoc 11 | ioctl(0x3, 0x80086804, 0x7FFEEFBFEC20) = 0 0 12 | close(0x3) = 0 0 13 | access("/AppleInternal/XBS/.isChrooted\0", 0x0, 0x0) = -1 Err#2 14 | thread_selfid(0x0, 0x0, 0x0) = 1895378 0 15 | geteuid(0x0, 0x0, 0x0) = 0 0 16 | getegid(0x0, 0x0, 0x0) = 0 0 17 | ``` 18 | 19 | `dtruss(1)`的工作方式与`strace(1)`实用程序相同。与`strace(1)`类似,当与`-c`参数一起使用时,`dtruss(1)`将打印系统调用计数: 20 | 21 | ```shell 22 | $ sudo dtruss -c go run unsafe.go 2>&1 23 | CALL COUNT 24 | access 1 25 | bsdthread_register 1 26 | getuid 1 27 | ioctl 1 28 | issetugid 1 29 | kqueue 1 30 | write 1 31 | mkdir 2 32 | read 244 33 | kevent 474 34 | fcntl 479 35 | lstat64 553 36 | psynch_cvsignal 649 37 | psynch_cvwait 654 38 | ``` 39 | 40 | 上面的输出将迅速告知你 Go 代码中的潜在瓶颈,甚至可以帮你比较两个不同的命令行程序的性能。 41 | 42 | > Tip: 诸如`strace(1)`,dtrace(1)和 dtruss(1)`之类的实用工具需要一段时间适应熟悉,但是这样的工具可以使我们的生活变得更加轻松和舒适,我强烈建议你立即开始学习至少一个这样的工具。 43 | 44 | 你可以阅读 Brendan Gregg 和 Jim Mauro 写的*DTrace: Dynamic Tracing in Oracle Solaris, Mac OS X*和*FreeBSD* ,或访问http://dtrace.org/,以了解有关`dtrace(1)`实用程序的更多信息。 45 | 46 | 虽然,`dtrace(1)`比`strace(1)`功能强大得多,因为它具有自己的编程语言。但是,当你要做的只是监视可执行文件的系统调用时,`strace(1)`的用途更加广泛。 47 | -------------------------------------------------------------------------------- /eBook/chapter2/02.7.md: -------------------------------------------------------------------------------- 1 | ## 两个好用的 UNIX 工具 2 | 3 | 有时 UNIX 程序由于某种未知原因而失败或无法正常运行,并且你想找出原因,但不想重写代码并添加大量调试语句。 4 | 5 | 本节将介绍两个命令行实用程序,使你可以查看由可执行文件执行的 C 系统调用。这两个工具的名称分别为`strace()`和`dtrace()`,它们使你可以检查程序的运行情况。 6 | 7 | > Tip: 请记住,所有在 UNIX 计算机上运行的程序最终都将使用 C 系统调用来与 UNIX 内核进行通信并执行大部分任务。 8 | 9 | 尽管这两个工具都可以使用`go run`命令,但是如果你首先使用`go build`创建可执行文件并使用该文件,则得到的无关输出会更少。发生这种情况的主要原因是`go run`在实际运行 Go 代码之前会生成各种临时文件。 10 | -------------------------------------------------------------------------------- /eBook/chapter2/02.8.md: -------------------------------------------------------------------------------- 1 | ## 配置 Go 开发环境 2 | 3 | 本节将讨论使用`runtime` package 的功能和属性查找有关当前 Go 环境的信息。本节中将程序命名为`goEnv.go`,它将分为两部分。 4 | 5 | 第一部分: 6 | 7 | ```Go 8 | package main 9 | import ( 10 | "fmt" 11 | "runtime" 12 | ) 13 | ``` 14 | 15 | `runtime` package 包含获取 runtime 信息的函数和属性。 `goEnv.go`的第二个代码部分包含`main()`函数的实现: 16 | 17 | ```Go 18 | func main() { 19 | fmt.Print("You are using ", runtime.Compiler, " ") 20 | fmt.Println("on a", runtime.GOARCH, "machine") 21 | fmt.Println("Using Go version", runtime.Version()) 22 | fmt.Println("Number of CPUs:", runtime.NumCPU()) 23 | fmt.Println("Number of Goroutines:", runtime.NumGoroutine()) 24 | } 25 | ``` 26 | 27 | 在装有 Go 1.11.4 的 macOS Mojave 机器上执行`goEnv.go`将输出: 28 | 29 | ```shell 30 | $ go run goEnv.go 31 | You are using gc on a amd64 machine 32 | Using Go version go1.11.4 33 | Number of CPUs: 8 34 | Number of Goroutines: 1 35 | ``` 36 | 37 | 同样的代码在装有 Go 1.3.3 的 Debian Linux 机器上输出如下: 38 | 39 | ```shell 40 | $ go run goEnv.go 41 | You are using gc on a amd64 machine 42 | Using Go version go1.3.3 43 | Number of CPUs: 1 44 | Number of Goroutines: 4 45 | ``` 46 | 47 | 在名为`requiredVersion.go`的程序中说明了获取到有关 Go 环境的信息有什么用处,该程序会告诉你是否使用的是 Go1.8 或更高版本: 48 | 49 | ```Go 50 | package main 51 | import ( 52 | "fmt" 53 | "runtime" 54 | "strconv" 55 | "strings" 56 | ) 57 | func main() { 58 | myVersion := runtime.Version() 59 | major := strings.Split(myVersion, ".")[0][2] 60 | minor := strings.Split(myVersion, ".")[1] 61 | m1, _ := strconv.Atoi(string(major)) 62 | m2, _ := strconv.Atoi(minor) 63 | if m1 == 1 && m2 < 8 { 64 | fmt.Println("Need Go version 1.8 or higher!") 65 | return 66 | } 67 | fmt.Println("You are using Go version 1.8 or higher!") 68 | } 69 | ``` 70 | 71 | `strings`Go 标准包用于拆分从`runtime.Version()`获得的 Go 版本字符串,以获取其前两个部分,而 `strconv.Atoi()`函数用于将字符串转换为整数。 72 | 73 | 在 macOS Mojave 机器上执行`requiredVersion.go`将输出: 74 | 75 | ```shell 76 | $ go run requiredVersion.go 77 | You are using Go version 1.8 or higher! 78 | ``` 79 | 80 | 但如果你在 Debian Linux 机器上运行`requiredVersion.go`,它将输出: 81 | 82 | ```shell 83 | $ go run requiredVersion.go 84 | Need Go version 1.8 or higher! 85 | ``` 86 | 87 | 因此,通过调用程序`requiredVersion.go`,你能够确定 UNIX 计算机是否具有所需的 Go 版本。 88 | 89 | 90 | -------------------------------------------------------------------------------- /eBook/chapter2/02.9.md: -------------------------------------------------------------------------------- 1 | ## go env 命令 2 | 3 | 如果需要获取 Go 和 Go 编译器支持的所有环境变量及其当前值的列表,你可以执行`go env`。 4 | 5 | 在使用 Go1.11.4 的 macOS Mojave 上,`go env`的功能非常丰富,如下所示: 6 | 7 | ```shell 8 | $ go env 9 | GOARCH="amd64" 10 | GOBIN="" 11 | GOCACHE="/Users/mtsouk/Library/Caches/go-build" 12 | GOEXE="" 13 | GOFLAGS="" 14 | GOHOSTARCH="amd64" 15 | GOHOSTOS="darwin" 16 | GOOS="darwin" 17 | GOPATH="/Users/mtsouk/go" 18 | GOPROXY="" 19 | GORACE="" 20 | GOROOT="/usr/local/Cellar/go/1.11.4/libexec" 21 | GOTMPDIR="" 22 | GOTOOLDIR="/usr/local/Cellar/go/1.11.4/libexec/pkg/tool/darwin_amd64" 23 | GCCGO="gccgo" 24 | CC="clang" 25 | CXX="clang++" 26 | CGO_ENABLED="1" 27 | GOMOD="" 28 | CGO_CFLAGS="-g -O2" 29 | CGO_CPPFLAGS="" 30 | CGO_CXXFLAGS="-g -O2" 31 | CGO_FFLAGS="-g -O2" 32 | CGO_LDFLAGS="-g -O2" 33 | PKG_CONFIG="pkg-config" 34 | GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused- 35 | arguments -fmessage-length=0 -fdebug-prefix- 36 | map=/var/folders/sk/ltk8cnw50lzdtr2hxcj5sv2m0000gn/T/go- 37 | build790367620=/tmp/go-build -gno-record-gcc-switches -fno-common" 38 | ``` 39 | 40 | 请注意,如果你使用其他 Go 版本,用户名不是`mtsouk`,在其他硬件上使用其他 UNIX 变体或在使用 Go 模块(GOMOD)时,其中某些环境变量可能会不是默认值。 41 | -------------------------------------------------------------------------------- /eBook/chapter3/03.0.md: -------------------------------------------------------------------------------- 1 | ### Go基本的数据类型 2 | 3 | 前面的章节讨论了很多有趣迷人的话题,这其中包括Go的垃圾回收机制,`panic()、recover()`函数的使用,不安全包,如何使用Go调用C代码以及如何使用C程序调用Go代码,还包含了Go编译器在编译Go时形成的程序树的介绍。 4 | 5 | 本章的主要话题是Go的基本数据类型,这包括**数值类型,数组,切片,和映射(maps)**. 尽管它们很简单,但是这些数据类型可以帮助你进行数值计算。你还可以以一种非常方便快捷的方式存储、检索和更改程序的数据。这一章还包括**指针、常量、循环**以及如何在**Go中处理日期和时间**。 6 | 7 | 8 | 本章你将会学习的主题: 9 | 10 | 11 | - 数值类型 12 | - Go数组 13 | - Go切片以及为什么切片比数组要好 14 | - 如何在已经存在的切片中加入数组 15 | - Go映射 16 | - Go指针 17 | - Go循环 18 | - Go常量 19 | - 使用时间工作 20 | - 测量命令和函数的执行时间 21 | - 操作日期 -------------------------------------------------------------------------------- /eBook/chapter3/03.1.1.md: -------------------------------------------------------------------------------- 1 | # **整数** 2 | Go支持四种不同大小的有符号整数和无符号整数,分别是`int8、int16、int32`和`int64`, 以及`uint8、uint16、uint32、uint64`。每种类型末尾的数字显示用于表示每种类型的位数。 3 | 4 | 另外,对于当前运算平台,`int`和`uint`分别代表最有效的有符号整数和无符号整数。因此,如果有疑问,可以使用`int`和`uint`,但要记住这些类型的大小取决于计算机结构。 5 | 6 | 有符号整数和无符号整数之间的区别如下:如果一个整数有8位并且没有符号,那么它的值可以是二进制的`00000000(0)`到二进制的`11111111(255)`。如果它有一个符号,那么它的值可以是-128 (```原文是-127,应该不对 补码10000000是-128```)到127。这意味着你有7个二进制数字来存储你的数字,因为第8位用于保存整数的符号。同样的规则也适用于其他大小的无符号整数。 -------------------------------------------------------------------------------- /eBook/chapter3/03.1.2.md: -------------------------------------------------------------------------------- 1 | # **浮点数** 2 | 3 | Go只支持两种类型的浮点数:`float32、float64`。第一个提供了大约小数点后6位的精度,而第二个提供了15位精度。 -------------------------------------------------------------------------------- /eBook/chapter3/03.1.3.md: -------------------------------------------------------------------------------- 1 | # **复数** 2 | 3 | 和浮点数相似,Go提供了两种复数类型:`complex64`和`complex128`。第一个使用两个32位浮点数:一个用于实部,另一个用于复数的虚部,而`complex128`使用两个64位浮点数。复数以`a + bi`的形式表示,其中 `a`和 `b` 是实数,i 是方程 $x^2 =−1$ 的解。 4 | 5 | 所有这些数字数据类型都在`numbers.go`中分为三个部分进行说明。 6 | 7 | 第一部分的代码如下: 8 | ```Go 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | ) 14 | 15 | func main() { 16 | c1 := 12 + 1i 17 | c2 := complex(5, 7) 18 | fmt.Printf("Type of c1: %T\n", c1) 19 | fmt.Printf("Type of c2: %T\n", c2) 20 | var c3 complex64 = complex64(c1 + c2) 21 | fmt.Println("c3:", c3) 22 | fmt.Printf("Type of c3: %T\n", c3) 23 | cZero := c3 - c3 24 | fmt.Println("cZero:", cZero) 25 | ``` 26 | 27 | 这部分使用了一些复数进行计算。有两种方法去创建复数:与`c1`和`c2`一样直接创建, 或间接地通过计算现有的复数得到,例如`c3`和`cZero`。 28 | 29 | >Tip: 如果你错误地尝试将一个复数创建为 `aComplex:= 12 + 2 * i`,那么将会有两种可能的结果,因为这个语句告诉Go你想要执行一个加法和一个乘法。如果当前作用域中没有名为i的数值变量,该语句会出现语法错误,你的Go代码编译将失败。但是,如果已经定义了一个名为i的数值变量,那么计算将会成功,但是你将不会得到 需的复数(**bug**)。 30 | 31 | 第二部分代码如下: 32 | 33 | ```Go 34 | x := 12 35 | k := 5 36 | fmt.Println(x) 37 | fmt.Printf("Type of x: %T\n", x) 38 | div := x / k 39 | fmt.Println("div", div) 40 | ``` 41 | 42 | 在这一部分中,我们使用带符号的整数。请注意,如果你想对两个整数做除法,Go认为你想得到整数答案并将计算返回整数除法的**商**。11除以2得到的是整数5而不是5.5。 43 | 44 | >Tip: 当你将浮点数转换为整数时,浮点数的分数将被丢弃且被截断为零,这意味着一些数据可能会在处理过程中丢失。 45 | 46 | 最后一部分代码: 47 | 48 | ```GO 49 | var m, n float64 50 | m = 1.223 51 | fmt.Println("m, n:", m, n) 52 | y := 4 / 2.3 53 | fmt.Println("y:", y) 54 | divFloat := float64(x) / float64(k) 55 | fmt.Println("divFloat", divFloat) 56 | fmt.Printf("Type of divFloat: %T\n", divFloat) 57 | } 58 | ``` 59 | 在程序的最后一部分中,我们将使用浮点数。在做除法时, 你可以看到如何使用```float64()```来告诉Go去创建一个```floating-point number```。如果你只是使用 ```divFloat: = float64 (x) / k```, 然后运行代码时你会得到以下错误消息: 60 | 61 | ```bash 62 | $ go run numbers.go 63 | # command-line-arguments 64 | ./numbers.go:35:25: invalid operation: 65 | float64(x) / k (mismatched types float64 and int) 66 | ``` 67 | 执行 ```numbers.go```,结果输出如下: 68 | 69 | ```bash 70 | Type of c1: complex128 71 | Type of c2: complex128 72 | c3: (17+8i) 73 | Type of c3: complex64 74 | cZero: (0+0i) 75 | 12 76 | Type of x: int 77 | div 2 78 | m, n: 1.223 0 79 | y: 1.7391304347826086 80 | divFloat 2.4 81 | Type of divFloat: float64 82 | ``` -------------------------------------------------------------------------------- /eBook/chapter3/03.1.4.md: -------------------------------------------------------------------------------- 1 | # **for循环代码示例** 2 | 3 | 在撰写本文时,有人建议对Go处理数值字面量(number literals)的方式进行更改。数值字面量与你在编程语言中定义和使用数字的方式有关。这个特别的建议与二进制整数字面量、八进制整数字面量、数字分隔符和十六进制浮点数有关。你可以在网站上找到更多关于Go 2和数值字面量的信息 https://golang.org/design/19308-number-literals。 4 | 5 | >Tip: 开发人员维护了详细发布页面,你可以查看 https://dev.golang.org/release。 -------------------------------------------------------------------------------- /eBook/chapter3/03.1.md: -------------------------------------------------------------------------------- 1 | # **Go数值类型** 2 | 3 | Go原生支持整数、浮点数和复数. 接下来的小节将深入介绍每一个Go所支持的数值类型. 4 | 5 | 6 | -------------------------------------------------------------------------------- /eBook/chapter3/03.10.md: -------------------------------------------------------------------------------- 1 | # **本章小结** 2 | 3 | 本章你学习了很多有趣的Go知识,包括映射(map)、数组、切片、指针、常量、循环以及Go处理时间与日期的技巧。学到现在,你应该能够理解为什么切片要优于数组。 4 | 5 | 下一章将介绍构建与使用组合类型的知识,主要是使用`struct`关键字创建的结构体,之后会讨论`string`变量和**元组**。 6 | 7 | 另外,下一章还会涉及到正则表达式和模式匹配,这是一个比较tricky的主题,不仅针对Go,对其他所有语言都是一样的,合理地使用正则表达式能够大大简化你的工作,是非常值得学习一下的。 8 | 9 | JSON是一种非常流行的文本格式,因此下一章还将讨论如何在Go中创建、导入和导出JSON数据。 10 | 11 | 你还会了解关于`switch`关键字的知识,以及使用`strings`包处理**UTF-8**字符串的技巧。 12 | 13 | -------------------------------------------------------------------------------- /eBook/chapter3/03.2.1.md: -------------------------------------------------------------------------------- 1 | # **for循环** 2 | for循环是编程中最常见的循环,它允许你迭代指定次数,或者在for循环开始的时候计算一个初始值然后只要条件满足就一直迭代下去,这些值可以是切片、数组、映射等的长度。这意味着使用for循环是获取一个数组、切片或者映射的所有元素的最通用的方式。另外一个方式就是使用range关键字。 3 | 4 | 下面的代码展示了一个for循环的最简单的使用方式,让一个变量在一定范围内进行遍历: 5 | 6 | ```go 7 | for i := 0; i < 100; i++ { 8 | } 9 | ``` 10 | 11 | 在上面的循环中,i的取值将是从0到99。一旦i达到了100,for循环将终止执行。在这段代码中,i是一个本地的临时变量,这意味着在for循环的执行终止之后,i会在未来某个时刻被垃圾回收从而消失。然而,如果i是在for循环之外定义的,那它将在for循环之后继续存在,并且它的值将是100。 12 | 13 | 你可以使用break关键字来退出for循环。break关键字同时也允许你创建一个不使用像 $i < 100$ 的跳出条件,因为终止条件可以通过`break`放在for循环内部的代码块中。在for循环中允许有多个退出循环的地方。 14 | 15 | 此外,你可以使用`continue`关键字在for循环中跳过一次循环。`continue`关键字会停止执行当前的代码块,跳回顶部的for循环,然后继续进行下一次循环。 -------------------------------------------------------------------------------- /eBook/chapter3/03.2.2.md: -------------------------------------------------------------------------------- 1 | # **while 循环** 2 | 正如先前提到的,Go没有提供while关键字来进行循环,但是它允许你使用for循环来替代while循环。这一节将用两个例子来展示for循环是怎么替代while循环的工作的。 3 | 4 | 下面是一个典型的你会写`while(true)`的例子: 5 | 6 | ```go 7 | for { 8 | } 9 | ``` 10 | 11 | 注意此时开发者需要使用`break`关键字来退出这个for循环! 12 | 13 | 此外,for循环还能模拟一个在其他编程语言中会出现的`do...while`循环。下面的代码给了一个Go语言中等价于`do...while(anExpression)`的操作: 14 | 15 | ```go 16 | for ok:=true; ok; ok = anExpression { 17 | } 18 | ``` 19 | 20 | 一旦变量ok的值是false,则for循环终止执行。 21 | 22 | Go中也有通过指定 `for condition {}`的循环,只要条件成立for循环就会一直执行。 23 | -------------------------------------------------------------------------------- /eBook/chapter3/03.2.3.md: -------------------------------------------------------------------------------- 1 | # **range关键字** 2 | Go同时提供了`range`关键字,它能配合for循环以帮助你在遍历Go的数据类型时写出更易于理解的代码。 3 | 4 | `range`关键字的主要优势在于并不需要知道一个切片或者字典的长度就可以一个接一个的对它们的元素进行操作。稍后你会看到`range`的示例。 -------------------------------------------------------------------------------- /eBook/chapter3/03.2.4.md: -------------------------------------------------------------------------------- 1 | # **for循环代码示例** 2 | 这一节将展示几个for循环的例子。这个代码示例的文件名是`loops.go`, 将被分为四部分进行展示。第一部分如下: 3 | 4 | ```Go 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | ) 10 | 11 | func main() { 12 | for i := 0; i < 100; i++ { 13 | if i%20 == 0 { 14 | continue 15 | } 16 | if i == 95 { 17 | break 18 | } 19 | fmt.Print(i, " ") 20 | ``` 21 | 22 | 上面的代码展示了一个典型的for循环的例子以及关键字`continue`和`break`的使用。 23 | 24 | 接下来的代码片段是: 25 | 26 | ```go 27 | fmt.Println() 28 | i := 10 29 | for { 30 | if i < 0 { 31 | break 32 | } 33 | fmt.Print(i, " ") 34 | i-- 35 | } 36 | fmt.Println() 37 | ``` 38 | 39 | 上面的代码模拟了一个典型的while循环。注意使用了关键字break来退出for循环。 40 | 41 | 第三段代码如下: 42 | 43 | ```go 44 | i = 0 45 | anExpression := true 46 | for ok := true; ok; ok = anExpression { 47 | if i > 10 { 48 | anExpression = false 49 | } 50 | fmt.Print(i, " ") 51 | i++ 52 | } 53 | fmt.Println() 54 | ``` 55 | 56 | 在这段代码中,你能看到前面所说的使用for循环来进行`do...while`循环的工作的操作。注意这种for循环很难读。 57 | 58 | ```loops.go```的最后一部分展示如下: 59 | 60 | ```go 61 | anArray := [5]int{0, 1, -1, 2, -2} 62 | for i, value := range anArray { 63 | fmt.Println("index:", i, "value: ", value) 64 | } 65 | } 66 | ``` 67 | 68 | 对一个数组使用`range`关键字会返回两个值:一个是数组的索引,一个是该索引上的值。你可以两个值都使用,或者使用其中一个,甚至如果你只是想单纯的计算一下数组的元素个数的话,也可以两个值都不使用。 69 | 70 | 执行`loops.go`会产生如下的输出: 71 | 72 | ```bash 73 | $ go run loops.go 74 | 75 | 1 2 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 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 81 82 83 84 85 86 87 88 89 90 91 92 93 94 76 | 10 9 8 7 6 5 4 3 2 1 0 77 | 0 1 2 3 4 5 6 7 8 9 10 11 78 | index: 0 value: 0 79 | index: 1 value: 1 80 | index: 2 value: -1 81 | index: 3 value: 2 82 | index: 4 value: -2 83 | ``` 84 | -------------------------------------------------------------------------------- /eBook/chapter3/03.2.md: -------------------------------------------------------------------------------- 1 | # **Go循环** 2 | 每个编程语言都有一种进行循环的方式,Go也不例外。Go提供了for循环,用来对多种数据类型进行遍历。 3 | 4 | > Go没有提供while关键字。但是,Go的for循环语句完全可以替代while循环。 5 | 6 | -------------------------------------------------------------------------------- /eBook/chapter3/03.3.2.md: -------------------------------------------------------------------------------- 1 | # **Go数组的缺点** 2 | 3 | Go数组有很多缺点,这将使你重新考虑在Go项目中使用它们。首先,一旦你定义了一个数组,你就不能改变它的大小,这意味着Go数组不是动态的。简单地说,如果需要将一个元素添加到一个已经没有空间的现有数组中,那么我们需要创建一个更大的数组,并将旧数组中的所有元素复制到新数组中。另外,当将数组作为参数传递给函数时,实际上传递的是数组的一个副本,这意味着对函数内部数组所做的任何更改都将在函数退出后丢失。最后,将一个大数组传递给一个函数可能会非常慢,这主要是因为Go必须创建一个数组的副本。所有这些问题的解决方案是使用Go切片,这将在下一节中介绍。 4 | 5 | >Tip: 由于它们的缺点,在Go中很少使用数组! 6 | -------------------------------------------------------------------------------- /eBook/chapter3/03.3.md: -------------------------------------------------------------------------------- 1 | # **Go数组** 2 | 3 | 数组是最流行的数据结构之一,原因有两个。第一个原因是它简单易懂,而第二个原因是,他们非常多样化,可以存储许多不同种类的数据。我们可以通过如下代码声明一个存储四个整数的数组: 4 | ```go 5 | anArray: = [4]int{1, 2, 4, 4} 6 | ``` 7 | 数组元素类型前的数字表示数组的大小。你可以通过函数:`len(anArray)` 得到一个数组的长度。 8 | 9 | 数组任意维度的第一个元素的索引为**0**;第二个元素的数组的索引为1,以此类推。这意味着一个命名为a的一维数组,有效的索引从 `0` 到 `len(a) - 1`。 10 | 11 | 在其他编程语言中,尽管你可能熟悉访问数组的元素和使用一个for循环以及一个或多个数值变量,但是在Go中有更多访问数组所有元素的惯用方法。它们涉及`range`关键字的使用和允许你不使用`len()`函数。`loops.go`代码中举了这样的例子。 12 | 13 | -------------------------------------------------------------------------------- /eBook/chapter3/03.4.1.md: -------------------------------------------------------------------------------- 1 | # **切片基本操作** 2 | 3 | 使用下面的代码可创建一个切片字面量: 4 | ```go 5 | aSliceLiteral := []int{1, 2, 3, 4, 5} 6 | ``` 7 | 与定义数组相比,**切片字面量**只是没有指定元素数量。如果你在[]中填入数字,你将得到的是数组。 8 | 9 | 也可以使用*make()*创建一个空切片,并指定切片的长度和容量。容量这个参数可以省略,在这种情况下容量等于长度。 10 | 11 | 下面定义一个长度和容量均为20的空切片,并且在其需要的时候会自动扩容: 12 | 13 | ```go 14 | integer := make([]int, 20) 15 | ``` 16 | 17 | Go自动将空切片的元素初始化为对应元素的初始值,意味着切片初始化时的值是由切片类型决定的。 18 | 19 | 使用下面的代码遍历切片中的元素: 20 | 21 | ```go 22 | for i :=0; i < len(integer); i++ { 23 | fmt.Println(i) 24 | } 25 | ``` 26 | 27 | 切片变量的零值是nil, 下面代码可将已有的切片置为空: 28 | 29 | ```go 30 | aSliceLiteral = nil 31 | ``` 32 | 33 | 可以使用**append()**函数追加元素到切片,此操作将触发切片自动扩容。 34 | 35 | ```go 36 | integer = append(integer, 12345) 37 | ``` 38 | 39 | `integer[0]`代表切片integer的第一个元素,`integer[len(integer)-1]`代表最后一个元素。 40 | 41 | 同时,使用`[:]`操作可以获取连续多个元素,下面的代码表示获取第2、3个元素: 42 | 43 | ```go 44 | integer[1:3] 45 | ``` 46 | 47 | `[:]`操作也可以帮助你从现有的切片或数组中创建新的切片或数组: 48 | 49 | ```go 50 | s2 := integer[1:3] 51 | ``` 52 | 53 | 这种操作叫做re-slicing,在某种情况下可能会导致bug: 54 | 55 | ```go 56 | package main 57 | 58 | import "fmt" 59 | 60 | func main() { 61 | ​ s1 := make([]int, 5) 62 | ​ reSlice := s1[1:3] 63 | ​ fmt.Println(s1) 64 | ​ fmt.Println(reSlice) 65 | ​ reSlice[0] = -100 66 | ​ reSlice[1] = 123456 67 | ​ fmt.Println(s1) 68 | ​ fmt.Println(reSlice) 69 | } 70 | ``` 71 | 72 | 我们使用`[:]`操作获取第2、3个元素。 73 | 74 | > Tip: 假设有一个数组a1,你可以执行`s1 := a1[:]` 来创建一个引用a1的切片 75 | 76 | 将上述代码保存为`reslice.go`并执行,将得到以下输出; 77 | 78 | ```bash 79 | $ go run reslice.go 80 | [0 0 0 0 0] 81 | [0 0] 82 | [0 -100 123456 0 0 ] 83 | [-100 123456] 84 | ``` 85 | 86 | 可以看到切片s1的输出是[0 -100 123456 0 0 ],但是我们并没有直接改变s1。这说明通过re-slicing操作得到的切片,与原切片指向同一片内存地址! 87 | 88 | re-slicing操作的第二个问题是,只要较小的重新切片存在,来自原始切片的底层数组就会被保存在内存中,因为较小的重新切片引用了原始切片, 尽管你可能是想通过使用re-slicing从原切片中得到较小的一个切片,这对于小切片来说并不是什么严重问题,但是在这种情况下就可能导致bug:你将大文件的内容读到切片中,但是你只是想使用其中一小部分。 89 | 90 | -------------------------------------------------------------------------------- /eBook/chapter3/03.4.2.md: -------------------------------------------------------------------------------- 1 | # **切片的自动扩容** 2 | 3 | 切片有两个主要的属性:**容量(cap)**和**长度(len)**,而且这两个属性的值往往是不一样的。一个与数组拥有相同数量元素的切片,二者的长度是相同的,且均可以通过函数`len()`获得。切片的容量是指切片能够容纳的元素空间,可以通过`cap()`函数获得。由于切片的大小是动态变化的,如果一个切片超出了其设置的容量,Go会自动将该切片的长度变为原来的两倍,以存放超出的元素。 4 | 5 | 简单来说,当切片的容量和长度相等时,你再往该切片追加一个元素,当然长度增加1,但是切片的容量会变为原来的两倍。然而这种操作可能对于小的切片效果比较好,对于大型切片来说会占用的内存会超出你预期。 6 | 7 | 下面的代码`lenCap.go`分三部分,清楚地阐述了切片的容量与长度的变化规律,第一部分代码: 8 | 9 | ```go 10 | package main 11 | 12 | func printSlice(x []int) { 13 | for _, number := range x { 14 | fmt.Println(number, " ") 15 | } 16 | fmt.Println() 17 | } 18 | ``` 19 | 20 | `printSlice()`打印一个slice的所有元素。 21 | 22 | 第二部分代码: 23 | 24 | ```go 25 | func main() { 26 | aSlice := []int{-1, 0, 4} 27 | fmt.Printf("aSlice: ") 28 | printSlice(aSlice) 29 | 30 | fmt.Printf("Cap: %d, Length: %d\n",cap(aSlice),len(aSlice)) 31 | aSlice = append(aSlice, -100) 32 | fmt.Printf("aSlice: ") 33 | printSlice(aSlice) 34 | fmt.Printf("Cap: %d, Length: %d\n",cap(aSlice),len(aSlice)) 35 | ``` 36 | 这部分代码中,我们往aSlice中添加元素以出发其长度和容量的变化。 37 | 38 | 第三部分代码: 39 | 40 | ```go 41 | aSlice = append(aSlice, -2) 42 | aSlice = append(aSlice, -3) 43 | aSlice = append(aSlice, -4) 44 | printSlice(aSlice) 45 | fmt.Printf("Cap: %d, Length: %d\n",cap(aSlice),len(aSlice)) 46 | } 47 | ``` 48 | 49 | 以上代码的执行结果是: 50 | 51 | ```bash 52 | $ go run lenCap.go 53 | aSlice: -1 0 4 54 | Cap: 3, Length: 3 55 | aSlice: -1 0 4 -100 56 | Cap: 6, Length: 4 57 | -1 0 4 -100 -2 -3 -4 58 | Cap: 12, Length: 7 59 | ``` 60 | 61 | 正如输出所示,初始的切片长度和容量均是3,在添加一个元素之后,其长度变为4,然后容量变为6。继续往切片中追加元素,其长度变为7,容量再一次翻倍即变为12。 -------------------------------------------------------------------------------- /eBook/chapter3/03.4.3.md: -------------------------------------------------------------------------------- 1 | # **byte切片** 2 | 3 | 类型为byte的切片成为字节切片,可通过下面的代码创建一个字节切片: 4 | 5 | ```go 6 | s := make([]byte,5) 7 | ``` 8 | 9 | 字节切片的操作与其他类型的切片并没有什么区别,但是在输入输出中(网络,文件流等)使用的非常多,你将在`第八章`大量使用字节切片。 10 | -------------------------------------------------------------------------------- /eBook/chapter3/03.4.4.md: -------------------------------------------------------------------------------- 1 | # **copy()函数** 2 | 3 | 你可以从现有的数组中创建一个切片,你也可以使用`copy()`函数复制一个切片。然而,`copy()`的用法可能会让你觉得非常奇怪,下面的`copySlice.go`代码将清楚地阐述其使用方法。 4 | 5 | >Tip: 使用copy()时你应该小心,因为内建函数copy(dst,src)会以len(dst)和len(src)中的最小值为复制长度。 6 | 7 | 第一部分代码是: 8 | 9 | ```go 10 | package main 11 | 12 | import "fmt" 13 | 14 | func main() { 15 | a6 := []int{-10, 1, 2, 3, 4, 5} 16 | a4 := []int{-1, -2, -3, -4} 17 | fmt.Println("a6:", a6) 18 | fmt.Printf("a4:", a4) 19 | 20 | copy(a6, a4) 21 | fmt.Println("a6:", a6) 22 | fmt.Printf("a4:", a4) 23 | fmt.Println() 24 | ``` 25 | 26 | 上面的代码,我们定义了两个切片分别是`a6`和`a4`, 打印之后,将`a4`拷贝到`a6`。由于`a6`比`a4`拥有更多的元素,a4中所有的元素将会拷贝至`a6`,`a6`中剩下的两个元素会保持原状。 27 | 28 | 第二部分代码: 29 | 30 | ```go 31 | b6 := []int{-10, 1, 2, 3, 4, 5} 32 | b4 := []int{-1, -2, -3, -4} 33 | fmt.Println("b6:", b6) 34 | fmt.Printf("b4:", b4) 35 | 36 | copy(b4,b6) 37 | fmt.Println("b6:", b6) 38 | fmt.Printf("b4:", b4) 39 | fmt.Println() 40 | ``` 41 | 42 | 在这一部分代码中,由于`a4`的长度为4,所以只有`b6`的前四个元素会拷贝到a4。 43 | 44 | `copySlice.go`的第三部分代码: 45 | 46 | ```go 47 | fmt.Println() 48 | array4 := [4]int{4, -4, 4, -4} 49 | s6 := []int{1,-1,1,-1,-5,5} 50 | 51 | copy(s6, array4[0:]) 52 | fmt.Println("array4:", array4[0:]) 53 | fmt.Printf("s6:", s6) 54 | fmt.Println() 55 | ``` 56 | 57 | 在这部分代码,我们尝试将具有四个元素的数组拷贝到具有6个元素的切片,注意,我们使用`[:]`操作将数组转为切片`(array4[0:])`。 58 | 59 | 最后一部分代码: 60 | 61 | ```go 62 | array5 := [5]int{5,-5,5,-5,5} 63 | s7 := []int{7,7,-7,7,-7,7} 64 | copy(array5[0:], s7) 65 | fmt.Println("array5:", array5) 66 | fmt.Printf("s7:", s7) 67 | fmt.Println() 68 | ``` 69 | 70 | 我们将一个切片拷贝至具有五个元素的切片,由于`copy()`函数只接收切片,所以依然需要[:]操作将数组转为切片。 71 | 72 | 如果你尝试将一个数组拷贝至切片或者将切片拷贝至数组,就会得到下面的编译错误: 73 | 74 | ```bash 75 | ./copySlice.go:37:6: first argument to copy should be slice; have [5]int 76 | ``` 77 | 78 | 最后,给出执行`copySlice.go`的输出: 79 | 80 | ```go 81 | $ go run copySlice.go 82 | a6: [-10 1 2 3 4 5] 83 | a4: [-1 -2 -3 -4] 84 | a6: [-1 -2 -3 -4 4 5] 85 | a4: [-1 -2 -3 -4] 86 | b6: [-10 1 2 3 4 5] 87 | b4: [-1 -2 -3 -4] 88 | b6: [-10 1 2 3 4 5] 89 | b4: [-10 1 2 3] 90 | array4: [4 -4 4 -4] 91 | s6: [4 -4 4 -4 5 -5] 92 | array5: [7 7 -7 -7 7] 93 | s7: [7 7 -7 -7 7 -7 7] 94 | 95 | ``` -------------------------------------------------------------------------------- /eBook/chapter3/03.4.5.md: -------------------------------------------------------------------------------- 1 | # **多维切片** 2 | 3 | 跟数组一样,切片同样可以是多维的,下面代码创建一个二维切片: 4 | 5 | ```go 6 | s1 := make([][]int, 4) 7 | ``` 8 | 9 | >Tip: 如果你发现你的代码中出现很多多维切片,你就要考虑你的代码设计是否合理并且使用不需要多维切片的更简单的设计。 10 | 11 | 下一小节中你会看到一段使用多维切片的代码示例。 12 | -------------------------------------------------------------------------------- /eBook/chapter3/03.4.6.md: -------------------------------------------------------------------------------- 1 | # **使用切片的代码示例** 2 | 3 | 希望`slices.go`中的代码能够扫除你对切片所有的困惑,本节代码分为5个部分。 4 | 5 | 第一部分包含切片的定义以及初始值的说明: 6 | 7 | ```go 8 | package main 9 | 10 | import "fmt" 11 | 12 | func main() { 13 | aSlice := []int{1, 2, 3, 4, 5} 14 | fmt.Println(aSlice) 15 | integer := make([]int,2) 16 | fmt.Println(integer) 17 | integer = nil 18 | fmt.Println(integer) 19 | ``` 20 | 21 | 第二部分展示了如何使用`[:]`以数组创建切片。注意,`[:]`操作只是引用指向数组,并没有创建一份数组的拷贝: 22 | 23 | ```go 24 | anArray := [5]int{-1, -2, -3, -4, -5} 25 | refAnArray := anArray[:] 26 | 27 | fmt.Println(anArray) 28 | fmt.Println(refAnArray) 29 | anArray[4] = -100 30 | fmt.Println(refAnArray) 31 | ``` 32 | 33 | 第三部分代码使用`make()`创建一个二维切片: 34 | 35 | ```go 36 | s := make([]byte, 5) 37 | fmt.Println(s) 38 | twoD := make([][]int, 3) 39 | fmt.Println(twoD) 40 | fmt.Println() 41 | ``` 42 | 43 | Go自动将切片的元素值初始化为对应切片类型的零值,例如int类型的零值是`0`,切片的零值是`nil`。记住,多维切片的元素类型是切片! 44 | 45 | slices.go的第四部分,你将看到初始化二维数组的方法: 46 | 47 | ```go 48 | for i := 0; i < len(twoD); i++ { 49 | for j := 0; j < 2; j++ { 50 | twoD[i] = append(twoD[i], i * j) 51 | } 52 | } 53 | ``` 54 | 55 | 上述代码使用append()函数往切片中追加元素,切记不要使用不存在的索引值,否则你将看到`panic:runtime error: index out of range`。 56 | 57 | 最后一部分代码展示了如何使用`range`关键字打印二维切片的所有元素: 58 | 59 | ```go 60 | for _, x := range twoD { 61 | for i, y := range x { 62 | fmt.Println("i:", i, "value:", y) 63 | } 64 | fmt.Println() 65 | } 66 | } 67 | ``` 68 | 69 | 执行`slices.go`,你将看到如下输出: 70 | 71 | ```bash 72 | $ go run slices.go 73 | [1 2 3 4 5] 74 | [0 0] 75 | [] 76 | [-1 -2 -3 -4 -5] 77 | [-1 -2 -3 -4 -5] 78 | [-1 -2 -3 -4 -100] 79 | [0 0 0 0 0] 80 | [[] [] []] 81 | i: 0 value: 0 82 | i: 1 value: 0 83 | i: 0 value: 0 84 | i: 1 value: 1 85 | i: 0 value: 0 86 | i: 1 value: 2 87 | ``` 88 | 89 | 由于切片的零值是`nil`,所以二维切片中的切片元素被初始化为`nil`,打印出来就是空的。 -------------------------------------------------------------------------------- /eBook/chapter3/03.4.7.md: -------------------------------------------------------------------------------- 1 | # **使用sort.slice()排序** 2 | 3 | 本节介绍`sort.Slice()`,这个函数是在Go 1.8中被初次引入的。这意味着`sortSlice.go`中的代码不能在低于1.8版本的Go环境中运行。这部分代码将分三部分解释,第一部分是: 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "sort" 11 | ) 12 | 13 | type aStructure struct { 14 | person string 15 | height int 16 | weight int 17 | } 18 | ``` 19 | 20 | 你可能是第一次在本书中看到`Go structure`,在第四章*组合类型的使用*中,你将全面了解`Go structure`的知识。现在你只需要记住,结构体是拥有不同类型的多个变量的数据类型。 21 | 22 | 下面是第二部分代码: 23 | ```go 24 | func main() { 25 | mySlice := make([]aStructure, 0) 26 | mySlice = append(mySlice, aStructure{"Mihalis", 180, 90}) 27 | mySlice = append(mySlice, aStructure{"Bill", 134, 45}) 28 | mySlice = append(mySlice, aStructure{"Marietta", 155, 45}) 29 | mySlice = append(mySlice, aStructure{"Epifanios", 144, 50}) 30 | mySlice = append(mySlice, aStructure{"Athina", 134, 40}) 31 | fmt.Println("0:", mySlice)go 32 | ``` 33 | 34 | 在这里你创建了一个名为`mySlice`的切片,元素是`aStructure`结构体。 35 | 36 | 最后一部分代码: 37 | 38 | ```go 39 | sort.Slice(mySlice, func(i, j int) bool { 40 | return mySlice[i].height < mySlice[j].height 41 | }) 42 | fmt.Println("<:", mySlice) 43 | 44 | sort.Slice(mySlice, func(i, j int) bool { 45 | return mySlice[i].height > mySlice[j].height 46 | }) 47 | fmt.Println(">:", mySlice) 48 | } 49 | ``` 50 | 51 | 我们使用了`sort.Slice()`及两个匿名函数对`mySlice`进行排序,匿名函数使用了`aStructure`的`height`字段。 52 | 53 | >Tip: sort.Slice()函数根据匿名排序函数对切片中的元素进行排序。 54 | 55 | 执行`sort.Slice.go`之后,将会得到下面的输出: 56 | 57 | ```bash 58 | $ go run sortSlice.go 59 | 0: [{Mihalis 180 90} {Bill 134 45} {Marietta 155 45} {Epifanios 144 50} 60 | {Athina 134 40}] 61 | <: [{Bill 134 45} {Athina 134 40} {Epifanios 144 50} {Marietta 155 45} 62 | {Mihalis 180 90}] 63 | >: [{Mihalis 180 90} {Marietta 155 45} {Epifanios 144 50} {Bill 134 45} 64 | {Athina 134 40}] 65 | ``` 66 | 67 | 如果你在Go版本低于1.8的UNIX系统中执行`sort.Slice()`,就会得到下面的错误信息: 68 | 69 | ```bash 70 | $ go version 71 | go version g01.3.3 linux/amd64 72 | $ go run sortSlice.go 73 | ./sortSlice.go:24: undefined: sort.Slice 74 | ./sortSlice.go:24: undefined: sort.Slice 75 | ``` -------------------------------------------------------------------------------- /eBook/chapter3/03.4.8.md: -------------------------------------------------------------------------------- 1 | # **Appending an array to a slice** 2 | 3 | 本小节,你将学到如何向一个切片中拼接数组,`appendArrayToSlice.go`中展示了如何使用,代码分成两部分来解释,第一部分如下: 4 | 5 | ```go 6 | package main 7 | import ( 8 | "fmt" 9 | ) 10 | func main() { 11 | s := []int{1, 2, 3} 12 | a := [3]int{4, 5, 6} 13 | ``` 14 | 到目前,我们已经创建并初始化了一个数组 `a` 和一个切片 `s`。 第二部分的代码如下: 15 | ```go 16 | ref := a[:] 17 | fmt.Println("Existing array:\t", ref) 18 | t := append(s, ref...) 19 | fmt.Println("New slice:\t", t) 20 | s = append(s, ref...) 21 | fmt.Println("Existing slice:\t", s) 22 | s = append(s, s...) 23 | fmt.Println("s+s:\t\t", s) 24 | } 25 | ``` 26 | 这里发生了两件重要的事情。首先,我们创建一个名为t的新切片,其中包含`a + s`的元素,另外我们也将名为 `a` 的数组追加到名为 `s` 的切片中,并将结果存储到 `s` 切片中。因此,你可以选择是否将新的片存储在现有的片变量中。这主要取决于你想要完成什么。 27 | 28 | 第二件重要的事情是,你必须创建一个对现有数组的引用(`ref := a[:]`)以使其工作。请注意在两个`append()`调用中使用`ref`变量的方式:三个点`...`将作为数组的参数,以使其可以附加到切片上。程序的最后两个语句说明了如何将一个切片复制到它的末端。三个点`...`仍然是必须的。 29 | 30 | 执行 `appendArrayToSlice.go`得到如下结果: 31 | 32 | ```bash 33 | $ go run appendArrayToSlice.go 34 | Existing array: [4 5 6] 35 | New slice: [1 2 3 4 5 6] 36 | Existing slice: [1 2 3 4 5 6] 37 | s+s: [1 2 3 4 5 6 1 2 3 4 5 6] 38 | ``` -------------------------------------------------------------------------------- /eBook/chapter3/03.4.md: -------------------------------------------------------------------------------- 1 | # **Go 切片** 2 | 3 | Go的切片十分强大,可以毫不夸张地说切片完全能够取代数组。只有非常少的情况下,你才需要创建数组而非切片,最常见的场景就是你非常确定你所存储的元素数量。 4 | 5 | >Tip: 切片的底层是数组,这意味着Go为每一个切片创建一个底层数组 6 | 7 | 切片作为函数的形参时是**传引用**操作,传递的是指向切片的内存地址,这意味着在函数中对切片的任何操作都会在函数结束后体现出来。另外,函数中传递切片要比传递同样元素数量的数组高效,因为Go只是传递指向切片的内存地址,而非拷贝整个切片。 8 | 9 | >Bryan:就和C/C++中的array和vector一样,vector会自动翻倍扩充。 -------------------------------------------------------------------------------- /eBook/chapter3/03.5.1.md: -------------------------------------------------------------------------------- 1 | # **Map值为nil的坑** 2 | 3 | 下面的代码会正常工作: 4 | 5 | ```go 6 | aMap := map[string]int{} 7 | aMap["test"] = 1 8 | ``` 9 | 10 | 然而下面的代码是不能工作的,因为你将`nil`赋值给map: 11 | ```go 12 | aMap := map[string]int{} 13 | aMap = nil 14 | fmt.Println(aMap) 15 | aMap["test"] = 1 16 | ``` 17 | 18 | 将以上代码保存至`failMap.go`,执行后会产生下面的错误信息(事实上,如果你用IDE编程,IDE就会提醒你有错误): 19 | 20 | ```bash 21 | $ go run failMap.go 22 | map[] 23 | panic: assiment to entry in nil map 24 | ``` 25 | 这意味着试图向nil map中插入值是不行的,但是查找、删除、长度以及使用range循环是可以的。 -------------------------------------------------------------------------------- /eBook/chapter3/03.5.2.md: -------------------------------------------------------------------------------- 1 | # **什么时候应该用Map** 2 | 3 | Map比切片和数组更通用,但这种灵活性是有代价的:实现Go map需要额外的处理能力。然而,内置的Go结构非常快,所以当你需要时,不要犹豫使用Go map。你应该记住的是,Go map非常方便,可以存储许多不同类型的数据,而且容易理解,处理起来也很快。 -------------------------------------------------------------------------------- /eBook/chapter3/03.5.md: -------------------------------------------------------------------------------- 1 | # **Go 映射(maps)** 2 | 3 | 一个 Go map(映射,下文不做翻译)就是在其它编程语言中众所周知的哈希表。map数据结构的主要优势就是其可以使用任意数据类型作为键值,但是对于Go map来说并不是所有的数据类型都能作为键值,只有可比较的类型才可以,意思是Go编译器能够区分不同的键值。或者简单来说,Go map的键值必须支持`==`操作符。显而易见,使用`bool`类型作为map的键值是非常不灵活的。另外,由于不同机器和操作系统的浮点数精度定义不同,使用浮点数作为键值可能会出现异常。 4 | 5 | >Tip: Go map的底层指向了一个哈希表!Go已经隐藏了哈希表的实现及其复杂性,你将在第五章学习如何使用Go实现一个哈希表。 6 | 7 | 下面使用`make()`函数以`string`为键类型,以`int`作为值类型创建一个空的map: 8 | 9 | ```go 10 | iMap := make(map[string]int) 11 | ``` 12 | 13 | 同样也可以使用map字面量创建并初始化一个map: 14 | 15 | ```go 16 | anotherMap := map[string]int { 17 | "k1": 12, 18 | "k2": 13 19 | } 20 | ``` 21 | 22 | 可以使用`anotherMap["k1"]`可以获得对应的值,使用`delete()`删除一个键值对: 23 | 24 | ```go 25 | delete(anotherMap, "k1") 26 | ``` 27 | 28 | 遍历map中得元素可使用如下代码: 29 | 30 | ```go 31 | for key, value := range iMap { 32 | fmt.Priintln(key, value) 33 | } 34 | ``` 35 | 36 | `usingMaps.go`中的代码将会更加详细地展示map的用法。代码将会分为3部分,第一部分是: 37 | 38 | ```go 39 | package main 40 | 41 | import ( 42 | "fmt" 43 | ) 44 | 45 | func main() { 46 | iMap := make(map[string]int) 47 | iMap["k1"] = 12 48 | iMap["k2"] = 13 49 | fmt.Println("iMap:", iMap) 50 | 51 | anotherMap := map[string]int { 52 | "k1": 12, 53 | "k2": 13, 54 | } 55 | ``` 56 | 57 | 第二部分是: 58 | 59 | ```go 60 | fmt.Println("anotherMap:",anotherMap) 61 | delete(anotherMap,"k1") 62 | delete(anotherMap, "k1") 63 | delete(anotherMap, "k1") 64 | fmt.Println("anotherMap:",anotherMap) 65 | 66 | _, ok := iMap["doseItExist"] 67 | if ok { 68 | fmt.Println("Exist!") 69 | } else { 70 | fmt.Println("dose NOT exist") 71 | } 72 | ``` 73 | 74 | 这里你将学习到如何判断map中拥有某个键值对,这是很重要的知识点,如果不了解的话,你将无法判断一个map是否拥有你想要的信息。 75 | 76 | > 需要注意的是,当你尝试使用一个并不存在的键去获取值的时候,返回值是0,但是你并不知道到底是某个键对应的值是0,还是由于所访问的键不存在而返回的0,这是为什么我们使用`_, ok`. 77 | 78 | 另外,代码中多次调用delete()去删除同一个元素并不会导致异常或者警告。 79 | 80 | 最后一部分代码展示了使用`range`关键字遍历map是非常简洁和方便的: 81 | 82 | ```go 83 | for key, value := range iMap { 84 | fmt.Println(key, value) 85 | } 86 | } 87 | ``` 88 | 89 | 执行`usingMaps.go`将会得到下面的输出: 90 | 91 | ```bash 92 | $ go run usingMaps.go 93 | 94 | iMap: map[k1:12 k2:13] 95 | anotherMap: map[k1:12 k2:13] 96 | anotherMap: map[k2:13] 97 | dose NOT exist 98 | k1 12 99 | k2 13 100 | ``` 101 | 102 | >Tip: 你不能也不应该期望键值对是按顺序打印的,因为遍历map时其顺序是随机的。 103 | 104 | -------------------------------------------------------------------------------- /eBook/chapter3/03.6.1.md: -------------------------------------------------------------------------------- 1 | # **常量生成器:iota** 2 | 3 | 常量生成器iota使用递增的数字来声明一系列相关的值,且无需明确定义其类型。 4 | 5 | 大部分与`const`关键字相关的概念,包括常量生成器iota,将会分成四部分,在`constants.go`中阐述。 6 | 7 | 第一部分代码: 8 | 9 | ```go 10 | package main 11 | 12 | import "fmt" 13 | 14 | type Digit int 15 | type Power2 int 16 | 17 | const PI = 3.1415926 18 | 19 | const ( 20 | C1 = "C1C1C1" 21 | C2 = "C2C2C2" 22 | C3 = "C3C3C3" 23 | ) 24 | ``` 25 | 26 | 这部分自定义了两个类型,分别叫做`Digit`和`Power2`,以及四个常量。 27 | 28 | >Tip: Go可以使用现有的类型自定义新的类型,目的是从名字上区分可能使用相同数据类型,但是具有不同含义的变量。 29 | 30 | 第二部分的代码: 31 | 32 | ```go 33 | func main() { 34 | const s1 = 123 35 | var v1 float32 = s1 * 12 36 | fmt.Println(v1) 37 | fmt.Println(PI) 38 | ``` 39 | 40 | 这部分声明了一个新的常量`s1`,并且在`v1`的声明中用到了它。 41 | 42 | 第三部分代码: 43 | 44 | ```go 45 | const ( 46 | Zero Digit = iota 47 | One 48 | Two 49 | Three 50 | Four 51 | ) 52 | fmt.Println(One) 53 | fmt.Println(Two) 54 | ``` 55 | 56 | 在这里我们使用常量生成器定义类型为`Digit`的常量,等同于下面的代码: 57 | 58 | ```go 59 | const ( 60 | Zero = 0 61 | One = 1 62 | Two = 2 63 | Three = 3 64 | Four = 4 65 | ) 66 | ``` 67 | 68 | 最后一部分代码: 69 | 70 | ```go 71 | const ( 72 | p2_0 Power2 = 1 << iota 73 | _ 74 | p2_2 75 | _ 76 | p2_4 77 | _ 78 | p2_6 79 | ) 80 | fmt.Println("2^0:", p2_0) 81 | fmt.Println("2^2:", p2_2) 82 | fmt.Println("2^4:", p2_4) 83 | fmt.Println("2^6:", p2_6) 84 | } 85 | ``` 86 | 87 | 这个常量生成器的使用与第三部分的略有不同,首先你会留意到`_`符号,意思是跳过本次常量声明,其次说明iota的递增属性是可以用到表达式中的。 88 | 89 | 接下来让我们深入`const`块中一探究竟。对于`p2_0`来说,`iota`的值是`0``,p2_0的值是`1`,对于`p2_2`来说`iota的值是2,`p2_2`的值是表达式`1 << 2`的运算结果,用二进制表示左移2位即`00000100`,相应十进制的值就是4。同样的道理,`p2_4`的值是`16`,`p2_6`的值是`64`。 90 | 91 | 如你所见,`iota`的使用能大大提高开发效率! 92 | 93 | 执行`constants.go`后得到下面的输出: 94 | ```bash 95 | $ go run constants.go 96 | 97 | 1476 98 | 3.1415926 99 | 1 100 | 2 101 | 2^0: 1 102 | 2^2: 4 103 | 2^4: 16 104 | 2^6: 64 105 | ``` 106 | -------------------------------------------------------------------------------- /eBook/chapter3/03.6.md: -------------------------------------------------------------------------------- 1 | # **Go 常量** 2 | 3 | 常量是的值是不能改变的,Go使用关键字`const`定义常量。 4 | 5 | >Tip: 通常来说,常量是全局变量。因此,当你的代码中出现大量在局部定义的常量时,你就应该考虑重新设计你的代码了。 6 | 7 | 显而易见,使用常量的好处就是保证了该值不会在程序运行过程中被修改! 8 | 9 | 严格来说,常量的值在编译期间就被确定了。在这种情况下,Go可以使用布尔类型、字符串、或者数字类型存储常量的值。 10 | 11 | 你可以使用下面的代码定义常量: 12 | 13 | ```go 14 | const HEIGHT = 200 15 | ``` 16 | 17 | 另外,你还可以一次性定义多个常量: 18 | 19 | ```go 20 | const ( 21 | C1 = "C1C1C1" 22 | C2 = "C2C2C2" 23 | C3 = "C3C3C3" 24 | ) 25 | ``` 26 | 下面这三种声明变量的方式在Go看来是一样的: 27 | 28 | ```go 29 | s1 := "My String" 30 | var s2 = "My String" 31 | var s3 string = "My String" 32 | ``` 33 | 以上三个变量的声明并没有使用`const`关键字,所以它们并不是常量。这并不意味着你不能使用相似的方式定义两个常量: 34 | 35 | ```go 36 | const s1 = "My String" 37 | const s2 string = "My String" 38 | ``` 39 | 40 | 尽管`s1`和`s2`都是常量,但是`s2`定义时声明了其类型,意味着它比常量`s1`的定义更加严格。这是因为一个声明类型的Go常量必须遵循与声明过类型的变量相同的严格规则,换句话说,未声明类型的常量无需遵循严格规则,使用起来会更加自由。但是,即使在定义常量时没有声明其类型,Go会根据其值判断其类型,因为你不想在使用该常量时考虑所有的规则。下面我们将用一个简单的例子来说明,当你为常量赋予具体类型时会遇到哪些问题: 41 | 42 | ```go 43 | const s1 = 123 44 | const s2 float64 = 123 //注意这里是float64 45 | var v1 float32 = s1*12 46 | var v2 float32 = s2*12 47 | ``` 48 | 49 | 编译器正常通过`v1`的声明及初始化,但是由于`s2`和`v2`的类型不同,编译器就会报错: 50 | 51 | ```bash 52 | $ go run a.go 53 | $ command-line-argument 54 | ./a.go:12:6: canot use s2 * 12 (type float64) as type float32 in assignment 55 | ``` 56 | 57 | 代码建议:如果你要用到许多常量,最好将它们定义到同一个包或者结构体中。 58 | 59 | -------------------------------------------------------------------------------- /eBook/chapter3/03.7.1.md: -------------------------------------------------------------------------------- 1 | # **为什么使用指针?** 2 | 在你的程序中使用指针有两个主要原因: 3 | - 指针允许你共享数据,特别是在Go函数之间。 4 | - 当你希望区分零值和未设置的值时,指针非常有用。 -------------------------------------------------------------------------------- /eBook/chapter3/03.7.md: -------------------------------------------------------------------------------- 1 | # **Go 指针** 2 | 3 | Go支持指针!**指针**是内存地址,它能够提升代码运行效率但是增加了代码的复杂度,C程序员深受指针的折磨。在`第2章`中当我们讨论不安全的代码时,就使用过指针,这一节哦我们将深入介绍Go指针的一些难点。另外,当你足够了解原生Go指针时,其安全性大可放心。 4 | 5 | 使用指针时,`*`可以获取指针的值,此操作成为**指针的解引用**,`*`也叫取值操作符;`&`可以获取非指针变量的地址,叫做取地址操作符。 6 | 7 | >Tip: 通常来说,经验较少的开发者应该尽量少使用指针,因为指针很容易产生难以察觉的bug。 8 | 9 | 你可以创建一个参数为指针的函数: 10 | 11 | ```go 12 | func getPointer(n *int) { 13 | } 14 | ``` 15 | 16 | 同样,一个函数的返回值也可以为指针: 17 | 18 | ```go 19 | func returnPointer(n int) *int { 20 | } 21 | ``` 22 | 23 | `pointers.go`展示了如何安全地使用Go指针,该文件分为4部分,其中第一部分是: 24 | 25 | ```go 26 | package main 27 | 28 | import "fmt" 29 | 30 | func getPointer(n *int) { 31 | *n = *n * *n 32 | } 33 | 34 | func returnPointer(n int) *int { 35 | v := n * n 36 | return &v 37 | } 38 | ``` 39 | 40 | `getPointer()`的作用是修改传递来的参数,而无需返回值。这是因为传递的参数是指针,其指向了变量的地址,所以能够将变量值的改变反映到原值上。 41 | 42 | `returnPointer()`的参数是一个整数,返回值是指向整数的指针,尽管这样看起来并没有什么用处,但是在第四章中,当我们讨论指向结构体的指针以及其他复杂数据结构时,你就会发现这种操作的优势。 43 | 44 | `getPointer()`和`returnPointer()`函数的作用都是求一个整数的平方,区别在于`getPointer()`使用传递来的参数存储计算结果,而`returnPointer()`函数重新声明了一个变量来存储运算结果。 45 | 46 | 第二部分: 47 | 48 | ```go 49 | func main() { 50 | i := -10 51 | j := 25 52 | 53 | pI := &i 54 | pJ := &j 55 | 56 | fmt.Println("pI memory:", pI) 57 | fmt.Println("pJ memory:", pJ) 58 | fmt.Println("pI value:", *pI) 59 | fmt.Println("pJ memory:", *pJ) 60 | ``` 61 | 62 | `i`和 `j`是整数,`pI`和`pJ`分别是指向`i`和`j`的指针,`pI`是变量的内存地址,`*pI`是变量的值。 63 | 64 | 第三部分: 65 | 66 | ```go 67 | *pI = 123456 68 | *pI-- 69 | fmt.Println("i:", i) 70 | ``` 71 | 72 | 这里我们使用指针`pI`改变了变量`i`的值。 73 | 74 | 最后一部分代码: 75 | 76 | ```go 77 | getPointer(pJ) 78 | fmt.Println("j:", j) 79 | k := returnPointer(12) 80 | fmt.Println(*k) 81 | fmt.Println(k) 82 | } 83 | ``` 84 | 85 | 根据前面的讨论,我们通过修改`pJ`的值就可以将改变反映到`j`上,因为`pJ`指向了`j`变量。我们将`returnPointer()`的返回值赋值给指针变量`k`。 86 | 87 | 运行`pointers.go`的输出是: 88 | 89 | $ go run pointers.go 90 | pI memory: 0xc420014088 91 | pJ memory: 0xc420014090 92 | pI value: -10 93 | pJ memory: 25 94 | i: 123455 95 | j: 625 96 | 144 97 | 0xc4200140c8 98 | 99 | 你可能对`pointers.go`中的某些代码感到困惑,因为我们在第六章才开始讨论函数及函数定义,可以去了解关于函数的更多信息。 100 | 101 | >Tip: 在Go中字符串是数值类型而不是指针这和C语言不一样。 102 | -------------------------------------------------------------------------------- /eBook/chapter3/03.8.1.md: -------------------------------------------------------------------------------- 1 | # **解析时间** 2 | 3 | 将时间类型的变量转换成其他格式的时间与日期是非常简单的,但是当你想要将`string`类型转换成时间类型,以检查其是否是一个有效的时间格式时,就会比较麻烦。`time`包提供了`time.Parse()`函数帮助你解析时间与日期字符串,将其转换成`time`类型。`time.Parse()`接收两个参数,第一个参数是你期望得到的时间格式,第二个参数是你要解析的时间字符串。第一个参数是Go与时间解析相关的一系列常量。 4 | 5 | >Tip: 这些常量可以用来创建你期望的时间日期格式,具体介绍见`https://golang.org./src/time/format.go`。Go并没有定义一些时间格式像`DDYYMM`、`%D%Y%M`等,最开始你可能会觉得Go的这种处理方式很蠢,但是用多了后你肯定会喜欢上这种简洁的方式。 6 | 7 | 这些与时间处理相关的常量分别是,`15`代表解析小时,`04`代表解析分钟,`05`解析秒,同时你可以使用`PM`将字符串中的字母转为大写,`pm`转为小写。 8 | 9 | >Tip: 开发者只需要将上面的常量按照字符串中日期的顺序排放就可以了。 10 | 11 | 其实你可以将`time.Parse()`的第一个参数看做正则表达式。 -------------------------------------------------------------------------------- /eBook/chapter3/03.8.2.md: -------------------------------------------------------------------------------- 1 | # **解析时间的代码示例** 2 | 3 | 本节将介绍`parseTime.go`,这段代码接收一个命令行参数,并将其由`string`类型转换成`time`类型。该代码分为三部分,仅是为了演示`time.Parse()`,可能会因为你的输入格式不正确而报错。 4 | 5 | 第一部分: 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | "path/filepath" 14 | "time" 15 | ) 16 | ``` 17 | 18 | 第二部分: 19 | 20 | ```go 21 | func main() { 22 | var myTime string 23 | 24 | if len(os.Args) != 2 { 25 | fmt.Printf("Usage: %s string\n", filepath.Base(os.Args[0]) 26 | os.Exit(0) 27 | } 28 | 29 | myTime = os.Args[1] 30 | ``` 31 | 32 | 最后一部分是展现神奇的代码: 33 | 34 | ```go 35 | d, err := time.Parse("15:04", myTime) 36 | if err == nil { 37 | fmt.Println("Full", d) 38 | fmt.Println("Time", d.Hour(), d.Minute()) 39 | } else { 40 | 41 | } 42 | } 43 | ``` 44 | 45 | 可以看到,为了解析一个包含小时和分钟的字符串,你需要使用常量来构建格式"15:04"。返回值`err`能够告诉你我们的解析是否成功。 46 | 47 | 执行`parseTime.go`后我们得到: 48 | 49 | ```bash 50 | $ go run parseTime.go 51 | usage: parseTime string 52 | exit status 1 53 | 54 | $ go run parseTime.go 12:10 55 | Full 0000-01-01 12:10:00 +0000 UTC 56 | Time 12 10 57 | ``` 58 | 59 | 可以看到Go将整个时间(Full 0000-01-01 12:10:00 +0000 UTC)都打印出来了,这是因为`time.Parse()`返回值是时间类型的变量。如果你只关心具体时间而不是日期,你应该只打印你关心的`time`变量部分。 60 | 61 | 如果你没有按照Go规定的时间常量来格式化你的字符串,例如使用`22:04`作为第一个参数,你就会得到下面的错误信息: 62 | 63 | ```bash 64 | $ go run parseTime.go 65 | parsing time "12:10" as "22:04" : cannot parse ":10" as "2" 66 | ``` 67 | 68 | 或者,如果你使用了用来处理月份的`11`来格式化,可能得到下面的错误信息: 69 | 70 | ```bash 71 | $ go run parseTime.go 12:10 72 | parsing time "12:10": month out of range 73 | ``` 74 | -------------------------------------------------------------------------------- /eBook/chapter3/03.8.3.md: -------------------------------------------------------------------------------- 1 | # **解析日期** 2 | 3 | 在本节中你将学习如何将字符串转为日期,同样是使用`time.Parse()`。 4 | 5 | Go解析日期的常量是:`Jan`用来解析月份(英文月份简写),`2006`用来解析年,`02`用来解析天,`Mon`用来解析周几(如果是`Monday`,那就是周几的英文全称),同样如果你使用`January`而不是`Jan`,你将会得到月份的英文全称而不是三个字母的简写。 -------------------------------------------------------------------------------- /eBook/chapter3/03.8.4.md: -------------------------------------------------------------------------------- 1 | # **解析日期的代码示例** 2 | 3 | 本节的代码`parseDate.go`,分两部分讲解。 4 | 5 | 第一部分: 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | "path/filepath" 14 | "time" 15 | ) 16 | 17 | func main() { 18 | var myDate string 19 | 20 | if len(os.Args) != 2 { 21 | fmt.Printf("Usage: %s date\n", filepath.Base(os.Args[0])) 22 | os.Exit(0) 23 | } 24 | 25 | myDate = os.Args[1] 26 | ``` 27 | 28 | 第二部分: 29 | 30 | ```go 31 | d, err := time.Parse("02 January 2006", myDate) 32 | if err == nil { 33 | fmt.Println("Full", d) 34 | fmt.Println("Time", d.Day(), d.Month(), d.Year()) 35 | } else { 36 | fmt.Println(err) 37 | } 38 | } 39 | ``` 40 | 41 | 如果你想要在月份和年之间加入"-",只需要将格式“02 January 2006“改成“02 January-2006”即可。 42 | 43 | 执行`parseDate.go`你将得到下面的输出: 44 | 45 | ```bash 46 | $ go run parseDate.go 47 | usage: parseDate string 48 | 49 | $ go run parseDate.go "12 January 2019" 50 | Full 2019-01-12 00:00:00 +0000 UTC 51 | Time 12 January 2019 52 | ``` 53 | -------------------------------------------------------------------------------- /eBook/chapter3/03.8.5.md: -------------------------------------------------------------------------------- 1 | # **格式化时间与日期** 2 | 3 | 在这一节中你将处理既有时间又有日期的字符串,这种格式的时间在web服务器中最常见到,例如**Apache**,**Nginx**等。由于目前还没有讲到文件IO,所以这里我将文本硬编码写到程序中,这不会影响程序的功能。 4 | 5 | 本节代码`timeDate.go`将分3部分展示,其中第一部分: 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "regexp" 13 | "time" 14 | ) 15 | 16 | func main() { 17 | 18 | logs := []string{"127.0.0.1 - - [16/Nov/2017:10:49:46 +0200] 325504", 19 | "127.0.0.1 - - [16/Nov/2017:10:16:41 +0200] \"GET /CVEN HTTP/1.1\" 200 12531 \"-\" \"Mozilla/5.0 AppleWebKit/537.36", 20 | "127.0.0.1 200 9412 - - [12/Nov/2017:06:26:05 +0200] \"GET \"http://www.mtsoukalos.eu/taxonomy/term/47\" 1507", 21 | "[12/Nov/2017:16:27:21 +0300]", 22 | "[12/Nov/2017:20:88:21 +0200]", 23 | "[12/Nov/2017:20:21 +0200]", 24 | } 25 | ``` 26 | 27 | 由于我们不能确定数据的格式,所以样本数据尽量覆盖不同格式的数据,包括像`"[12/Nov/2017:20:21 +0200]"`这种不完整的数据,`[12/Nov/2017:20:88:21 +0200]`这种本身存在错误的数据(秒数是88)。 28 | 29 | 第二部分代码: 30 | 31 | ```go 32 | for _, logEntry := range logs { 33 | r := regexp.MustCompile(`.*\[(\d\d\/\w+/\d\d\d\d:\d\d:\d\d:\d\d.*)\].*`) 34 | if r.MatchString(logEntry) { 35 | match := r.FindStringSubmatch(logEntry) 36 | ``` 37 | 38 | 我们使用正则表达式来匹配正确的时间格式,在拿到时间字符串后,剩下的任务就交给`time.Parse()`好了。 39 | 40 | 最后一部分代码: 41 | 42 | ```go 43 | dt, err := time.Parse("02/Jan/2006:15:04:05 -0700", match[1]) 44 | if err == nil { 45 | newFormat := dt.Format(time.RFC850) 46 | fmt.Println(newFormat) 47 | } else { 48 | fmt.Println("Not a valid date time format!") 49 | } else { 50 | fmt.Println("Not a match!") 51 | } 52 | } 53 | } 54 | ``` 55 | 56 | 一旦正则表达式匹配到了时间字符串,`time.Parse()`就会去解析判断其是否是一个有效的时间。如果是,`timeDate.go`就会将时间以**RFC850**格式打印出来。 57 | 58 | 执行`timeDate.go`,你将得到下面的输出: 59 | 60 | ```bash 61 | $ go run timeDate.go 62 | Thursday, 16-Nov-17 10:49:46 +0200 63 | Thursday, 16-Nov-17 10:16:41 +0200 64 | Sunday, 12-Nov-17 06:26:05 +0200 65 | Sunday, 12-Nov-17 16:27:21 +0300 66 | Not a valid date time format! 67 | Not a match! 68 | ``` 69 | -------------------------------------------------------------------------------- /eBook/chapter3/03.8.6.md: -------------------------------------------------------------------------------- 1 | # 测量执行时间 2 | 3 | 在本节中,你将学习如何度量Go中的一个或多个命令的执行时间。同样的技术也可以用于测量一个函数或一组函数的执行时间。通过`execTime.go`进行介绍,其分为三个部分。 4 | 5 | > Tip: 在Go中,这个技术易于实现并且非常方便和强大。不要低估Go的简单性。 6 | 7 | 第一部分: 8 | 9 | ```Go 10 | package main 11 | import ( 12 | "fmt" 13 | "time" 14 | ) 15 | func main() { 16 | start := time.Now() 17 | time.Sleep(time.Second) 18 | duration := time.Since(start) 19 | fmt.Println("It took time.Sleep(1)", duration, "to finish.") 20 | ``` 21 | 22 | 你需要时间包提供的函数去衡量一个命令的执行时间。所有工作通过`time.Since()`函数,它接受一个过去的时间作为参数。这测量了执行一个`time.sleep(time.Second)`的时间,因为这个程序是`time.Now()`和`time.Since()`之间唯一语句。 23 | 24 | 第二部分代码: 25 | 26 | ```go 27 | start = time.Now() 28 | time.Sleep(2 * time.Second) 29 | duration = time.Since(start) 30 | fmt.Println("It took time.Sleep(2)", duration, "to finish.") 31 | ``` 32 | 这一次我们测量的是执行一次`time.Sleep(2 * time.Second)`所花费的时间,这对于找出time.Sleep()函数的精度非常有用,它主要与内部Go时钟的精度有关。 33 | 34 | 最后一部分代码: 35 | ```go 36 | start = time.Now() 37 | for i := 0; i < 200000000; i++ { 38 | _ = i 39 | } 40 | duration = time.Since(start) 41 | fmt.Println("It took the for loop", duration, "to finish.") 42 | sum := 0 43 | start = time.Now() 44 | for i := 0; i < 200000000; i++ { 45 | sum += i 46 | } 47 | duration = time.Since(start) 48 | fmt.Println("It took the for loop", duration, "to finish.") 49 | ``` 50 | 51 | 在最后一部分,我们测量两个for循环的速度。第一个什么都不做,而第二个做一些计算。正如将在程序的输出中看到的,第二个for循环比第一个更快。 52 | 53 | 执行`execTime.go`输出如下结果 54 | ```go 55 | $ go run execTime.go 56 | It took time.Sleep(1) 1.000768881s to finish. 57 | It took time.Sleep(2) 2.00062487s to finish. 58 | It took the for loop 50.497931ms to finish. 59 | It took the for loop 47.70599ms to finish 60 | ``` 61 | -------------------------------------------------------------------------------- /eBook/chapter3/03.8.7.md: -------------------------------------------------------------------------------- 1 | # **测量Go的垃圾回收速度** 2 | 3 | 现在我们可以重写上一章的`sliceGC.go`,`mapNoStar.go`,`mapStar.go`, `mapSplit.go`来获得更精确的结果,而不需要使用`time(1) UNIX`命令行工具。实际上,在每个文件中需要做的惟一一件事是在`time.Now()`和`time.Since()`之间插入`runtime.GC()`的调用,并打印结果。 4 | 执行更新的程序得到如下结果: 5 | ```bash 6 | $ go run sliceGCTime.go 7 | It took GC() 281.563μs to finish 8 | 9 | $ go run mapNoStarTime.go 10 | It took GC() 9.483966ms to finish 11 | 12 | $ go run mapStarTime.go 13 | It took GC() 651.704424ms to finish 14 | 15 | $ go run mapSplitTime.go 16 | It took GC() 12.743889ms to finish 17 | ``` 18 | 这些结果比之前的版本准确得多,因为它们只显示了`runtime.GC()`执行所花费的时间,而不包括程序填充切片或用于存储值的映射所花费的时间。然而,结果仍然验证了Go垃圾收集器处理具有大量数据的map变量的速度有多慢。 -------------------------------------------------------------------------------- /eBook/chapter3/03.8.md: -------------------------------------------------------------------------------- 1 | # **时间与日期的处理技巧** 2 | 3 | 本节你将学习到如何解析时间与日期字符串、格式化日期与时间、以你期望的格式打印时间与日期。你可能会觉得这部分内容没有那么重要,但是当你想要实现多任务同步或者从文本、用户读取日期时,就会发现这一节的作用。 4 | 5 | Go自带一个处理时间与日期的神器-`time`包,这里将介绍几个实用的函数。 6 | 7 | 在学习如何将字符串解析为时间和日期之前,先看一段简单的代码`usingTime.go`以对`time`包有个简单的了解,代码分为三个部分,第一部分引入了我们准备使用的包: 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "time" 15 | ) 16 | ``` 17 | 18 | 第二部分: 19 | 20 | ```go 21 | func main() { 22 | fmt.Println("Epoch Time:", time.Now().Unix()) 23 | t := time.Now() 24 | fmt.Println(t, t.Format(time.RFC3339)) 25 | fmt.Println(t.Weekday(), t.Day(), t.Month(), t.Year()) 26 | 27 | time.Sleep(time.Second) 28 | t1 := time.Now() 29 | fmt.Println("Time difference:", t1.Sub(t)) 30 | ``` 31 | 32 | `time.Now().Unix()`返回UNIX时间(UNIX时间是计算了从00:00:00 UTC,1970年1月1日以来的秒数)。`Format()`能够将`time`类型的变量转换成其他格式,例如`RFC3339`格式。 33 | 34 | 你会发现`time.Sleep()`在本书中频繁出现,这是一种最简单的产生延时的函数。`time.Second意思是1秒,如果你想产生10s的延迟,只需将`time.Second*10`即可。对于`time.Nanosecond`、`time.Microsecond`、`time.minute`、`time.Hour`是同样的道理。使用`time`包能够定义的最小时间间隔是1纳秒。最后,`time.Sub()`函数能够得到两个时间之间的时间差。 35 | 36 | 第三部分: 37 | 38 | ```go 39 | formatT := t.Format("01 January 2006") 40 | fmt.Println(formatT) 41 | loc, _ := time.LoadLocation("Europe/Paris") 42 | LondonTime := t.In(loc) 43 | fmt.Println("Paris:", LondonTime) 44 | } 45 | ``` 46 | 47 | 我们使用`time.Format`定义了一个新的日期格式,并且得到指定时区的时间。 48 | 49 | 执行`usingTime.go`的输出如下: 50 | 51 | ```bash 52 | $ go run usingTime.go 53 | Epoch Time: 1547279979 54 | 2019-01-12 15:59:39.959594352 +0800 CST m=+0.000392272 19-01-12T15:59:39+08:00 55 | Saturday 12 January 2019 56 | Time difference: 1.000820609s 57 | 01 January 2019 58 | Paris: 2019-01-12 08:59:39.959594352 +0100 CET 59 | ``` 60 | 现在你应该对`time`包有了一个基本的了解,是时候去深入了解`time`更多的功能了! 61 | -------------------------------------------------------------------------------- /eBook/chapter3/03.9.md: -------------------------------------------------------------------------------- 1 | # **有用的链接和练习** 2 | 3 | - 使用`iota`编写常量生成器表示4的指数 4 | - 使用`iota``编写常量生成器表示一周的天数 5 | - 编写程序将数组转为map 6 | - 自己动手编写`parseTime.go`,不要忘记写测试 7 | - 编写`timeDate.go`使其能够处理两种格式的时间与日期 8 | - 自己动手编写`parseDate.go` 9 | - 阅读官方的`time`包:https://golang.org/pkg/time 10 | https://golang.org/pkg/time/. 11 | - 你也可以访问GitHub的页面,那里正在讨论Go 2和数字字面量的变化,这将帮助你了解Go正在发生的变化以及它们是如何发生的: 12 | https://github.com/golang/proposal/blob/master/design/19308-number-literals.md. -------------------------------------------------------------------------------- /eBook/chapter4/04.0.md: -------------------------------------------------------------------------------- 1 | # 组合类型的使用 2 | 3 | 在前一章节,我们已经讨论了多核心主题,包括数值类型,数组,切片,字典,指针,常量,**for**循环,关键字**range**,以及如何操作时间和日期。 4 | 5 | 这一章将会介绍更多Go语言的高级特性,比如元组和字符串,标准包`strings`,`switch`语句,当然,最重要的,我们要学习结构体,它在Go语言中被大量的使用。 6 | 7 | 本章还会学习如何操作**json**和**Xml**文件,如何实现一个简单的**key-value**存储,如何定义正则表达式,以及如何进行模式匹配。 8 | 9 | 本章将会覆盖以下这些主题: 10 | 11 | * 结构体和`struct`关键字 12 | * 元组 13 | * 字符串和字符串字面量 14 | * 操作**json**文本 15 | * 操作**xml**文本 16 | * 正则表达式 17 | * 模式匹配 18 | * `switch`语句 19 | * `strings`包提供的功能 20 | * 计算高精度的**Pi**值 21 | * 实现一个k-v存储 -------------------------------------------------------------------------------- /eBook/chapter4/04.1.md: -------------------------------------------------------------------------------- 1 | # 关于组合类型 2 | 3 | 虽然Go语言的标准类型是十分易用,快速且灵活,但是这些标准类型往往并不能覆盖我们所想要的所有的数据类型。Go语言通过提供**结构体**来解决这个问题,结构体是开发者定义的定制类型。另外,Go以自己的方式提供了元组,元组允许函数可以返回多个值,且并不用把这些值组成一个结构体(C语言就是是这样的)。 4 | 5 | -------------------------------------------------------------------------------- /eBook/chapter4/04.2.1.md: -------------------------------------------------------------------------------- 1 | # 结构体指针 2 | 3 | 在第三章的时候,我们已经讨论过指针。本节我们会继续指讨论结构体指针,并且提供一个与**结构体指针**有关的示例。这个示例的名字是`pointerStruct.go`,我们共分四部分展示。 4 | 5 | 第一部分代码: 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | ) 13 | 14 | type myStructure struct { 15 | Name string 16 | Surname string 17 | Height int32 18 | } 19 | ``` 20 | 21 | 第二部分代码: 22 | 23 | ```go 24 | func createStructure(n,s string, h int32) *myStructure { 25 | if h > 300 { 26 | h = 0 27 | } 28 | return &myStructure{n, s, h} 29 | } 30 | ``` 31 | 32 | 相对于直接手动初始化一个结构体变量,函数`createStructure()`提供了一种更加优雅的方法来创建结构体变量。这种方式不仅能够检查提供给结构体字段的值是否正确和有效,还能再当定义的结构体出现问题之后很快找到问题所在,因为知道这个变量是在哪里被初始化的。注意有些人可能更喜欢把该函数命名为`NewStructure()`。 33 | 34 | > 对于有C/C++背景的开发者来说,Go函数返回局部变量的内存地址是很容易理解的,所以Go的这种设计皆大欢喜! 35 | > 36 | 37 | 第三部分: 38 | 39 | ```go 40 | func retStructure(n,s string, h int32) myStructure { 41 | if h > 300 { 42 | h = 0 43 | } 44 | return myStructure{n, s, h} 45 | } 46 | ``` 47 | 48 | 这部分实现了`createStructure()`的无指针版本`retStructure()`。这两个函数都可以正常工作,所以选择哪一种就看开发者的喜好了。其实这两个函数命名为`NewStructurePointer()`和`NewStructure()`是比较规范的。 49 | 50 | 最后一部分代码: 51 | 52 | ```go 53 | func main() { 54 | s1 := createStructure("Mihalis","Tsoukalos",123) 55 | s2 := retStructure("Mihalis","Tsoukalos",123) 56 | fmt.Println((*s1).Name) 57 | fmt.Println(s2.Name) 58 | fmt.Println(s1) 59 | fmt.Println(s2) 60 | } 61 | ``` 62 | 63 | 执行`pointerStruct.go`我们得到下面的输出: 64 | 65 | ```shell 66 | $ go run pointerStruct.go 67 | Mihalis 68 | Mihalis 69 | &{Mihalis Tsoukalos 123} 70 | {Mihalis Tsoukalos 123} 71 | ``` 72 | 73 | 通过输出你能够发现`createStructure()`和`retStructure()`的主要区别,就是前者返回指向结构体的指针,这意味着你在想使用指针指向的对象的时候,就必须先解引用,有些人会觉得这种操作不太优雅。 74 | 75 | > 结构体是一个非常重要的概念,在实际编程时会经常用到,它能够帮你把很多不同类型的变量聚合到一起,把这些值当做一个整体的值处理。 76 | 77 | -------------------------------------------------------------------------------- /eBook/chapter4/04.2.2.md: -------------------------------------------------------------------------------- 1 | # 使用new关键字 2 | 3 | Go支持使用`new`关键字来分配新的对象,这里有一个非常重要的细节:`new`返回的是分配的对象的内存地址,简单说就是返回指针。 4 | 5 | 你可以创建一个`aStructure`类型的变量: 6 | ```go 7 | pS := new(aStructure) 8 | ``` 9 | 执行上述代码后,你就可以操作这个新建的对象了,已经为这个对象分配了内存,但是并没有初始化。 10 | 11 | > `new`和`make`最大的区别就是:使用`make`创建的变量已经初始化,并不是空的内存地址,即没有做初始化。另外,`make`仅可以用来创建映射,切片和通道,而且并不是返回指针。 12 | 13 | 下面的代码将会创建一个指向切片的指针,并且值为`nil`: 14 | 15 | ```go 16 | sP := new([]aStructure) 17 | ``` 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /eBook/chapter4/04.2.md: -------------------------------------------------------------------------------- 1 | # 结构体 2 | 3 | 虽然array, slice,map这类数据结构的确很不错,但是呢,它们不能将多个值聚合到同一个地方。当你需要将不同类型的不同变量聚合到一个地方来创建一个新的类型的时候,你就可以使用结构体。结构体中的元素叫**结构体的字段**或者就是字段。 4 | 5 | 我会以讲解一个简单的结构体为例来开始这一节,这个结构体就是上一章在`sortSlie.go`中定义的: 6 | ```go 7 | type aStructure struct { 8 | person string 9 | height int 10 | weight int 11 | } 12 | ``` 13 | 14 | 结构体的字段通常是大写字母开头,这很大程度上取决于如何使用这个字段。当你学习过第六章*What You Might Not Know About Go Packages and Go*后,这个问题的原因就会非常显而易见。这个结构体有三个字段,分别是`person`,`height`,`weight`。现在,你可以创建一个类型为`aStructure`的变量了: 15 | 16 | ```go 17 | var s1 aStructure 18 | ``` 19 | 20 | 另外,你可以通过字段的名字来访问这个字段。所以,为了获取变量`s1`的`person`字段的值,你可以输入`s1.person`。 21 | 22 | 一个**结构体字面量**可以这样定义: 23 | 24 | ```go 25 | p1 := aStructure{"fmt",12,-2} 26 | ``` 27 | 28 | 当然记住结构体中字段的声明顺序可能确实比较困难,Go语言还允许你用另外一种方式来定义结构体字面量: 29 | 30 | ```go 31 | p1 := aStructure{weight: 12, height:-2} 32 | ``` 33 | 34 | 用这种方式定义的时候,你不必为结构体中的每个字段都提供初始值。 35 | 36 | 现在你已经了解了结构体的基本操作,让我们尝试一些实战性更强的代码吧,这部分代码保存在`structures.go`中,分为四部分。 37 | 38 | 第一部分代码: 39 | 40 | ```go 41 | package main 42 | 43 | import ( 44 | "fmt" 45 | ) 46 | 47 | ``` 48 | 49 | > 通常来说,Go结构体和Go基本类型都是是定义在`main()`函数外面的,这样在整个Go package中可以拥有全局的属性,除非你想声明这个结构提类型值在当前空间内有效,不想它在别的地方被使用。 50 | > 51 | 52 | 第二部分代码: 53 | 54 | ```go 55 | func main(){ 56 | type XYZ struct { 57 | X int 58 | Y int 59 | Z int 60 | } 61 | 62 | var s1 XYZ 63 | fmt.Println(s1.Y, s1.Z) 64 | 65 | ``` 66 | 67 | 从这里我们就可以看到确实可以在函数内部定义结构体,但是这样做的时候一定要有充分的理由 68 | 69 | 我们使用两种定义结构体字面量的方式定义了`p1`与`p2`,并且打印出来。 70 | 71 | 第三部分: 72 | 73 | ```go 74 | p1 := XYZ{23,12, -2} 75 | p2 := XYZ{Z:12,Y:13} 76 | fmt.Println(p1) 77 | fmt.Println(p2) 78 | ``` 79 | 80 | 这里你定义了两个结构体字面量,分别命名p1和p2,然后打印这两个变量 81 | 82 | 最后一部分代码: 83 | 84 | ```go 85 | pSlice := [4]XYZ{} 86 | pSlice[2] = p1 87 | pSlice[0] = p2 88 | fmt.Println(pSlice) 89 | p2 = XYZ{1,2,3} 90 | fmt.Println(pSlice) 91 | } 92 | ``` 93 | 94 | 最后一部分代码中,我们创建了一个结构体数组`pSlice`,当你将一个结构体分配给结构体数组,那么这个结构体就会被深拷贝至这个数组,这意味着改变原结构体是对数组中的结构体没有影响的,从下面的打印输出中我们能够看出来: 95 | 96 | ```shell 97 | $ go run structures.go 98 | 0 0 99 | {23 12 -2} 100 | {0 13 12} 101 | [{0 13 12} {0 0 0} {23 12 -2} {0 0 0}] 102 | [{0 13 12} {0 0 0} {23 12 -2} {0 0 0}] 103 | ``` 104 | 105 | > 注意,结构体中字段的定义顺序是有意义的,简单来说,就算两个结构体拥有相同的字段,但是字段的声明顺序不同,那么这两个结构体也是不相同的。 106 | > 107 | 108 | 从程序输出中我们能够知道,结构体中的变量是初始化为其类型的零值。 109 | -------------------------------------------------------------------------------- /eBook/chapter4/04.3.md: -------------------------------------------------------------------------------- 1 | # **元组** 2 | 3 | 严格来讲,**元组**是由有限多个部分组成的有序列表,最重要的是Go本身不支持元组类型,虽然Go名义上并不关心元组,但实际上它提供了元组的某些操作。 4 | 5 | 有趣的是我们在第一章已经接触过Go的元组操作,像下面的这种操作,使用一条语句获取两个返回值: 6 | 7 | ```go 8 | min, _ := strconv.ParseFloat(arguments[1], 64) 9 | ``` 10 | 11 | 我们把解释Go的元组的代码命名为`tuples.go`,分成三部分来展示,请注意下面代码中的函数将返回值以元组的形式返回。你会在第六章中学习更多关于函数的知识。 12 | 13 | 第一部分: 14 | 15 | ```go 16 | package main 17 | 18 | import "fmt" 19 | 20 | func retThree(x int) (int, int, int) { 21 | return 2 * x, x*x, -x 22 | } 23 | ``` 24 | 25 | `retThree()`函数返回了包含三个整数元素的元组,这种能力使得函数能够返回多个数据,无需将它们聚合到结构体中,然后返回一个结构体变量。 26 | 27 | 在第六章你将会学习到如何给函数的返回值命名,这是一个非常方便的特性,可以把你从各种类型错误中解放出来。 28 | 29 | 第二部分代码: 30 | 31 | ```go 32 | func main() { 33 | fmt.Println(retThree(10)) 34 | n1, n2, n3 := retThree(20) 35 | fmt.Println(n1,n2,n3) 36 | ``` 37 | 38 | 这里我们使用了两次`retThree()`函数。第一次我们并没有将其返回值保存,第二次使用三个变量保存返回值,在Go的术语中这叫做元组赋值,看到这里,你是不是有了一种`Go支持元组!`的错觉。 39 | 40 | 如果有些返回值你并不关心,可以使用`_`操作符忽略掉它们。要知道,在Go的代码里声明了但是未使用的变量是会导致编译错误的。 41 | 42 | 第三部分代码: 43 | 44 | ```go 45 | n1, n2 = n2, n1 46 | fmt.Println(n1, n2, n3) 47 | x1, x2, x3 := n1*2, n1*n1, -n1 48 | fmt.Println(x1, x2, x3) 49 | } 50 | ``` 51 | 52 | 可以看到,依靠这种元组操作,我们无需借助`temp`变量就可以实现两个值的交换,还有计算表达式。 53 | 54 | 执行`tuples.go`可得到如下输出: 55 | 56 | ```shell 57 | $ go run tuples.go 58 | 20 100 -10 59 | 40 400 -20 60 | 400 40 -20 61 | 800 160000 -400 62 | ``` -------------------------------------------------------------------------------- /eBook/chapter4/04.4.1.md: -------------------------------------------------------------------------------- 1 | # 理论知识 2 | 3 | 每一个正则表达式都通过构建一种叫做**有限自动机**的状态转换图表,被编译成识别器。一个有限自动机可以是确定性的也可以是不确定性的。不确定性的意思是一种输入可能会产生多种输出状态。识别器是一种程序,它接收一个字符串`x`并且能够分辨出`x`是否是给定语言的一个句子。 4 | 5 | **语法**是格式化的语言为字符串定义一系列生成规则。这些规则描述了根据词法生成有效字符串的方法。语法器只能描述字符串的结构,不能描述一个字符串的含义或者描述该字符串能在什么上下文中用来做什么。要注意,语法是定义或者使用正则表达式的核心。 6 | 7 | > 尽管正则表达式可以用来解决棘手的问题,但是不要任何问题都使用正则表达式,一定要在正确的地方选择正确的工具! 8 | 9 | 接下来的这一小节将会展示三个正则表达式和模式匹配的例子。 10 | 11 | -------------------------------------------------------------------------------- /eBook/chapter4/04.4.md: -------------------------------------------------------------------------------- 1 | # 正则表达式与模式匹配 2 | 3 | 模式匹配在Go语言中占有重要的地位,这是能够根据正则表达式查询出特定的字符集的技术。模式匹配成功后你就可以提取出你想要的字符串,并对其进行替换、删除等进一步操作。 4 | 5 | Go标准库中`regexp`包负责定义正则表达式和模式匹配。你会在本章后面的部分看到这个包。 6 | 7 | > 在代码中使用正则表达式时,定义符合条件的正则表达式是重中之重,因为代码的功能取决于你正则表达式。 8 | 9 | -------------------------------------------------------------------------------- /eBook/chapter4/04.5.1.md: -------------------------------------------------------------------------------- 1 | # rune是什么? 2 | 3 | **rune**是一个`int32`的值,同时也是一个Go类型,用来代表一个Unicode码点。Unicode码点是一个代表Unicode字符的数值。当然,也可以包含其他型,比如一些格式化信息。 4 | 5 | > **NOTE:**你可以认为字符串是一系列rune的集合 6 | 7 | **rune字面量**实际上是一个用单引号括起来的字符,你可以认为一个rune字面量就是一个**rune常量**,并且与与Unicode码点的概念相关联。 8 | 9 | `rune.go`将分两部分阐述rune的使用,第一部分是: 10 | 11 | ```go 12 | package main 13 | 14 | import ( 15 | "fmt" 16 | ) 17 | ``` 18 | 第二部分: 19 | ```go 20 | func main() { 21 | const r1 = '€' 22 | fmt.Println("(int32) r1:", r1) 23 | fmt.Printf("(HEX) r1: %x\n", r1) 24 | fmt.Printf("(as a String) r1: %s\n", r1) 25 | fmt.Printf("(as a character) r1: %c\n", r1) 26 | ``` 27 | 28 | 首先定义了一个rune字面量`r1`,(注意欧元的符号不属于ASCII码表)然后使用不同的方式去打印,分别是int32、十六进制、字符串、字符,最终你会发现使用字符格式打印出的与定义`r1`的值相同。 29 | 30 | 第三部分: 31 | 32 | ```go 33 | fmt.Println("A string is a collection of runes:", []byte("Mihalis")) 34 | aString := []byte("Mihalis") 35 | for x, y := range aString { 36 | fmt.Println(x, y) 37 | fmt.Printf("Char: %c\n", aString[x]) 38 | } 39 | fmt.Printf("%s\n", aString) 40 | } 41 | ``` 42 | 43 | 显而易见,**字节切片**实际上就是一系列runes的集合,并且如果你使用`fmt.Println()`打印字节切片,结果很可能不会符合你的预期。`fmt.Printf()`语句结合`%c`可以将runes转换为字符输出;如果想要以字符串的形式输出字节数组,应使用`fmt.Printf()`结合`%s`。 44 | 45 | 执行runes.go得到下面的输出: 46 | 47 | 48 | ```shell 49 | $ go run runes.go 50 | (int32) r1: 8364 51 | (HEX) r1: 20ac 52 | (as a String) r1: %!s(int32=8364) 53 | (as a character) r1: € 54 | A string is a collection of runes: [77 105 104 97 108 105 115] 55 | 0 77 56 | Char: M 57 | 1 105 58 | Char: i 59 | 2 104 60 | Char: h 61 | 3 97 62 | Char: a 63 | 4 108 64 | Char: l 65 | 5 105 66 | Char: i 67 | 6 115 68 | Char: s 69 | Mihalis 70 | 71 | ``` 72 | 73 | 最后,举一个产生`illegal rune literal`错误的例子:在导包的时候使用单引号。 74 | 75 | ```shell 76 | $ cat a.go 77 | package main 78 | import ( 79 | 'fmt' 80 | ) 81 | func main() { 82 | } 83 | $ go run a.go 84 | package main:a.go:4:2: illegal rune literal 85 | ``` 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /eBook/chapter4/04.5.2.md: -------------------------------------------------------------------------------- 1 | # 关于Unicode的包 2 | 3 | Go提供的`unicode`标准库提供了很多便捷的函数,其中`unicode.IsPrint()`能帮助你判断字符串的某一部分是否能够以rune的类型打印出来。接下来将会在代码`unicode.go`中分两部分展示该函数的用法。 4 | 5 | 第一部分: 6 | 7 | ```go 8 | package main 9 | import ( 10 | "fmt" 11 | "unicode" 12 | ) 13 | 14 | func main() { 15 | const sL= "\x99\x00ab\x50\x00\x23\x50\x29\x9c" 16 | ``` 17 | 18 | 第二部分: 19 | 20 | ```go 21 | for i := 0; i < len(sL); i++ { 22 | if unicode.IsPrint(rune(sL[i])) { 23 | fmt.Printf("%c\n", sL[i]) 24 | } else { 25 | fmt.Println("Not printable!") 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | `unicode.IsPrint()`函数将检查字符串`sL`的每个元素是否是rune类型,如果是的话将返回`true`否则返回false。如果你需要更多操作Unicode字符的方法,可以参考官方`unicode`包的介绍。 32 | 33 | 执行`unicode.go`将会打印: 34 | 35 | 36 | 37 | ```shell 38 | $ go run unicode.go 39 | Not printable! 40 | Not printable! 41 | a 42 | b 43 | P 44 | Not printable! 45 | # 46 | P 47 | ) 48 | Not printable! 49 | ``` 50 | 51 | -------------------------------------------------------------------------------- /eBook/chapter4/04.9.1.md: -------------------------------------------------------------------------------- 1 | # 读取JSON数据 2 | 3 | 本章中,你将会学习如何读取磁盘上的JSON文件。我们用`readJSON.go`来展示,共分为三部分。 4 | 5 | 第一部分: 6 | 7 | ```go 8 | package main 9 | import ( 10 | "encoding/json" 11 | "fmt" 12 | "os" 13 | ) 14 | type Record struct { 15 | Name string 16 | Surname string 17 | Tel []Telephone 18 | } 19 | type Telephone struct { 20 | Mobile bool 21 | Number string 22 | } 23 | ``` 24 | 25 | 在这些代码中,我们定义了用来存储JSON数据的结构体变量。 26 | 27 | 第二部分: 28 | 29 | ```go 30 | func loadFromJSON(filename string, key interface{}) error { 31 | in, err := os.Open(filename) 32 | if err != nil { 33 | return err 34 | } 35 | decodeJSON := json.NewDecoder(in) 36 | err = decodeJSON.Decode(key) 37 | if err != nil { 38 | return err 39 | } 40 | in.Close() 41 | return nil 42 | } 43 | ``` 44 | 45 | 这里我们用定义了一个新的函数`loadFromJSON()`,这个函数用第二个参数给定的数据结构来解码JSON文件中的数据。我们首先调用`json.NewDecoder()`创建一个新的JSON解码变量和传入的文件建立联系,然后调用`Decode()`函数解码文件中的内容,把解码的结果存到第二个参数中。 46 | 47 | 最后一部分: 48 | 49 | ```go 50 | func main() { 51 | arguments := os.Args 52 | if len(arguments) == 1 { 53 | fmt.Println("Please provide a filename!") 54 | return 55 | } 56 | filename := arguments[1] 57 | var myRecord Record 58 | err := loadFromJSON(filename, &myRecord) 59 | if err == nil { 60 | fmt.Println(myRecord) 61 | } else { 62 | fmt.Println(err) } 63 | } 64 | ``` 65 | 66 | 实例的JSON文件的名字是`readMe.json`,包含以下内容: 67 | 68 | ```shell 69 | $ cat readMe.json 70 | { 71 | "Name":"Mihalis", 72 | "Surname":"Tsoukalos", 73 | "Tel":[ 74 | {"Mobile":true,"Number":"1234-567"}, 75 | {"Mobile":true,"Number":"1234-abcd"}, 76 | {"Mobile":false,"Number":"abcc-567"} 77 | ] 78 | } 79 | ``` 80 | 81 | 执行`readJSON.go`程序会生成以下输出: 82 | 83 | ```shell 84 | $ go run readJSON.go readMe.json 85 | {Mihalis Tsoukalos [{true 1234-567} {true 1234-abcd} {false abcc-567}]} 86 | ``` 87 | 88 | -------------------------------------------------------------------------------- /eBook/chapter4/04.9.2.md: -------------------------------------------------------------------------------- 1 | # 保存JSON数据 2 | 3 | 在这一小节,你将会学习如何写JSON数据。实例的程序叫`writeJSON.go`,会分为三个部分。这个程序会在标准输出(`os.Stdout`)中写入JSON数据,即直接写在屏幕上。 4 | 5 | 第一部分: 6 | 7 | ```go 8 | package main 9 | import ( 10 | "encoding/json" 11 | "fmt" 12 | "os" 13 | ) 14 | type Record struct { 15 | Name string 16 | Surname string 17 | Tel []Telephone 18 | } 19 | type Telephone struct { 20 | Mobile bool 21 | Number string 22 | } 23 | ``` 24 | 25 | 第二部分: 26 | 27 | ```go 28 | func saveToJSON(filename *os.File, key interface{}) { 29 | encodeJSON := json.NewEncoder(filename) 30 | err := encodeJSON.Encode(key) 31 | if err != nil { 32 | fmt.Println(err) 33 | return 34 | } 35 | } 36 | ``` 37 | 38 | 这个函数完成了所有的工作。它创建了一个JSON编码变量,并且这个变量和一个文件向关联,数据就是写到这个文件中去的。调用函数`Encode()`把数据编码后保存到指定文件中。 39 | 40 | 最后一部分: 41 | 42 | ```go 43 | func main() { 44 | myRecord := Record{ 45 | Name: "Mihalis", 46 | Surname: "Tsoukalos", 47 | Tel: []Telephone{Telephone{Mobile: true, Number: "1234-567"}, 48 | Telephone{Mobile: true, Number: "1234-abcd"}, 49 | Telephone{Mobile: false, Number: "abcc-567"}, 50 | }, 51 | } 52 | saveToJSON(os.Stdout, myRecord) 53 | } 54 | ``` 55 | 56 | 上面的代码是定义了一个存储数据的结构体变量,这个变量中保存了要以JSON格式存储的数据。因为我们使用了`os.Stdout`作为参数,数据会打印在屏幕上而不是保存到一个文件中。 57 | 58 | 执行这个程序,生成以下输出: 59 | 60 | ```shell 61 | $ go run writeJSON.go 62 | {"Name":"Mihalis","Surname":"Tsoukalos","Tel":[{"Mobile":true,"Number":"123 4-567"},{"Mobile":true,"Number":"1234- abcd"},{"Mobile":false,"Number":"abcc-567"}]} 63 | ``` 64 | 65 | -------------------------------------------------------------------------------- /eBook/chapter4/04.9.3.md: -------------------------------------------------------------------------------- 1 | # 使用函数Marshal()和Unmashal() 2 | 3 | 目前为止,我们已经知道如何处理事先就知道格式的JSON数据。这种数据可以存储到Go结构体中,这部分内容我们已经在之前的小节描述过了。 4 | 5 | 这一小节来告诉你如何读写**无结构的JSON数据**。这里有很重要的一点需要记住:无结构的JSON数据是和之前的处理不一样,并不是放到结构体中,而是放到Go map中的。我们将用`parsingJSON.go`这个程序来演示,分为四部分. 6 | 7 | 第一部分: 8 | 9 | ```go 10 | package main 11 | import ( 12 | "encoding/json" 13 | "fmt" 14 | "io/ioutil" 15 | "os" 16 | ) 17 | ``` 18 | 19 | 这一部分中,我们只是导入了需要的包。 20 | 21 | 第二部分: 22 | 23 | ```go 24 | func main() { 25 | arguments := os.Args 26 | if len(arguments) == 1 { 27 | fmt.Println("Please provide a filename!") 28 | return 29 | } 30 | filename := arguments[1] 31 | ``` 32 | 33 | 这部分代码读取命令参数,并取第一个参数,这个参数是我们要读取的JSON文件。 34 | 35 | 第三部分: 36 | 37 | ```go 38 | fileData, err := ioutil.ReadFile(filename) 39 | if err != nil { 40 | fmt.Println(err) 41 | return 42 | } 43 | var parsedData map[string]interface{} 44 | json.Unmarshal([]byte(fileData), &parsedData) 45 | ``` 46 | 47 | 函数`ioutil.ReadFile()`可以一次读取整个文件,这正是我们这里需要的。 48 | 49 | 这部分中,我们还定义了一个叫`parsedData`的map,这个map会保存我们读取的JSON文件的内容。这个map的每一个key,都是`string`类型的,对应一个JSON属性。每一个key对应的value都是`interface()`类型,可以是任何类型,也就是说key对应value可以是另一个map(递归?)。 50 | 51 | 函数`json.Unmarshal() `用来把文件的内容存到`parsedData`这个map中 52 | 53 | > 在第七章,你会学习到接口和反射。反射可以让你动态的获取一个对象的类型,还有这个对象的结构信息。 54 | 55 | 最后一部分: 56 | 57 | ```go 58 | for key, value := range parsedData { 59 | fmt.Println("key:", key, "value:", value) 60 | } 61 | } 62 | ``` 63 | 64 | 上面的代码展示了你可以遍历map,获取map的内容。当然,获取这个内容的信息就是另外一件事情了,这取决于数据的结构信息,而这些信息,我们并不知道。 65 | 66 | 我们用来测试的JSON文件叫`noStr.json`,包含以下内容: 67 | 68 | ```shell 69 | $ cat noStr.json 70 | { 71 | "Name": "John", 72 | "Surname": "Doe", 73 | "Age": 25, 74 | "Parents": [ 75 | "Jim", 76 | "Mary" 77 | ], 78 | "Tel":[ 79 | {"Mobile":true,"Number":"1234-567"}, 80 | {"Mobile":true,"Number":"1234-abcd"}, 81 | {"Mobile":false,"Number":"abcc-567"} 82 | ] 83 | } 84 | ``` 85 | 86 | 执行`parsingJSON.go `文件会得到以下输出: 87 | 88 | ```shell 89 | $ go run parsingJSON.go noStr.json 90 | key: Tel value: [map[Mobile:true Number:1234-567] map[Mobile:true Number:1234-abcd] map[Mobile:false Number:abcc-567]] 91 | key: Name value: John 92 | key: Surname value: Doe 93 | key: Age value: 25 94 | key: Parents value: [Jim Mary] 95 | ``` 96 | 97 | 从输出我们可以又一次看到,打印出map的key是无序的。 -------------------------------------------------------------------------------- /eBook/chapter4/04.9.md: -------------------------------------------------------------------------------- 1 | #Go和JSON格式 2 | 3 | **JSON**是一种非常流行的,基于文本的格式,它被设计用来在JavaScript系统之间传递信息。当然,JSON也可以用来为应用创建配置文件,以结构化的格式存储信息。 4 | 5 | 标准库包`encoding/json`提供了`Encode()`和`Decode()`两个函数,可以使用这两个函数在JSON文本和Go对象之间相互转换。另外,这个包来提供了`Marshal()`和`Unmarshal()`两个函数,这两个函数和`Encode()`和`Decode()`的工作方式是相似,也确实是基于这两个函数的。这两对函数之间最主要的区别是`Encode()`和`Decode()`处理单个对象,而后者可以处理多个对象或者字节流。 6 | 7 | -------------------------------------------------------------------------------- /eBook/chapter4/4.10.1.md: -------------------------------------------------------------------------------- 1 | #读取XML文件 2 | 3 | 你将会在本节中学习如何读磁盘上的XML文件,并把它存储到一个Go结构体中。程序的名字是` readXML.go`,分三部分展示。 4 | 5 | 第一部分: 6 | 7 | ```go 8 | package main 9 | import ( 10 | "encoding/xml" 11 | "fmt" 12 | "os" 13 | ) 14 | type Record struct { 15 | Name string 16 | Surname string 17 | Tel []Telephone 18 | } 19 | type Telephone struct { 20 | Mobile bool 21 | Number string 22 | } 23 | ``` 24 | 25 | 第二部分: 26 | 27 | ```go 28 | func loadFromXML(filename string, key interface{}) error { 29 | in, err := os.Open(filename) 30 | if err != nil { 31 | return err 32 | } 33 | decodeXML := xml.NewDecoder(in) err = decodeXML.Decode(key) 34 | if err != nil { 35 | return err 36 | } 37 | in.Close() 38 | return nil 39 | } 40 | ``` 41 | 42 | 上面代码展示的过程和你读取磁盘上的JSON的过程很像。 43 | 44 | 最后一部分: 45 | 46 | ```go 47 | func main() { 48 | arguments := os.Args 49 | if len(arguments) == 1 { 50 | fmt.Println("Please provide a filename!") 51 | return 52 | } 53 | filename := arguments[1] 54 | var myRecord Record 55 | err := loadFromXML(filename, &myRecord) 56 | if err == nil { 57 | fmt.Println("XML:", myRecord) 58 | } else { 59 | fmt.Println(err) 60 | } 61 | } 62 | ``` 63 | 64 | 执行这个程序会生成以下输出: 65 | 66 | ```shell 67 | $ go run readXML.go data.xml 68 | XML: {Dimitris Tsoukalos [{true 1234-567} {true 1234-abcd} {false abcc-567}]} 69 | ``` 70 | 71 | 文件`data.xml`的内容是这样的: 72 | 73 | ```shell 74 | $ cat data.xml 75 | xmlData: 76 | 77 | Dimitris 78 | Tsoukalos 79 | 80 | true 81 | 1234-567 82 | 83 | 84 | true 85 | 1234-abcd 86 | 87 | 88 | false 89 | abcc-567 90 | 91 | 92 | ``` 93 | 94 | -------------------------------------------------------------------------------- /eBook/chapter4/4.11.md: -------------------------------------------------------------------------------- 1 | # **Go和YAML格式** 2 | 3 | **YAML Ain't Markup Language** (**YAML**)是另一种非常流行的文本格式。虽然Go标准库并没有支持YAML格式,但是你可以看看这个外部库[https://github.com/go-yaml/yaml](https://github.com/go-yaml/yaml),这个库提供了对YAML的支持。 -------------------------------------------------------------------------------- /eBook/chapter4/4.12.md: -------------------------------------------------------------------------------- 1 | # **其他资源** 2 | 3 | 看看下面这个资源: 4 | 5 | - 阅读`regexp`标准库的官方文档-https://golang.org/pkg/regexp 6 | - 阅读`grep`工具的使用说明 7 | - 关于`math/big`包的详细讲解-https://golang.org/pkg/math/big 8 | - 在[https://yaml.org/](https://yaml.org/),你可以找到关于YAML的更多信息 9 | - 你可能会觉得`sync.Map`这个类型很有趣,这里[https://golang.org/pkg/sync/](https://golang.org/pkg/sync/)有这个类型的解释 10 | - 阅读`unicode`标准库的官方文档-https://golang.org/pkg/unicode 11 | - 开始阅读官方的Go语言规范吧-https://golang.org/ref/spec 12 | 13 | -------------------------------------------------------------------------------- /eBook/chapter4/4.13.md: -------------------------------------------------------------------------------- 1 | # **练习** 2 | 3 | - 写一个Go程序,识别无效的IPv4地址 4 | - 说说`make`和`new`的区别 5 | - 说说字符、字节和`rune`之间的区别 6 | - 在不使用UNIX工具的前提下,修改`findIPv4.go`使之能够打印出出现次数最多的IPv4地址 7 | - 写一个Go程序,能够识别出log文件中产生404错误的IPv4地址 8 | - 使用`math/big`包计算高精度平方根,算法自选 9 | - 写一个Go程序,从给定时间与日期中寻找特定格式的时间。 10 | - 写一个正则表达式匹配200-400之间的整数 11 | - 为`keyValue.go`添加日志打印 12 | - 修改`findIPv4.go`中的`findIP()`函数,保证`findIp()`多次调用时正则表达式只编译一次 13 | 14 | -------------------------------------------------------------------------------- /eBook/chapter4/4.14.md: -------------------------------------------------------------------------------- 1 | # **本章小结** 2 | 3 | 在本章中,你学到了Go的很多实用特性,包括创建与使用结构体,元组,字符串,runes以及Unicode标准库。另外,你也掌握了模式匹配与正则表达式,处理JSON和XML文件,`switch`语句,`strings`标准库。 4 | 5 | 最后,你用Go语言实现了一个简单版本的K-V存储,使用`math/big`包计算出高精度的Pi值。 6 | 7 | 在下一章中,你将学习使用更加高级的数据结构来组织和操作数据,比如二叉树,链表,双向链表,队列,栈,哈希表。你还会发现`container`标准库中的数据结构,如何进行矩阵操作,如何确认数独谜题。下一章最后的主题是**随机数**,将生成难以破解的密码字符串。 8 | -------------------------------------------------------------------------------- /eBook/chapter5/05.0.md: -------------------------------------------------------------------------------- 1 | ### Go数据结构 2 | 3 | 在上一章里,我们讨论了以**struct**关键字定义的组合类型,Go中JSON和XML使用,正则表达式,模式匹配,元组,runes,字符串,以及unicode和字符串的标准Go包。最后,我们基于Go开发了一个简单的key-value存储。 4 | 5 | 但是,有时候编程语言提供的结构并不能适用于特定的问题,在这种情况下,你需要创建自己的数据结构,以便以准确和专业的方式来存储、搜索、接收数据。 6 | 7 | 因此,本章将介绍如何在Go中开发和使用许多著名的数据结构,包括**二叉树**,**链表**,**哈希表**,**堆栈**和**队列**,以及学习它们的优点。没有什么比图解能更好的描述数据结构,因此你将会在本章看到很多图示。 8 | 9 | 本章的最后一部分将讨论验证数独游戏和执行使用切片的矩阵计算。 10 | 11 | 本章你将会学习的主题: 12 | 13 | - 图和节点 14 | - 分析算法的复杂度 15 | - 二叉树 16 | - 哈希表 17 | - 链表 18 | - 双向链表 19 | - Go的队列 20 | - 堆栈 21 | - Go标准包container所提供的数据结构 22 | - 执行矩阵计算 23 | - 运用于数独游戏 24 | - Go生成随机数 25 | - 创建难以破解的随机字符串密码 -------------------------------------------------------------------------------- /eBook/chapter5/05.01.md: -------------------------------------------------------------------------------- 1 | # **图和节点** 2 | 图(G(V,E))是由一组有限的,非空的顶点(V)(或节点)和一组边(E)组成的。其有两种主要的类型:**有环图**和**无环图**。有环图是指所有或多个顶点连接在一个闭环中的图。在无环图里,没有任何闭环。 3 | 4 | **有向图**是指其边具有方向性,**有向无环图**就是没有环的有向图 5 | 6 | 提示:因为节点可能包含任何类型的信息,所以节点通常是由于其通用性而可以使用Go结构实现。 7 | 8 | -------------------------------------------------------------------------------- /eBook/chapter5/05.02.md: -------------------------------------------------------------------------------- 1 | # **算法复杂度** 2 | 算法的效率取决于其计算复杂度,这主要是与算法完成工作而需要访问其输入数据的次数有关。在计算机科学中,通常是使用大`O`符号来描述一个算法的复杂度。因此,一个O(n)的算法只需要访问一次输入,要优于O(n2)算法和O(n3)算法。最差的算法就是O(n!),其在输入超过300个元素后几乎无法运行。 3 | 4 | 最后,大多数Go内置类型的查找操作都是在常量时间内完成的,以O(1)来表示,例如通过map的key来查找value或者访问一个数组元素。这意味着这些内置类型比自定义类型更快,所以你应该优先使用这些它们,除非你想完全控制幕后发生的事情。 5 | 6 | 此外,并非所有的数据结构都是以同等方式创建的。通常来说,数组操作会比map操作更快,这也是为了map的多功能性而不得不付出的代价。 7 | 8 | 提示:尽管每个算法都会有它的缺点,但是当数据量不大的时候,只要能正确的执行所需完成的工作,算法本身其实并不重要。 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /eBook/chapter5/05.03.01.md: -------------------------------------------------------------------------------- 1 | # **使用Go实现一个二叉树** 2 | 3 | 本节介绍了如何使用Go实现一个二叉树,示例代码在`binTree.go`里。`binTree.go`的内容将会分为5个部分。第一部分如下: 4 | ```go 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "math/rand" 10 | "time" 11 | ) 12 | 13 | type Tree struct { 14 | Left *Tree 15 | Value int 16 | Right *Tree 17 | } 18 | 19 | ``` 20 | 你看到的是使用Go的结构体定义的树的节点。因为我们没有真实数据,所以在这里使用`math/rand`包生成随机数据来填充树 21 | 22 | `binTree.go`的第二部分代码如下: 23 | ```go 24 | 25 | func traverse(t *Tree) { 26 | if t == nil { 27 | return 28 | } 29 | traverse(t.Left) 30 | fmt.Print(t.Value, " ") 31 | traverse(t.Right) 32 | } 33 | 34 | ``` 35 | `traverse()`函数展示了怎样使用递归来访问一个二叉树里的所有节点。 36 | 37 | 第三部分代码如下: 38 | ```go 39 | 40 | func create(n int) *Tree { 41 | var t *Tree 42 | rand.Seed(time.Now().Unix()) 43 | for i := 0; i < 2*n; i++ { 44 | temp := rand.Intn(n * 2) 45 | t = insert(t, temp) 46 | } 47 | return t 48 | } 49 | 50 | ``` 51 | `create()`函数只是用来向该二叉树填充随机整数。 52 | 53 | 第四部分代码如下: 54 | ```go 55 | 56 | func insert(t *Tree, v int) *Tree { 57 | if t == nil { 58 | return &Tree{nil, v, nil} 59 | } 60 | if v == t.Value { 61 | return t 62 | } 63 | if v < t.Value { 64 | t.Left = insert(t.Left, v) 65 | return t 66 | } 67 | t.Right = insert(t.Right, v) 68 | return t 69 | } 70 | 71 | ``` 72 | 73 | `insert()`函数使用`if`语句做了很多重要的工作。 74 | 75 | 第一个`if`语句判断了当前要操作的树是否为空,如果为空,那么将会通过`&Tree{nil, v, nil}`创建一个新的节点作为树的根节点。 76 | 77 | 第二个`if`语句判断了当前要插入的数是否已经存在,如果已经存在的话,这个函数将什么也不做直接返回。 78 | 79 | 第三个`if`语句判断了当前要插入的数是在当前节点的左边还是右边,并执行相应操作。 80 | 81 | 请注意,这里所要实现的是创建非平衡树。 82 | 83 | `binTree.go`的最后一部分代码如下: 84 | 85 | 86 | ```go 87 | 88 | func main() { 89 | tree := create(10) 90 | fmt.Println("The value of the root of the tree is", 91 | tree.Value) 92 | traverse(tree) 93 | fmt.Println() 94 | tree = insert(tree, -10) 95 | tree = insert(tree, -2) 96 | traverse(tree) 97 | fmt.Println() 98 | fmt.Println("The value of the root of the tree is", 99 | tree.Value) 100 | } 101 | 102 | ``` 103 | 执行 `binTree.go` 后将会生成类似如下输入: 104 | 105 | ``` 106 | $ go run binTree.go 107 | The value of the root of the tree is 18 108 | 0 3 4 5 7 8 9 10 11 14 16 17 18 19 109 | -10 -2 0 3 4 5 7 8 9 10 11 14 16 17 18 19 110 | The value of the root of the tree is 18 111 | ``` 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /eBook/chapter5/05.03.02.md: -------------------------------------------------------------------------------- 1 | # **二叉树的优点** 2 | 3 | 当你要描述多层级数据的时候,二叉树是最好的选择。因此,树被广泛使用于编程语言的编译器以解析计算机程序。 4 | 5 | 此外,树结构设计出来是*有序的*,这意味你不需要花费额外的功夫去排序(将元素插入正确的位置以保证树的有序性)。然而,由于树是按照这样方式构造出来的,删除一个元素并不简单。 6 | 7 | 如果一个二叉树是平衡的,那么它的搜索,插入,和删除操作都需要花费 *log(n)* 步,其中 *n* 是这个树的所有元素总数。此外,一个平衡二叉树的高度大约为 *log2(n)*,这意味着一个拥有10,000个元素的平衡树的高度大约为14,这个高度很明显非常小。 8 | 9 | 同样的,一个拥有100,000个元素的平衡树的高度大约为17,一个拥有1,000,000个元素的平衡树的高度大约为20。换句话说,将大量的元素放入一个平衡二叉树后,并不会大幅度影响树的速度。而且,你只需要不超过20步就能访问到一个拥有1,000,000个元素的树的任何一个节点。 10 | 11 | 二叉树的一个主要缺点就是它的形状取决于它的元素的插入顺序。如果一个树上的键又长又复杂,由于会进行大量的比较,所以插入或搜索操作将会变的十分缓慢。最后,如果一个树是非平衡树,那么就很难对其性能进行预测。 12 | 13 | 提示:尽管你创建的链表或者数组比二叉树更加快,但是二叉树在搜索操作里提供的灵活性使其完全值得付出更多的开销和维护。当你在二叉树里查找某一个元素时,需要检查所要查找的元素的值是大于还是小于当前节点的值,并由此来决定下一条子树,这样做可以节省大量时间。 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /eBook/chapter5/05.03.md: -------------------------------------------------------------------------------- 1 | # **Go语言里的二叉树** 2 | 3 | **二叉树**是一种每个节点下方最多只能有两个节点的数据结构。其意味着一个节点能连接到一至两个子节点,或者没有任何子节点。**树的根节点**就是树的第一个节点。**树的深度**(也称**树的高度**),指的就是从根节点到任一节点最长的一条路径。**节点的深度**就是指从该节点到树的根节点所经过的路径里边的数量。**叶节点**就是没有任何子节点的节点。 4 | 5 | 当树的根节点到叶节点的最大距离和根节点到叶节点的最短距离之差不大于1时,则认为这个树为**平衡的**。反之则为**非平衡树**。树的平衡操作可能是困难又缓慢的,所以最好一开始就让你的树保持平衡,而不是在创建后再去平衡它,尤其是当你的树有很多节点的时候。 6 | 7 | 下图展示了一个非平衡树,其根节点为J,叶节点为A,G,W和D。 8 | 9 | ![](images/image050301.png) -------------------------------------------------------------------------------- /eBook/chapter5/05.04.02.md: -------------------------------------------------------------------------------- 1 | # **实现查找功能** 2 | 在本节中,你将会实现一个`lookup()`函数,其功能为判断某个给定的元素是否存在于哈希表中。`lookup()`函数的代码基于`traverse()`函数实现,如下所示: 3 | 4 | ```go 5 | func lookup(hash *HashTable, value int) bool { 6 | index := hashFunction(value, hash.Size) 7 | if hash.Table[index] != nil { 8 | t := hash.Table[index] 9 | for t != nil { 10 | if t.Value == value { 11 | return true 12 | } 13 | t = t.Next 14 | } 15 | } 16 | return false 17 | } 18 | ``` 19 | 20 | 你能在`hashTableLookup.go`源文件中找到上述代码,执行`hashTableLookup.go`后将会看到如下输出: 21 | ```gotemplate 22 | $ go run hashTableLookup.go 23 | 120 is not in the hash table! 24 | 121 is not in the hash table! 25 | 122 is not in the hash table! 26 | 123 is not in the hash table! 27 | 124 is not in the hash table! 28 | ``` 29 | 30 | 上面的输入代表着`lookup()`函数能正确的完成工作。 31 | 32 | -------------------------------------------------------------------------------- /eBook/chapter5/05.04.03.md: -------------------------------------------------------------------------------- 1 | # **哈希表的优点** 2 | 3 | 如果你觉得哈希表不实用、不方便或不灵活,请考虑以下场景:当一个哈希表拥有*n*个键和*k*个存储桶时,查找n个键的时间复杂度将由线性查找的*O(n)*下降到*O(n/k)*。尽管这看起来提升不大,但是对于一个拥有20个槽的哈希表来说,其查找速度加快了20倍!这使得哈希表非常适合用于字典或其他这类需要大量数据查找的应用。 4 | -------------------------------------------------------------------------------- /eBook/chapter5/05.04.md: -------------------------------------------------------------------------------- 1 | # **Go语言里的哈希表** 2 | 3 | 严格来说,哈希表是一种以一个或多个键值对存储的数据结构,其使用了一个 **哈希函数** 来通过存放在存储桶或槽里的索引计算找出正确的值。理想情况下,哈希函数应该将每一个键和存储桶一一对应,前提是你得拥有足够的存储桶。 4 | 5 | 一个好的哈希函数其生成的哈希值一定要是均匀分布的,如果存在未使用的存储桶或者存储桶的差异很大,那么其工作效率将会很低。此外,哈希函数应该具有一致性,对于相同的键值其需要输出相同的哈希值,否则将无法找到想要的信息。 6 | 7 | 下图展示了一个拥有10个存储桶的哈希表: 8 | 9 | ![](images/image050401.png) 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /eBook/chapter5/05.05.02.md: -------------------------------------------------------------------------------- 1 | # **链表的优点** 2 | 3 | 链表最大的优点就是其易于理解和实现,并且通用性强,可以在很多不同的情况下使用。这意味着可以用链表对各种不同类型的数据建模。此外,在链表中使用指针进行顺序检索的速度非常快。 4 | 5 | 链表不仅可以对数据进行排序,还可以在插入或删除元素之后保持数据的有序性。有序链表和无序链表的删除元素操作是相同的,但由于新加入的节点必须要移到正确的位置才能保证排序,所以插入元素操作是不同的。实际上,如果你有大量数据并且会频繁删除数据,那么相对于哈希表和二叉树,使用链表是一个更好的选择。 6 | 7 | 最后,有很多技术可以优化有序链表中搜索和查找节点的过程。其中最常用的一种就是保存一个指向有序链表中间节点的指针,之后每次都从这个节点开始查找。这个简单的优化方法可以将查找操作的时间减少一半! -------------------------------------------------------------------------------- /eBook/chapter5/05.05.md: -------------------------------------------------------------------------------- 1 | # **Go语言里的链表** 2 | 3 | 链表是一种包含有限元素集的数据结构,其每个元素至少使用了两个内存位置:一个是用于存储实际的数据,另一个是用于存储将当前元素链接到下一个元素的指针,链表就是由这些元素构建出来的。 4 | 5 | 链表的第一个元素称之为**头**,最后一个元素称之为**尾**。在定义链表的过程中,你需要做的第一件事就是将链表头使用单独的变量保存,因为链表头是访问整个链表的唯一方式。注意,如果你丢失了指向单向链表第一个节点的指针,那就再也找不到它了。 6 | 7 | 下图展示了一个拥有5个节点的链表: 8 | ![image050501](images/image050501.png) 9 | 10 | 下图向你展示了如何将一个节点从链表中移除,以便更好的理解该过程中所涉及到的步骤。你需要做的就是调整被删除节点的左节点的指针,使其指向被删除节点的右节点。 11 | 12 | ![image050502](images/image050502.png) 13 | 14 | 接下来要实现的链表功能将会相对简单,其中不包含删除节点的功能。该功能的实现将会是你的练习题。 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /eBook/chapter5/examples/binTree.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | type Tree struct { 10 | Left *Tree 11 | Value int 12 | Right *Tree 13 | } 14 | 15 | func traverse(t *Tree) { 16 | if t == nil { 17 | return 18 | } 19 | traverse(t.Left) 20 | fmt.Print(t.Value, " ") 21 | traverse(t.Right) 22 | } 23 | 24 | func create(n int) *Tree { 25 | var t *Tree 26 | rand.Seed(time.Now().Unix()) 27 | for i := 0; i < 2*n; i++ { 28 | temp := rand.Intn(n * 2) 29 | t = insert(t, temp) 30 | } 31 | return t 32 | } 33 | 34 | func insert(t *Tree, v int) *Tree { 35 | if t == nil { 36 | return &Tree{nil, v, nil} 37 | } 38 | if v == t.Value { 39 | return t 40 | } 41 | if v < t.Value { 42 | t.Left = insert(t.Left, v) 43 | return t 44 | } 45 | t.Right = insert(t.Right, v) 46 | return t 47 | } 48 | 49 | func main() { 50 | tree := create(10) 51 | fmt.Println("The value of the root of the tree is", 52 | tree.Value) 53 | traverse(tree) 54 | fmt.Println() 55 | tree = insert(tree, -10) 56 | tree = insert(tree, -2) 57 | traverse(tree) 58 | fmt.Println() 59 | fmt.Println("The value of the root of the tree is", 60 | tree.Value) 61 | } 62 | -------------------------------------------------------------------------------- /eBook/chapter5/examples/hashTable.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | const SIZE = 15 6 | 7 | type Node struct { 8 | Value int 9 | Next *Node 10 | } 11 | 12 | type HashTable struct { 13 | Table map[int]*Node 14 | Size int 15 | } 16 | 17 | func hashFunction(i, size int) int { 18 | return (i % size) 19 | } 20 | 21 | func insert(hash *HashTable, value int) int { 22 | index := hashFunction(value, hash.Size) 23 | element := Node{Value: value, Next: hash.Table[index]} 24 | hash.Table[index] = &element 25 | return index 26 | } 27 | 28 | func traverse(hash *HashTable) { 29 | for k := range hash.Table { 30 | if hash.Table[k] != nil { 31 | t := hash.Table[k] 32 | for t != nil { 33 | fmt.Printf("%d -> ", t.Value) 34 | t = t.Next 35 | } 36 | fmt.Println() 37 | } 38 | } 39 | } 40 | 41 | func main() { 42 | table := make(map[int]*Node, SIZE) 43 | hash := &HashTable{Table: table, Size: SIZE} 44 | fmt.Println("Number of spaces:", hash.Size) 45 | for i := 0; i < 120; i++ { 46 | insert(hash, i) 47 | } 48 | traverse(hash) 49 | } 50 | -------------------------------------------------------------------------------- /eBook/chapter5/examples/hashTableLookup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const SIZE = 15 8 | 9 | type Node struct { 10 | Value int 11 | Next *Node 12 | } 13 | 14 | type HashTable struct { 15 | Table map[int]*Node 16 | Size int 17 | } 18 | 19 | func hashFunction(i, size int) int { 20 | return (i % size) 21 | } 22 | 23 | func insert(hash *HashTable, value int) int { 24 | index := hashFunction(value, hash.Size) 25 | element := Node{Value: value, Next: hash.Table[index]} 26 | hash.Table[index] = &element 27 | return index 28 | } 29 | 30 | func traverse(hash *HashTable) { 31 | for k := range hash.Table { 32 | if hash.Table[k] != nil { 33 | t := hash.Table[k] 34 | for t != nil { 35 | fmt.Printf("%d -> ", t.Value) 36 | t = t.Next 37 | } 38 | fmt.Println() 39 | } 40 | } 41 | } 42 | 43 | func lookup(hash *HashTable, value int) bool { 44 | index := hashFunction(value, hash.Size) 45 | if hash.Table[index] != nil { 46 | t := hash.Table[index] 47 | for t != nil { 48 | if t.Value == value { 49 | return true 50 | } 51 | t = t.Next 52 | } 53 | } 54 | return false 55 | } 56 | 57 | func main() { 58 | table := make(map[int]*Node, SIZE) 59 | hash := &HashTable{Table: table, Size: SIZE} 60 | for i := 0; i < 120; i++ { 61 | insert(hash, i) 62 | } 63 | 64 | for i := 10; i < 125; i++ { 65 | if !lookup(hash, i) { 66 | fmt.Println(i, "is not in the hash table!") 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /eBook/chapter5/examples/linkedList.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Node struct { 8 | Value int 9 | Next *Node 10 | } 11 | 12 | var root = new(Node) 13 | 14 | func addNode(t *Node, v int) int { 15 | if root == nil { 16 | t = &Node{v, nil} 17 | root = t 18 | return 0 19 | } 20 | 21 | if v == t.Value { 22 | fmt.Println("Node already exists:", v) 23 | return -1 24 | } 25 | 26 | if t.Next == nil { 27 | t.Next = &Node{v, nil} 28 | return -2 29 | } 30 | 31 | return addNode(t.Next, v) 32 | } 33 | 34 | func traverse(t *Node) { 35 | if t == nil { 36 | fmt.Println("-> Empty list!") 37 | return 38 | } 39 | 40 | for t != nil { 41 | fmt.Printf("%d -> ", t.Value) 42 | t = t.Next 43 | } 44 | fmt.Println() 45 | } 46 | 47 | func lookupNode(t *Node, v int) bool { 48 | if root == nil { 49 | t = &Node{v, nil} 50 | root = t 51 | return false 52 | } 53 | 54 | if v == t.Value { 55 | return true 56 | } 57 | 58 | if t.Next == nil { 59 | return false 60 | } 61 | 62 | return lookupNode(t.Next, v) 63 | } 64 | 65 | func size(t *Node) int { 66 | if t == nil { 67 | fmt.Println("-> Empty list!") 68 | return 0 69 | } 70 | 71 | i := 0 72 | for t != nil { 73 | i++ 74 | t = t.Next 75 | } 76 | return i 77 | } 78 | 79 | func main() { 80 | fmt.Println(root) 81 | root = nil 82 | traverse(root) 83 | addNode(root, 1) 84 | addNode(root, -1) 85 | traverse(root) 86 | addNode(root, 10) 87 | addNode(root, 5) 88 | addNode(root, 45) 89 | addNode(root, 5) 90 | addNode(root, 5) 91 | traverse(root) 92 | addNode(root, 100) 93 | traverse(root) 94 | 95 | if lookupNode(root, 100) { 96 | fmt.Println("Node exists!") 97 | } else { 98 | fmt.Println("Node does not exist!") 99 | } 100 | 101 | if lookupNode(root, -100) { 102 | fmt.Println("Node exists!") 103 | } else { 104 | fmt.Println("Node does not exist!") 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /eBook/chapter5/images/image050301.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantmac/Mastering_Go_Second_Edition_Zh_CN/1338952db08c951b1de87a35045d377f84054cff/eBook/chapter5/images/image050301.png -------------------------------------------------------------------------------- /eBook/chapter5/images/image050401.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantmac/Mastering_Go_Second_Edition_Zh_CN/1338952db08c951b1de87a35045d377f84054cff/eBook/chapter5/images/image050401.png -------------------------------------------------------------------------------- /eBook/chapter5/images/image050501.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantmac/Mastering_Go_Second_Edition_Zh_CN/1338952db08c951b1de87a35045d377f84054cff/eBook/chapter5/images/image050501.png -------------------------------------------------------------------------------- /eBook/chapter5/images/image050502.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantmac/Mastering_Go_Second_Edition_Zh_CN/1338952db08c951b1de87a35045d377f84054cff/eBook/chapter5/images/image050502.png -------------------------------------------------------------------------------- /eBook/chapter6/06.0.md: -------------------------------------------------------------------------------- 1 | # 你不知道的Go packages和功能 2 | 3 | 上一章讲解了如何开发和如何自定的数据结构,如:linked lists、binary trees、hash table。用Go语言生成随机数和密码、执行高性能的矩阵操作等。 4 | 5 | 这章主要的重点是Go packages,它是用Go的方式组织、交付、使用代码。最重要的通用组件就是Go package中的函数,它使得在Go语言中变得可扩展。紧接着,这章将会讲解Go modules,这是带有版本号的Go packages。在这章的最后,你将了解一些属于Go标准库的packages,为了更好的理解Go packages的不同的创建方式。 6 | 7 | 在本章,你将学习到以下主题: 8 | - 用 Go 编写函数 9 | - 匿名函数 10 | - 多返回值函数 11 | - 命名函数返回值 12 | - 函数作为返回值 13 | - 函数作为参数 14 | - 可变函数 15 | - 编写 Go packages 16 | - 编写和使用 Go modules 17 | - 私有和公共 packages 对象 18 | - init 函数在 packages 中的使用 19 | - Go 标准包 `html/template`,复杂精炼 20 | - Go 标准包 `text/template`,另一个真正复杂的 Go 标准包,有自己的语言 21 | - Go 高级包 `go/scanner`、`go/parser` 和 `go/token` 22 | 23 | -------------------------------------------------------------------------------- /eBook/chapter6/06.1.md: -------------------------------------------------------------------------------- 1 | # 关于Go package 2 | 3 | 任何内容在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` 的语法形式单独使用。 4 | 5 | 为了和Go标准库做分离,存在一些能够使用全路径导入的外部包,在第一次使用之前会被下载。如`github.com/matryer/is`,是一个Github仓库。 6 | 7 | Packages主要使用于将函数、变量、 常量分组。为的是在使用自己的Go程序时迁移这些分组更加便捷。注意,除了主要的main package,其他Go packages 程序不能够被编译到可执行文件。这意味着它们需要被main package直接或者间接的引用。如果你执行一个自己的go package(不是main package)。出现如下错误: 8 | 9 | ```go 10 | $ go run aPackage.go 11 | go run: cannot run non-main package 12 | ``` -------------------------------------------------------------------------------- /eBook/chapter6/06.10.md: -------------------------------------------------------------------------------- 1 | # 练习 2 | 3 | 1. 寻找更多实现`fmt.Printf()`的信息。 4 | 5 | 2. 你能写一个函数用来存储3个int值吗?尝试写2个版本的函数:一个带有命名返回值,另一个没有命名返回值。你认为哪个更好? 6 | 7 | 3. 你能修改`htmlT.go`中的代码用`text/template`代替`html/template`吗? 8 | 9 | 10 | 4. 你能修改`htmlT.go`中的代码,使用[https://github.com/feyeleanor/gosqlite3](https:// 11 | github.com/feyeleanor/gosqlite3)或者[https://github.com/phf/go-sqlite3](https://github.com/phf/go-sqlite3)让它和SQLite数据库交互。 12 | 13 | 5. 创建你自己的Go module并且开发针对于它的3个大版本。 14 | 15 | 6. 写一个类似于`htmlT.go`一样能够读取Mysql数据库的程序。 16 | 17 | 7. 写下你所做的代码更改。 -------------------------------------------------------------------------------- /eBook/chapter6/06.11.md: -------------------------------------------------------------------------------- 1 | # 总结 2 | 3 | 这章主要展示了三部分主题:Go functions, Go packages, Go modules. 4 | 5 | Go modules主要的优势是准确的记录依赖信息,它能够重用多模块,让编程变得简单直接。这一章也给你提供了大量的关于开发优秀Go package的建议。随后讲了`text/template`和`html/template`包,它允许你创建基于模板的简单的text和html的输出。也有`go/token, go/parser, go/scanner`这些包。最后,讲了`syscall`标准Go库的高级特性。 6 | 7 | 下一章将要讨论关于Go语言重要的俩个特征:**接口和反射**。它将讨论OOP、Debug和Go type方法。所有的这些主题都比较高级,在第一次接触你可能感觉他们比较困难。然而学的更多你将成为了一个十分优秀的程序员。 8 | 9 | 最后,下一章包括简介git,就是在这章创建Go module的工具git。 -------------------------------------------------------------------------------- /eBook/chapter6/06.2.1.md: -------------------------------------------------------------------------------- 1 | # 关于Go函数 2 | 3 | 函数在任何编程语言中都是非常重要的一环,因为它允许你可以将大的程序分割成更小、更便于管理的部分。函数必须尽可能独立,必须做好一件工作,而且只做好一件工作。所以如果你发现你写一个函数做很多事,应该考虑用多个函数去替换它。(单一职责原则) 4 | 5 | Go语言中最流行的函数就是`main()`,它是每个独立的Go程序都使用的。你应该已经知道所有的函数定义都以`func`关键字开头。 6 | 7 | -------------------------------------------------------------------------------- /eBook/chapter6/06.2.10.md: -------------------------------------------------------------------------------- 1 | # 参数可变函数 2 | 3 | Go也支持**参数可变函数**,这些函数能够接收可变的参数个数。最为流行的函数在`fmt`包中可以找到。接下来通过`variadic.go`来讲解。将分3部分展示,第一部分如下所示: 4 | 5 | ``` go 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | ) 12 | 13 | func varFunc(input ...string) { 14 | fmt.Println(input) 15 | } 16 | ``` 17 | 18 | 这部分展示了参数可变(变长)函数`varFunc()`的实现,它接收string类型的可变参数。传入的参数`input`在函数`varFunc()`函数内部作为切片使用。而“...”操作叫做包装操作。而拆包操作以“...”结尾,以切片类型开始。可以参数函数的包装操作最多一次。第二部分代码如下所示: 19 | 20 | ```go 21 | func oneByOne(message string, s ...int) int { 22 | fmt.Println(message) 23 | sum := 0 24 | for i, a := range s { 25 | fmt.Println(i, a) 26 | sum = sum + a 27 | } 28 | s[0] = -1000 29 | return sum 30 | } 31 | ``` 32 | 33 | 这里你能看到其他可变参数函数`oneByOne()`,它接收单个string和一个int类型的可变参数。形参s其实就是一个切片。最后一部分代码如下所示: 34 | 35 | ```go 36 | func main() { 37 | arguments := os.Args 38 | varFunc(arguments...) 39 | sum := oneByOne("Adding numbers...", 1, 2, 3, 4, 5, -1, 10) 40 | fmt.Println("Sum:", sum) 41 | s := []int{1, 2, 3} 42 | sum = oneByOne("Adding numbers...", s...) 43 | fmt.Println(s) 44 | } 45 | ``` 46 | 47 | `main`函数中使用了2个可变参数函数。对第二个函数`oneByOne()`使用了切片。对可变参数函数内的切片所做的任何更改在函数退出后仍将保留。 48 | 49 | 执行代码后输出如下: 50 | 51 | ```shell 52 | $ ./variadic 1 2 53 | [./variadic 1 2] 54 | Adding numbers... 55 | 0 1 56 | 1 2 57 | 2 3 58 | 3 4 59 | 4 5 60 | 5 -1 61 | 6 10 62 | Sum: 24 63 | Adding numbers... 64 | 0 1 65 | 1 2 66 | 2 3 67 | [-1000 2 3] 68 | ``` 69 | -------------------------------------------------------------------------------- /eBook/chapter6/06.2.3.md: -------------------------------------------------------------------------------- 1 | # 匿名函数 2 | 3 | 匿名函数被定义在内部,不需要函数名,经常用它来实现一段代码量很少的功能。在Go语言中,函数能够返回一个匿名函数,或者将一个匿名函数作为函数的一个参数,此外,匿名函数能够被附加到Go变量。注意匿名函数也被称为闭包,特别是在函数式编程术语中。 4 | 5 | > 对于一个匿名函数来说,需要实现最小的一个重点。**如果匿名函数没有重点,你就要考虑将它改造为一个常规的函数。** 6 | 7 | 当一个匿名函数只提供一个功能时,它绝对会使你的工作更加简单便捷。只是如果没有合适的场景时,不要在你的程序中使用太多的匿名函数。稍后你将会看到匿名函数的实际使用。 8 | 9 | -------------------------------------------------------------------------------- /eBook/chapter6/06.2.4.md: -------------------------------------------------------------------------------- 1 | # 多返回值函数 2 | 3 | 如果你已经知道了`strconv.Atoi()`函数,此函数可以返回多个不同的值,这使你不必为它创建专用的结构 4 | 保存函数返回的多个值。你可以定义一个返回4个值的函数,如下 5 | 6 | ```go 7 | func aFunction() (int, int, float64, string) { 8 | } 9 | ``` 10 | 11 | 接下来解释匿名函数和多个返回值的函数,更详细内容使用functions.go文件来说明,用五部分代码来说明。 12 | 13 | 14 | 第一部分如下: 15 | 16 | ```go 17 | package main 18 | import ( 19 | "fmt" 20 | "os" 21 | "strconv" 22 | ) 23 | ``` 24 | 25 | 第二部分代码如下: 26 | 27 | ```go 28 | func doubleSquare(x int) (int, int) { 29 | return x * 2, x * x 30 | } 31 | ``` 32 | 33 | 这里有一个`doubleSquare()`的函数,它传入一个int类型的参数,返回两个int类型的值,第三部分代码如下: 34 | 35 | ```go 36 | func main() { 37 | arguments := os.Args 38 | if len(arguments) != 2 { 39 | fmt.Println("The program needs 1 argument!") 40 | return 41 | } 42 | y, err := strconv.Atoi(arguments[1]) 43 | if err != nil { 44 | fmt.Println(err) 45 | return 46 | } 47 | ``` 48 | 49 | 之前的程序是用命令行中附加的参数来处理的。第四部分包含以下内容: 50 | 51 | ```go 52 | square := func(s int) int { 53 | return s * s 54 | } 55 | fmt.Println("The square of", y, "is", square(y)) 56 | double := func(s int) int { 57 | return s + s 58 | } 59 | fmt.Println("The double of", y, "is", double(y)) 60 | ``` 61 | 62 | 每个square和double指向一个匿名函数的地址。不够好的部分是你可以改变square和double的值,或者其他变量在匿名函数定义之后也可以指向该匿名函数的地址。这意味着这些变量能够改变和计算其他内容。 63 | 64 | > 改变保存匿名函数的变量代码不是好的编程实践,因为这可能是产生bug的根本原因。 65 | 66 | 最后一部分代码如下: 67 | 68 | ```go 69 | fmt.Println(doubleSquare(y)) 70 | d, s := doubleSquare(y) 71 | fmt.Println(d, s) 72 | ``` 73 | 74 | 所以,你能同时打印函数的返回值,或者将它们分配给其他变量。 75 | 76 | 执行functions.go后: 77 | 78 | ```shell 79 | $ go run functions.go 1 21 80 | The program needs 1 argument! 81 | $ go run functions.go 10.2 82 | strconv.Atoi: parsing "10.2": invalid syntax 83 | $ go run functions.go 10 84 | The square of 10 is 100 85 | The double of 10 is 20 86 | 20 100 87 | 20 100 88 | ``` -------------------------------------------------------------------------------- /eBook/chapter6/06.2.5.md: -------------------------------------------------------------------------------- 1 | # 函数的返回值可以被命名 2 | 3 | 不像C语言,Go允许将函数的返回值命名。当这样的函数有一个不带任何参数的return语句时,**该函数会按照函数定义中声明的顺序自动返回每个命名的返回值的当前值。** 4 | 5 | returnNames.go源码将会说明,已经命名了返回值的函数是怎样工作的。 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | "strconv" 14 | ) 15 | 16 | func namedMinMax(x, y int) (min, max int) { 17 | if x > y { 18 | min = y 19 | max = x 20 | } else { 21 | min = x 22 | max = y 23 | } 24 | return 25 | } 26 | ``` 27 | 28 | 在这段代码中,可以看到实现了`namedMinMax()`的函数,它使用了已经命名的返回参数。然而,这里有一个容易出错的地方:`namedMinMax()`函数在它的return语句中,不能明确的返回一些变量或值。尽管如此,当函数在命名时指定了返回值,min和max参数将按照它们放入函数定义的顺序自动返回。 29 | 30 | 第二段代码如下: 31 | 32 | ```go 33 | func minMax(x, y int) (min, max int) { 34 | if x > y { 35 | min = y 36 | max = x 37 | } else { 38 | min = x 39 | max = y 40 | } 41 | return min, max 42 | } 43 | ``` 44 | 45 | `minMax()`函数也命名了返回值,但是它的return语句明确定义了返回的顺序和变量。 46 | 47 | 最后一部分代码如下: 48 | 49 | ```go 50 | func main() { 51 | arguments := os.Args 52 | if len(arguments) < 3 { 53 | fmt.Println("The program needs at least 2 arguments!") 54 | return 55 | } 56 | a1, _ := strconv.Atoi(arguments[1]) 57 | a2, _ := strconv.Atoi(arguments[2]) 58 | 59 | fmt.Println(minMax(a1, a2)) 60 | min, max := minMax(a1, a2) 61 | fmt.Println(min, max) 62 | fmt.Println(namedMinMax(a1, a2)) 63 | min, max = namedMinMax(a1, a2) 64 | fmt.Println(min, max) 65 | } 66 | ``` 67 | 68 | `main()`函数的目的是为了验证所有的方法执行的结果是否相同。 69 | 70 | 最后一段代码执行后输入如下: 71 | 72 | ```shell 73 | $ go run returnNames.go -20 1 74 | -20 1 75 | -20 1 76 | -20 1 77 | -20 1 78 | ``` 79 | 80 | 81 | -------------------------------------------------------------------------------- /eBook/chapter6/06.2.6.md: -------------------------------------------------------------------------------- 1 | # 带有指针参数的函数 2 | 3 | 只要签名允许,函数可以接受指针形参。ptrFun.go将会讲解如何在函数中使用指针。 4 | 5 | 第一部分: 6 | 7 | ```go 8 | package main 9 | import ( 10 | "fmt" 11 | ) 12 | func getPtr(v *float64) float64 { 13 | return *v * *v 14 | } 15 | ``` 16 | 17 | `getPtr()`函数接收一个值指向类型为float64的指针变量。第二部分代码展示如下: 18 | 19 | ```go 20 | func main() { 21 | x := 12.2 22 | fmt.Println(getPtr(&x)) 23 | x = 12 24 | fmt.Println(getPtr(&x)) 25 | } 26 | ``` 27 | 28 | 比较复杂的部分是函数参数中需要传入变量的地址,因为它是指针参数所需要的类型。通过在变量前加"&"符号实现。 29 | 30 | 执行`ptrFun.go`后将生成如下输出: 31 | 32 | ```shell 33 | $ go run ptrFun.go 34 | 148.83999999999997 35 | 144 36 | 37 | ``` 38 | 39 | 如果想传入一个简单的值到`getPtr()`函数中调用,类似于`getPtr(12.12)`。这样程序将会失败,并出现如下错误: 40 | 41 | ```shell 42 | $ go run ptrFun.go 43 | # command-line-arguments 44 | ./ptrFun.go:15:21: cannot use 12.12 (type float64) as type *float64 in argument to getPtr 45 | ``` -------------------------------------------------------------------------------- /eBook/chapter6/06.2.7.md: -------------------------------------------------------------------------------- 1 | # 返回指针的函数 2 | 3 | 从第四章开始,`pointerStruct.go`文件就用来作为案例代码。对于复合类型的使用,最好的做法是使用单独的函数创建新的结构变量,并从该函数返回指向它们的指针。所以,函数返回指针是很常见的。通常来说,这种函数简化了程序结构,并且允许开发者集中于更多重要的事情上,而不是总是复制一些相同的代码片段。(`Ctrl+C`和`Ctrl+v`,造就了一代又一代的程序员,皮一下)。接下来将通过`pointerStruct.go`的代码来说明本小节的内容。第一部分代码如下: 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | ) 11 | 12 | func returnPtr(x int) *int { 13 | y := x * x 14 | return &y 15 | } 16 | ``` 17 | 18 | 这个函数返回了一个指向`int`变量的指针。唯一的功能就是使用`&y`返回y变量的内存地址。第二部分如下: 19 | 20 | ```go 21 | func main() { 22 | sq := returnPtr(10) 23 | fmt.Println("sq value:", *sq) 24 | ``` 25 | 26 | `“*”`这个符号可以得到指针变量的值,这意味着它返回了一个在内存地址中实际代表的值。(而不是值的地址),最后一段代码如下所示: 27 | 28 | ```go 29 | fmt.Println("sq memory address:", sq) 30 | } 31 | ``` 32 | 33 | 前面的代码将返回sq变量的内存地址。而不是存在其内存中的值。 34 | 35 | 如果执行`returnPtr.go`,你将看到以下输出。不同的内存地址,(因为我们在不同的机器执行,所以变量内存不一定分配在哪一片段) 36 | 37 | ```shell 38 | $ go run returnPtr.go 39 | sq value: 100 40 | sq memory address: 0xc00009a000 41 | ``` -------------------------------------------------------------------------------- /eBook/chapter6/06.2.8.md: -------------------------------------------------------------------------------- 1 | # 返回其他函数的函数 2 | 3 | 在这个章节中,我们一起学习如何用Go语言实现一个返回其他函数的函数。分为三个部分展示,第一部分`returnFunction.go`如下: 4 | 5 | ```go 6 | package main 7 | import ( 8 | "fmt" 9 | ) 10 | func funReturnFun() func() int { 11 | i := 0 12 | return func() int { 13 | i++ 14 | return i * i 15 | } 16 | } 17 | ``` 18 | 19 | 我们可以看到`funReturnFun()`的实现,它的返回值是一个匿名函数`function (func() int)`。 20 | 21 | 第二部分代码如下: 22 | 23 | ```go 24 | func main() { 25 | i := funReturnFun() 26 | j := funReturnFun() 27 | ``` 28 | 29 | 在这段代码中,调用funReturnFun()两次,并将其返回值(一个函数)赋给两个独立的变量i和j。我们可以看到程序输出中,这两个变量完全不相关。最后一部分代码如下: 30 | 31 | ```go 32 | fmt.Println("1:", i()) 33 | fmt.Println("2:", i()) 34 | fmt.Println("j1:", j()) 35 | fmt.Println("j2:", j()) 36 | fmt.Println("3:", i()) 37 | } 38 | ``` 39 | 40 | 在这段代码中,使用i变量3次,j变量2次。但是尽管i和j都是通过`funReturnFun()函数`被创建。但是他们彼此之间相互独立,没有任何相同之处。 41 | 42 | 执行代码后输出: 43 | 44 | ```go 45 | $ go run returnFunction.go 46 | 1: 1 47 | 2: 4 48 | j1: 1 49 | j2: 4 50 | 3: 9 51 | ``` 52 | 53 | As you can see from the output of returnFunction.go, the value of i in funReturnFun() keeps increasing and does not become 0 after each call either to i() or j(). 54 | 55 | 从输出内容可以看出,在每一次调用i()或j()之后,i的值保持自增没有变为0, -------------------------------------------------------------------------------- /eBook/chapter6/06.2.9.md: -------------------------------------------------------------------------------- 1 | # 将函数作为参数的函数 2 | 3 | Go函数能够接受其他Go函数作为其参数,它的特征是能够添加其他Go函数实现的其他用途。两个最常用的功能就是元素排序和`filepath.Walk()`函数。然而,在展示出的例子中,它被命名为`funFun.go`。我们将实现最简单的处理int值的例子。有关实现将分为三部分实现。 4 | 5 | 第一部分代码如下: 6 | 7 | ```go 8 | package main 9 | 10 | import "fmt" 11 | 12 | func function1(i int) int { 13 | return i + i 14 | } 15 | func function2(i int) int { 16 | return i * i 17 | } 18 | ``` 19 | 20 | 我们有两个函数都接收int返回int。这些函数将在一段时间内被作为其他函数的参数来使用。第二部分代码如下图所示: 21 | 22 | ```go 23 | func funFun(f func(int) int, v int) int { 24 | return f(v) 25 | } 26 | ``` 27 | 28 | `funFun()`函数接收两个参数,一个名为f的函数参数,一个名为v的int参数。f形参应该是一个接受int形参并返回一个int值的函数。最后一部分代码如下: 29 | 30 | ```go 31 | func main() { 32 | fmt.Println("function1:", funFun(function1, 123)) 33 | fmt.Println("function2:", funFun(function2, 123)) 34 | fmt.Println("Inline:", funFun(func(i int) int {return i * i * i}, 123)) 35 | } 36 | ``` 37 | 38 | The first fmt.Println() call uses funFun() with function1, without any parentheses, 39 | as its first parameter, whereas the second fmt.Println() call uses funFun() with 40 | function2 as its first parameter. 41 | 42 | 43 | 第一个`fmt.Println()`调用`funFun()`,用不带任何圆括号的`function1`作为第一个形参,而第二个`fmt.Println()`调用`funFun()`和`function2`作为形参。 44 | 45 | 最后一个`fmt.Println()`语句,有趣的事情发生了,函数的实现放在了方法`funFun()`第一个参数的位置上,尽管这种方法适用于简单和功能小的函数参数。但是对于包含许多行Go代码的函数来说就不那么好了。 46 | 47 | 执行代码输出如下: 48 | 49 | ```shell 50 | $ go run funFun.go 51 | function1: 246 52 | function2: 15129 53 | Inline: 1860867 54 | ``` 55 | -------------------------------------------------------------------------------- /eBook/chapter6/06.3.0.md: -------------------------------------------------------------------------------- 1 | # 开发自己的Go packages 2 | 3 | Go package的源码,可以包含多个文件和文件夹,能够在以包名命名的单个目录内找到。除了显而易见的main包可以放在任何地方。这章的目标,我们将开发一个名为`aPackage`的Go package。包的源文件为`aPackage.go`,它的源码分两部分展示。第一部分如下所示: 4 | 5 | ```go 6 | package aPackage 7 | 8 | import ( 9 | "fmt" 10 | ) 11 | 12 | func A() { 13 | fmt.Println("This is function A!") 14 | } 15 | ``` 16 | 17 | 在Go package中注意使用首写字母大写。在这里的`– aPackage`仅仅是作为例子在使用。第二部分代码如下: 18 | 19 | ```go 20 | func B() { 21 | fmt.Println("privateConstant:", privateConstant) 22 | } 23 | const MyConstant = 123 24 | const privateConstant = 21 25 | ``` 26 | 27 | 可以看出,开发一个新的Go package非常简单。当前,你不能单独使用这个包,你需要创建一个名为main的包,其中包含main()函数,以便创建一个可执行文件。在这个例子中,将使用aPackage的作为usepackage.go的包名 28 | 29 | ```go 30 | package main 31 | 32 | import ( 33 | "aPackage" 34 | "fmt" 35 | ) 36 | 37 | func main() { 38 | fmt.Println("Using aPackage!") 39 | aPackage.A() 40 | aPackage.B() 41 | fmt.Println(aPackage.MyConstant) 42 | } 43 | ``` 44 | 45 | 如果此时执行,会得到以下错误,这意味着我们的程序还没有完成。 46 | 47 | ```shell 48 | $ go run useAPackage.go 49 | useAPackage.go:4:2: cannot find package "aPackage" in any of: 50 | /usr/local/Cellar/go/1.9.2/libexec/src/aPackage (from $GOROOT) 51 | /Users/mtsouk/go/src/aPackage (from $GOPATH) 52 | ``` 53 | 54 | 这里有另外一件事情需要我们处理。假设你已经知道了第一章的内容和操作系统的知识。Go需要在UNIX中执行一些特殊的shell命令来下载所有的外部包,其中也包含了你本地开发的包。因此,在打包之前你要确保当前的UNIX的用户是可用的,并且将之前的包放在合适的路劲下。因此,下载你自己的包涉及到执行以下UNIX的shell命令: 55 | 56 | ```shell 57 | $ mkdir ~/go/src/aPackage 58 | $ cp aPackage.go ~/go/src/aPackage/ 59 | $ go install aPackage 60 | $ cd ~/go/pkg/darwin_amd64/ 61 | $ ls -l aPackage.a 62 | -rw-r--r-- 1 mtsouk staff 4980 Dec 22 06:12 aPackage.a 63 | ``` 64 | 65 | > 如果 ~/go 文件夹不是已经存在的, 你将需要用`mkdir`命令创建一个。在这个例子中。你将需要和 `~/go/src`相似的目录,代码执行后如下图所示: 66 | 67 | ```shell 68 | $ go run useAPackage.go 69 | Using aPackage! 70 | This is function A! 71 | privateConstant: 21 72 | 123 73 | ``` -------------------------------------------------------------------------------- /eBook/chapter6/06.3.1.md: -------------------------------------------------------------------------------- 1 | # 编译一个Go package 2 | 3 | 虽然你不能执行不在main函数内的Go package,但是你仍然可以编译和创建对应的目标文件。如下所示: 4 | 5 | ```shell 6 | $ go tool compile aPackage.go 7 | $ ls -l aPackage.* 8 | -rw-r--r--@ 1 mtsouk staff 201 Jan 10 22:08 aPackage.go 9 | -rw-r--r-- 1 mtsouk staff 16316 Mar 4 20:01 aPackage.o# 10 | ``` -------------------------------------------------------------------------------- /eBook/chapter6/06.3.2.md: -------------------------------------------------------------------------------- 1 | # 私有变量和函数 2 | 3 | 私有变量和函数与公共变量和函数的区别在于,私有变量和函数可以在包的内部严格使用和调用。控制哪些函数、常量和变量是公共的,也称为封装。Go使用一个简单的规则区分函数或者变量私有或是共有。**公有的函数,变量都是以大写字母开头,相反,私有的函数,变量都是以小写字母开头**,这就是`fmt.Println()`中用`Println()`而不用`println()`的原因。然而,**这个规则不影响包名的大小写。** -------------------------------------------------------------------------------- /eBook/chapter6/06.3.3.md: -------------------------------------------------------------------------------- 1 | # `init()` 初始化函数 2 | 3 | 任何一个Go package能够拥有一个私有函数`init()`,该函数在其他方法执行之前最开始自动执行。 4 | 5 | > `init()`函数被设计为私有函数,这意味着不能从包外部调用它。作为package的用户不能完全控制`init()`函数,你应该在使用公共包或在`init()`函数中修改全局状态时,仔细思考。 6 | 7 | 我们将从多个Go packages中展示一个多个`init()`函数的例子。案例代码的包名简化为`a` 8 | 9 | ```go 10 | package a 11 | 12 | import ( 13 | "fmt" 14 | ) 15 | 16 | func init() { 17 | fmt.Println("init() a") 18 | } 19 | 20 | func FromA() { 21 | fmt.Println("fromA()") 22 | } 23 | ``` 24 | 25 | 这个a包实现了`init()`函数和一个名为`FromA()`的函数。在这之后,我们需要使用如下shell命令让这个包成为当前UNIX用户可用的包。 26 | 27 | ```shell 28 | $ mkdir ~/go/src/a 29 | $ cp a.go ~/go/src/a/ 30 | $ go install a 31 | ``` 32 | 现在,我们看下一个Go语言的包b。 33 | 34 | ```go 35 | package b 36 | 37 | import ( 38 | "a" 39 | "fmt" 40 | ) 41 | 42 | func init() { 43 | fmt.Println("init() b") 44 | } 45 | func FromB() { 46 | fmt.Println("fromB()") 47 | a.FromA() 48 | } 49 | ``` 50 | 51 | 这里发生了什么?a包使用了`fmt`的Go语言标准库。然而,b包需要导入a包才能使用`a.FromA()`函数。a和b都有一个`init()`函数。 52 | 53 | 在这之后,我们需要下载这个包使得它在当前UNIX用户下可用。执行如下shell脚本。 54 | 55 | ```shell 56 | $ mkdir ~/go/src/b 57 | $ cp b.go ~/go/src/b 58 | $ go install b 59 | ``` 60 | 61 | 因此,我们当前有两个包含`init()`初始话函数的Go packages。现在我们试着猜测下执行`manyInit.go`后会输出什么,请看以下代码: 62 | 63 | ```go 64 | package main 65 | 66 | import ( 67 | "a" 68 | "b" 69 | "fmt" 70 | ) 71 | 72 | func init() { 73 | fmt.Println("init() manyInit") 74 | } 75 | 76 | func main() { 77 | a.FromA() 78 | b.FromB() 79 | } 80 | ``` 81 | 82 | 实际的问题是:在a包执行时`init()`函数执行了几次?执`manyInit.go`生成如下输出,并对这个问题给出一些解释: 83 | 84 | ```shell 85 | $ go run manyInit.go 86 | init() a 87 | init() b 88 | init() manyInit 89 | fromA() 90 | fromB() 91 | fromA() 92 | ``` 93 | 94 | 虽然事实是a包被两个不同的包两次导入,但是之前的输出说明了`init()`函数只执行了一次。作为从`manyInit.go`导入的第一次执行的代码块,包a和包b的`init()`函数在`manyInit.go`的`init()`函数之前执行,这比较合理。主要原因是`manyInit.go`的`init()`函数允许使用a或b中的元素。 95 | 96 | 当你想要设置一些不需要外部使用的内部变量是,`init()`函数是非常有用的。举例来说,我们可以在`init()`函数中找到当前的时区。记住,一个文件中可以有多个init()函数;然而,Go的这个特点很少被使用。 97 | -------------------------------------------------------------------------------- /eBook/chapter6/06.4.0.md: -------------------------------------------------------------------------------- 1 | # Go modules 2 | 3 | Go modules在GO 1.11版本中第一次被提及。在写这些内容的时候最新的Go版本是1.13。但在未来的Go版本中,所介绍的一些细节可能会发生变化。 4 | 5 | Go module类似于一个有版本好的Go package。Go对版本控制模块使用语义版本控制。这意味着版本从字母v开头后面加版本号,因此,我们能够有一些类似于v1.0.0, v1.0.5, 和v2.0.2的版本号。 6 | 7 | 第一位版本号,v1, v2, or v3代表不向后兼容的Go package的大版本。这意味着如果我们的程序工作在v1版本,它不一定能在v2和v3版本的Go环境下正常工作。第二位版本号是特性相关的,通常v1.1.0版本要比v1.0.2或者v1.0.0有更多的新的特性,也比所有老的版本更加完整。最后一位版本号是和bug fix相关的。没有新的饿特性。注意,语义版本控制也用于Go版本。 8 | 9 | 注意Go modules允许我们在GOPATH路径外写一些东西。 10 | 11 | 12 | -------------------------------------------------------------------------------- /eBook/chapter6/06.4.1.0.md: -------------------------------------------------------------------------------- 1 | # Go module的创建和使用 2 | 3 | 这部分,我们将创建第一个基础版本的Go module,我们需要一个Github仓库来存储代码。在这部分内容中,代码将放在[https://github.com/mactsouk/myModule](https://github.com/mactsouk/myModule)。我们将创建一个只有`README.md`的仓库。所以第一次将在shell命令行下执行如下命令: 4 | 5 | ```shell 6 | $ git clone git@github.com:mactsouk/myModule.git 7 | Cloning into 'myModule'... 8 | remote: Enumerating objects: 7, done. 9 | remote: Counting objects: 100% (7/7), done. 10 | remote: Compressing objects: 100% (6/6), done. 11 | remote: Total 7 (delta 1), reused 0 (delta 0), pack-reused 0 12 | Receiving objects: 100% (7/7), done. 13 | Resolving deltas: 100% (1/1), done. 14 | ``` 15 | 16 | 执行之后,我们将有一个作者的Github仓库,如果你想从头创建你自己的Go模块,你需要创建你自己的空GitHub仓库。 17 | -------------------------------------------------------------------------------- /eBook/chapter6/06.4.1.1.md: -------------------------------------------------------------------------------- 1 | # 创建v1.0.0版本 2 | 3 | 我们将需要执行如下命令来创建一个我们自己的v1.0.0 Go module。 4 | 5 | ```shell 6 | $ go mod init 7 | go: creating new go.mod: module github.com/mactsouk/myModule 8 | $ touch myModule.go 9 | $ vi myModule.go 10 | $ git add . 11 | $ git commit -a -m "Initial version 1.0.0" 12 | $ git push 13 | $ git tag v1.0.0 14 | $ git push -q origin v1.0.0 15 | $ go list 16 | github.com/mactsouk/myModule 17 | $ go list -m 18 | github.com/mactsouk/myModule 19 | ``` 20 | 21 | `myModule.go`文件内容如下所示: 22 | 23 | ```go 24 | package myModule 25 | 26 | import ( 27 | "fmt" 28 | ) 29 | 30 | func Version() { 31 | fmt.Println("Version 1.0.0") 32 | } 33 | ``` 34 | 35 | `go.mod`之前就被创建了,如下: 36 | 37 | ```shell 38 | $ cat go.mod 39 | module github.com/mactsouk/myModule 40 | go 1.12 41 | ``` -------------------------------------------------------------------------------- /eBook/chapter6/06.4.1.2.md: -------------------------------------------------------------------------------- 1 | # 使用v1.0.0版本 2 | 3 | 在这章中,我们将学习如何使用之前创建的v1.0.0的Go module。为了使用Go modules,我们将创建一个Go程序,`useModule.go`文件如下所示: 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | v1 "github.com/mactsouk/myModule" 10 | ) 11 | 12 | func main() { 13 | v1.Version() 14 | } 15 | ``` 16 | 17 | 我们需要包含Go module(github.com/mactsouk/myModule)路径。在这例子这个路径的别名为v1。在Go语言中使用别名是不整洁的。为使阅读方便所以使用了别名。虽然如此,这个特征也不推荐用在生产代码中。 18 | 19 | 如果我们试着在`/tmp`路径下执行`useModule.go`。它将报“找不到github.com/mactsouk/myModule”错误。 20 | 21 | ```shell 22 | $ pwd 23 | /tmp 24 | $ go run useModule.go 25 | useModule.go:4:2: cannot find package "github.com/mactsouk/myModule" in any 26 | of: 27 | /usr/local/Cellar/go/1.12/libexec/src/github.com/mactsouk/myModule 28 | (from $GOROOT) 29 | /Users/mtsouk/go/src/github.com/mactsouk/myModule (from $GOPATH) 30 | ``` 31 | 32 | 因此,我们需要执行如下命令获得Go modules,然后成功执行`useModule.go` 33 | 34 | ```shell 35 | $ export GO111MODULE=on 36 | $ go run useModule.go 37 | go: finding github.com/mactsouk/myModule v1.0.0 38 | go: downloading github.com/mactsouk/myModule v1.0.0 39 | go: extracting github.com/mactsouk/myModule v1.0.0 40 | Version 1.0.0 41 | ``` 42 | 43 | 所以,`useModule.go`是正确的且能被执行,现在是时候让`useModule.go`更加规范了。去命名并创建它。 44 | 45 | ```shell 46 | $ go mod init hello 47 | go: creating new go.mod: module hello 48 | $ go build 49 | ``` 50 | 51 | 最后一部分命令是在`/tmp`下生成一个可执行文件,以及两个额外的名为`go.mod`和`go.dum`文件。 52 | 53 | ```shell 54 | $ cat go.sum 55 | github.com/mactsouk/myModule v1.0.0 56 | h1:eTCn2Jewnajw0REKONrVhHmeDEJ0Q5TAZ0xsSbh8kFs= 57 | github.com/mactsouk/myModule v1.0.0/go.mod 58 | h1:s3ziarTDDvaXaHWYYOf/ULi97aoBd6JfnvAkM8rSuzg= 59 | ``` 60 | 61 | `go.sum`文件的作用是检查所有的module是否已经下载,内容如下所示: 62 | 63 | ```shell 64 | $ cat go.mod 65 | module hello 66 | go 1.12 67 | require github.com/mactsouk/myModule v1.0.0 68 | ``` 69 | 70 | > 注意如果`go.mod`文件在我们的项目中制定了v1.3.0 Go module版本,即使有最新版本的Go module可以使用,项目也不会用到最新的Go module。 71 | -------------------------------------------------------------------------------- /eBook/chapter6/06.4.1.3.md: -------------------------------------------------------------------------------- 1 | # # 创建v1.1.0版本 2 | 3 | 在这部分,我们将用不同的tag创建一个新版本的`myModule`文件。然而,这一次不需要执行命令`go mod init`,像之前那样。我们将需要执行如下命令: 4 | 5 | ```shell 6 | $ vi myModule.go 7 | $ git commit -a -m "v1.1.0" 8 | [master ddd0742] v1.1.0 9 | 1 file changed, 1 insertion(+), 1 deletion(-) 10 | $ git push 11 | $ git tag v1.1.0 12 | $ git push -q origin v1.1.0 13 | ``` 14 | 15 | `myModule.go`文件内容如下所示: 16 | 17 | ```go 18 | package myModule 19 | 20 | import ( 21 | "fmt" 22 | ) 23 | 24 | func Version() { 25 | fmt.Println("Version 1.1.0") 26 | } 27 | ``` -------------------------------------------------------------------------------- /eBook/chapter6/06.4.1.4.md: -------------------------------------------------------------------------------- 1 | # 使用v1.1.0版本 2 | 3 | 在这章中,我们将学习如何使用我们创建的Go module v1.1.0。这一次,我们将使用Docker镜像,以便尽可能独立用于开发模块的机器。下面的命令会使我们得到Docker镜像然后进入这个程序的运行环境中。 4 | 5 | ```shell 6 | $ docker run --rm -it golang:latest 7 | root@884c0d188694:/go# cd /tmp 8 | root@58c5688e3ee0:/tmp# go version 9 | go version go1.13 linux/amd64 10 | ``` 11 | 12 | 正如我们所见,Docker镜像使用最新的Go版本,在写本文时就是1.13版本。为了使用一个或多个Go modules。我们将需要创建一个Go程序,命名为`useUpdatedModule.go`。内容如下: 13 | 14 | ```go 15 | 16 | package main 17 | 18 | import ( 19 | v1 "github.com/mactsouk/myModule" 20 | ) 21 | 22 | func main() { 23 | v1.Version() 24 | } 25 | ``` 26 | 27 | `useUpdatedModule.go`和`useModule.go`比较相似。非常棒的一件事情就是我们将自动更新到最新的v1版本。在Docker镜像中写完代码后,我们做如下操作。 28 | 29 | ```shell 30 | root@58c5688e3ee0:/tmp# ls -l 31 | total 4 32 | -rw-r--r-- 1 root root 91 Mar 2 19:59 useUpdatedModule.go 33 | root@58c5688e3ee0:/tmp# export GO111MODULE=on 34 | root@58c5688e3ee0:/tmp# go run useUpdatedModule.go 35 | go: finding github.com/mactsouk/myModule v1.1.0 36 | go: downloading github.com/mactsouk/myModule v1.1.0 37 | go: extracting github.com/mactsouk/myModule v1.1.0 38 | Version 1.1.0 39 | ``` 40 | 41 | 这意味着`useUpdatedModule.go`被自动使用最新的v1版本的Go module。当你开启module支持`GO111MODULE=on`时是比较危险的。 42 | 43 | 如果我们尝试执在本地机器上`/tmp`执行`useModule.go`时,会得到如下输出: 44 | 45 | ```shell 46 | $ ls -l go.mod go.sum useModule.go 47 | -rw------- 1 mtsouk wheel 67 Mar 2 21:29 go.mod 48 | -rw------- 1 mtsouk wheel 175 Mar 2 21:29 go.sum 49 | -rw-r--r-- 1 mtsouk wheel 92 Mar 2 21:12 useModule.go 50 | $ go run useModule.go 51 | Version 1.0.0 52 | ``` 53 | 54 | 这意味着`useModule.go`依旧使用老版本的Go module。如果想使用最新的Go module,执行如下命令: 55 | 56 | ```shell 57 | $ rm go.mod go.sum 58 | $ go run useModule.go 59 | go: finding github.com/mactsouk/myModule v1.1.0 60 | go: downloading github.com/mactsouk/myModule v1.1.0 61 | go: extracting github.com/mactsouk/myModule v1.1.0 62 | Version 1.1.0 63 | ``` 64 | 65 | 如果又想使用v1.0.0的Go module时,执行如下命令: 66 | 67 | ```shell 68 | $ go mod init hello 69 | go: creating new go.mod: module hello 70 | $ go build 71 | $ go run useModule.go 72 | Version 1.1.0 73 | $ cat go.mod 74 | module hello 75 | go 1.12 76 | require github.com/mactsouk/myModule v1.1.0 77 | $ vi go.mod 78 | $ cat go.mod 79 | module hello 80 | go 1.12 81 | require github.com/mactsouk/myModule v1.0.0 82 | $ go run useModule.go 83 | Version 1.0.0 84 | ``` 85 | 86 | 下一章节将会创建一个新Go module的大版本,这意味着用不同的tag代替。我们需要使用一个不同的Github分支。 -------------------------------------------------------------------------------- /eBook/chapter6/06.4.1.5.md: -------------------------------------------------------------------------------- 1 | # 创建v2.0.0版本 2 | 3 | 在这章中,我们将创建一个第二大的myModule版本。注意,对于主要版本,你需要在import语句中明确表示。 4 | 5 | 所以`github.com/mactsouk/myModule`将成为v2版本的`github.com/mactsouk/myModule/v2`和v3版本的`github.com/mactsouk/myModule/v3`。第一件事情就是创建一个新的Github分支: 6 | 7 | 8 | ```shell 9 | $ git checkout -b v2 10 | Switched to a new branch 'v2' 11 | $ git push --set-upstream origin v2 12 | ``` 13 | 14 | 然后输入如下命令: 15 | 16 | ```shell 17 | $ vi go.mod 18 | $ cat go.mod 19 | module github.com/mactsouk/myModule/v2 20 | go 1.12 21 | $ git commit -a -m "Using 2.0.0" 22 | [v2 5af2269] Using 2.0.0 23 | 2 files changed, 2 insertions(+), 2 deletions(-) 24 | $ git tag v2.0.0 25 | $ git push --tags origin v2 26 | Counting objects: 4, done. 27 | Delta compression using up to 8 threads. 28 | Compressing objects: 100% (3/3), done. 29 | Writing objects: 100% (4/4), 441 bytes | 441.00 KiB/s, done. 30 | Total 4 (delta 1), reused 0 (delta 0) 31 | remote: Resolving deltas: 100% (1/1), completed with 1 local object. 32 | To github.com:mactsouk/myModule.git 33 | * [new branch] v2 -> v2 34 | * [new tag] v2.0.0 -> v2.0.0 35 | $ git --no-pager branch -a 36 | master 37 | * v2 38 | remotes/origin/HEAD -> origin/master 39 | remotes/origin/master 40 | remotes/origin/v2 41 | ``` 42 | 43 | 这个大版本的内容`myModule.go`如下: 44 | 45 | ```go 46 | package myModule 47 | 48 | import ( 49 | "fmt" 50 | ) 51 | func Version() { 52 | fmt.Println("Version 2.0.0") 53 | } 54 | ``` -------------------------------------------------------------------------------- /eBook/chapter6/06.4.1.6.md: -------------------------------------------------------------------------------- 1 | # 使用v2.0.0版本 2 | 3 | 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: 4 | 5 | 为了使用我们的Go module,我们将需要创建一个名为`useV2.go`的Go程序: 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | v "github.com/mactsouk/myModule/v2" 12 | ) 13 | 14 | func main() { 15 | v.Version() 16 | } 17 | ``` 18 | 19 | 我们将使用Docker镜像。这是最便捷的使用Go module的方式,因为我们会从一个新的go环境下安装。 20 | 21 | ```shell 22 | $ docker run --rm -it golang:latest 23 | root@191d84fc5571:/go# cd /tmp 24 | root@191d84fc5571:/tmp# cat > useV2.go 25 | ``` 26 | 27 | ```go 28 | package main 29 | 30 | import ( 31 | v "github.com/mactsouk/myModule/v2" 32 | ) 33 | 34 | func main() { 35 | v.Version() 36 | } 37 | ``` 38 | 39 | ```shell 40 | root@191d84fc5571:/tmp# export GO111MODULE=on 41 | root@191d84fc5571:/tmp# go run useV2.go 42 | go: finding github.com/mactsouk/myModule/v2 v2.0.0 43 | go: downloading github.com/mactsouk/myModule/v2 v2.0.0 44 | go: extracting github.com/mactsouk/myModule/v2 v2.0.0 45 | Version 2.0.0 46 | ``` 47 | 48 | Docker镜像下使用v2.0.0版本的myModule工作正常。 -------------------------------------------------------------------------------- /eBook/chapter6/06.4.1.7.md: -------------------------------------------------------------------------------- 1 | # 创建v2.1.0版本 2 | 3 | 现在我们要创建`myModule.go`的更新版本。这与使用不同的GitHub标签有关。执行如下命令: 4 | 5 | ```shell 6 | $ vi myModule.go 7 | $ git commit -a -m "v2.1.0" 8 | $ git push 9 | $ git tag v2.1.0 10 | $ git push -q origin v2.1.0 11 | ``` 12 | 13 | The updated contents of myModule.go will be as follows: 14 | 15 | ```go 16 | package myModule 17 | 18 | import ( 19 | "fmt" 20 | ) 21 | func Version() { 22 | fmt.Println("Version 2.1.0") 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /eBook/chapter6/06.4.1.8.md: -------------------------------------------------------------------------------- 1 | # 使用v2.1.0版本 2 | 3 | 我们目前已经知道,为了使用我们的Go module,我们将需要创建一个名为`useUpdatedV2.go`的Go程序: 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | v "github.com/mactsouk/myModule/v2" 10 | ) 11 | 12 | func main() { 13 | v.Version() 14 | } 15 | ``` 16 | 17 | 这里不需要定义我们想要使用的最新的v2版本的Go module,因为这是由Go处理的,这是使用`useUpdatedV2.go`的主要原因。`useV2.go`和它都是一样的。 18 | 19 | 使用Docker镜像的原因就是因为它足够简单。使用vcat(1)命令创建`useUpdatedV2.go`的原因是因为Doker镜像的独立的,其中并没有vi(1)被下载。 20 | 21 | ```shell 22 | $ docker run --rm -it golang:1.12 23 | root@ccfcd675e333:/go# cd /tmp/ 24 | root@ccfcd675e333:/tmp# cat > useUpdatedV2.go 25 | ``` 26 | 27 | ```go 28 | package main 29 | 30 | import ( 31 | v "github.com/mactsouk/myModule/v2" 32 | ) 33 | 34 | func main() { 35 | v.Version() 36 | } 37 | ``` 38 | 39 | ```shell 40 | root@ccfcd675e333:/tmp# ls -l 41 | total 4 42 | -rw-r--r-- 1 root root 92 Mar 2 20:34 useUpdatedV2.go 43 | root@ccfcd675e333:/tmp# go run useUpdatedV2.go 44 | useUpdatedV2.go:4:2: cannot find package "github.com/mactsouk/myModule/v2" 45 | in any of: 46 | /usr/local/go/src/github.com/mactsouk/myModule/v2 (from $GOROOT) 47 | /go/src/github.com/mactsouk/myModule/v2 (from $GOPATH) 48 | root@ccfcd675e333:/tmp# export GO111MODULE=on 49 | root@ccfcd675e333:/tmp# go run useUpdatedV2.go 50 | go: finding github.com/mactsouk/myModule/v2 v2.1.0 51 | go: downloading github.com/mactsouk/myModule/v2 v2.1.0 52 | go: extracting github.com/mactsouk/myModule/v2 v2.1.0 53 | Version 2.1.0 54 | ``` 55 | 56 | > 我们将在第7章反射和接口中学习更多的关于git(1)和Github的命令。 -------------------------------------------------------------------------------- /eBook/chapter6/06.4.2.md: -------------------------------------------------------------------------------- 1 | # 一个Go module使用不同的版本 2 | 3 | 在这章中,我们将学习一个Go module使用的两个以上主要版本。如果你想同时使用一个Go模块的两个以上主要版本,也可以使用相同的技术。 4 | 5 | Go文件`useTwo.go`如下所示: 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | v1 "github.com/mactsouk/myModule" 12 | v2 "github.com/mactsouk/myModule/v2" 13 | ) 14 | 15 | func main() { 16 | v1.Version() 17 | v2.Version() 18 | } 19 | ``` 20 | 21 | 所以我们需要明确的导入想使用的不同的版本,并且要给它起别名。 22 | 23 | 执行`useTwo.go`输出如下: 24 | 25 | ```shell 26 | $ export GO111MODULE=on 27 | $ go run useTwo.go 28 | go: creating new go.mod: module github.com/PacktPublishing/Mastering-GoSecond-Edition 29 | go: finding github.com/mactsouk/myModule/v2 v2.1.0 30 | go: downloading github.com/mactsouk/myModule/v2 v2.1.0 31 | go: extracting github.com/mactsouk/myModule/v2 v2.1.0 32 | Version 1.1.0 33 | Version 2.1.0 34 | ``` 35 | -------------------------------------------------------------------------------- /eBook/chapter6/06.4.3.md: -------------------------------------------------------------------------------- 1 | # Go代码和Go module的存储 2 | 3 | 在本节中,我们将了解Go如何存储代码和正在使用的Go模块的信息。使用我们的Go module作为一个例子。在我们使用我们的Go module就会生成一些内容在本地的macOS机器的`~/go/pkg/mod/github.com/mactsouk`文件夹下面。 4 | 5 | ```shell 6 | $ ls -lR ~/go/pkg/mod/github.com/mactsouk 7 | total 0 8 | drwxr-xr-x 3 mtsouk staff 96B Mar 2 22:38 my!module 9 | dr-x------ 6 mtsouk staff 192B Mar 2 21:18 my!module@v1.0.0 10 | dr-x------ 6 mtsouk staff 192B Mar 2 22:07 my!module@v1.1.0 11 | /Users/mtsouk/go/pkg/mod/github.com/mactsouk/my!module: 12 | total 0 13 | dr-x------ 6 mtsouk staff 192B Mar 2 22:38 v2@v2.1.0 14 | /Users/mtsouk/go/pkg/mod/github.com/mactsouk/my!module/v2@v2.1.0: 15 | total 24 16 | -r--r--r-- 1 mtsouk staff 28B Mar 2 22:38 README.md 17 | -r--r--r-- 1 mtsouk staff 48B Mar 2 22:38 go.mod 18 | -r--r--r-- 1 mtsouk staff 86B Mar 2 22:38 myModule.go 19 | /Users/mtsouk/go/pkg/mod/github.com/mactsouk/my!module@v1.0.0: 20 | total 24 21 | -r--r--r-- 1 mtsouk staff 28B Mar 2 21:18 README.md 22 | -r--r--r-- 1 mtsouk staff 45B Mar 2 21:18 go.mod 23 | -r--r--r-- 1 mtsouk staff 86B Mar 2 21:18 myModule.go 24 | /Users/mtsouk/go/pkg/mod/github.com/mactsouk/my!module@v1.1.0: 25 | total 24 26 | -r--r--r-- 1 mtsouk staff 28B Mar 2 22:07 README.md 27 | -r--r--r-- 1 mtsouk staff 45B Mar 2 22:07 go.mod 28 | -r--r--r-- 1 mtsouk staff 86B Mar 2 22:07 myModule.go 29 | ``` 30 | 31 | > 最好的学习Go module的方式就是去实验尝试(多写多用)。你用或不用,Go module就在那里,所以我们开始使用他们吧。 -------------------------------------------------------------------------------- /eBook/chapter6/06.4.4.md: -------------------------------------------------------------------------------- 1 | # go mod vendor命令 2 | 3 | 有时,我们需要将所有依赖项存储在同一个地方,并将它们保存在项目文件。在这种情况下`go mod vendor`命令会帮助我们准确的完成: 4 | 5 | ```shell 6 | $ cd useTwoVersions 7 | $ go mod init useV1V2 8 | go: creating new go.mod: module useV1V2 9 | $ go mod vendor 10 | $ ls -l 11 | total 24 12 | -rw------- 1 mtsouk staff 114B Mar 2 22:43 go.mod 13 | -rw------- 1 mtsouk staff 356B Mar 2 22:43 go.sum 14 | -rw-r--r--@ 1 mtsouk staff 143B Mar 2 19:36 useTwo.go 15 | drwxr-xr-x 4 mtsouk staff 128B Mar 2 22:43 vendor 16 | $ ls -l vendor/github.com/mactsouk/myModule 17 | total 24 18 | -rw-r--r-- 1 mtsouk staff 28B Mar 2 22:43 README.md 19 | -rw-r--r-- 1 mtsouk staff 45B Mar 2 22:43 go.mod 20 | -rw-r--r-- 1 mtsouk staff 86B Mar 2 22:43 myModule.go 21 | drwxr-xr-x 6 mtsouk staff 192B Mar 2 22:43 v2 22 | $ ls -l vendor/github.com/mactsouk/myModule/v2 23 | total 24 24 | -rw-r--r-- 1 mtsouk staff 28B Mar 2 22:43 README.md 25 | -rw-r--r-- 1 mtsouk staff 48B Mar 2 22:43 go.mod 26 | -rw-r--r-- 1 mtsouk staff 86B Mar 2 22:43 myModule.go 27 | ``` 28 | 29 | 关键点在于在执行`go mod vendor`命令之前去执行`go mod init ` -------------------------------------------------------------------------------- /eBook/chapter6/06.5.md: -------------------------------------------------------------------------------- 1 | # 创建优秀的Go packages 2 | 3 | 这章将提供一些好的的建议帮助我们开发更好的Go packages。我们屏蔽Go packages将其组织在文件夹中,能够包含一些共有和私有的元素。公有元素可以在包内部或者包外部使用,私有元素只能在包的内部使用。 4 | 5 | - 这里有一些创建go package优秀的规则:第一点非官方的规则就是元素之间必须有一定的关联。因此,你可以创建一个支持car的包,但是创建一个包含有car和bicycles的包就不是那么合理了。简单来说,最好的方式就是将不必要的package放在多个package中,而不是在一个单独的包中添加很多函数。此外,package应该简单和易用,但不能太过于简单和凌乱。(包中功能的粒度不能太小,将同类的功能放一个包中即可) 6 | 7 | - 第二部分实际的规则就是我们应该多次使用自己的package,在使用多次没有问题然后在提供给别人使用。这将会帮助我们发现一些愚蠢的bug,确保package的使用是我们预期的结果。在此之后,在公开package之前,将package交给其他开发人员进行额外的测试。 8 | 9 | - 紧接着,尝试假设一些愉快地使用我们package的用户类型,确保我们的package在使用过程中,会给他们带来超出他们能力范围的问题。 10 | 11 | - 除非我们有很好的理由,否则package不应该输出无穷尽的函数列表。你的package内的很少的函数列表,会使我们很好的理解,使用的方便。在此之后,尝试使用简短的名字命令你的函数名称而不是很长的名字。 12 | 13 | - 接口能够提高函数的可用性,所以当我们认为合适的时候,使用接口代替单一类型的函数参数或者返回类型。 14 | 15 | - 当更新一个包时,除非绝对必要,尽量不要破坏其他package,也不要创建与旧版本产生不兼容的代码。 16 | 17 | - 当开发一个新的Go package时,尝试使用多个文件,为了将相似的任务或者概念分组。 18 | 19 | - 此外,尝试仿照标准库的Go package的规则,阅读标准库的代码将会对我们有益。 20 | 21 | - 不要创建一个已经在之前存在的package。对现有的包进行更改,并创建自己的版本。 22 | 23 | - 没有人想要一个在屏幕上打印日志信息的Go package。在需要用到的时候设置一个日志打开标记来打开日志记录会更专业。 24 | 25 | - 在Go代码里引用的Go package使用要和谐。这意味着,如果你看到一个使用你的包和函数名的程序在代码中以一种糟糕的方式出现,最好更改函数的名称。由于package名称几乎在任何地方都被使用,所以尽量使用简洁而富有表现力的包名。 26 | 27 | - 如果你将新的Go类型定义放在第一次使用它们的地方附近,会更方便,因为包括你在内的任何人都不希望在源文件中搜索新数据类型的定义。 28 | 29 | - 试着在我们的包里创建测试文件,因为有测试文件package比没有的要更加标准,专业。小小的细节将会给使用的人产生很大的便捷,说明你是一个很认真的开发者!请注意,为包编写测试是不是随便的,你应该避免使用不包含测试的包。在11章将学到更多关于测试的内容。 30 | 31 | - 最后,不要写Go包,因为你没有更好的事情做-在这种情况下,找一些更好的事情做,不要浪费你的时间! 32 | 33 | > 永远记住,除了包中的实际Go代码应该是没有bug的这一事实之外,优秀package中最重要的元素就是它的文档以及一些代码示例,澄清其使用和展示的功能包的特性。 -------------------------------------------------------------------------------- /eBook/chapter6/06.7.0.md: -------------------------------------------------------------------------------- 1 | # `go/scanner, go/parser, 和go/token`包 2 | 3 | 这章将讲解关于`go/scanner, go/parser, 和go/token`包的内容,和`go/ast`一样,这是有关Go扫码和解析Go代码的底层知识,可以帮助我们理解Go是如何工作的。然而,如果底层知识太难,你可能想要跳过这一章。 4 | 5 | 解析语法需要两个阶段。第一个阶段是将输入分解为标记(词法分析),第二个阶段是将所有这些标记提供给解析器,以确保这些标记有意义且顺序正确(语法分析)。仅仅结合英语单词并不总能创造出有效的句子。 -------------------------------------------------------------------------------- /eBook/chapter6/06.7.1.md: -------------------------------------------------------------------------------- 1 | # `go/ast`包 2 | 3 | 抽象语法树(AST)是Go源代码的结构化表示的程序。这个树是根据语言中指定的一些规则构造的规范。`go/ast`在go中一般用在定义数据类型。如果你想要找到更多的有关`ast.*`类型。`go/ast`包的源码将是你学习的好地方。 -------------------------------------------------------------------------------- /eBook/chapter6/06.7.2.md: -------------------------------------------------------------------------------- 1 | # `go/scanner`包 2 | 3 | 扫描器就是一个在程序中能够读取写入的组件。在案例中用来生成token。`go/scanner`包用于读取go程序并生成一系列token。`go/scanner`的使用将在`goScanner.go`中分三部分解释说明。第一部分如下: 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "go/scanner" 11 | "go/token" 12 | "io/ioutil" 13 | "os" 14 | ) 15 | 16 | func main() { 17 | if len(os.Args) == 1 { 18 | fmt.Println("Not enough arguments!") 19 | return 20 | } 21 | ``` 22 | 23 | `go/token`包定义了一些Go程序中的表示字典token的常量。第二部分如下: 24 | 25 | ```go 26 | for _, file := range os.Args[1:] { 27 | fmt.Println("Processing:", file) 28 | f, err := ioutil.ReadFile(file) 29 | if err != nil { 30 | fmt.Println(err) 31 | return 32 | } 33 | One := token.NewFileSet() 34 | files := one.AddFile(file, one.Base(), len(f)) 35 | ``` 36 | 37 | 将被标记化的源文件存储在file变量中而其内容存储在f,最后一部分代码如下: 38 | 39 | ```go 40 | var myScanner scanner.Scanner 41 | myScanner.Init(files, f, nil, scanner.ScanComments) 42 | for { 43 | pos, tok, lit := myScanner.Scan() 44 | if tok == token.EOF { 45 | break 46 | } 47 | fmt.Printf("%s\t%s\t%q\n", one.Position(pos), tok, lit) 48 | } 49 | } 50 | } 51 | ``` 52 | 53 | `for`循环用来遍历输入的文件。源码中用`toker.EOF`表示结尾退出`for`循环。`scanner.Scan()`函数返回当前文件的下标、token、和文件名。`scanner.Init()`的`scanner.ScanComments`的使用告诉扫描器返回token的注解。你可以使用1代替`scanner.ScanComments`,如果你不想看到token的注解输出,用0代替`scanner.ScanComments`。build后执行代码如下: 54 | 55 | ```shell 56 | $ ./goScanner a.go 57 | Processing: a.go 58 | a.go:1:1 package "package" 59 | a.go:1:9 IDENT "a" 60 | a.go:1:10 ; "\n" 61 | a.go:3:1 import "import" 62 | a.go:3:8 ( "" 63 | a.go:4:2 STRING "\"fmt\"" 64 | a.go:4:7 ; "\n" 65 | a.go:5:1 ) "" 66 | a.go:5:2 ; "\n" 67 | a.go:7:1 func "func" 68 | a.go:7:6 IDENT "init" 69 | a.go:7:10 ( "" 70 | a.go:7:11 ) "" 71 | a.go:7:13 { "" 72 | a.go:8:2 IDENT "fmt" 73 | a.go:8:5 . "" 74 | a.go:8:6 IDENT "Println" 75 | a.go:8:13 ( "" 76 | a.go:8:14 STRING "\"init() a\"" 77 | a.go:8:24 ) "" 78 | a.go:8:25 ; "\n" 79 | a.go:9:1 } "" 80 | a.go:9:2 ; "\n" 81 | a.go:11:1 func "func" 82 | a.go:11:6 IDENT "FromA" 83 | a.go:11:11 ( "" 84 | a.go:11:12 ) "" 85 | a.go:11:14 { "" 86 | a.go:12:2 IDENT "fmt" 87 | a.go:12:5 . "" 88 | a.go:12:6 IDENT "Println" 89 | a.go:12:13 ( "" 90 | a.go:12:14 STRING "\"fromA()\"" 91 | a.go:12:23 ) "" 92 | a.go:12:24 ; "\n" 93 | a.go:13:1 } "" 94 | a.go:13:2 ; "\n" 95 | ``` 96 | 97 | `goScanner.go`输出后很简单。注意`goScanner.go`可以扫描任何类型的文件,即使是二进制文件。然而,如果你扫描二进制文件。你可能得到看不懂的输出。从输出中可以看到,Go扫描器自动加了分隔符。请注意,IDENT通知一个标识符,这是最流行的token类型。 98 | 99 | 下一章将处理解析过程 -------------------------------------------------------------------------------- /eBook/chapter6/06.7.3.md: -------------------------------------------------------------------------------- 1 | # `go/parser`包 2 | 3 | 解析器读取scanner的输出为了生成这些token的结构。解析器使用语法器描述语法,为了确保给的token是有效的语法。这个结构展示出来如同树的结构,它就是AST。 4 | 5 | `goParser.go`说明了处理`go/token`输出的`go/parser`包的使用。接下来通过4个部分展示说明: 6 | 7 | 第一部分代码如下: 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "go/ast" 15 | "go/parser" 16 | "go/token" 17 | "os" 18 | "strings" 19 | ) 20 | 21 | type visitor int 22 | ``` 23 | 24 | 第二部分代码如下: 25 | 26 | ```go 27 | func (v visitor) Visit(n ast.Node) ast.Visitor { 28 | if n == nil { 29 | return nil 30 | } 31 | fmt.Printf("%s%T\n", strings.Repeat("\t", int(v)), n) 32 | return v + 1 33 | } 34 | ``` 35 | 36 | `Visit()`函数将被AST的每个Node调用。第三部分代码如下: 37 | 38 | ```go 39 | func main() { 40 | if len(os.Args) == 1 { 41 | fmt.Println("Not enough arguments!") 42 | return 43 | } 44 | ``` 45 | 46 | 最后一部分代码如下: 47 | 48 | ```go 49 | for _, file := range os.Args[1:] { 50 | fmt.Println("Processing:", file) 51 | one := token.NewFileSet() 52 | var v visitor 53 | f, err := parser.ParseFile(one, file, nil, parser.AllErrors) 54 | if err != nil { 55 | fmt.Println(err) 56 | return 57 | } 58 | ast.Walk(v, f) 59 | } 60 | } 61 | ``` 62 | 63 | `Walk()`函数被递归调用,用深度遍历优先遍历AST,访问所有的节点。 64 | 65 | building和执行`goParser.go`去找到简单又便捷的AST。输出如下: 66 | 67 | ```shell 68 | $ ./goParser a.go 69 | Processing: a.go 70 | *ast.File 71 | *ast.Ident 72 | *ast.GenDecl 73 | *ast.ImportSpec 74 | *ast.BasicLit 75 | *ast.FuncDecl 76 | *ast.Ident 77 | *ast.FuncType 78 | *ast.FieldList 79 | *ast.BlockStmt 80 | *ast.ExprStmt 81 | *ast.CallExpr 82 | *ast.SelectorExpr 83 | *ast.Ident 84 | *ast.Ident 85 | *ast.BasicLit 86 | *ast.FuncDecl 87 | *ast.Ident 88 | *ast.FuncType 89 | *ast.FieldList 90 | *ast.BlockStmt 91 | *ast.ExprStmt 92 | *ast.CallExpr 93 | *ast.SelectorExpr 94 | *ast.Ident 95 | *ast.Ident 96 | *ast.BasicLit 97 | ``` 98 | 99 | `goParser.go`简单的输出就得到了。然而它和`goScanner.go.`的输出完全不同。 100 | 101 | 现在你已经知道了Go扫描器和Go解析器的输出结果,接下来准备看一些更实用的例子。 -------------------------------------------------------------------------------- /eBook/chapter6/06.7.4.md: -------------------------------------------------------------------------------- 1 | # 实用案例 2 | 3 | 在这章中,我们将写一个在输入文件中计算关键字出现次数的Go程序。在这个例子中,关键字为“var”。功能的名字为`varTimes.go`,它将分4部分展示。第一部分如下: 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "go/scanner" 11 | "go/token" 12 | "io/ioutil" 13 | "os" 14 | ) 15 | 16 | var KEYWORD = "var" 17 | var COUNT = 0 18 | ``` 19 | 20 | 你可以搜索任何你想要的Go关键字——如果你修改了`varTimes.go`,你甚至可以在运行时设置全局关键字变量的值。 21 | 22 | ```go 23 | func main() { 24 | if len(os.Args) == 1 { 25 | fmt.Println("Not enough arguments!") 26 | return 27 | } 28 | for _, file := range os.Args[1:] { 29 | fmt.Println("Processing:", file) 30 | f, err := ioutil.ReadFile(file) 31 | if err != nil { 32 | fmt.Println(err) 33 | sreturn 34 | } 35 | one := token.NewFileSet() 36 | files := one.AddFile(file, one.Base(), len(f)) 37 | ``` 38 | 39 | 第三部分代码如下: 40 | 41 | ```go 42 | var myScanner scanner.Scanner 43 | myScanner.Init(files, f, nil, scanner.ScanComments) 44 | localCount := 0 45 | for { 46 | _, tok, lit := myScanner.Scan() 47 | if tok == token.EOF { 48 | break 49 | } 50 | ``` 51 | 52 | 在本例中,发现标记的位置被忽略,因为这不重要。但是,要找出文件的结尾,需要使用tok变量。 53 | 54 | 最后一部分代码如下: 55 | 56 | ```go 57 | if lit == KEYWORD { 58 | COUNT++ 59 | localCount++ 60 | } 61 | } 62 | fmt.Printf("Found _%s_ %d times\n", KEYWORD, localCount) 63 | } 64 | fmt.Printf("Found _%s_ %d times in total\n", KEYWORD, COUNT) 65 | } 66 | ``` 67 | 68 | 编译执行后`varTimes.go`输出如下: 69 | 70 | ```shell 71 | $ go build varTimes.go 72 | $ ./varTimes varTimes.go variadic.go a.go 73 | Processing: varTimes.go 74 | Found _var_ 3 times 75 | Processing: variadic.go 76 | Found _var_ 0 times 77 | Processing: a.go 78 | Found _var_ 0 times 79 | Found _var_ 3 times in total 80 | ``` -------------------------------------------------------------------------------- /eBook/chapter6/06.8.0.md: -------------------------------------------------------------------------------- 1 | # Text和HTML模板 2 | 3 | 这章的主题将会惊艳到你。因为当前展示的包会带给你很大的灵活性,我确信你将会找到创造性的方式去使用它们,模板主要用于分离输出的格式化部分和数据部分。请注意Go模板能够成为文件或者string,一般的想法是对较小的模板使用内联字符串,对较大的模板使用外部文件。 4 | 5 | 在Go语言中,不能同时导入`text/template`和`html/template`,因为两个包共享相似的包名(`template`)。如果绝对需要的话,你可以将其中一个起一个别名。在第四章可以看`useStrings.go`的使用,复合类型的使用。 6 | 7 | Text的输出经常展示在你的屏幕上,因此html的输出需要浏览器的帮助。然而,text的输出要比html的输出更好些。如果你认为你将需要使用其他UNIX的命令行工具执行Go工具的输出,你应该用`text/template`代替`html/template` 8 | 9 | `text/template`和`html/template`会告诉你Go包有多么复杂。你很快就会看到,这两个包都支持它们自己的编程语言——好的软件使复杂的东西看起来简单而优雅。 -------------------------------------------------------------------------------- /eBook/chapter6/06.8.1.md: -------------------------------------------------------------------------------- 1 | # 生成text输出 2 | 3 | 如果需要创建简单的输出,使用`text/template`包是一个很好的选择。`text/templat`包将在`textT.go`文件中分5部分说明。 4 | 5 | template经常存储在外部文件中,例子将展示`text.gotext`模板文件,将分3个部分分析。数据是在text中或者网络中是类型可读的。然而最简单的原因就是`text.gotext`文件中的数据被程序使用切片转成硬编码。接下来我们将看`textT.go`的Go代码。第一部分代码如下所示: 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | "text/template" 14 | ) 15 | ``` 16 | 17 | 第二部分代码如下: 18 | 19 | ```go 20 | type Entry struct { 21 | Number int 22 | Square int 23 | } 24 | ``` 25 | 26 | 你将需要定义一个新的数据类型存储你的数据,除非你处理非常简单的数据。第三部分代码如下: 27 | 28 | ```go 29 | func main() { 30 | arguments := os.Args 31 | if len(arguments) != 2 { 32 | fmt.Println("Need the template file!") 33 | return 34 | } 35 | tFile := arguments[1] 36 | DATA := [][]int{{-1, 1}, {-2, 4}, {-3, 9}, {-4, 16}} 37 | ``` 38 | 39 | `DATA`变量是一个二维切片,用来初始化你的数据的版本。 40 | 41 | 第四部分代码如下: 42 | 43 | ```go 44 | var Entries []Entry 45 | for _, i := range DATA { 46 | if len(i) == 2 { 47 | temp := Entry{Number: i[0], Square: i[1]} 48 | Entries = append(Entries, temp) 49 | } 50 | } 51 | ``` 52 | 53 | 这个程序从`DATA`变量中创建了一个切片的数据结构。 54 | 55 | 最后一部分`textT.go`的代码如下: 56 | 57 | ```go 58 | t := template.Must(template.ParseGlob(tFile)) 59 | t.Execute(os.Stdout, Entries) 60 | } 61 | ``` 62 | 63 | `template.Must()`用在了初始化。它返回的数据类型是一个Template,它控制一个解析后的展示内容。`template.ParseGlob()`函数读取外部template文件。注意对于外部template文件我更喜欢使用`gohtml`扩展。但是你能使用任何你想要的,目的一致就好了。 64 | 65 | 最后,`template.Execute()`函数不能全部工作,它包括执行程序和打印输出到指定的文件,使用`os.Stdout`。 66 | 67 | 现在是时候看一下template文件的代码,第一部分text template如下: 68 | 69 | ```html 70 | Calculating the squares of some integers 71 | ``` 72 | 73 | 注意空行在text template文件中也是有效的,它将作为空行在最后的文件中展示出来。 74 | 75 | 第二部分代码如下: 76 | 77 | ```html 78 | {{ range . }} The square of {{ printf "%d" .Number}} is {{ printf 79 | "%d" .Square}} 80 | ``` 81 | 82 | 在这里将会有很多有趣的事情发生。关键字的范围允许你迭代输入的行,作为一个给定的切片结构。简单的text像这样会被打印,因此变量和动态的text必须以“{{”开始以“}}”结束。数据结构的 存取可以用`.Number`和`.Square`。注意“.”字符在在这个数据类型的前面。最后,格式化打印命令会将最后的输出文件格式化。 83 | 84 | 第三部分代码如下: 85 | 86 | ```html 87 | {{ end }} 88 | ``` 89 | 90 | `{{ range }}`命令以`{{ end }}`结尾,意外的将`{{ end }}`放在错误的地方将会影响你的输出。第一次,在text template模板中时刻注意空行的有效性,因为它将会输出在最后的文件中。 91 | 92 | Executing textT.go will generate the following type of output: 93 | 执行`textT.go`输出如下: 94 | 95 | ```shell 96 | $ go run textT.go text.gotext 97 | Calculating the squares of some integers 98 | The square of -1 is 1 99 | The square of -2 is 4 100 | The square of -3 is 9 101 | The square of -4 is 16 102 | ``` -------------------------------------------------------------------------------- /eBook/chapter6/06.9.md: -------------------------------------------------------------------------------- 1 | # 资源 2 | 3 | 你将会在下面找到非常有用的资源: 4 | 5 | + 访问`syscall`标准Go库[https://golang.org/pkg/syscall/](https://golang.org/pkg/syscall/)。这是目前为止我看到过最大的Go文档。 6 | 7 | + 访问`text/template`标准Go库[https://golang.org/pkg/text/template/](https://golang.org/pkg/text/template/) 8 | 9 | 10 | + 访问`html/template`标准Go库[https://golang.org/pkg/html/template/](https://golang.org/pkg/html/template/) 11 | 12 | + 访问`go/token`标准Go库[https://golang.org/pkg/go/token/](https://golang.org/pkg/go/token/) 13 | 14 | + 访问`go/parser`标准Go库[https://golang.org/pkg/go/parser/](https://golang.org/pkg/go/parser/) 15 | 16 | + 访问`go/scanner`标准Go库[https://golang.org/pkg/go/scanner/](https://golang.org/pkg/go/scanner/) 17 | 18 | + 访问`go/ast`标准Go库[https://golang.org/pkg/go/ast/](https://golang.org/pkg/go/ast/) 19 | 20 | + 访问`SQLite3`[https://www.sqlite.org/](https://www.sqlite.org/) 21 | 22 | + 访问"用Go语言写出最漂亮的package"[https://www.youtube.com/watch?v=cAWlv2SeQus](https://www.youtube.com/watch?v=cAWlv2SeQus) 23 | 24 | + 如果你想知道`Plan 9`,查看[https://plan9.io/plan9/](https://plan9.io/plan9/) 25 | 26 | + 花点时间通过`man`命令`(man 1 find)`。查看 `find(1)` 命令行工具。 -------------------------------------------------------------------------------- /eBook/chapter8/08.0.md: -------------------------------------------------------------------------------- 1 | # Unix 系统编程 2 | 3 | 在前一章中,我们讨论了 Go 语言中两个高级但有点偏理论的主题:接口和反射,而这一章中的 Go 代码则完全注重于实践! 4 | 5 | 这一章的主题是系统编程,毕竟 Go 语言是一门成熟的系统编程语言。Go 语言的创造者们对他们之前开发系统软件所能使用的编程语言并不满意,所以决定创造一门新的编程语言。 6 | 7 | > 这一章中有些 *Go System Programming(Packt Publishing, 2017)* 中没有的有趣的高级主题。 8 | 9 | 这章的主要内容如下: 10 | 11 | * Unix 进程 12 | * `flag` 包 13 | * `viper` 包 14 | * `cobra` 包 15 | * `io.Reader` 和 `io.Writer` 接口的使用 16 | * 用 `os/signal` 包处理 Unix **信号** 17 | * 在 Unix 系统工具中支持 Unix **管道** 18 | * 创建 Docker 的 Go 语言客户端 19 | * 读取文本文件 20 | * 读取 CSV 文件 21 | * 写入文件 22 | * `bytes` 包 23 | * `syscall` 包的进阶使用 24 | * Unix 文件权限 25 | -------------------------------------------------------------------------------- /eBook/chapter8/08.1.md: -------------------------------------------------------------------------------- 1 | # 关于 Unix 进程 2 | 3 | 严格来说,**进程**是一个执行环境,包含指令、用户数据、系统数据和其他运行时获取的资源。另一方面,**程序**是一个二进制文件,包含初始化进程的指令和用户数据会用到的指令和数据。每个运行中的进程都有一个无符号整数作为唯一标识,叫做**进程 ID**。 4 | 5 | 进程分为三类:用户进程、守护进程、内核进程。用户进程运行在用户空间,通常没有特殊访问权限。守护进程是一个运行在用户空间的程序,它在后台运行不需要终端。内核进程仅在内核空间执行并且可以完全访问所有内核数据结构。 6 | 7 | > C 语言创建新进程的过程包含 `fork()` 系统调用。程序员通过 `fork()` 的返回值区分父进程和子进程。与此相反,Go 语言不支持类似功能,而是通过 goroutines 来实现。 8 | -------------------------------------------------------------------------------- /eBook/chapter8/08.3.1.md: -------------------------------------------------------------------------------- 1 | ## 一个简单的 viper 实例 2 | 3 | 在讨论更深入的例子前,我会展示一个 `viper` 的使用示例。这个程序名为 `usingViper.go`,分为三个部分。 4 | 5 | `useViper.go` 的第一部分如下: 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "github.com/spf13/viper" 13 | ) 14 | ``` 15 | 16 | `useViper.go` 的第一部分如下: 17 | 18 | ```go 19 | func main() { 20 | viper.BindEnv("GOMAXPROCS") 21 | val := viper.Get("GOMAXPROCS") 22 | fmt.Println("GOMAXPROCS:", val) 23 | viper.Set("GOMAXPROCS", 10) 24 | val = viper.Get("GOMAXPROCS") 25 | fmt.Println("GOMAXPROCS:", val) 26 | ``` 27 | 28 | `useViper.go` 的最后一部分如下: 29 | 30 | ```go 31 | viper.BindEnv("NEW_VARIABLE") 32 | val = viper.Get("NEW_VARIABLE") 33 | if val == nil { 34 | fmt.Println("NEW_VARIABLE not defined.") 35 | return 36 | } 37 | fmt.Println(val) 38 | } 39 | ``` 40 | 41 | 这个程序展示了如何使用 `viper` 读取和修改环境变量。`flag` 包不支持这项功能,`os` 包尽管支持了这个功能,但却不如 `viper` 包易用。 42 | 43 | 第一次使用 `viper` 之前,你需要先下载。如果你没有用 Go Modules 的话,可以通过下面的命令下载: 44 | 45 | ```bash 46 | $ go get -u github.com/spf13/viper 47 | ``` 48 | 49 | 如果你是用 Go modules 的话,Go 会在你第一次执行包含了 `viper` 的程序时自动下载。 50 | 51 | 执行 `useViper.go` 会得到如下输出: 52 | 53 | ```go 54 | $ go run useViper.go 55 | GOMAXPROCS: 56 | GOMAXPROCS: 10 57 | NEW_VARIABLE not defined. 58 | ``` 59 | -------------------------------------------------------------------------------- /eBook/chapter8/08.3.2.md: -------------------------------------------------------------------------------- 1 | ## 从 flag 转到 viper 2 | 3 | 可能您有一个 Go 程序已经使用了 `flag` 包,并且想转到 `viper` 包。例如您有如下一段代码使用了 `flag` 包: 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "flag" 10 | "fmt" 11 | ) 12 | 13 | func main() { 14 | minusI := flag.Int("i", 100, "i parameter") 15 | flag.Parse() 16 | i := *minusI 17 | fmt.Println(i) 18 | } 19 | ``` 20 | 21 | 我们会使用 `viper` 包更新这个程序,然后保存到 `flagToViper.go`,一共分为三个部分。第一部分如下: 22 | 23 | ```go 24 | package main 25 | 26 | import ( 27 | "flag" 28 | "fmt" 29 | "github.com/spf13/pflag" 30 | "github.com/spf13/viper" 31 | ) 32 | ``` 33 | 34 | 您需要使用 `pflag` 来处理 `viper` 中的命令行参数。 35 | 36 | `flagToViper.go` 的第二部分如下: 37 | 38 | ```go 39 | func main() { 40 | flag.Int("i", 100, "i parameter") 41 | pflag.CommandLine.AddGoFlagSet(flag.CommandLine) 42 | pflag.Parse() 43 | ``` 44 | 45 | 为了尽量减少要修改的代码,您仍然可以使用 `flag.Int()`,但是需要用 `pflag.Parse()` 来解析。然而,所有的魔法都发生在 `pflag.CommandLine.AddGoFlagSet(flag.CommandLine)` 这一步,因为它将 `flag` 包的数据导入到了 `pflag` 包中。 46 | 47 | `flagToViper.go` 的最后一部分如下: 48 | 49 | ```go 50 | viper.BindPFlags(pflag.CommandLine) 51 | i := viper.GetInt("i") 52 | fmt.Println(i) 53 | } 54 | ``` 55 | 56 | 最后,您需要调用 `viper.BindFlags()`。然后,您就可以使用 `viper.GetInt()` 获取命令行参数中的整数值了。对于其他的数据类型,您需要调用不同的 `viper` 函数。 57 | 58 | 此时,为了让 `flagToViper.go` 正常工作,您可能需要下载 `pflag`,操作如下: 59 | 60 | ```bash 61 | $ go get -u github.com/spf13/pflag 62 | ``` 63 | 64 | 执行 `flagToViper.go` 输出如下: 65 | 66 | ```bash 67 | $ go run flagToViper.go 68 | 100 69 | $ go build flagToViper.go 70 | $ ./flagToViper -i 0 71 | 0 72 | $ ./flagToViper -i abcd 73 | invalid argument "abcd" for "-i, --i" flag: parse error 74 | Usage of ./flagToViper: 75 | -i, --i int i parameter 76 | invalid argument "abcd" for "-i, --i" flag: parse error 77 | ``` 78 | 79 | 如果您向 `flagToViper.go` 输入了位置的命令行参数,`viper` 会报错: 80 | 81 | ```bash 82 | $ ./flagToViper -j 200 83 | unknown shorthand flag: 'j' in -j 84 | Usage of ./flagToViper: 85 | -i, --i int i parameter (default 100) 86 | unknown shorthand flag: 'j' in -j 87 | exit status 2 88 | ``` -------------------------------------------------------------------------------- /eBook/chapter8/08.3.3.md: -------------------------------------------------------------------------------- 1 | ## 读取 JSON 配置文件 2 | 3 | 在这一小节中,您会学到如何使用 `viper` 读取 JSON 配置文件。对应的程序名为 `readJSON.go`,分为三个部分。第一部分如下: 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "github.com/spf13/viper" 11 | ) 12 | ``` 13 | 14 | `readJSON.go` 的第二部分如下: 15 | 16 | ```go 17 | func main() { 18 | viper.SetConfigType("json") 19 | viper.SetConfigFile("./myJSONConfig.json") 20 | fmt.Printf("Using config: %s\n", viper.ConfigFileUsed()) 21 | viper.ReadInConfig() 22 | ``` 23 | 24 | 这一步中对配置文件进行了解析。注意,这里 JSON 配置文件的文件名通过调用 `viper.SetConfigFile("./myJSONConfig.json")` 硬编码写在了 `readJSON.go` 中。 25 | 26 | `readJSON.go` 的最后一部分如下: 27 | 28 | ```go 29 | if viper.IsSet("item1.key1") { 30 | fmt.Println("item1.key1:", viper.Get("item1.key1")) 31 | } else { 32 | fmt.Println("item1.key1 not set!") 33 | } 34 | 35 | if viper.IsSet("item2.key3") { 36 | fmt.Println("item2.key3:", viper.Get("item2.key3")) 37 | } else { 38 | fmt.Println("item2.key3 is not set!") 39 | } 40 | 41 | if !viper.IsSet("item3.key1") { 42 | fmt.Println("item3.key1 is not set!") 43 | } 44 | } 45 | ``` 46 | 47 | 这一步中程序检查了 JSON 配置文件中的值,判断需要的值是否存在。 48 | 49 | `myJSONConfig.json` 的内容如下: 50 | 51 | ```json 52 | { 53 | "item1": { 54 | "key1": "val1", 55 | "key2": false, 56 | "key3": "val3" 57 | }, 58 | "item2": { 59 | "key1": "val1", 60 | "key2": true, 61 | "key3": "val3" 62 | } 63 | } 64 | ``` 65 | 66 | 执行 `readJSON.go` 输出如下: 67 | 68 | ```go 69 | $ go run readJSON.go 70 | Using config: ./myJSONConfig.json 71 | item1.key1: val1 72 | item2.key3: val3 73 | item3.key1 is not set! 74 | ``` 75 | 76 | 如果 `myJSONConfig.json` 不能正确定位,程序也不会报错,但是行为和读取了空的 JSON 配置文件一样: 77 | 78 | ```bash 79 | $ mv myJSONConfig.json .. 80 | $ go run readJSON.go 81 | Using config: ./myJSONConfig.json 82 | item1.key1 not set! 83 | item2.key3 is not set! 84 | item3.key1 is not set! 85 | ``` -------------------------------------------------------------------------------- /eBook/chapter8/08.3.md: -------------------------------------------------------------------------------- 1 | # viper 包 2 | 3 | `viper` 是一个非常强大的包,它支持丰富的选项。所有项目使用 `viper` 的步骤都是一样的。首先,初始化 `viper` 并定义您需要的元素。然后,读取您得到的元素的值备用。注意,`viper` 包可以完全取代 `flag` 包。 4 | 5 | 您可以想使用 Go 语言标准库 `flag` 包一样,直接获取想要的值,也可以通过配置文件间接获取。使用 JSON、YAML、TOML、HCL 或者 Java 属性这些格式的配置时,`viper` 会自动完成解析,替您省掉了大量开发和调试的时间。`viper` 也支持解析参数并存储到 Go 结构体中。不过,这需要 Go 结构体中的字段和配置文件中的相匹配。 6 | 7 | `viper` 的主页发布在 GitHub(https://github.com/spf13/viper)。请注意,您的工具不必用到 `viper` 的所有功能,只要用到您需要的就行了。总的原则就是要使用 `viper` 中哪些能够简化您代码的功能。简单来说,如果您的命令行工具需要特别多命令行参数和标签,那么使用配置文件会是一个更好的选择。 8 | -------------------------------------------------------------------------------- /eBook/chapter9/09.0.md: -------------------------------------------------------------------------------- 1 | # **GO并发-协程,通道和管道** 2 | 3 | 上一章我们讨论了Go系统编程,包括Go函数和与操作系统通信的技术。系统编程中,前面章节未涉及的两个领域是并发编程以及创建和管理多个线程。这两个主题将在本章和下一章中讨论。 4 | 5 | GO提供了自己独特而新颖的方式来实现并发,这就是协程(**`goroutine`**)和通道(**`channels`**)。协程是Go程序中可独立执行的最小实体单元,而通道是协程间并发有效获取数据的方式,这允许协程间具有引用点并可以相互通信。Go中的所有内容都是使用协程执行的,这是完全合理的,因为Go是一种并发编程语言。因此,当Go程序开始执行时,单个协程调用`main()`函数,该函数执行实际的程序。 6 | 7 | 本章的内容和代码都比较简单,你应该可以很容易地理解它们。`goroutines`和`channels`中更高级的部分留到第10章中。 8 | 9 | 本章的主要内容如下: 10 | 11 | - 进程,线程和Go协程之间的区别 12 | - Go调度器 13 | - 并发与并行 14 | - `Erlang`和`Rust`中的并发模型 15 | - 创建Go协程 16 | - 创建通道 17 | - 从通道读取或接收数据 18 | - 往通道里写或发送数据 19 | - 创建管道 20 | - 等待你的Go协程结束 21 | -------------------------------------------------------------------------------- /eBook/chapter9/09.1.1.md: -------------------------------------------------------------------------------- 1 | # **Go调度器** 2 | 3 | Unix内核调度负责程序线程的执行。另一方面,Go运行时也有自己的调度程序,它使用称为 `m:n scheduling` 的调度技术负责执行`goroutine`,通过多路复用使n个操作系统线程执行m个`goroutine`。Go调度器是Go中负责Go程序中`goroutine`的执行方式和执行顺序的组件。这使得Go调度器成为Go编程语言中非常重要的一部分,因为所有的Go程序都是以`goroutine`的形式执行的。 4 | 5 | 需要留意的是,由于Go调度程序仅处理单个程序的`goroutine`,因此其操作比内核调度程序的操作更简单,更轻量,更快。 6 | 7 | `Chapter 10, Concurrency in Go – Advanced Topics` 将讨论更多关于Go调度器的更多细节。 -------------------------------------------------------------------------------- /eBook/chapter9/09.1.2.md: -------------------------------------------------------------------------------- 1 | # **并发与并行** 2 | 3 | 有一个非常普遍的误解,认为并发与并行是一回事 - 其实不然!并行是同时执行某种类型的多个实体,而并发是构建你的组件的一种方式,以便它们可以在可能的情况下独立执行。 4 | 5 | 当你的操作系统和硬件允许时,只有当并发地构建软件组件时,才能以并行地方式安全执行。早在CPU拥有多个内核且计算机拥有大量RAM之前,`Erlang`编程语言就有过类似的实践。 6 | 7 | 在有效的并发设计中,添加并发实体会使整个系统运行得更快,因为可以并行运行更多内容。因此,好的并行性来自于更好的并发表达和解决问题的方式。开发人员负责在系统的设计阶段考虑并发性,并从系统组件的潜在并行执行中受益。因此,开发人员不应该考虑并行性,而应该将程序分解为独立的组件,以便于通过组合来解决前面提到的问题。 8 | 9 | 即使你无法在`Unix`机器上并行运行你的函数,有效的并发设计仍可以改进程序的设计和可维护性。换句话说,并发性优于并行性! -------------------------------------------------------------------------------- /eBook/chapter9/09.1.md: -------------------------------------------------------------------------------- 1 | # **关于进程,线程和Go协程** 2 | 3 | 进程是包含计算机指令,用户数据和系统数据, 以及包含其运行时获得的其他类型资源的程序执行环境,而程序是一个文件,其中包含用于初始化进程的指令和用户数据部分的指令和数据。 4 | 5 | 线程相对于进程是更加小巧而轻量的实体,线程由进程创建且包含自己的控制流和栈。区分线程和进程的一个简单的方式是:假如进程是正在运行的二进制文件,线程就是其子集。 6 | 7 | `goroutine`是Go程序并发执行的最小单元,因为`goroutine`不是像`Unix`进程那样是自治的实体,`goroutine`存在于Unix进程的线程中,它的主要优点是非常轻巧,运行成千上万或几十万都没有问题。 8 | 9 | 总结一下,`goroutine`比线程更轻量,而线程比进程更轻量。实际上,一个进程可以有多个线程以及许多`goroutine`,而`goroutine`需要一个进程才能存在。因此,为了创建一个`goroutine`,你需要有一个进程且这个进程至少有一个线程--`Unix`负责进程和线程管理,而Go工程师只需要处理`goroutine`,这极大的降低了开发的成本。 10 | 11 | 到现在为止,你知道了关于进程,线程和协程的基本知识,下一小节我们聊聊Go调度器。 -------------------------------------------------------------------------------- /eBook/chapter9/09.10.md: -------------------------------------------------------------------------------- 1 | # **练习题** 2 | 3 | - 创建一个管道来读取文本文件,找到每个文件里给定短语的出现次数,并计算所有文件中该短语出现的总数。 4 | - 创建一个管道来计算给定范围的所有自然数的平方和。 5 | - 从 `simple.go` 程序总移除 `time.Sleep(1 * time.Second)` 表达式,看看会发生什么。为什么? 6 | - 修改 `pipeling.go` 的代码来创建一个管道,用五个函数和适当的通道。 7 | - 修改 `pipleline.go` 代码来找出当你忘记关闭 `first()` 函数中的 `out` 通道时会发生什么。 -------------------------------------------------------------------------------- /eBook/chapter9/09.11.md: -------------------------------------------------------------------------------- 1 | # 本章小结 2 | 3 | 在这章里,你了解到了许多 Go 的独特功能,包括 `goroutines`,通道和管道。另外,你学到了使用 `sync` 包提供的功能来给 `goroutines` 提供足够的时间去完成它们的任务。最后,介绍了如何使用通道作为函数的参数。这允许开发者创建数据流管道。 4 | 5 | 下章将通过介绍 `select` 关键字来继续讨论 Go 的并发。这个关键字可以让通道执行许多有趣的任务,你会被它的强大所震惊。 6 | 7 | 之后,你将看到两个技巧,用于处理一个或多个因为某些原因而超时的 `goroutines`。然后,你将了解空通道,信号通道,通道的通道和缓冲通道,还有 `context` 包。 8 | 9 | 在下章你也会了解到**共享内存**,它是同一个 Unix 进程中的线程间共享信息的传统方式,它也适用于 `goroutines`。不过,共享内存在 Go 开发者中并不流行,因为 Go 提供了更好,更安全和更快速的方法给 `goroutines` 来交换数据。 -------------------------------------------------------------------------------- /eBook/chapter9/09.2.1.md: -------------------------------------------------------------------------------- 1 | # **创建一个Goroutine** 2 | 3 | 在本小节中,你将学习两种创建`goroutine`的方法。第一种方法是使用常规的函数,而第二种方法是使用匿名函数 - 这两种方法是等价的。 4 | 5 | 本小节所展示的程序文件为`simple.go`,它分为三个部分。 6 | 7 | 第一部分代码如下: 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "time" 15 | ) 16 | 17 | func function() { 18 | for i := 0; i < 10; i++ { 19 | fmt.Print(i) 20 | } 21 | fmt.Println() 22 | } 23 | ``` 24 | 25 | 除了import包之外,上面的代码定义了一个名为`function()`的函数,该函数将在下面的代码内使用。 26 | 27 | 接下来是`simple.go`的第二部分代码: 28 | 29 | ```go 30 | func main() { 31 | go function() 32 | ``` 33 | 34 | 上面的代码启用一个新的Goroutine来运行function()函数。然后主程序会继续执行,而function()函数开始在后台运行。 35 | 36 | simple.go的最后一部分代码如下: 37 | 38 | ```go 39 | go func() { 40 | for i := 10; i < 20; i++ { 41 | fmt.Print(i, " ") 42 | } 43 | }() 44 | time.Sleep(1 * time.Second) 45 | } 46 | ``` 47 | 48 | 如上所示,你也可以使用匿名函数创建Goroutine。此方法适合相对较小的功能。如果函数体有大量代码,最好使用go关键字创建常规函数来执行它。 49 | 50 | 正如你将在下一节中看到的,你可以按照自己的方式创建多个Goroutine,当然也可以使用for循环。 51 | 52 | 执行`simple.go`两次后的输出如下: 53 | 54 | ```bash 55 | $ go run simple.go 56 | 10 11 12 13 14 15 16 17 18 19 0123456789 57 | 58 | $ go run simple.go 59 | 10 11 12 13 14 15 16 0117 2345678918 19 60 | 61 | $ go run simple.go 62 | 10 11 12 012345678913 14 15 16 17 18 19 63 | ``` 64 | 65 | 尽管对于你的程序你想要的是对于同一个输入有相同的输出,但从上面的执行结果来看,三次的输出并不是相同的。我们可以总结一下:在不做额外工作的情况下我们是无法控制`goroutine`的执行顺序的,如果要控制它,我们需要编写额外的代码。在下一章我们将学习到这部分内容。 66 | -------------------------------------------------------------------------------- /eBook/chapter9/09.2.2.md: -------------------------------------------------------------------------------- 1 | # **创建多个Goroutine** 2 | 3 | 在本小节中,你将学习如何创建任意数量的`goroutine`。本节中展示的程序文件为`create.go`。它将分为四个部分,它将允许你创建动态数量的`goroutine`,而且`goroutine`的数量将以命令行参数的形式传递给程序,我们将使用`flag`函数来处理和解析命令行参数。 4 | 5 | `create.go`的第一部分代码如下: 6 | 7 | ```go 8 | import ( 9 | "flag" 10 | "fmt" 11 | "time" 12 | ) 13 | ``` 14 | 15 | 第二部分代码如下: 16 | 17 | ```go 18 | func main() { 19 | n := flag.Int("n", 10, "Number of goroutines") 20 | flag.Parse() 21 | 22 | count := *n 23 | fmt.Printf("Going to create %d goroutines.\n", count) 24 | ``` 25 | 26 | 上面的代码读取命令行选项n的值,该选项决定了我们将要创建的`goroutine`的数量。 27 | 28 | 第三部分代码如下: 29 | 30 | ```go 31 | for i := 0; i < count; i++ { 32 | go func(x int) { 33 | fmt.Printf("%d ", x) 34 | }(i) 35 | } 36 | ``` 37 | 38 | for循环用于创建所需数量的`goroutine`。需要再次注意的是,你不能对它们被创建和执行的顺序做出任何假设和期望。 39 | 40 | 最后一部分代码如下: 41 | 42 | ```go 43 | time.Sleep(time.Second) 44 | fmt.Println("\nExiting...") 45 | } 46 | ``` 47 | 48 | 为了能看到程序的输出,我们使用`time.Sleep()`函数来为`goroutine`提供足够的时间以便能完成它们的工作。但在实际编程中,我们都希望`goroutine`能尽快执行完成,所以并不需要`time.Sleep()`语句,后面我们将学习一种更好的技巧来保证在`main()`函数返回之前让所有的`goroutine`执行完成。 49 | 50 | 多次执行`create.go`的输出如下: 51 | 52 | ```bash 53 | $ go run create.go -n 100 54 | Going to create 100 goroutines. 55 | 5 3 2 4 19 9 0 1 7 11 10 12 13 14 15 31 16 20 17 22 8 18 28 29 21 52 30 45 25 24 49 38 41 46 6 56 57 54 23 26 53 27 59 47 69 66 51 44 71 48 74 33 35 73 39 37 58 40 50 78 85 86 90 67 72 91 32 64 65 95 75 97 99 93 36 60 34 77 94 61 88 89 83 84 43 80 82 87 81 68 92 62 55 98 96 63 76 79 42 70 56 | Exiting... 57 | 58 | $ go run create.go -n 100 59 | Going to create 100 goroutines. 60 | 2 5 3 16 6 7 8 9 1 22 10 12 13 17 11 18 15 14 19 20 31 23 26 21 29 24 30 25 37 32 36 38 35 33 45 41 43 42 40 39 34 44 48 46 47 56 53 50 0 49 55 59 58 28 54 27 60 4 57 51 52 64 61 65 72 62 63 67 69 66 74 73 71 75 89 70 76 84 85 68 79 80 93 97 83 82 99 78 88 91 92 77 81 95 94 98 87 90 96 86 61 | Exiting... 62 | ``` 63 | 64 | 同样,你可以看到输出是乱序的。另外,如果你没有用`time.Sleep()`来保证合适的延迟,你将无法看到`goroutine`的输出。 65 | 66 | 在下一节中,你将学习如何在程序结束之前让你的`goroutine`有足够的时间来完成他们正在做的事情,而不再需要调用`time.Sleep()`。 67 | -------------------------------------------------------------------------------- /eBook/chapter9/09.2.md: -------------------------------------------------------------------------------- 1 | # **协程(Goroutines)** 2 | 3 | 在Go语言中使用go关键字后跟函数名称或定义完整的匿名函数即可开启一个新的`goroutine`,使用`go`关键字调用函数后会立即返回,该函数在后台作为`goroutine`运行,程序的其余部分继续执行 4 | 5 | 但是,如上所述,你无法控制你的`goroutine`的执行顺序,因为这取决于操作系统的调度程序,Go调度程序以及操作系统的负载。 -------------------------------------------------------------------------------- /eBook/chapter9/09.3.1.md: -------------------------------------------------------------------------------- 1 | # **当Add()和Done()的数量不匹配时会发生什么?** 2 | 3 | 当`sync.Add()`和`sync.Done()`调用的数量相等时,程序会正常运行。但是,本节将告诉你当调用数量不一致时会发生什么。 4 | 5 | 假如我们执行`sync.Add()`的次数大于执行`sync.Done()`的次数,这种情况下,通过在第一个`fmt.Printf(“%#v \ n”,waitGroup)`之前添加`waitGroup.Add(1)`语句,然后执行go run的输出如下: 6 | 7 | ```bash 8 | $ go run syncGo.go 9 | Going to create 20 goroutines. 10 | sync.WaitGroup{noCopy:sync.noCopy{}, state1:[12]uint8{0x0, 0x0, 0x0, 0x0, 11 | 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, sema:0x0} 12 | sync.WaitGroup{noCopy:sync.noCopy{}, state1:[12]uint8{0x0, 0x0, 0x0, 0x0, 13 | 0x15, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, sema:0x0} 14 | 19 10 11 12 13 17 18 8 5 4 6 14 1 0 7 3 2 15 9 16 fatal error: all 15 | goroutines are asleep - deadlock! 16 | goroutine 1 [semacquire]: 17 | sync.runtime_Semacquire(0xc4200120bc) 18 | /usr/local/Cellar/go/1.9.3/libexec/src/runtime/sema.go:56 +0x39 19 | sync.(*WaitGroup).Wait(0xc4200120b0) 20 | /usr/local/Cellar/go/1.9.3/libexec/src/sync/waitgroup.go:131 +0x72 21 | main.main() 22 | /Users/mtsouk/Desktop/masterGo/ch/ch9/code/syncGo.go:28 +0x2d7 23 | exit status 2 24 | ``` 25 | 26 | 错误消息是很清楚的: `fatal error: all goroutines are asleep - deadlock!`.,这是因为你通过调用`sync.Add(1)`函数`n+1`次来告诉程序等待`n+1`个`goroutine`,而`n`个`goroutine`只执行了`n`个`sync.Done()`语句。因此,`sync.Wait()`调用将无限期地等待一个或多个对`sync.Done()`的调用,而不会有任何结果,这显然是死锁的情况。 27 | 28 | 如果使用的`sync.Add()`调用比`sync.Done()`调用少,那么可以通过在`syncGo.go`的for循环之后添加waitGroup.Done()语句来进行模拟。那么执行后输出类似如下的结果: 29 | 30 | ```bash 31 | $ go run syncGo.go 32 | Going to create 20 goroutines. 33 | sync.WaitGroup{noCopy:sync.noCopy{}, state1:[12]uint8{0x0, 0x0, 0x0, 0x0, 34 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, sema:0x0} 35 | sync.WaitGroup{noCopy:sync.noCopy{}, state1:[12]uint8{0x0, 0x0, 0x0, 0x0, 36 | 0x12, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, sema:0x0} 37 | 19 6 1 2 9 7 8 15 13 0 14 16 17 3 11 4 5 12 18 10 panic: sync: negative 38 | WaitGroup counter 39 | goroutine 22 [running]: 40 | sync.(*WaitGroup).Add(0xc4200120b0, 0xffffffffffffffff) 41 | /usr/local/Cellar/go/1.9.3/libexec/src/sync/waitgroup.go:75 +0x134 42 | sync.(*WaitGroup).Done(0xc4200120b0) 43 | /usr/local/Cellar/go/1.9.3/libexec/src/sync/waitgroup.go:100 +0x34 44 | main.main.func1(0xc4200120b0, 0x11) 45 | /Users/mtsouk/Desktop/masterGo/ch/ch9/code/syncGo.go:25 +0xd8 46 | created by main.main 47 | /Users/mtsouk/Desktop/masterGo/ch/ch9/code/syncGo.go:21 +0x206 48 | exit status 2 49 | ``` 50 | 51 | 这次问题的根源也非常清楚:`panic: sync: negative WaitGroup counter`。虽然这两种情况下的错误消息都非常具体,可以帮助你解决实际的问题,但是你应该非常小心地处理放入程序中的`sync.Add()`和`sync.Done()`调用的数量。另外,请注意,第二个错误情况(`panic: sync: negative WaitGroup counter`)可能并不总是出现。 -------------------------------------------------------------------------------- /eBook/chapter9/09.4.1.md: -------------------------------------------------------------------------------- 1 | # **通道的写入** 2 | 3 | 这小节的代码将教你怎样往通道写入数据。把 `x` 值写到 `c` 通道,通过 `c <- x` 就可以实现,这非常简单。这个箭头表示值的方向,只要 `x` 和 `c` 是相同的类型,用这个表达式就不会有问题。这节的示例保存在 `writeCh.go` 中,并分三部分介绍。 4 | 5 | `writeCh.go` 的第一段代码如下: 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "time" 13 | ) 14 | 15 | func writeToChannel(c chan int, x int) { 16 | fmt.Println(x) 17 | c <- x 18 | close(c) 19 | fmt.Println(x) 20 | } 21 | ``` 22 | 23 | `chan` 关键字是用于声明函数参数 `c` 是一个通道,并且伴随通道(`int`) 类型。`c <-x` 表达式允许你写 `x` 值到 `c` 通道,并用`close()` 函数关闭这个通道;那样就不会再和它通信了。 24 | 25 | `writeCh.go` 的第二部分代码如下: 26 | 27 | ```go 28 | func main() { 29 | c := make(chan int) 30 | ``` 31 | 32 | 上面的代码定义了一个名为 `c` 的通道变量,第一次在这章使用 `make()` 函数和 `chan` 关键字。所有的通道都有一个指定的类型。 33 | 34 | `writeCh.go` 的其余代码如下: 35 | 36 | ```go 37 | go writeToChannel(c, 10) 38 | time.Sleep(1 * time.Second) 39 | } 40 | ``` 41 | 42 | 这里以 goroutine 的方式执行 `writeToChannel()` 函数并调用 `time.Sleep()` 来给 `writeToChannel()` 函数足够的时间来执行。 43 | 44 | 执行 `writeCh.go` 将产生如下输出: 45 | 46 | ```shell 47 | $go run writeCh.go 48 | 10 49 | ``` 50 | 51 | 奇怪的是 `writeToChannel()` 函数只打印了一次给定的值。这是由于第二个 `fmt.Println(x)` 表达式没有执行。一旦你理解了通道的工作原理,这个原因就非常简单了:`c <- x` 表达式阻塞了 `writeChannel()` 函数下面的执行,因为没人读取 `c` 通道内写入的值。所以,当 `time.Sleep(1 * time.Second)` 表达式结束的时候,程序没有等待 `writeChannel()` 就结束了。 52 | 53 | 下节将说明怎么从通道读数据。 -------------------------------------------------------------------------------- /eBook/chapter9/09.4.2.md: -------------------------------------------------------------------------------- 1 | # **从通道接收数据** 2 | 3 | 这小节,你将了解到如何从通道读取数据。你可以执行 `<-c` 从名为 `c` 的通道读取一个值。如此,箭头方向是从通道到外部。 4 | 5 | 我将使用名为 `readCh.go` 的程序帮你理解怎样从通道读取数据,并它分为三部分介绍。 6 | 7 | `readCh.go` 的第一段代码如下: 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "time" 15 | ) 16 | 17 | func writeToChannel(c chan int, x int) { 18 | fmt.Println("l", x) 19 | c <- x 20 | close(c) 21 | fmt.Println("2", x) 22 | } 23 | ``` 24 | 25 | `writeToChannel()` 函数的实现与之前一样。 26 | 27 | `readCh.go` 的第二部分如下: 28 | 29 | ```go 30 | func main() { 31 | c := make(chan int) 32 | go writeToChannel(c, 10) 33 | time.Sleep(1 * time.Second) 34 | fmt.Println("Read:", <-c) 35 | time.Sleep(1 * time.Second) 36 | ``` 37 | 38 | 上面的代码,使用 `<-c` 语法从 `c` 通道读取数据。如果你想要保存数据到名为 `k` 的变量而不只是打印它的话,你可以使用 `k := <-c` 表达式。第二个 `time.Sleep(1 * time.Second)` 语句给你时间来读取通道数据。 39 | 40 | `readCh.go` 的最后一段代码如下: 41 | 42 | ```go 43 | _, ok := <-c 44 | if ok { 45 | fmt.Println("Channel is open!") 46 | } else { 47 | fmt.Println("Channel is closed!") 48 | } 49 | } 50 | ``` 51 | 52 | 从上面的代码,你能看到一个判断一个通道是打开还是关闭的技巧。当通道关闭时表明当前代码运行的还不错。但是,如果通道被打开,这里的代码就会丢弃从通道读取的值,因为在 `_, ok := <-c` 语句中使用了 `_` 字符。如果你也想在通道打开时读取通道的值,就使用一个有意义的变量名代替 `_`。 53 | 54 | 执行 `readCh.go` 产生如下输出: 55 | 56 | ```shell 57 | $ go run readCh.go 58 | 1 10 59 | Read: 10 60 | 2 10 61 | Channel is closed! 62 | $ go run readCh.go 63 | 1 10 64 | 2 10 65 | Read: 10 66 | Channel is closed! 67 | ``` 68 | 69 | 尽管输出不确定,但 `writeToChannel()` 函数的两个 `fmt->Println(x)` 表达式都被执行了,因为当你从通道读取数据时,它就被解除阻塞了。 70 | 71 | >Bryan: 我的实验结果是第一个结果出现的概率大些。 -------------------------------------------------------------------------------- /eBook/chapter9/09.4.3.md: -------------------------------------------------------------------------------- 1 | # **从关闭的channel中读数据会发生什么** 2 | 3 | 来看代码: 4 | 5 | ```go 6 | package main 7 | import ( 8 | "fmt" 9 | ) 10 | func main() { 11 | willClose := make(chan int, 10) 12 | willClose <- -1 13 | willClose <- 0 14 | willClose <- 2 15 | 16 | <-willClose 17 | <-willClose 18 | <-willClose 19 | 20 | close(willClose) 21 | read := <-willClose 22 | fmt.Println(read) 23 | } 24 | ``` 25 | 代码创建了一个int类型的通道`willClose`,并且向其中写入三个值,然后再依次读出来。后面关闭这个通道后,再尝试从其中读数据,执行这个程序输出结果: 26 | 27 | ```bash 28 | $ go run readClose.go 29 | 0 30 | ``` 31 | 这个结果表明,如果我们尝试从关闭的通道中读取数据,其会返回基本类型的初始值。 32 | -------------------------------------------------------------------------------- /eBook/chapter9/09.4.4.md: -------------------------------------------------------------------------------- 1 | # **通道作为函数参数传递** 2 | 3 | 虽然 `readCh.go` 和 `writeCh.go` 没有使用这一功能,但 Go 允许你在把通道作为函数的参数时指定它的方向;那就是它是否用于读取或写入数据。通道有两种类:**单向通道**和默认的**双向通道**。 4 | 5 | 查看下面两个函数代码: 6 | 7 | ```go 8 | func f1(c chan int, x int) { 9 | fmt.Println(x) 10 | c <- x 11 | } 12 | 13 | func f2(c chan<- int, x int){ 14 | fmt.Println(x) 15 | c <- x 16 | } 17 | ``` 18 | 19 | 虽然两个函数实现了相同的功能,但它们的定义略有不同。`f2()` 函数的 `chan` 关键字右侧有个 `<-` 符号。这说明过 `c` 通道只能用于写数据。如果 Go 代码试图从一个只读通道(只发通道)读取数据的话,Go 编译器就会产生如下错误信息: 20 | 21 | ```shell 22 | # command-line-arguments 23 | a.go:19:11: invalid operation: range in (recevie from send-only type chan<- int) 24 | ``` 25 | 26 | 同样,请看下面的函数: 27 | 28 | ```go 29 | func f1(out chan int64, in chan int64) { 30 | } 31 | 32 | func f2(out chan<- int64, in <-chan int64) { 33 | } 34 | ``` 35 | 36 | `f2` 定义含有名为只读通道`in`和 只写通道`out` 。如果你试图向函数的一个只读通道(只收通道)参数写数据和关闭它时,会得到如下错误信息: 37 | 38 | ```shell 39 | # command-line-arguments 40 | a.go:13:7: invalid operation: in <- i(send to receive-only type <-chan int) 41 | a.go:15:7: invalid operation: close(in)(cannot close receive-only channel) 42 | ``` 43 | -------------------------------------------------------------------------------- /eBook/chapter9/09.4.md: -------------------------------------------------------------------------------- 1 | # **通道(Channel)** 2 | 3 | 通道(`channel`)是Go提供的一种通信机制,允许`goroutines`之间进行数据传输。但也有一些明确的规则,首先,每个通道只允许交换指定类型的数据,也称为通道的元素类型,其次,要使通道正常运行,还需要保证通道有数据接收方。使用`chan`关键字即可声明一个新通道,且可以使用`close()`函数来关闭通道。 4 | 5 | 最后,有一个非常重要的细节:当你使用通道作为函数参数时,你可以指定其方向; 也就是说,该通道是用于发送数据或是接收数据。在我看来,如果你事先知道一个通道的用途,你应该使用这个功能,因为它会使你的程序更健壮,也更安全。你将无法意外地将数据发送到应该只从其接收数据的通道,或从应该只向其发送数据的通道接收数据。因此,如果你声明一个通道函数参数将被用于只读,并尝试对其进行写操作,那么你将得到一条错误消息,它可能会帮助你从讨厌的bug中解救出来。我们将在本章后面讨论这个问题。 6 | 7 | >Tip: 作者说第十章一定要学习呀,会有更有很好的理解。 -------------------------------------------------------------------------------- /eBook/chapter9/09.6.md: -------------------------------------------------------------------------------- 1 | # **竞态(Race conditions)** 2 | 3 | `pipeline.go`并不完美,它包含一个逻辑错误,在并发术语中称为**竞态**。这可以通过执行以下命令来揭示: 4 | 5 | ```shell 6 | $ go run -race pipeline.go 1 10 7 | 2 2 ================== 8 | WARNING: DATA RACE 9 | Write at 0x00000122bae8 by goroutine 7: 10 | main.second() 11 | /Users/mtsouk/ch09/pipeline.go:34 +0x15c 12 | Previous read at 0x00000122bae8 by goroutine 6: 13 | main.first() 14 | /Users/mtsouk/ch09/pipeline.go:21 +0xa3 15 | Goroutine 7 (running) created at: 16 | main.main() 17 | /Users/mtsouk/ch09/pipeline.go:72 +0x2a1 18 | Goroutine 6 (running) created at: 19 | main.main() 20 | /Users/mtsouk/ch09/pipeline.go:71 +0x275 21 | ================== 22 | 2 23 | The sum of the random numbers is 2. 24 | Found 1 data race(s) 25 | exit status 66 26 | ``` 27 | 这里的问题是,当`first()`函数读取CLOSEA变量时,执行`second()`函数的`goroutine`可能会更改CLOSEA变量的值。因为先发生什么和后发生什么是不确定的,所以被认为是竞态。为了修正这个竞态条件,我们需要使用一个信号通道和`select`关键字。 28 | 29 | >Tip:第十章会有更多`select`的介绍。 30 | 31 | `diff(1)`命令会揭示新代码`plNoRace.go`和之前代码的区别: 32 | ```bash 33 | $ diff pipeline.go plNoRace.go 34 | 14a15,16 35 | > var signal chan struct{} 36 | > 37 | 21c23,24 38 | < if CLOSEA { 39 | --- 40 | > select { 41 | > case <-signal: 42 | 23a27 43 | > case out <- random(min, max): 44 | 25d28 45 | < out <- random(min, max) 46 | 31d33 47 | < fmt.Print(x, " ") 48 | 34c36 49 | < CLOSEA = true 50 | --- 51 | > signal <- struct{}{} 52 | 35a38 53 | > fmt.Print(x, " ") 54 | 61d63 55 | < 56 | 66a69,70 57 | > signal = make(chan struct{}) 58 | > 59 | ``` 60 | `plNoRace.go`的正确性可以通过如下命令验证: 61 | ```bash 62 | $ go run -race plNoRace.go 1 10 63 | 8 1 4 9 3 64 | The sum of the random numbers is 25. 65 | ``` -------------------------------------------------------------------------------- /eBook/chapter9/09.7.md: -------------------------------------------------------------------------------- 1 | # **比较Go和Rust的并发模型** 2 | 3 | Rust是一种非常流行的系统编程语言,它也支持并发编程。简要来说,一些Rust的特点和的并发模型如下: 4 | 5 | - Rust线程是UNIX线程,这意味着他们不是轻量的,但可以做许多事情。 6 | - Rust支持**消息传递**和**共享状态**并发,就像Go支持通道、互斥锁和共享变量一样。 7 | - 基于其严格的类型和所有制、Rust提供了一个安全线程可变状态。这些规则由Rust编译器强制执行。 8 | - 有些Rust的结构允许你共享状态。 9 | - 如果一个线程开始行为不正常,系统将不会崩溃。这种情况可以被处理和控制。 10 | - Rust语言在不断发展,这可能会阻止一些人用它,他们可能需要修改现有代码。 11 | 12 | 所以,Rust有一个灵活的并发模型,它甚至比Go的并发模型还要灵活。然而,你为这种灵活性付出的代价是你不得不接受Rust的独特特性。 -------------------------------------------------------------------------------- /eBook/chapter9/09.8.md: -------------------------------------------------------------------------------- 1 | # 比较Go和Erlang并发模型 2 | 3 | Erlang是一种非常流行的并发函数式编程语言,它在设计时考虑了高可用性。简单地说,Erlang和Erlang并发模型的主要特点如下: 4 | 5 | - Erlang是一个成熟的编程语言——这也适用于它的并发模型。 6 | - 如果你不喜欢Erlang代码的工作方式,你总是可以试着使用**Elixir** - 基于Erlang并使用Erlang虚拟机,但其代码更加简洁。 7 | - Erlang只使用异步通信。 8 | - Erlang使用错误处理来开发健壮的并发系统。 9 | - Erlang进程可以崩溃,但是如果崩溃是妥善处理,该系统可以继续工作。 10 | - 就像`goroutine`, Erlang进程之间是隔离的,意味着它们之间没有共享状态。Erlang进程之间通信的唯一途径是通过消息传递。- Erlang线程是轻量级的,就像Go goroutines一样。这意味着你可以创建尽可能多的过程。 11 | 12 | 13 | 总之,只要愿意使用Erlang并发方法,Erlang和Elixir都是可靠和高可用性系统开发的不错选择。 -------------------------------------------------------------------------------- /eBook/chapter9/09.9.md: -------------------------------------------------------------------------------- 1 | # **其他学习资源** 2 | 3 | 访问以下有用资源: 4 | 5 | - Visit the Rust web site at https://www.rust-lang.org/. 6 | - Visit the Erlang web site at https://www.erlang.org/. 7 | - `sync` 包的文档页在[https://golang.org/pkg/sync](https://golang.org/pkg/sync)。 8 | - 再次阅读`sync`包的文档。关注 `sync.Mutext` 和 `sync.RWMutex` 类型,它们将在下章出现。 -------------------------------------------------------------------------------- /test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func main() { 8 | fmt.Println("Thanks for the argument(s)!") 9 | } --------------------------------------------------------------------------------